From 8e75e154fe18eb13bb4cec18fc02e1a9085aecc2 Mon Sep 17 00:00:00 2001 From: Quinn Date: Fri, 17 Oct 2025 21:01:31 +0000 Subject: [PATCH 01/22] Test a generic clp package start up and spin down (default: clp-text). --- integration-tests/.pytest.ini | 3 + integration-tests/.python-version | 2 +- integration-tests/pyproject.toml | 7 +- integration-tests/tests/conftest.py | 1 + .../fixtures/integration_test_packages.py | 84 +++++++++++ integration-tests/tests/test_package_start.py | 103 +++++++++++++ integration-tests/tests/utils/config.py | 141 +++++++++++++++++- integration-tests/tests/utils/utils.py | 10 ++ integration-tests/uv.lock | 91 +++++++++-- 9 files changed, 426 insertions(+), 16 deletions(-) create mode 100644 integration-tests/tests/fixtures/integration_test_packages.py create mode 100644 integration-tests/tests/test_package_start.py diff --git a/integration-tests/.pytest.ini b/integration-tests/.pytest.ini index 6235100f27..f139fd860e 100644 --- a/integration-tests/.pytest.ini +++ b/integration-tests/.pytest.ini @@ -3,6 +3,7 @@ addopts = --capture=no --code-highlight=yes --color=yes + -rA --strict-config --strict-markers --verbose @@ -10,6 +11,7 @@ env = D:CLP_BUILD_DIR=../build D:CLP_CORE_BINS_DIR=../build/core D:CLP_PACKAGE_DIR=../build/clp-package + D:CLP_LOG_DIR=../build/clp-package/var/log log_cli = True log_cli_date_format = %Y-%m-%d %H:%M:%S,%f log_cli_format = %(name)s %(asctime)s [%(levelname)s] %(message)s @@ -18,3 +20,4 @@ markers = clp: mark tests that use the CLP storage engine clp_s: mark tests that use the CLP-S storage engine core: mark tests that test the CLP core binaries + package: mark tests that test a CLP package diff --git a/integration-tests/.python-version b/integration-tests/.python-version index bd28b9c5c2..c8cfe39591 100644 --- a/integration-tests/.python-version +++ b/integration-tests/.python-version @@ -1 +1 @@ -3.9 +3.10 diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index 06b5752775..359ffa99c9 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" authors = [ { name = "YScope Inc.", email = "dev@yscope.com" } ] -requires-python = ">=3.9" +requires-python = ">=3.10" [project.scripts] integration-tests = "integration_tests:main" @@ -21,6 +21,8 @@ dev = [ "ruff>=0.11.12", "pytest>=8.4.1", "pytest-env>=1.1.5", + "PyYAML>=6.0", + "types-PyYAML>=6.0.12.20240808", ] [tool.mypy] @@ -46,8 +48,10 @@ ignore = [ "D400", # First line of docstrings may not end in period "D401", # Docstrings should be written in present tense (not imperative) "D415", # First line of docstrings may not end in a period, question mark, or exclamation point + "EM102", # Exception must not use an f-string literal, assign to variable first "FBT", # Allow bool positional parameters since other value positions are allowed "FIX002", # Allow todo statements + "G004", # Logging statement uses f-string "PERF401", # Allow for loops when creating lists "PERF403", # Allow for loops when creating dicts "S311", # Allow usage of `random` package @@ -56,6 +60,7 @@ ignore = [ "SIM300", # Skip Yoda-condition format fixes "TD002", # Author unnecessary for todo statement "TD003", # Issue link unnecessary for todo statement + "TRY003", # Avoid specifying long messages outside the exception class "UP015", # Explicit open modes are helpful ] isort.order-by-type = false diff --git a/integration-tests/tests/conftest.py b/integration-tests/tests/conftest.py index 2cb709a249..637ebe9bbe 100644 --- a/integration-tests/tests/conftest.py +++ b/integration-tests/tests/conftest.py @@ -3,4 +3,5 @@ pytest_plugins = [ "tests.fixtures.integration_test_config", "tests.fixtures.integration_test_logs", + "tests.fixtures.integration_test_packages", ] diff --git a/integration-tests/tests/fixtures/integration_test_packages.py b/integration-tests/tests/fixtures/integration_test_packages.py new file mode 100644 index 0000000000..80f1005d40 --- /dev/null +++ b/integration-tests/tests/fixtures/integration_test_packages.py @@ -0,0 +1,84 @@ +"""Define test packages fixtures.""" + +import logging +import subprocess +from collections.abc import Iterator +from typing import Literal + +import pytest + +from tests.utils.config import ( + PackageConfig, + PackageRun, +) + +logger = logging.getLogger(__name__) + + +@pytest.fixture(scope="session") +def clp_package( + package_config: PackageConfig, +) -> Iterator[PackageRun]: + """Fixture that starts up an instance of clp, and stops the package after yield.""" + mode: Literal["clp-text", "clp-json"] = "clp-text" + instance = _start_clp_package(package_config, mode) + + yield instance + _stop_clp_package(instance) + + +def _start_clp_package( + package_config: PackageConfig, + mode: Literal["clp-text", "clp-json"], +) -> PackageRun: + """Starts up an instance of clp.""" + logger.info(f"Starting up the {mode} package...") + + start_script_path = package_config.clp_package_dir / "sbin" / "start-clp.sh" + + try: + # fmt: off + start_cmd = [ + start_script_path + ] + # fmt: on + subprocess.run(start_cmd, check=True) + except Exception as e: + err_msg = f"Failed to start an instance of the {mode} package." + raise RuntimeError(err_msg) from e + + package_run = PackageRun( + package_config=package_config, + mode=mode, + ) + + logger.info( + f"An instance of the {package_run.mode} package was started successfully." + f" Its instance ID is '{package_run.clp_instance_id}'" + ) + + return package_run + + +def _stop_clp_package( + instance: PackageRun, +) -> None: + """Stops an instance of clp.""" + mode = instance.mode + instance_id = instance.clp_instance_id + logger.info(f"Now stopping the {mode} package with instance ID '{instance_id}'...") + + stop_script_path = instance.package_config.clp_package_dir / "sbin" / "stop-clp.sh" + + try: + # fmt: off + stop_cmd = [ + stop_script_path + ] + # fmt: on + subprocess.run(stop_cmd, check=True) + except Exception as e: + err_msg = f"Failed to stop an instance of the {mode} package." + raise RuntimeError(err_msg) from e + + logger.info(f"The {mode} package with instance ID '{instance_id}' was stopped successfully.") diff --git a/integration-tests/tests/test_package_start.py b/integration-tests/tests/test_package_start.py new file mode 100644 index 0000000000..2014291d94 --- /dev/null +++ b/integration-tests/tests/test_package_start.py @@ -0,0 +1,103 @@ +"""Integration tests verifying that the CLP package can be started and stopped.""" + +import logging +import shutil +import subprocess + +import pytest + +from tests.utils.config import ( + PackageRun, +) + +pytestmark = pytest.mark.package + + +package_configurations = pytest.mark.parametrize( + "test_package_fixture", + [ + "clp_package", + ], +) + +logger = logging.getLogger(__name__) + + +@pytest.mark.package +@package_configurations +def test_clp_package( + request: pytest.FixtureRequest, + test_package_fixture: str, +) -> None: + """ + Validate that all of the components of the clp package start up successfully. The package is + started up in whatever configuration is currently described in clp-config.yml; default is + clp-text. + + :param request: + :param test_package_fixture: + """ + package_run: PackageRun = request.getfixturevalue(test_package_fixture) + + assert _is_package_running(package_run) + + +def _is_package_running(package_run: PackageRun) -> bool: + """Checks that the package specified in package_run is running correctly.""" + mode = package_run.mode + instance_id = package_run.clp_instance_id + + component_basenames = [ + "clp-database", + "clp-queue", + "clp-redis", + "clp-results_cache", + "clp-compression_scheduler", + "clp-query_scheduler", + "clp-compression_worker", + "clp-query_worker", + "clp-reducer", + "clp-webui", + "clp-garbage_collector", + ] + + logger.info( + "Checking if all components of %s package with instance ID '%s' are running properly.", + mode, + instance_id, + ) + + docker_bin = shutil.which("docker") + if docker_bin is None: + error_msg = "docker not found in PATH" + raise RuntimeError(error_msg) + + for component in component_basenames: + name = f"{component}-{instance_id}" + + proc = subprocess.run( + [docker_bin, "inspect", "-f", "{{.State.Running}}", name], + capture_output=True, + text=True, + check=False, + ) + + if proc.returncode != 0: + err = (proc.stderr or proc.stdout or "").strip() + if "No such object" in err: + logger.error("Component container not found: %s", name) + return False + error_msg = f"Error inspecting container {name}: {err}" + raise RuntimeError(error_msg) + + status = (proc.stdout or "").strip().lower() + if status != "true": + logger.error("Component container not running: %s (status=%s)", name, status) + return False + + logger.info( + "All components of the %s package with instance ID '%s' are running properly.", + mode, + instance_id, + ) + return True diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index f663e4ca33..044c14b63d 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -2,12 +2,18 @@ from __future__ import annotations +import re +import socket from dataclasses import dataclass, field, InitVar from pathlib import Path +from typing import Any, Literal + +import yaml from tests.utils.utils import ( unlink, validate_dir_exists, + validate_file_exists, ) @@ -49,13 +55,19 @@ def clp_s_binary_path(self) -> Path: @dataclass(frozen=True) class PackageConfig: - """The configuration for the clp package subject to testing.""" + """The configuration for the clp package being tested.""" #: clp_package_dir: Path + clp_config_file_path: Path = field(init=False, repr=True) + + #: Hostname of the machine running the test. + hostname: str = field(init=False, repr=True) + def __post_init__(self) -> None: - """Validates that the CLP package directory exists and contains all required directories.""" + """Validates the values specified at init, and initialises attributes.""" + # Validate that the CLP package directory exists and contains all required directories. clp_package_dir = self.clp_package_dir validate_dir_exists(clp_package_dir) @@ -69,6 +81,131 @@ def __post_init__(self) -> None: ) raise ValueError(err_msg) + # Set clp_config_file_path and validate it. + object.__setattr__( + self, "clp_config_file_path", self.clp_package_dir / "etc" / "clp-config.yml" + ) + validate_file_exists(self.clp_config_file_path) + + # Set hostname. + object.__setattr__(self, "hostname", socket.gethostname()) + + +@dataclass(frozen=True) +class PackageRun: + """Metadata for the running CLP package.""" + + #: + package_config: PackageConfig + + #: + mode: Literal["clp-text", "clp-json"] + + #: + clp_log_dir: Path = field(init=False, repr=True) + + clp_run_config_file_path: Path = field(init=False, repr=True) + + #: + clp_instance_id: str = field(init=False, repr=True) + + def __post_init__(self) -> None: + """Validates the values specified at init, and initialises attributes.""" + # Set clp_log_dir after validating it. + clp_log_dir = self.package_config.clp_package_dir / "var" / "log" + validate_dir_exists(clp_log_dir) + object.__setattr__(self, "clp_log_dir", clp_log_dir) + + # Set clp_run_config_file_path after validating it. + clp_run_config_file_path = ( + self.clp_log_dir / self.package_config.hostname / ".clp-config.yml" + ) + validate_file_exists(clp_run_config_file_path) + object.__setattr__(self, "clp_run_config_file_path", clp_run_config_file_path) + + # Set clp_instance_id. + clp_instance_id_file_path = self.clp_log_dir / self.package_config.hostname / "instance-id" + validate_file_exists(clp_instance_id_file_path) + clp_instance_id = self._get_clp_instance_id(clp_instance_id_file_path) + object.__setattr__(self, "clp_instance_id", clp_instance_id) + + # Validate mode. + self._assert_mode_matches_run_config() + + def _assert_mode_matches_run_config(self) -> None: + """Validates that self.mode matches the package values in the run config file.""" + config_dict = self._load_run_config(self.clp_run_config_file_path) + config_mode = self._extract_mode_from_config(config_dict, self.clp_run_config_file_path) + + if config_mode != self.mode: + error_msg = ( + f"Mode mismatch: the mode specified to the PackageRun object was {self.mode}," + f" but the package is running in {config_mode} mode." + ) + raise ValueError(error_msg) + + @staticmethod + def _load_run_config(path: Path) -> dict[str, Any]: + """Load the run config file into a dictionary.""" + try: + with path.open("r", encoding="utf-8") as file: + config_dict = yaml.safe_load(file) + except yaml.YAMLError as err: + raise ValueError(f"Invalid YAML in run config {path}: {err}") from err + except OSError as err: + raise ValueError(f"Cannot read run config {path}: {err}") from err + + if not isinstance(config_dict, dict): + raise TypeError(f"Run config {path} must be a mapping at the top level") + + return config_dict + + @staticmethod + def _extract_mode_from_config( + config_dict: dict[str, Any], + path: Path, + ) -> Literal["clp-text", "clp-json"]: + """Determine the package mode from the contents of the run-config dictionary.""" + package = config_dict.get("package") + if not isinstance(package, dict): + raise TypeError(f"Run config {path} is missing the 'package' mapping.") + + query_engine = package.get("query_engine") + storage_engine = package.get("storage_engine") + if query_engine is None or storage_engine is None: + raise ValueError( + f"Run config {path} must specify both 'package.query_engine' and" + " 'package.storage_engine'." + ) + + if query_engine == "clp" and storage_engine == "clp": + return "clp-text" + if query_engine == "clp-s" and storage_engine == "clp-s": + return "clp-json" + + raise ValueError( + f"Run config {path} specifies running conditions for which integration testing is not" + f"supported: query_engine={query_engine}, storage_engine={storage_engine}." + ) + + @staticmethod + def _get_clp_instance_id(clp_instance_id_file_path: Path) -> str: + """Return the 4-digit hexadecimal CLP instance-id stored at clp_instance_id_file_path.""" + try: + contents = clp_instance_id_file_path.read_text(encoding="utf-8").strip() + except OSError as err: + raise ValueError( + f"Cannot read instance-id file {clp_instance_id_file_path}: {err}" + ) from err + + if not re.fullmatch(r"[0-9a-fA-F]{4}", contents): + raise ValueError( + f"Invalid instance ID in {clp_instance_id_file_path}: expected a 4-character" + f" hexadecimal string, but read {contents}." + ) + + return contents + @dataclass(frozen=True) class IntegrationTestConfig: diff --git a/integration-tests/tests/utils/utils.py b/integration-tests/tests/utils/utils.py index 1dca8ba162..3594b34b64 100644 --- a/integration-tests/tests/utils/utils.py +++ b/integration-tests/tests/utils/utils.py @@ -85,6 +85,16 @@ def validate_dir_exists(dir_path: Path) -> None: raise ValueError(err_msg) +def validate_file_exists(file_path: Path) -> None: + """ + :param file_path: + :raise: ValueError if the path does not exist or is not a file. + """ + if not file_path.is_file(): + err_msg = f"Path does not exist or is not a file: {file_path}" + raise ValueError(err_msg) + + def _sort_json_keys_and_rows(json_fp: Path) -> IO[str]: """ Normalize a JSON file to a stable, deterministically ordered form for comparison. diff --git a/integration-tests/uv.lock b/integration-tests/uv.lock index 67070d7ec5..4b18e52264 100644 --- a/integration-tests/uv.lock +++ b/integration-tests/uv.lock @@ -1,10 +1,6 @@ version = 1 -revision = 2 -requires-python = ">=3.9" -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version < '3.10'", -] +revision = 3 +requires-python = ">=3.10" [[package]] name = "colorama" @@ -46,7 +42,9 @@ dev = [ { name = "mypy" }, { name = "pytest" }, { name = "pytest-env" }, + { name = "pyyaml" }, { name = "ruff" }, + { name = "types-pyyaml" }, ] [package.metadata] @@ -56,7 +54,9 @@ dev = [ { name = "mypy", specifier = ">=1.16.0" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-env", specifier = ">=1.1.5" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "ruff", specifier = ">=0.11.12" }, + { name = "types-pyyaml", specifier = ">=6.0.12.20240808" }, ] [[package]] @@ -101,12 +101,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, - { url = "https://files.pythonhosted.org/packages/29/cb/673e3d34e5d8de60b3a61f44f80150a738bff568cd6b7efb55742a605e98/mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9", size = 10992466, upload-time = "2025-07-31T07:53:57.574Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d0/fe1895836eea3a33ab801561987a10569df92f2d3d4715abf2cfeaa29cb2/mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99", size = 10117638, upload-time = "2025-07-31T07:53:34.256Z" }, - { url = "https://files.pythonhosted.org/packages/97/f3/514aa5532303aafb95b9ca400a31054a2bd9489de166558c2baaeea9c522/mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8", size = 11915673, upload-time = "2025-07-31T07:52:59.361Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c3/c0805f0edec96fe8e2c048b03769a6291523d509be8ee7f56ae922fa3882/mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8", size = 12649022, upload-time = "2025-07-31T07:53:45.92Z" }, - { url = "https://files.pythonhosted.org/packages/45/3e/d646b5a298ada21a8512fa7e5531f664535a495efa672601702398cea2b4/mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259", size = 12895536, upload-time = "2025-07-31T07:53:06.17Z" }, - { url = "https://files.pythonhosted.org/packages/14/55/e13d0dcd276975927d1f4e9e2ec4fd409e199f01bdc671717e673cc63a22/mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d", size = 9512564, upload-time = "2025-07-31T07:53:12.346Z" }, { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, ] @@ -186,6 +180,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141, upload-time = "2024-09-17T22:39:16.942Z" }, ] +[[package]] +name = "pyyaml" +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 = "ruff" version = "0.12.9" @@ -251,6 +309,15 @@ wheels = [ { 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" }, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.1" From bbe0575c8ab9d5bb71980990b871bf193e9016a2 Mon Sep 17 00:00:00 2001 From: Quinn Date: Fri, 24 Oct 2025 20:27:13 +0000 Subject: [PATCH 02/22] Expand to clp-text and clp-json; store JSON config pairs in temp config file; address comments. --- integration-tests/.pytest.ini | 3 +- integration-tests/pyproject.toml | 3 - integration-tests/tests/conftest.py | 1 + .../tests/fixtures/integration_test_config.py | 4 +- .../fixtures/integration_test_packages.py | 121 +++++------ .../tests/fixtures/package_config_fixtures.py | 95 +++++++++ integration-tests/tests/test_package_start.py | 94 ++++---- integration-tests/tests/utils/config.py | 156 +++++++++----- integration-tests/tests/utils/docker_utils.py | 71 +++++++ .../tests/utils/package_utils.py | 200 ++++++++++++++++++ 10 files changed, 566 insertions(+), 182 deletions(-) create mode 100644 integration-tests/tests/fixtures/package_config_fixtures.py create mode 100644 integration-tests/tests/utils/docker_utils.py create mode 100644 integration-tests/tests/utils/package_utils.py diff --git a/integration-tests/.pytest.ini b/integration-tests/.pytest.ini index f139fd860e..07fb7b9ee4 100644 --- a/integration-tests/.pytest.ini +++ b/integration-tests/.pytest.ini @@ -11,7 +11,6 @@ env = D:CLP_BUILD_DIR=../build D:CLP_CORE_BINS_DIR=../build/core D:CLP_PACKAGE_DIR=../build/clp-package - D:CLP_LOG_DIR=../build/clp-package/var/log log_cli = True log_cli_date_format = %Y-%m-%d %H:%M:%S,%f log_cli_format = %(name)s %(asctime)s [%(levelname)s] %(message)s @@ -20,4 +19,4 @@ markers = clp: mark tests that use the CLP storage engine clp_s: mark tests that use the CLP-S storage engine core: mark tests that test the CLP core binaries - package: mark tests that test a CLP package + package: mark tests that run when the CLP package is active diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index 359ffa99c9..7158d936c7 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -48,10 +48,8 @@ ignore = [ "D400", # First line of docstrings may not end in period "D401", # Docstrings should be written in present tense (not imperative) "D415", # First line of docstrings may not end in a period, question mark, or exclamation point - "EM102", # Exception must not use an f-string literal, assign to variable first "FBT", # Allow bool positional parameters since other value positions are allowed "FIX002", # Allow todo statements - "G004", # Logging statement uses f-string "PERF401", # Allow for loops when creating lists "PERF403", # Allow for loops when creating dicts "S311", # Allow usage of `random` package @@ -60,7 +58,6 @@ ignore = [ "SIM300", # Skip Yoda-condition format fixes "TD002", # Author unnecessary for todo statement "TD003", # Issue link unnecessary for todo statement - "TRY003", # Avoid specifying long messages outside the exception class "UP015", # Explicit open modes are helpful ] isort.order-by-type = false diff --git a/integration-tests/tests/conftest.py b/integration-tests/tests/conftest.py index 637ebe9bbe..eeff7e3cee 100644 --- a/integration-tests/tests/conftest.py +++ b/integration-tests/tests/conftest.py @@ -4,4 +4,5 @@ "tests.fixtures.integration_test_config", "tests.fixtures.integration_test_logs", "tests.fixtures.integration_test_packages", + "tests.fixtures.package_config_fixtures", ] diff --git a/integration-tests/tests/fixtures/integration_test_config.py b/integration-tests/tests/fixtures/integration_test_config.py index 3d53203c31..dacfd42371 100644 --- a/integration-tests/tests/fixtures/integration_test_config.py +++ b/integration-tests/tests/fixtures/integration_test_config.py @@ -24,7 +24,9 @@ def core_config() -> CoreConfig: def package_config() -> PackageConfig: """Fixture that provides a PackageConfig shared across tests.""" return PackageConfig( - clp_package_dir=Path(get_env_var("CLP_PACKAGE_DIR")).expanduser().resolve() + clp_package_dir=Path(get_env_var("CLP_PACKAGE_DIR")).expanduser().resolve(), + test_root_dir=Path(get_env_var("CLP_BUILD_DIR")).expanduser().resolve() + / "integration-tests", ) diff --git a/integration-tests/tests/fixtures/integration_test_packages.py b/integration-tests/tests/fixtures/integration_test_packages.py index 80f1005d40..942ff80d78 100644 --- a/integration-tests/tests/fixtures/integration_test_packages.py +++ b/integration-tests/tests/fixtures/integration_test_packages.py @@ -1,84 +1,77 @@ """Define test packages fixtures.""" import logging -import subprocess from collections.abc import Iterator -from typing import Literal import pytest from tests.utils.config import ( - PackageConfig, - PackageRun, + PackageInstance, + PackageInstanceConfigFile, +) +from tests.utils.package_utils import ( + start_clp_package, + stop_clp_package, ) logger = logging.getLogger(__name__) -@pytest.fixture(scope="session") -def clp_package( - package_config: PackageConfig, -) -> Iterator[PackageRun]: - """Fixture that starts up an instance of clp, and stops the package after yield.""" - mode: Literal["clp-text", "clp-json"] = "clp-text" - instance = _start_clp_package(package_config, mode) +@pytest.fixture +def clp_text_package( + clp_text_config: PackageInstanceConfigFile, +) -> Iterator[PackageInstance]: + """Fixture that launches a clp-text instance, and gracefully stops it after its scope ends.""" + log_msg = f"Starting up the {clp_text_config.mode} package..." + logger.info(log_msg) - yield instance - _stop_clp_package(instance) - - -def _start_clp_package( - package_config: PackageConfig, - mode: Literal["clp-text", "clp-json"], -) -> PackageRun: - """Starts up an instance of clp.""" - logger.info(f"Starting up the {mode} package...") - - start_script_path = package_config.clp_package_dir / "sbin" / "start-clp.sh" - - try: - # fmt: off - start_cmd = [ - start_script_path - ] - # fmt: on - subprocess.run(start_cmd, check=True) - except Exception as e: - err_msg = f"Failed to start an instance of the {mode} package." - raise RuntimeError(err_msg) from e - - package_run = PackageRun( - package_config=package_config, - mode=mode, - ) + start_clp_package(clp_text_config) + instance = PackageInstance(package_instance_config_file=clp_text_config) - logger.info( - f"An instance of the {package_run.mode} package was started successfully." - f" Its instance ID is '{package_run.clp_instance_id}'" + mode = instance.package_instance_config_file.mode + instance_id = instance.clp_instance_id + log_msg = ( + f"An instance of the {mode} package was started successfully." + f" Its instance ID is '{instance_id}'" ) + logger.info(log_msg) + + yield instance + + log_msg = f"Now stopping the {mode} package with instance ID '{instance_id}'..." + logger.info(log_msg) + + stop_clp_package(instance) - return package_run + log_msg = f"The {mode} package with instance ID '{instance_id}' was stopped successfully." + logger.info(log_msg) -def _stop_clp_package( - instance: PackageRun, -) -> None: - """Stops an instance of clp.""" - mode = instance.mode +@pytest.fixture +def clp_json_package( + clp_json_config: PackageInstanceConfigFile, +) -> Iterator[PackageInstance]: + """Fixture that launches a clp-json instance, and gracefully stops it after its scope ends.""" + log_msg = f"Starting up the {clp_json_config.mode} package..." + logger.info(log_msg) + + start_clp_package(clp_json_config) + instance = PackageInstance(package_instance_config_file=clp_json_config) + + mode = instance.package_instance_config_file.mode instance_id = instance.clp_instance_id - logger.info(f"Now stopping the {mode} package with instance ID '{instance_id}'...") - - stop_script_path = instance.package_config.clp_package_dir / "sbin" / "stop-clp.sh" - - try: - # fmt: off - stop_cmd = [ - stop_script_path - ] - # fmt: on - subprocess.run(stop_cmd, check=True) - except Exception as e: - err_msg = f"Failed to stop an instance of the {mode} package." - raise RuntimeError(err_msg) from e - - logger.info(f"The {mode} package with instance ID '{instance_id}' was stopped successfully.") + log_msg = ( + f"An instance of the {mode} package was started successfully." + f" Its instance ID is '{instance_id}'" + ) + logger.info(log_msg) + + yield instance + + log_msg = f"Now stopping the {mode} package with instance ID '{instance_id}'..." + logger.info(log_msg) + + stop_clp_package(instance) + + log_msg = f"The {mode} package with instance ID '{instance_id}' was stopped successfully." + logger.info(log_msg) diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py new file mode 100644 index 0000000000..8f08e460fa --- /dev/null +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -0,0 +1,95 @@ +"""Define test package config file fixtures.""" + +import logging +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +import pytest + +from tests.utils.config import ( + PackageConfig, + PackageInstanceConfigFile, +) +from tests.utils.package_utils import ( + get_dict_from_mode, + write_temp_config_file, +) + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def clp_text_config( + package_config: PackageConfig, +) -> Iterator[PackageInstanceConfigFile]: + """Fixture that creates and maintains a config file for clp-text.""" + mode = "clp-text" + + log_msg = f"Creating a temporary config file for the {mode} package..." + logger.info(log_msg) + + run_config = PackageInstanceConfigFile( + package_config=package_config, + mode=mode, + ) + + # Create a temporary config file for the package run. + mode_kv_dict: dict[str, Any] = get_dict_from_mode(mode) + temp_config_file_path: Path = write_temp_config_file( + mode_kv_dict=mode_kv_dict, + temp_config_dir=package_config.temp_config_dir, + merge_original=True, + original_config_file_path=run_config.original_config_file_path, + ) + object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) + + log_msg = f"The temporary config file has been written for the {mode} package." + logger.info(log_msg) + + yield run_config + + # Delete the temporary config file. + logger.info("Deleting the temporary config file...") + + temp_config_file_path.unlink() + + logger.info("The temporary config file has been deleted.") + + +@pytest.fixture +def clp_json_config( + package_config: PackageConfig, +) -> Iterator[PackageInstanceConfigFile]: + """Fixture that creates and maintains a config file for clp-json.""" + mode = "clp-json" + + log_msg = f"Creating a temporary config file for the {mode} package..." + logger.info(log_msg) + + run_config = PackageInstanceConfigFile( + package_config=package_config, + mode=mode, + ) + + # Create a temporary config file for the package run. + mode_kv_dict: dict[str, Any] = get_dict_from_mode(mode) + temp_config_file_path: Path = write_temp_config_file( + mode_kv_dict=mode_kv_dict, + temp_config_dir=package_config.temp_config_dir, + merge_original=True, + original_config_file_path=run_config.original_config_file_path, + ) + object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) + + log_msg = f"The temporary config file has been written for the {mode} package." + logger.info(log_msg) + + yield run_config + + # Delete the temporary config file. + logger.info("Deleting the temporary config file...") + + temp_config_file_path.unlink() + + logger.info("The temporary config file has been deleted.") diff --git a/integration-tests/tests/test_package_start.py b/integration-tests/tests/test_package_start.py index 2014291d94..41dc7e7de1 100644 --- a/integration-tests/tests/test_package_start.py +++ b/integration-tests/tests/test_package_start.py @@ -2,21 +2,25 @@ import logging import shutil -import subprocess import pytest from tests.utils.config import ( - PackageRun, + PackageInstance, +) +from tests.utils.docker_utils import ( + inspect_container_state, + list_prefixed_containers, +) +from tests.utils.package_utils import ( + CLP_COMPONENT_BASENAMES, ) - -pytestmark = pytest.mark.package - package_configurations = pytest.mark.parametrize( "test_package_fixture", [ - "clp_package", + "clp_text_package", + "clp_json_package", ], ) @@ -37,63 +41,43 @@ def test_clp_package( :param request: :param test_package_fixture: """ - package_run: PackageRun = request.getfixturevalue(test_package_fixture) - - assert _is_package_running(package_run) - - -def _is_package_running(package_run: PackageRun) -> bool: - """Checks that the package specified in package_run is running correctly.""" - mode = package_run.mode - instance_id = package_run.clp_instance_id - - component_basenames = [ - "clp-database", - "clp-queue", - "clp-redis", - "clp-results_cache", - "clp-compression_scheduler", - "clp-query_scheduler", - "clp-compression_worker", - "clp-query_worker", - "clp-reducer", - "clp-webui", - "clp-garbage_collector", - ] + assert _is_package_running(request.getfixturevalue(test_package_fixture)) + + +def _is_package_running(package_instance: PackageInstance) -> bool: + """Checks that the package specified in package_instance is running correctly.""" + mode = package_instance.package_instance_config_file.mode + instance_id = package_instance.clp_instance_id logger.info( - "Checking if all components of %s package with instance ID '%s' are running properly.", + "Checking if all components of %s package with instance ID '%s' are running properly...", mode, instance_id, ) docker_bin = shutil.which("docker") if docker_bin is None: - error_msg = "docker not found in PATH" - raise RuntimeError(error_msg) - - for component in component_basenames: - name = f"{component}-{instance_id}" - - proc = subprocess.run( - [docker_bin, "inspect", "-f", "{{.State.Running}}", name], - capture_output=True, - text=True, - check=False, - ) - - if proc.returncode != 0: - err = (proc.stderr or proc.stdout or "").strip() - if "No such object" in err: - logger.error("Component container not found: %s", name) - return False - error_msg = f"Error inspecting container {name}: {err}" - raise RuntimeError(error_msg) - - status = (proc.stdout or "").strip().lower() - if status != "true": - logger.error("Component container not running: %s (status=%s)", name, status) - return False + err_msg = "docker not found in PATH" + raise RuntimeError(err_msg) + + for component in CLP_COMPONENT_BASENAMES: + # The container name may have any numeric suffix, and there may be multiple of them. + prefix = f"clp-package-{instance_id}-{component}-" + + candidates = list_prefixed_containers(docker_bin, prefix) + if not candidates: + pytest.fail(f"No component container was found with the prefix '{prefix}'") + + # Inspect each matching container; require that each one is running. + not_running = [] + for name in candidates: + desired_state = "running" + if not inspect_container_state(docker_bin, name, desired_state): + not_running.append(name) + + if not_running: + details = ", ".join(not_running) + pytest.fail(f"Component containers not running: {details}") logger.info( "All components of the %s package with instance ID '%s' are running properly.", diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index 044c14b63d..8c5a5c1b41 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -3,10 +3,9 @@ from __future__ import annotations import re -import socket from dataclasses import dataclass, field, InitVar from pathlib import Path -from typing import Any, Literal +from typing import Any import yaml @@ -55,17 +54,19 @@ def clp_s_binary_path(self) -> Path: @dataclass(frozen=True) class PackageConfig: - """The configuration for the clp package being tested.""" + """Metadata for the clp package test on this system.""" - #: + #: The directory the package is located in. clp_package_dir: Path - clp_config_file_path: Path = field(init=False, repr=True) + #: Root directory for package tests output. + test_root_dir: Path - #: Hostname of the machine running the test. - hostname: str = field(init=False, repr=True) + temp_config_dir_init: InitVar[Path | None] = None + #: Directory to store any cached package config files. + temp_config_dir: Path = field(init=False, repr=True) - def __post_init__(self) -> None: + def __post_init__(self, temp_config_dir_init: Path | None) -> None: """Validates the values specified at init, and initialises attributes.""" # Validate that the CLP package directory exists and contains all required directories. clp_package_dir = self.clp_package_dir @@ -81,112 +82,153 @@ def __post_init__(self) -> None: ) raise ValueError(err_msg) - # Set clp_config_file_path and validate it. - object.__setattr__( - self, "clp_config_file_path", self.clp_package_dir / "etc" / "clp-config.yml" - ) - validate_file_exists(self.clp_config_file_path) + # Initialize and create required cache directory for package tests. + if temp_config_dir_init is not None: + object.__setattr__(self, "temp_config_dir", temp_config_dir_init) + else: + object.__setattr__(self, "temp_config_dir", self.test_root_dir / "config-cache") - # Set hostname. - object.__setattr__(self, "hostname", socket.gethostname()) + self.test_root_dir.mkdir(parents=True, exist_ok=True) + self.temp_config_dir.mkdir(parents=True, exist_ok=True) + + @property + def start_script_path(self) -> Path: + """:return: The absolute path to the package start script.""" + return self.clp_package_dir / "sbin" / "start-clp.sh" + + @property + def stop_script_path(self) -> Path: + """:return: The absolute path to the package stop script.""" + return self.clp_package_dir / "sbin" / "stop-clp.sh" @dataclass(frozen=True) -class PackageRun: - """Metadata for the running CLP package.""" +class PackageInstanceConfigFile: + """Metadata for the clp-config.yml file used to configure a clp package instance.""" - #: + #: The PackageConfig object corresponding to this package run. package_config: PackageConfig + #: The mode name of this config file's configuration. + mode: str + + #: The location of the configfile used during this package run. + temp_config_file_path: Path = field(init=False, repr=True) + + #: The path to the original pre-test clp-config.yml file. + original_config_file_path: Path = field(init=False, repr=True) + + def __post_init__(self) -> None: + """Validates the values specified at init, and initialises attributes.""" + # Set original_config_file_path and validate it. + object.__setattr__( + self, + "original_config_file_path", + self.package_config.clp_package_dir / "etc" / "clp-config.yml", + ) + validate_file_exists(self.original_config_file_path) + + +@dataclass(frozen=True) +class PackageInstance: + """Metadata for a run of the clp package.""" + #: - mode: Literal["clp-text", "clp-json"] + package_instance_config_file: PackageInstanceConfigFile #: clp_log_dir: Path = field(init=False, repr=True) - clp_run_config_file_path: Path = field(init=False, repr=True) + #: The path to the .clp-config.yml file constructed by the package during spin-up. + dot_config_file_path: Path = field(init=False, repr=True) #: clp_instance_id: str = field(init=False, repr=True) def __post_init__(self) -> None: """Validates the values specified at init, and initialises attributes.""" - # Set clp_log_dir after validating it. - clp_log_dir = self.package_config.clp_package_dir / "var" / "log" + # Set clp_log_dir and validate that it exists. + clp_log_dir = ( + self.package_instance_config_file.package_config.clp_package_dir / "var" / "log" + ) validate_dir_exists(clp_log_dir) object.__setattr__(self, "clp_log_dir", clp_log_dir) - # Set clp_run_config_file_path after validating it. - clp_run_config_file_path = ( - self.clp_log_dir / self.package_config.hostname / ".clp-config.yml" - ) - validate_file_exists(clp_run_config_file_path) - object.__setattr__(self, "clp_run_config_file_path", clp_run_config_file_path) + # Set dot_config_file_path after validating it. + dot_config_file_path = self.clp_log_dir / ".clp-config.yml" + validate_file_exists(dot_config_file_path) + object.__setattr__(self, "dot_config_file_path", dot_config_file_path) # Set clp_instance_id. - clp_instance_id_file_path = self.clp_log_dir / self.package_config.hostname / "instance-id" + clp_instance_id_file_path = self.clp_log_dir / "instance-id" validate_file_exists(clp_instance_id_file_path) clp_instance_id = self._get_clp_instance_id(clp_instance_id_file_path) object.__setattr__(self, "clp_instance_id", clp_instance_id) - # Validate mode. - self._assert_mode_matches_run_config() - - def _assert_mode_matches_run_config(self) -> None: - """Validates that self.mode matches the package values in the run config file.""" - config_dict = self._load_run_config(self.clp_run_config_file_path) - config_mode = self._extract_mode_from_config(config_dict, self.clp_run_config_file_path) - - if config_mode != self.mode: - error_msg = ( - f"Mode mismatch: the mode specified to the PackageRun object was {self.mode}," - f" but the package is running in {config_mode} mode." + # Sanity check: validate that the package is running in the correct mode. + running_mode = self._get_running_mode() + intended_mode = self.package_instance_config_file.mode + if running_mode != intended_mode: + err_msg = ( + f"Mode mismatch: the package is running in {running_mode}," + f" but it should be running in {intended_mode}." ) - raise ValueError(error_msg) + raise ValueError(err_msg) + + def _get_running_mode(self) -> str: + """Gets the current running mode of the clp package.""" + config_dict = self._load_dot_config(self.dot_config_file_path) + return self._extract_mode_from_dot_config(config_dict, self.dot_config_file_path) @staticmethod - def _load_run_config(path: Path) -> dict[str, Any]: + def _load_dot_config(path: Path) -> dict[str, Any]: """Load the run config file into a dictionary.""" try: with path.open("r", encoding="utf-8") as file: config_dict = yaml.safe_load(file) except yaml.YAMLError as err: - raise ValueError(f"Invalid YAML in run config {path}: {err}") from err + err_msg = f"Invalid YAML in run config {path}: {err}" + raise ValueError(err_msg) from err except OSError as err: - raise ValueError(f"Cannot read run config {path}: {err}") from err + err_msg = f"Cannot read run config {path}: {err}" + raise ValueError(err_msg) from err if not isinstance(config_dict, dict): - raise TypeError(f"Run config {path} must be a mapping at the top level") + err_msg = f"Run config {path} must be a mapping at the top level" + raise TypeError(err_msg) return config_dict @staticmethod - def _extract_mode_from_config( + def _extract_mode_from_dot_config( config_dict: dict[str, Any], path: Path, - ) -> Literal["clp-text", "clp-json"]: - """Determine the package mode from the contents of the run-config dictionary.""" + ) -> str: + """Determine the package mode from the contents of `config_dict`.""" package = config_dict.get("package") if not isinstance(package, dict): - raise TypeError(f"Run config {path} is missing the 'package' mapping.") + err_msg = f"Running config {path} is missing the 'package' mapping." + raise TypeError(err_msg) query_engine = package.get("query_engine") storage_engine = package.get("storage_engine") if query_engine is None or storage_engine is None: - raise ValueError( - f"Run config {path} must specify both 'package.query_engine' and" + err_msg = ( + f"Running config {path} must specify both 'package.query_engine' and" " 'package.storage_engine'." ) + raise ValueError(err_msg) if query_engine == "clp" and storage_engine == "clp": return "clp-text" if query_engine == "clp-s" and storage_engine == "clp-s": return "clp-json" - raise ValueError( + err_msg = ( f"Run config {path} specifies running conditions for which integration testing is not" f"supported: query_engine={query_engine}, storage_engine={storage_engine}." ) + raise ValueError(err_msg) @staticmethod def _get_clp_instance_id(clp_instance_id_file_path: Path) -> str: @@ -194,15 +236,15 @@ def _get_clp_instance_id(clp_instance_id_file_path: Path) -> str: try: contents = clp_instance_id_file_path.read_text(encoding="utf-8").strip() except OSError as err: - raise ValueError( - f"Cannot read instance-id file {clp_instance_id_file_path}: {err}" - ) from err + err_msg = f"Cannot read instance-id file {clp_instance_id_file_path}: {err}" + raise ValueError(err_msg) from err if not re.fullmatch(r"[0-9a-fA-F]{4}", contents): - raise ValueError( + err_msg = ( f"Invalid instance ID in {clp_instance_id_file_path}: expected a 4-character" f" hexadecimal string, but read {contents}." ) + raise ValueError(err_msg) return contents diff --git a/integration-tests/tests/utils/docker_utils.py b/integration-tests/tests/utils/docker_utils.py new file mode 100644 index 0000000000..a6826d8ed3 --- /dev/null +++ b/integration-tests/tests/utils/docker_utils.py @@ -0,0 +1,71 @@ +"""Provide utility functions related to the use of Docker during integration tests.""" + +import re +import subprocess + +import pytest + +DOCKER_STATUS_FIELD_VALS = [ + "created", + "restarting", + "running", + "removing", + "paused", + "exited", + "dead", +] + + +def list_prefixed_containers(docker_bin: str, prefix: str) -> list[str]: + """Returns a list of docker containers whose names begin with `prefix`.""" + ps_proc = subprocess.run( + [ + docker_bin, + "ps", + "-a", + "--format", + "{{.Names}}", + "--filter", + f"name={prefix}", + ], + capture_output=True, + text=True, + check=False, + ) + + if ps_proc.returncode != 0: + err_out = (ps_proc.stderr or ps_proc.stdout or "").strip() + err_msg = f"Error listing containers for prefix {prefix}: {err_out}" + raise RuntimeError(err_msg) + + candidates: list[str] = [] + for line in (ps_proc.stdout or "").splitlines(): + name_candidate = line.strip() + if re.fullmatch(re.escape(prefix) + r"\d+", name_candidate): + candidates.append(name_candidate) + + return candidates + + +def inspect_container_state(docker_bin: str, name: str, desired_state: str) -> bool: + """Return True if the container status equals `desired_state`, else False. Raises on errors.""" + if desired_state not in DOCKER_STATUS_FIELD_VALS: + err_msg = f"Unsupported desired_state: {desired_state}" + raise ValueError(err_msg) + + inspect_proc = subprocess.run( + [docker_bin, "inspect", "--format", "{{.State.Status}}", name], + capture_output=True, + text=True, + check=False, + ) + + if inspect_proc.returncode != 0: + err_out = (inspect_proc.stderr or inspect_proc.stdout or "").strip() + if "No such object" in err_out: + pytest.fail(f"Component container not found: {name}") + err_msg = f"Error inspecting container {name}: {err_out}" + raise RuntimeError(err_msg) + + status = (inspect_proc.stdout or "").strip().lower() + return status == desired_state diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py new file mode 100644 index 0000000000..426b4cb9b3 --- /dev/null +++ b/integration-tests/tests/utils/package_utils.py @@ -0,0 +1,200 @@ +"""Provide utility functions related to the clp-package used across `integration-tests`.""" + +import shutil +import subprocess +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +import yaml + +from tests.utils.config import ( + PackageInstance, + PackageInstanceConfigFile, +) + +CLP_TEXT_KV_DICT = {"package": {"storage_engine": "clp", "query_engine": "clp"}} +CLP_JSON_KV_DICT = {"package": {"storage_engine": "clp-s", "query_engine": "clp-s"}} + +CLP_COMPONENT_BASENAMES = [ + "database", + "queue", + "redis", + "results-cache", + "compression-scheduler", + "query-scheduler", + "compression-worker", + "query-worker", + "reducer", + "webui", + "garbage-collector", +] + + +def start_clp_package(run_config: PackageInstanceConfigFile) -> None: + """Starts an instance of the clp package.""" + start_script_path = run_config.package_config.start_script_path + try: + # fmt: off + start_cmd = [ + str(start_script_path), + "--config", + str(run_config.temp_config_file_path) + ] + # fmt: on + subprocess.run(start_cmd, check=True) + except Exception as e: + err_msg = f"Failed to start an instance of the {run_config.mode} package." + raise RuntimeError(err_msg) from e + + +def stop_clp_package( + instance: PackageInstance, +) -> None: + """Stops an instance of the clp package.""" + run_config = instance.package_instance_config_file + stop_script_path = run_config.package_config.stop_script_path + try: + # fmt: off + stop_cmd = [ + stop_script_path + ] + # fmt: on + subprocess.run(stop_cmd, check=True) + except Exception as e: + err_msg = f"Failed to start an instance of the {run_config.mode} package." + raise RuntimeError(err_msg) from e + + +def write_temp_config_file( + mode_kv_dict: dict[str, Any], + temp_config_dir: Path, + merge_original: bool, + original_config_file_path: Path, +) -> Path: + """ + Writes the set of key-value pairs in `mode_kv_dict` to a new yaml config file located in + `temp_config_dir`. + + If `merge_original` is `True`, merges the kv-pairs in `original_config_file_path` together with + `mode_kv_dict` and places them all in the new config file. If a key is present in both + `mode_kv_dict` and the file at `original_config_file_path`, the value given in the + `original_config_file_path` file is overridden by the value given in `mode_kv_dict`. + + Returns the path to the temp config file. + """ + if merge_original: + # Incorporate the current contents of the original config file. + if not original_config_file_path.is_file(): + err_msg = f"Source config file not found: {original_config_file_path}" + raise FileNotFoundError(err_msg) + + with original_config_file_path.open("r", encoding="utf-8") as f: + origin_data = yaml.safe_load(f) or {} + + if not isinstance(origin_data, dict): + err_msg = "YAML root must be a mapping." + raise TypeError(err_msg) + + origin_kv_dict: dict[str, Any] = origin_data + union_kv_dict = make_union_kv_dict(origin_kv_dict, mode_kv_dict) + else: + # Write only the kv-pairs provided in mode_kv_dict. + if not isinstance(mode_kv_dict, dict): + err_msg = "`mode_kv_dict` must be a mapping." + raise TypeError(err_msg) + + union_kv_dict = dict(mode_kv_dict) + + # Make the temp config directory if it doesn't already exist. + temp_config_dir.mkdir(parents=True, exist_ok=True) + + # Use the same filename as the original. + temp_config_filename = original_config_file_path.name + temp_config_file_path = temp_config_dir / temp_config_filename + + # Write the merged content to the temp config file. + tmp_path = temp_config_file_path.with_suffix(temp_config_file_path.suffix + ".tmp") + with tmp_path.open("w", encoding="utf-8") as f: + yaml.safe_dump(union_kv_dict, f, sort_keys=False) + tmp_path.replace(temp_config_file_path) + + return temp_config_file_path + + +def make_union_kv_dict( + base_dict: Mapping[str, Any], + override_dict: Mapping[str, Any], +) -> dict[str, Any]: + """ + Finds the union of the two argument Dicts. If a key exists in both, the value from + `override_dict` takes precedence. Nested keys will remain untouched even if its parent is an + overridden key. + + :param base_dict: + :param override_dict: + :return: The merged dictionary. + """ + return deep_merge_with_override(base_dict, override_dict) + + +def deep_merge_with_override( + base_dict: Mapping[str, Any], + override_dict: Mapping[str, Any], +) -> dict[str, Any]: + """ + Merges the two dicts. + The values in `override_dict` take precedence in the case of identical keys. + """ + merged_dict: dict[str, Any] = dict(base_dict) + for key, override_value in override_dict.items(): + if ( + key in merged_dict + and isinstance(merged_dict[key], Mapping) + and isinstance(override_value, Mapping) + ): + merged_dict[key] = deep_merge_with_override(merged_dict[key], override_value) + else: + merged_dict[key] = override_value + return merged_dict + + +def restore_config_file( + config_file_path: Path, + temp_cache_file_path: Path, +) -> bool: + """ + Replaces the clp-config file at `config_file_path` with `temp_cache_file_path`. Deletes + `temp_cache_file_path` after it is successfully restored. + + :param config_file_path: + :param temp_cache_file_path: + :return: `True` if the config file was restored successfully. + """ + if not temp_cache_file_path.is_file(): + err_msg = f"Cached copy not found: {temp_cache_file_path}" + raise FileNotFoundError(err_msg) + + if not config_file_path.is_file(): + err_msg = f"Modified config file not found: {config_file_path}" + raise FileNotFoundError(err_msg) + + # Replace the config file with the cached copy. + tmp_path = config_file_path.with_suffix(config_file_path.suffix + ".tmp") + shutil.copyfile(temp_cache_file_path, tmp_path) + tmp_path.replace(config_file_path) + + # Remove the cached copy. + temp_cache_file_path.unlink() + + return True + + +def get_dict_from_mode(mode: str) -> dict[str, Any]: + """Returns the dict that corresponds to the mode.""" + if mode == "clp-text": + return CLP_TEXT_KV_DICT + if mode == "clp-json": + return CLP_JSON_KV_DICT + err_msg = f"Unsupported mode: {mode}" + raise ValueError(err_msg) From c5375ebcd41fcda537e265b16b054362ae93717c Mon Sep 17 00:00:00 2001 From: Quinn Date: Sat, 1 Nov 2025 19:39:44 +0000 Subject: [PATCH 03/22] Include and employ clp_py_utils.clp_config; refrain from using the user-level clp-config.yml --- integration-tests/pyproject.toml | 1 + integration-tests/tests/conftest.py | 41 +++- .../fixtures/integration_test_packages.py | 14 +- .../tests/fixtures/package_config_fixtures.py | 16 +- integration-tests/tests/test_package_start.py | 72 +++++- integration-tests/tests/utils/config.py | 117 ++------- .../tests/utils/package_utils.py | 229 ++++++++---------- 7 files changed, 246 insertions(+), 244 deletions(-) diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index 7158d936c7..5c100c2fbb 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -27,6 +27,7 @@ dev = [ [tool.mypy] strict = true +ignore_missing_imports = true # Additional output pretty = true diff --git a/integration-tests/tests/conftest.py b/integration-tests/tests/conftest.py index eeff7e3cee..afcc1a2ac4 100644 --- a/integration-tests/tests/conftest.py +++ b/integration-tests/tests/conftest.py @@ -1,4 +1,11 @@ -"""Make the fixtures defined in `tests/fixtures/` globally available without imports.""" +"""Pytest config: register fixtures, validate env, adjust paths, and other test setup.""" + +import sys +from pathlib import Path + +from _pytest.config import Config + +from tests.utils.utils import get_env_var pytest_plugins = [ "tests.fixtures.integration_test_config", @@ -6,3 +13,35 @@ "tests.fixtures.integration_test_packages", "tests.fixtures.package_config_fixtures", ] + + +def pytest_configure(config: Config) -> None: # noqa: ARG001 + """ + Returns the /lib/python3/site-packages path. + Raises a clear error if the directory is not usable. + """ + clp_package_dir = Path(get_env_var("CLP_PACKAGE_DIR")).expanduser().resolve() + if not clp_package_dir: + err_msg = ( + "CLP_PACKAGE_DIR is not set. Point it to the root of the CLP package that contains" + " 'lib/python3/site-packages'." + ) + raise RuntimeError(err_msg) + + site_packages = Path(clp_package_dir) / "lib" / "python3" / "site-packages" + if not site_packages.is_dir(): + err_msg = ( + f"Could not find the 'site-packages' directory at: '{site_packages}'. Verify" + " CLP_PACKAGE_DIR points at the CLP package root." + ) + raise RuntimeError(err_msg) + + # Sanity check: ensure clp_py_utils exists. + if not (site_packages / "clp_py_utils").exists(): + err_msg = ( + f"'clp_py_utils' not found under: '{site_packages}'. These integration tests expect the" + " package to contain clp_py_utils." + ) + raise RuntimeError(err_msg) + + sys.path.insert(0, str(site_packages)) diff --git a/integration-tests/tests/fixtures/integration_test_packages.py b/integration-tests/tests/fixtures/integration_test_packages.py index 942ff80d78..86a3783e1b 100644 --- a/integration-tests/tests/fixtures/integration_test_packages.py +++ b/integration-tests/tests/fixtures/integration_test_packages.py @@ -7,7 +7,7 @@ from tests.utils.config import ( PackageInstance, - PackageInstanceConfigFile, + PackageInstanceConfig, ) from tests.utils.package_utils import ( start_clp_package, @@ -19,16 +19,16 @@ @pytest.fixture def clp_text_package( - clp_text_config: PackageInstanceConfigFile, + clp_text_config: PackageInstanceConfig, ) -> Iterator[PackageInstance]: """Fixture that launches a clp-text instance, and gracefully stops it after its scope ends.""" log_msg = f"Starting up the {clp_text_config.mode} package..." logger.info(log_msg) start_clp_package(clp_text_config) - instance = PackageInstance(package_instance_config_file=clp_text_config) + instance = PackageInstance(package_instance_config=clp_text_config) - mode = instance.package_instance_config_file.mode + mode = instance.package_instance_config.mode instance_id = instance.clp_instance_id log_msg = ( f"An instance of the {mode} package was started successfully." @@ -49,16 +49,16 @@ def clp_text_package( @pytest.fixture def clp_json_package( - clp_json_config: PackageInstanceConfigFile, + clp_json_config: PackageInstanceConfig, ) -> Iterator[PackageInstance]: """Fixture that launches a clp-json instance, and gracefully stops it after its scope ends.""" log_msg = f"Starting up the {clp_json_config.mode} package..." logger.info(log_msg) start_clp_package(clp_json_config) - instance = PackageInstance(package_instance_config_file=clp_json_config) + instance = PackageInstance(package_instance_config=clp_json_config) - mode = instance.package_instance_config_file.mode + mode = instance.package_instance_config.mode instance_id = instance.clp_instance_id log_msg = ( f"An instance of the {mode} package was started successfully." diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py index 8f08e460fa..ad76069854 100644 --- a/integration-tests/tests/fixtures/package_config_fixtures.py +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -9,7 +9,7 @@ from tests.utils.config import ( PackageConfig, - PackageInstanceConfigFile, + PackageInstanceConfig, ) from tests.utils.package_utils import ( get_dict_from_mode, @@ -22,14 +22,14 @@ @pytest.fixture def clp_text_config( package_config: PackageConfig, -) -> Iterator[PackageInstanceConfigFile]: +) -> Iterator[PackageInstanceConfig]: """Fixture that creates and maintains a config file for clp-text.""" mode = "clp-text" log_msg = f"Creating a temporary config file for the {mode} package..." logger.info(log_msg) - run_config = PackageInstanceConfigFile( + run_config = PackageInstanceConfig( package_config=package_config, mode=mode, ) @@ -39,8 +39,7 @@ def clp_text_config( temp_config_file_path: Path = write_temp_config_file( mode_kv_dict=mode_kv_dict, temp_config_dir=package_config.temp_config_dir, - merge_original=True, - original_config_file_path=run_config.original_config_file_path, + mode=mode, ) object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) @@ -60,14 +59,14 @@ def clp_text_config( @pytest.fixture def clp_json_config( package_config: PackageConfig, -) -> Iterator[PackageInstanceConfigFile]: +) -> Iterator[PackageInstanceConfig]: """Fixture that creates and maintains a config file for clp-json.""" mode = "clp-json" log_msg = f"Creating a temporary config file for the {mode} package..." logger.info(log_msg) - run_config = PackageInstanceConfigFile( + run_config = PackageInstanceConfig( package_config=package_config, mode=mode, ) @@ -77,8 +76,7 @@ def clp_json_config( temp_config_file_path: Path = write_temp_config_file( mode_kv_dict=mode_kv_dict, temp_config_dir=package_config.temp_config_dir, - merge_original=True, - original_config_file_path=run_config.original_config_file_path, + mode=mode, ) object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) diff --git a/integration-tests/tests/test_package_start.py b/integration-tests/tests/test_package_start.py index 41dc7e7de1..0490388bf0 100644 --- a/integration-tests/tests/test_package_start.py +++ b/integration-tests/tests/test_package_start.py @@ -2,8 +2,11 @@ import logging import shutil +from pathlib import Path +from typing import Any import pytest +import yaml from tests.utils.config import ( PackageInstance, @@ -14,6 +17,7 @@ ) from tests.utils.package_utils import ( CLP_COMPONENT_BASENAMES, + get_mode_from_dict, ) package_configurations = pytest.mark.parametrize( @@ -35,18 +39,19 @@ def test_clp_package( ) -> None: """ Validate that all of the components of the clp package start up successfully. The package is - started up in whatever configuration is currently described in clp-config.yml; default is - clp-text. + started up in whatever configuration is currently described in clp-config.yml. :param request: :param test_package_fixture: """ - assert _is_package_running(request.getfixturevalue(test_package_fixture)) + package_instance = request.getfixturevalue(test_package_fixture) + assert _is_package_running(package_instance) + assert _is_running_mode_correct(package_instance) def _is_package_running(package_instance: PackageInstance) -> bool: - """Checks that the package specified in package_instance is running correctly.""" - mode = package_instance.package_instance_config_file.mode + """Ensures the components of `package_instance` are running correctly.""" + mode = package_instance.package_instance_config.mode instance_id = package_instance.clp_instance_id logger.info( @@ -85,3 +90,60 @@ def _is_package_running(package_instance: PackageInstance) -> bool: instance_id, ) return True + + +def _is_running_mode_correct(package_instance: PackageInstance) -> bool: + """ + Ensures that the mode intended for the package specified in package_instance matches the mode of + operation described by the package's shared config file. + """ + mode = package_instance.package_instance_config.mode + instance_id = package_instance.clp_instance_id + + logger.info( + "Checking that the %s package with instance ID '%s' is running in the correct mode...", + mode, + instance_id, + ) + + running_mode = _get_running_mode(package_instance) + intended_mode = package_instance.package_instance_config.mode + if running_mode != intended_mode: + err_msg = ( + f"Mode mismatch: the package is running in {running_mode}," + f" but it should be running in {intended_mode}." + ) + raise ValueError(err_msg) + + logger.info( + "The %s package with instance ID '%s' is running in the correct mode.", + mode, + instance_id, + ) + + return True + + +def _get_running_mode(package_instance: PackageInstance) -> str: + """Gets the current running mode of the clp package.""" + shared_config_dict = _load_shared_config(package_instance.shared_config_file_path) + return get_mode_from_dict(shared_config_dict) + + +def _load_shared_config(path: Path) -> dict[str, Any]: + """Load the content of the shared config file into a dictionary.""" + try: + with path.open("r", encoding="utf-8") as file: + shared_config_dict = yaml.safe_load(file) + except yaml.YAMLError as err: + err_msg = f"Invalid YAML in shared config {path}: {err}" + raise ValueError(err_msg) from err + except OSError as err: + err_msg = f"Cannot read shared config {path}: {err}" + raise ValueError(err_msg) from err + + if not isinstance(shared_config_dict, dict): + err_msg = f"Shared config {path} must be a mapping at the top level." + raise TypeError(err_msg) + + return shared_config_dict diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index 120a3c1762..79191c16ed 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -1,13 +1,15 @@ """Define all python classes used in `integration-tests`.""" -from __future__ import annotations +# from __future__ import annotations import re from dataclasses import dataclass, field, InitVar from pathlib import Path -from typing import Any -import yaml +from clp_py_utils.clp_config import ( + QueryEngine, + StorageEngine, +) from tests.utils.utils import ( unlink, @@ -111,7 +113,16 @@ def stop_script_path(self) -> Path: @dataclass(frozen=True) -class PackageInstanceConfigFile: +class PackageModeConfig: + """Defines details related to a package instance's mode of operation.""" + + name: str + storage_engine: StorageEngine + query_engine: QueryEngine + + +@dataclass(frozen=True) +class PackageInstanceConfig: """Metadata for the clp-config.yml file used to configure a clp package instance.""" #: The PackageConfig object corresponding to this package run. @@ -123,120 +134,40 @@ class PackageInstanceConfigFile: #: The location of the configfile used during this package run. temp_config_file_path: Path = field(init=False, repr=True) - #: The path to the original pre-test clp-config.yml file. - original_config_file_path: Path = field(init=False, repr=True) - - def __post_init__(self) -> None: - """Validates the values specified at init, and initialises attributes.""" - # Set original_config_file_path and validate it. - object.__setattr__( - self, - "original_config_file_path", - self.package_config.clp_package_dir / "etc" / "clp-config.yml", - ) - validate_file_exists(self.original_config_file_path) - @dataclass(frozen=True) class PackageInstance: """Metadata for a run of the clp package.""" #: - package_instance_config_file: PackageInstanceConfigFile + package_instance_config: PackageInstanceConfig #: clp_log_dir: Path = field(init=False, repr=True) - #: The path to the .clp-config.yml file constructed by the package during spin-up. - dot_config_file_path: Path = field(init=False, repr=True) - #: clp_instance_id: str = field(init=False, repr=True) + #: The path to the .clp-config.yml file constructed by the package during spin-up. + shared_config_file_path: Path = field(init=False, repr=True) + def __post_init__(self) -> None: """Validates the values specified at init, and initialises attributes.""" # Set clp_log_dir and validate that it exists. - clp_log_dir = ( - self.package_instance_config_file.package_config.clp_package_dir / "var" / "log" - ) + clp_log_dir = self.package_instance_config.package_config.clp_package_dir / "var" / "log" validate_dir_exists(clp_log_dir) object.__setattr__(self, "clp_log_dir", clp_log_dir) - # Set dot_config_file_path after validating it. - dot_config_file_path = self.clp_log_dir / ".clp-config.yml" - validate_file_exists(dot_config_file_path) - object.__setattr__(self, "dot_config_file_path", dot_config_file_path) - # Set clp_instance_id. clp_instance_id_file_path = self.clp_log_dir / "instance-id" validate_file_exists(clp_instance_id_file_path) clp_instance_id = self._get_clp_instance_id(clp_instance_id_file_path) object.__setattr__(self, "clp_instance_id", clp_instance_id) - # Sanity check: validate that the package is running in the correct mode. - running_mode = self._get_running_mode() - intended_mode = self.package_instance_config_file.mode - if running_mode != intended_mode: - err_msg = ( - f"Mode mismatch: the package is running in {running_mode}," - f" but it should be running in {intended_mode}." - ) - raise ValueError(err_msg) - - def _get_running_mode(self) -> str: - """Gets the current running mode of the clp package.""" - config_dict = self._load_dot_config(self.dot_config_file_path) - return self._extract_mode_from_dot_config(config_dict, self.dot_config_file_path) - - @staticmethod - def _load_dot_config(path: Path) -> dict[str, Any]: - """Load the run config file into a dictionary.""" - try: - with path.open("r", encoding="utf-8") as file: - config_dict = yaml.safe_load(file) - except yaml.YAMLError as err: - err_msg = f"Invalid YAML in run config {path}: {err}" - raise ValueError(err_msg) from err - except OSError as err: - err_msg = f"Cannot read run config {path}: {err}" - raise ValueError(err_msg) from err - - if not isinstance(config_dict, dict): - err_msg = f"Run config {path} must be a mapping at the top level" - raise TypeError(err_msg) - - return config_dict - - @staticmethod - def _extract_mode_from_dot_config( - config_dict: dict[str, Any], - path: Path, - ) -> str: - """Determine the package mode from the contents of `config_dict`.""" - package = config_dict.get("package") - if not isinstance(package, dict): - err_msg = f"Running config {path} is missing the 'package' mapping." - raise TypeError(err_msg) - - query_engine = package.get("query_engine") - storage_engine = package.get("storage_engine") - if query_engine is None or storage_engine is None: - err_msg = ( - f"Running config {path} must specify both 'package.query_engine' and" - " 'package.storage_engine'." - ) - raise ValueError(err_msg) - - if query_engine == "clp" and storage_engine == "clp": - return "clp-text" - if query_engine == "clp-s" and storage_engine == "clp-s": - return "clp-json" - - err_msg = ( - f"Run config {path} specifies running conditions for which integration testing is not" - f"supported: query_engine={query_engine}, storage_engine={storage_engine}." - ) - raise ValueError(err_msg) + # Set shared_config_file_path after validating it. + shared_config_file_path = self.clp_log_dir / ".clp-config.yml" + validate_file_exists(shared_config_file_path) + object.__setattr__(self, "shared_config_file_path", shared_config_file_path) @staticmethod def _get_clp_instance_id(clp_instance_id_file_path: Path) -> str: diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index 426b4cb9b3..ab0ba25e47 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -1,37 +1,67 @@ -"""Provide utility functions related to the clp-package used across `integration-tests`.""" +"""Provides utility functions related to the clp-package used across `integration-tests`.""" -import shutil import subprocess -from collections.abc import Mapping from pathlib import Path from typing import Any import yaml +from clp_py_utils.clp_config import ( + COMPRESSION_SCHEDULER_COMPONENT_NAME, + COMPRESSION_WORKER_COMPONENT_NAME, + DB_COMPONENT_NAME, + GARBAGE_COLLECTOR_COMPONENT_NAME, + Package, + QUERY_SCHEDULER_COMPONENT_NAME, + QUERY_WORKER_COMPONENT_NAME, + QueryEngine, + QUEUE_COMPONENT_NAME, + REDIS_COMPONENT_NAME, + REDUCER_COMPONENT_NAME, + RESULTS_CACHE_COMPONENT_NAME, + StorageEngine, + WEBUI_COMPONENT_NAME, +) from tests.utils.config import ( PackageInstance, - PackageInstanceConfigFile, + PackageInstanceConfig, + PackageModeConfig, ) -CLP_TEXT_KV_DICT = {"package": {"storage_engine": "clp", "query_engine": "clp"}} -CLP_JSON_KV_DICT = {"package": {"storage_engine": "clp-s", "query_engine": "clp-s"}} + +def _to_container_basename(name: str) -> str: + return name.replace("_", "-") + CLP_COMPONENT_BASENAMES = [ - "database", - "queue", - "redis", - "results-cache", - "compression-scheduler", - "query-scheduler", - "compression-worker", - "query-worker", - "reducer", - "webui", - "garbage-collector", + _to_container_basename(DB_COMPONENT_NAME), + _to_container_basename(QUEUE_COMPONENT_NAME), + _to_container_basename(REDIS_COMPONENT_NAME), + _to_container_basename(RESULTS_CACHE_COMPONENT_NAME), + _to_container_basename(COMPRESSION_SCHEDULER_COMPONENT_NAME), + _to_container_basename(QUERY_SCHEDULER_COMPONENT_NAME), + _to_container_basename(COMPRESSION_WORKER_COMPONENT_NAME), + _to_container_basename(QUERY_WORKER_COMPONENT_NAME), + _to_container_basename(REDUCER_COMPONENT_NAME), + _to_container_basename(WEBUI_COMPONENT_NAME), + _to_container_basename(GARBAGE_COLLECTOR_COMPONENT_NAME), ] - -def start_clp_package(run_config: PackageInstanceConfigFile) -> None: +CLP_MODES: dict[str, PackageModeConfig] = { + "clp-text": PackageModeConfig( + name="clp-text", + storage_engine=StorageEngine.CLP, + query_engine=QueryEngine.CLP, + ), + "clp-json": PackageModeConfig( + name="clp-json", + storage_engine=StorageEngine.CLP_S, + query_engine=QueryEngine.CLP_S, + ), +} + + +def start_clp_package(run_config: PackageInstanceConfig) -> None: """Starts an instance of the clp package.""" start_script_path = run_config.package_config.start_script_path try: @@ -52,7 +82,7 @@ def stop_clp_package( instance: PackageInstance, ) -> None: """Stops an instance of the clp package.""" - run_config = instance.package_instance_config_file + run_config = instance.package_instance_config stop_script_path = run_config.package_config.stop_script_path try: # fmt: off @@ -69,132 +99,73 @@ def stop_clp_package( def write_temp_config_file( mode_kv_dict: dict[str, Any], temp_config_dir: Path, - merge_original: bool, - original_config_file_path: Path, + mode: str, ) -> Path: """ - Writes the set of key-value pairs in `mode_kv_dict` to a new yaml config file located in - `temp_config_dir`. - - If `merge_original` is `True`, merges the kv-pairs in `original_config_file_path` together with - `mode_kv_dict` and places them all in the new config file. If a key is present in both - `mode_kv_dict` and the file at `original_config_file_path`, the value given in the - `original_config_file_path` file is overridden by the value given in `mode_kv_dict`. - - Returns the path to the temp config file. + Writes the set of key-value pairs in `mode_kv_dict` to a new yaml config file called + "clp-config-`{mode}`.yml" located in `temp_config_dir`. Returns the path to the temp config + file. """ - if merge_original: - # Incorporate the current contents of the original config file. - if not original_config_file_path.is_file(): - err_msg = f"Source config file not found: {original_config_file_path}" - raise FileNotFoundError(err_msg) - - with original_config_file_path.open("r", encoding="utf-8") as f: - origin_data = yaml.safe_load(f) or {} - - if not isinstance(origin_data, dict): - err_msg = "YAML root must be a mapping." - raise TypeError(err_msg) - - origin_kv_dict: dict[str, Any] = origin_data - union_kv_dict = make_union_kv_dict(origin_kv_dict, mode_kv_dict) - else: - # Write only the kv-pairs provided in mode_kv_dict. - if not isinstance(mode_kv_dict, dict): - err_msg = "`mode_kv_dict` must be a mapping." - raise TypeError(err_msg) - - union_kv_dict = dict(mode_kv_dict) - - # Make the temp config directory if it doesn't already exist. - temp_config_dir.mkdir(parents=True, exist_ok=True) + if not isinstance(mode_kv_dict, dict): + err_msg = "`mode_kv_dict` must be a mapping." + raise TypeError(err_msg) - # Use the same filename as the original. - temp_config_filename = original_config_file_path.name + temp_config_dir.mkdir(parents=True, exist_ok=True) + temp_config_filename = f"clp-config-{mode}.yml" temp_config_file_path = temp_config_dir / temp_config_filename - # Write the merged content to the temp config file. tmp_path = temp_config_file_path.with_suffix(temp_config_file_path.suffix + ".tmp") with tmp_path.open("w", encoding="utf-8") as f: - yaml.safe_dump(union_kv_dict, f, sort_keys=False) + yaml.safe_dump(mode_kv_dict, f, sort_keys=False) tmp_path.replace(temp_config_file_path) return temp_config_file_path -def make_union_kv_dict( - base_dict: Mapping[str, Any], - override_dict: Mapping[str, Any], -) -> dict[str, Any]: - """ - Finds the union of the two argument Dicts. If a key exists in both, the value from - `override_dict` takes precedence. Nested keys will remain untouched even if its parent is an - overridden key. - - :param base_dict: - :param override_dict: - :return: The merged dictionary. - """ - return deep_merge_with_override(base_dict, override_dict) - - -def deep_merge_with_override( - base_dict: Mapping[str, Any], - override_dict: Mapping[str, Any], -) -> dict[str, Any]: - """ - Merges the two dicts. - The values in `override_dict` take precedence in the case of identical keys. - """ - merged_dict: dict[str, Any] = dict(base_dict) - for key, override_value in override_dict.items(): - if ( - key in merged_dict - and isinstance(merged_dict[key], Mapping) - and isinstance(override_value, Mapping) - ): - merged_dict[key] = deep_merge_with_override(merged_dict[key], override_value) - else: - merged_dict[key] = override_value - return merged_dict - - -def restore_config_file( - config_file_path: Path, - temp_cache_file_path: Path, -) -> bool: +def build_dict_from_config(mode_config: PackageModeConfig) -> dict[str, Any]: """ - Replaces the clp-config file at `config_file_path` with `temp_cache_file_path`. Deletes - `temp_cache_file_path` after it is successfully restored. - - :param config_file_path: - :param temp_cache_file_path: - :return: `True` if the config file was restored successfully. + Return {"package": {...}} where the inner mapping is produced from clp_config.Package + with proper enum serialization. """ - if not temp_cache_file_path.is_file(): - err_msg = f"Cached copy not found: {temp_cache_file_path}" - raise FileNotFoundError(err_msg) - - if not config_file_path.is_file(): - err_msg = f"Modified config file not found: {config_file_path}" - raise FileNotFoundError(err_msg) - - # Replace the config file with the cached copy. - tmp_path = config_file_path.with_suffix(config_file_path.suffix + ".tmp") - shutil.copyfile(temp_cache_file_path, tmp_path) - tmp_path.replace(config_file_path) - - # Remove the cached copy. - temp_cache_file_path.unlink() - - return True + storage_engine = mode_config.storage_engine + query_engine = mode_config.query_engine + package_model = Package(storage_engine=storage_engine, query_engine=query_engine) + return {"package": package_model.model_dump()} def get_dict_from_mode(mode: str) -> dict[str, Any]: """Returns the dict that corresponds to the mode.""" - if mode == "clp-text": - return CLP_TEXT_KV_DICT - if mode == "clp-json": - return CLP_JSON_KV_DICT - err_msg = f"Unsupported mode: {mode}" + mode_config = CLP_MODES.get(mode) + if mode_config is None: + err_msg = f"Unsupported mode: {mode}" + raise ValueError(err_msg) + + return build_dict_from_config(mode_config) + + +def get_mode_from_dict(dictionary: dict[str, Any]) -> str: + """Returns the mode that corresponds to dict.""" + package_dict = dictionary.get("package") + if not isinstance(package_dict, dict): + err_msg = "`dictionary` does not carry any mapping for 'package'." + raise TypeError(err_msg) + + dict_query_engine = package_dict.get("query_engine") + dict_storage_engine = package_dict.get("storage_engine") + if dict_query_engine is None or dict_storage_engine is None: + err_msg = ( + "`dictionary` must specify both 'package.query_engine' and 'package.storage_engine'." + ) + raise ValueError(err_msg) + + for mode_name, mode_config in CLP_MODES.items(): + if str(mode_config.query_engine.value) == str(dict_query_engine) and str( + mode_config.storage_engine.value + ) == str(dict_storage_engine): + return mode_name + + err_msg = ( + "The set of kv-pairs described in `dictionary` does not correspond to any mode of operation" + " for which integration testing is supported." + ) raise ValueError(err_msg) From 2be837f471af8e67bff0a8332f0d4413f59a9543 Mon Sep 17 00:00:00 2001 From: Quinn Date: Sat, 1 Nov 2025 19:41:31 +0000 Subject: [PATCH 04/22] Lint. --- integration-tests/tests/utils/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index 79191c16ed..afb0496e66 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -1,7 +1,5 @@ """Define all python classes used in `integration-tests`.""" -# from __future__ import annotations - import re from dataclasses import dataclass, field, InitVar from pathlib import Path From bf14b5eb4b69f8e4be337fe70b79352b76a3db86 Mon Sep 17 00:00:00 2001 From: Quinn Date: Sun, 2 Nov 2025 20:28:19 +0000 Subject: [PATCH 05/22] Refactor; move all utility functions to utils files as appropriate. --- integration-tests/tests/conftest.py | 2 +- .../tests/fixtures/package_config_fixtures.py | 42 ++-- ...ckages.py => package_instance_fixtures.py} | 30 +-- integration-tests/tests/test_package_start.py | 111 ++-------- integration-tests/tests/utils/config.py | 4 +- integration-tests/tests/utils/docker_utils.py | 12 +- .../tests/utils/package_utils.py | 201 ++++++++++++------ 7 files changed, 204 insertions(+), 198 deletions(-) rename integration-tests/tests/fixtures/{integration_test_packages.py => package_instance_fixtures.py} (61%) diff --git a/integration-tests/tests/conftest.py b/integration-tests/tests/conftest.py index afcc1a2ac4..863bdbc7a2 100644 --- a/integration-tests/tests/conftest.py +++ b/integration-tests/tests/conftest.py @@ -10,7 +10,7 @@ pytest_plugins = [ "tests.fixtures.integration_test_config", "tests.fixtures.integration_test_logs", - "tests.fixtures.integration_test_packages", + "tests.fixtures.package_instance_fixtures", "tests.fixtures.package_config_fixtures", ] diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py index ad76069854..6b93759e73 100644 --- a/integration-tests/tests/fixtures/package_config_fixtures.py +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -1,4 +1,4 @@ -"""Define test package config file fixtures.""" +"""Fixtures that create and remove temporary config files for CLP packages.""" import logging from collections.abc import Iterator @@ -10,9 +10,11 @@ from tests.utils.config import ( PackageConfig, PackageInstanceConfig, + PackageModeConfig, ) from tests.utils.package_utils import ( - get_dict_from_mode, + CLP_MODE_CONFIGS, + get_dict_from_mode_name, write_temp_config_file, ) @@ -24,36 +26,36 @@ def clp_text_config( package_config: PackageConfig, ) -> Iterator[PackageInstanceConfig]: """Fixture that creates and maintains a config file for clp-text.""" - mode = "clp-text" - - log_msg = f"Creating a temporary config file for the {mode} package..." + mode_name = "clp-text" + log_msg = f"Creating a temporary config file for the {mode_name} package..." logger.info(log_msg) + mode_config: PackageModeConfig = CLP_MODE_CONFIGS[mode_name] run_config = PackageInstanceConfig( package_config=package_config, - mode=mode, + mode_config=mode_config, ) # Create a temporary config file for the package run. - mode_kv_dict: dict[str, Any] = get_dict_from_mode(mode) + mode_kv_dict: dict[str, Any] = get_dict_from_mode_name(mode_name) temp_config_file_path: Path = write_temp_config_file( mode_kv_dict=mode_kv_dict, temp_config_dir=package_config.temp_config_dir, - mode=mode, + mode_name=mode_name, ) object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) - log_msg = f"The temporary config file has been written for the {mode} package." + log_msg = f"The temporary config file has been written for the {mode_name} package." logger.info(log_msg) yield run_config # Delete the temporary config file. - logger.info("Deleting the temporary config file...") + logger.info("Removing the temporary config file...") temp_config_file_path.unlink() - logger.info("The temporary config file has been deleted.") + logger.info("The temporary config file has been removed.") @pytest.fixture @@ -61,33 +63,33 @@ def clp_json_config( package_config: PackageConfig, ) -> Iterator[PackageInstanceConfig]: """Fixture that creates and maintains a config file for clp-json.""" - mode = "clp-json" - - log_msg = f"Creating a temporary config file for the {mode} package..." + mode_name = "clp-json" + log_msg = f"Creating a temporary config file for the {mode_name} package..." logger.info(log_msg) + mode_config: PackageModeConfig = CLP_MODE_CONFIGS[mode_name] run_config = PackageInstanceConfig( package_config=package_config, - mode=mode, + mode_config=mode_config, ) # Create a temporary config file for the package run. - mode_kv_dict: dict[str, Any] = get_dict_from_mode(mode) + mode_kv_dict: dict[str, Any] = get_dict_from_mode_name(mode_name) temp_config_file_path: Path = write_temp_config_file( mode_kv_dict=mode_kv_dict, temp_config_dir=package_config.temp_config_dir, - mode=mode, + mode_name=mode_name, ) object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) - log_msg = f"The temporary config file has been written for the {mode} package." + log_msg = f"The temporary config file has been written for the {mode_name} package." logger.info(log_msg) yield run_config # Delete the temporary config file. - logger.info("Deleting the temporary config file...") + logger.info("Removing the temporary config file...") temp_config_file_path.unlink() - logger.info("The temporary config file has been deleted.") + logger.info("The temporary config file has been removed.") diff --git a/integration-tests/tests/fixtures/integration_test_packages.py b/integration-tests/tests/fixtures/package_instance_fixtures.py similarity index 61% rename from integration-tests/tests/fixtures/integration_test_packages.py rename to integration-tests/tests/fixtures/package_instance_fixtures.py index 86a3783e1b..17ffdec4fe 100644 --- a/integration-tests/tests/fixtures/integration_test_packages.py +++ b/integration-tests/tests/fixtures/package_instance_fixtures.py @@ -1,4 +1,4 @@ -"""Define test packages fixtures.""" +"""Fixtures that start and stop CLP package instances for integration tests.""" import logging from collections.abc import Iterator @@ -21,29 +21,29 @@ def clp_text_package( clp_text_config: PackageInstanceConfig, ) -> Iterator[PackageInstance]: - """Fixture that launches a clp-text instance, and gracefully stops it after its scope ends.""" - log_msg = f"Starting up the {clp_text_config.mode} package..." + """Fixture that launches a clp-text instance, and gracefully stops it at scope.""" + mode_name = clp_text_config.mode_config.name + log_msg = f"Starting up the {mode_name} package..." logger.info(log_msg) start_clp_package(clp_text_config) - instance = PackageInstance(package_instance_config=clp_text_config) - mode = instance.package_instance_config.mode + instance = PackageInstance(package_instance_config=clp_text_config) instance_id = instance.clp_instance_id log_msg = ( - f"An instance of the {mode} package was started successfully." + f"An instance of the {mode_name} package was started successfully." f" Its instance ID is '{instance_id}'" ) logger.info(log_msg) yield instance - log_msg = f"Now stopping the {mode} package with instance ID '{instance_id}'..." + log_msg = f"Now stopping the {mode_name} package with instance ID '{instance_id}'..." logger.info(log_msg) stop_clp_package(instance) - log_msg = f"The {mode} package with instance ID '{instance_id}' was stopped successfully." + log_msg = f"The {mode_name} package with instance ID '{instance_id}' was stopped successfully." logger.info(log_msg) @@ -51,27 +51,27 @@ def clp_text_package( def clp_json_package( clp_json_config: PackageInstanceConfig, ) -> Iterator[PackageInstance]: - """Fixture that launches a clp-json instance, and gracefully stops it after its scope ends.""" - log_msg = f"Starting up the {clp_json_config.mode} package..." + """Fixture that launches a clp-json instance, and gracefully stops it at scope.""" + mode_name = clp_json_config.mode_config.name + log_msg = f"Starting up the {mode_name} package..." logger.info(log_msg) start_clp_package(clp_json_config) - instance = PackageInstance(package_instance_config=clp_json_config) - mode = instance.package_instance_config.mode + instance = PackageInstance(package_instance_config=clp_json_config) instance_id = instance.clp_instance_id log_msg = ( - f"An instance of the {mode} package was started successfully." + f"An instance of the {mode_name} package was started successfully." f" Its instance ID is '{instance_id}'" ) logger.info(log_msg) yield instance - log_msg = f"Now stopping the {mode} package with instance ID '{instance_id}'..." + log_msg = f"Now stopping the {mode_name} package with instance ID '{instance_id}'..." logger.info(log_msg) stop_clp_package(instance) - log_msg = f"The {mode} package with instance ID '{instance_id}' was stopped successfully." + log_msg = f"The {mode_name} package with instance ID '{instance_id}' was stopped successfully." logger.info(log_msg) diff --git a/integration-tests/tests/test_package_start.py b/integration-tests/tests/test_package_start.py index 0490388bf0..c3a189d341 100644 --- a/integration-tests/tests/test_package_start.py +++ b/integration-tests/tests/test_package_start.py @@ -1,23 +1,12 @@ """Integration tests verifying that the CLP package can be started and stopped.""" import logging -import shutil -from pathlib import Path -from typing import Any import pytest -import yaml -from tests.utils.config import ( - PackageInstance, -) -from tests.utils.docker_utils import ( - inspect_container_state, - list_prefixed_containers, -) from tests.utils.package_utils import ( - CLP_COMPONENT_BASENAMES, - get_mode_from_dict, + is_package_running, + is_running_mode_correct, ) package_configurations = pytest.mark.parametrize( @@ -39,111 +28,47 @@ def test_clp_package( ) -> None: """ Validate that all of the components of the clp package start up successfully. The package is - started up in whatever configuration is currently described in clp-config.yml. + tested in each of the configurations described in `test_package_fixture`. :param request: :param test_package_fixture: """ package_instance = request.getfixturevalue(test_package_fixture) - assert _is_package_running(package_instance) - assert _is_running_mode_correct(package_instance) - - -def _is_package_running(package_instance: PackageInstance) -> bool: - """Ensures the components of `package_instance` are running correctly.""" - mode = package_instance.package_instance_config.mode + mode_name = package_instance.package_instance_config.mode_config.name instance_id = package_instance.clp_instance_id + # Ensure that all package components are running. logger.info( "Checking if all components of %s package with instance ID '%s' are running properly...", - mode, + mode_name, instance_id, ) - docker_bin = shutil.which("docker") - if docker_bin is None: - err_msg = "docker not found in PATH" - raise RuntimeError(err_msg) - - for component in CLP_COMPONENT_BASENAMES: - # The container name may have any numeric suffix, and there may be multiple of them. - prefix = f"clp-package-{instance_id}-{component}-" - - candidates = list_prefixed_containers(docker_bin, prefix) - if not candidates: - pytest.fail(f"No component container was found with the prefix '{prefix}'") - - # Inspect each matching container; require that each one is running. - not_running = [] - for name in candidates: - desired_state = "running" - if not inspect_container_state(docker_bin, name, desired_state): - not_running.append(name) - - if not_running: - details = ", ".join(not_running) - pytest.fail(f"Component containers not running: {details}") + running, fail_msg = is_package_running(package_instance) + if not running: + assert fail_msg is not None + pytest.fail(fail_msg) logger.info( "All components of the %s package with instance ID '%s' are running properly.", - mode, + mode_name, instance_id, ) - return True - - -def _is_running_mode_correct(package_instance: PackageInstance) -> bool: - """ - Ensures that the mode intended for the package specified in package_instance matches the mode of - operation described by the package's shared config file. - """ - mode = package_instance.package_instance_config.mode - instance_id = package_instance.clp_instance_id + # Ensure that the package is running in the correct mode. logger.info( "Checking that the %s package with instance ID '%s' is running in the correct mode...", - mode, + mode_name, instance_id, ) - running_mode = _get_running_mode(package_instance) - intended_mode = package_instance.package_instance_config.mode - if running_mode != intended_mode: - err_msg = ( - f"Mode mismatch: the package is running in {running_mode}," - f" but it should be running in {intended_mode}." - ) - raise ValueError(err_msg) + correct, fail_msg = is_running_mode_correct(package_instance) + if not correct: + assert fail_msg is not None + pytest.fail(fail_msg) logger.info( "The %s package with instance ID '%s' is running in the correct mode.", - mode, + mode_name, instance_id, ) - - return True - - -def _get_running_mode(package_instance: PackageInstance) -> str: - """Gets the current running mode of the clp package.""" - shared_config_dict = _load_shared_config(package_instance.shared_config_file_path) - return get_mode_from_dict(shared_config_dict) - - -def _load_shared_config(path: Path) -> dict[str, Any]: - """Load the content of the shared config file into a dictionary.""" - try: - with path.open("r", encoding="utf-8") as file: - shared_config_dict = yaml.safe_load(file) - except yaml.YAMLError as err: - err_msg = f"Invalid YAML in shared config {path}: {err}" - raise ValueError(err_msg) from err - except OSError as err: - err_msg = f"Cannot read shared config {path}: {err}" - raise ValueError(err_msg) from err - - if not isinstance(shared_config_dict, dict): - err_msg = f"Shared config {path} must be a mapping at the top level." - raise TypeError(err_msg) - - return shared_config_dict diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index afb0496e66..078bacf8d8 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -126,8 +126,8 @@ class PackageInstanceConfig: #: The PackageConfig object corresponding to this package run. package_config: PackageConfig - #: The mode name of this config file's configuration. - mode: str + #: The PackageModeConfig object describing this config file's configuration. + mode_config: PackageModeConfig #: The location of the configfile used during this package run. temp_config_file_path: Path = field(init=False, repr=True) diff --git a/integration-tests/tests/utils/docker_utils.py b/integration-tests/tests/utils/docker_utils.py index a6826d8ed3..5c721fb0aa 100644 --- a/integration-tests/tests/utils/docker_utils.py +++ b/integration-tests/tests/utils/docker_utils.py @@ -3,8 +3,6 @@ import re import subprocess -import pytest - DOCKER_STATUS_FIELD_VALS = [ "created", "restarting", @@ -17,7 +15,7 @@ def list_prefixed_containers(docker_bin: str, prefix: str) -> list[str]: - """Returns a list of docker containers whose names begin with `prefix`.""" + """Returns a list of Docker containers whose names begin with `prefix`.""" ps_proc = subprocess.run( [ docker_bin, @@ -48,7 +46,10 @@ def list_prefixed_containers(docker_bin: str, prefix: str) -> list[str]: def inspect_container_state(docker_bin: str, name: str, desired_state: str) -> bool: - """Return True if the container status equals `desired_state`, else False. Raises on errors.""" + """ + Inspects the state of the container called `name`. Returns `True` if that container carries + `desired_state`, and `False` otherwise. + """ if desired_state not in DOCKER_STATUS_FIELD_VALS: err_msg = f"Unsupported desired_state: {desired_state}" raise ValueError(err_msg) @@ -63,7 +64,8 @@ def inspect_container_state(docker_bin: str, name: str, desired_state: str) -> b if inspect_proc.returncode != 0: err_out = (inspect_proc.stderr or inspect_proc.stdout or "").strip() if "No such object" in err_out: - pytest.fail(f"Component container not found: {name}") + err_msg = f"Component container that should be present was not found: {name}" + raise FileNotFoundError(err_msg) err_msg = f"Error inspecting container {name}: {err_out}" raise RuntimeError(err_msg) diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index ab0ba25e47..462e50a368 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -1,5 +1,6 @@ """Provides utility functions related to the clp-package used across `integration-tests`.""" +import shutil import subprocess from pathlib import Path from typing import Any @@ -27,6 +28,10 @@ PackageInstanceConfig, PackageModeConfig, ) +from tests.utils.docker_utils import ( + inspect_container_state, + list_prefixed_containers, +) def _to_container_basename(name: str) -> str: @@ -47,7 +52,7 @@ def _to_container_basename(name: str) -> str: _to_container_basename(GARBAGE_COLLECTOR_COMPONENT_NAME), ] -CLP_MODES: dict[str, PackageModeConfig] = { +CLP_MODE_CONFIGS: dict[str, PackageModeConfig] = { "clp-text": PackageModeConfig( name="clp-text", storage_engine=StorageEngine.CLP, @@ -61,6 +66,94 @@ def _to_container_basename(name: str) -> str: } +def get_dict_from_mode_name(mode_name: str) -> dict[str, Any]: + """Returns the dictionary that describes the operation of `mode_name`.""" + mode_config = CLP_MODE_CONFIGS.get(mode_name) + if mode_config is None: + err_msg = f"Unsupported mode: {mode_name}" + raise ValueError(err_msg) + + return _build_dict_from_config(mode_config) + + +def get_mode_name_from_dict(dictionary: dict[str, Any]) -> str: + """Returns the name of the mode of operation described by the contents of `dictionary`.""" + package_dict = dictionary.get("package") + if not isinstance(package_dict, dict): + err_msg = "`dictionary` does not carry any mapping for 'package'." + raise TypeError(err_msg) + + dict_query_engine = package_dict.get("query_engine") + dict_storage_engine = package_dict.get("storage_engine") + if dict_query_engine is None or dict_storage_engine is None: + err_msg = ( + "`dictionary` must specify both 'package.query_engine' and 'package.storage_engine'." + ) + raise ValueError(err_msg) + + for mode_name, mode_config in CLP_MODE_CONFIGS.items(): + if str(mode_config.query_engine.value) == str(dict_query_engine) and str( + mode_config.storage_engine.value + ) == str(dict_storage_engine): + return mode_name + + err_msg = ( + "The set of kv-pairs described in `dictionary` does not correspond to any mode of operation" + " for which integration testing is supported." + ) + raise ValueError(err_msg) + + +def write_temp_config_file( + mode_kv_dict: dict[str, Any], + temp_config_dir: Path, + mode_name: str, +) -> Path: + """ + Writes a temporary config file to `temp_config_dir`. Returns the path to the temporary file on + success. + """ + if not isinstance(mode_kv_dict, dict): + err_msg = "`mode_kv_dict` must be a mapping." + raise TypeError(err_msg) + + temp_config_dir.mkdir(parents=True, exist_ok=True) + temp_config_filename = f"clp-config-{mode_name}.yml" + temp_config_file_path = temp_config_dir / temp_config_filename + + tmp_path = temp_config_file_path.with_suffix(temp_config_file_path.suffix + ".tmp") + with tmp_path.open("w", encoding="utf-8") as f: + yaml.safe_dump(mode_kv_dict, f, sort_keys=False) + tmp_path.replace(temp_config_file_path) + + return temp_config_file_path + + +def _build_dict_from_config(mode_config: PackageModeConfig) -> dict[str, Any]: + storage_engine = mode_config.storage_engine + query_engine = mode_config.query_engine + package_model = Package(storage_engine=storage_engine, query_engine=query_engine) + return {"package": package_model.model_dump()} + + +def _load_shared_config(path: Path) -> dict[str, Any]: + try: + with path.open("r", encoding="utf-8") as file: + shared_config_dict = yaml.safe_load(file) + except yaml.YAMLError as err: + err_msg = f"Invalid YAML in shared config {path}: {err}" + raise ValueError(err_msg) from err + except OSError as err: + err_msg = f"Cannot read shared config {path}: {err}" + raise ValueError(err_msg) from err + + if not isinstance(shared_config_dict, dict): + err_msg = f"Shared config {path} must be a mapping at the top level." + raise TypeError(err_msg) + + return shared_config_dict + + def start_clp_package(run_config: PackageInstanceConfig) -> None: """Starts an instance of the clp package.""" start_script_path = run_config.package_config.start_script_path @@ -74,7 +167,7 @@ def start_clp_package(run_config: PackageInstanceConfig) -> None: # fmt: on subprocess.run(start_cmd, check=True) except Exception as e: - err_msg = f"Failed to start an instance of the {run_config.mode} package." + err_msg = f"Failed to start an instance of the {run_config.mode_config.name} package." raise RuntimeError(err_msg) from e @@ -92,80 +185,64 @@ def stop_clp_package( # fmt: on subprocess.run(stop_cmd, check=True) except Exception as e: - err_msg = f"Failed to start an instance of the {run_config.mode} package." + err_msg = f"Failed to stop an instance of the {run_config.mode_config.name} package." raise RuntimeError(err_msg) from e -def write_temp_config_file( - mode_kv_dict: dict[str, Any], - temp_config_dir: Path, - mode: str, -) -> Path: +def is_package_running(package_instance: PackageInstance) -> tuple[bool, str | None]: """ - Writes the set of key-value pairs in `mode_kv_dict` to a new yaml config file called - "clp-config-`{mode}`.yml" located in `temp_config_dir`. Returns the path to the temp config - file. + Checks that the `package_instance` is running properly by examining each of its component + containers. Records which containers are not found or not running. """ - if not isinstance(mode_kv_dict, dict): - err_msg = "`mode_kv_dict` must be a mapping." - raise TypeError(err_msg) + docker_bin = shutil.which("docker") + if docker_bin is None: + err_msg = "docker not found in PATH" + raise RuntimeError(err_msg) - temp_config_dir.mkdir(parents=True, exist_ok=True) - temp_config_filename = f"clp-config-{mode}.yml" - temp_config_file_path = temp_config_dir / temp_config_filename + instance_id = package_instance.clp_instance_id + problems: list[str] = [] - tmp_path = temp_config_file_path.with_suffix(temp_config_file_path.suffix + ".tmp") - with tmp_path.open("w", encoding="utf-8") as f: - yaml.safe_dump(mode_kv_dict, f, sort_keys=False) - tmp_path.replace(temp_config_file_path) + for component in CLP_COMPONENT_BASENAMES: + prefix = f"clp-package-{instance_id}-{component}-" - return temp_config_file_path - - -def build_dict_from_config(mode_config: PackageModeConfig) -> dict[str, Any]: - """ - Return {"package": {...}} where the inner mapping is produced from clp_config.Package - with proper enum serialization. - """ - storage_engine = mode_config.storage_engine - query_engine = mode_config.query_engine - package_model = Package(storage_engine=storage_engine, query_engine=query_engine) - return {"package": package_model.model_dump()} + candidates = list_prefixed_containers(docker_bin, prefix) + if not candidates: + problems.append(f"No component container was found with the prefix '{prefix}'") + continue + not_running: list[str] = [] + for name in candidates: + if not inspect_container_state(docker_bin, name, "running"): + not_running.append(name) -def get_dict_from_mode(mode: str) -> dict[str, Any]: - """Returns the dict that corresponds to the mode.""" - mode_config = CLP_MODES.get(mode) - if mode_config is None: - err_msg = f"Unsupported mode: {mode}" - raise ValueError(err_msg) + if not_running: + details = ", ".join(not_running) + problems.append(f"Component containers not running: {details}") - return build_dict_from_config(mode_config) + if problems: + return False, "; ".join(problems) + return True, None -def get_mode_from_dict(dictionary: dict[str, Any]) -> str: - """Returns the mode that corresponds to dict.""" - package_dict = dictionary.get("package") - if not isinstance(package_dict, dict): - err_msg = "`dictionary` does not carry any mapping for 'package'." - raise TypeError(err_msg) - dict_query_engine = package_dict.get("query_engine") - dict_storage_engine = package_dict.get("storage_engine") - if dict_query_engine is None or dict_storage_engine is None: - err_msg = ( - "`dictionary` must specify both 'package.query_engine' and 'package.storage_engine'." +def is_running_mode_correct(package_instance: PackageInstance) -> tuple[bool, str | None]: + """ + Checks if the mode described in the shared config file matches the mode described in + `mode_config` of `package_instance`. Returns `True` if correct, `False` with message on + mismatch. + """ + running_mode = _get_running_mode(package_instance) + intended_mode = package_instance.package_instance_config.mode_config.name + if running_mode != intended_mode: + return ( + False, + f"Mode mismatch: the package is running in {running_mode}, but it should be running in" + f" {intended_mode}.", ) - raise ValueError(err_msg) - for mode_name, mode_config in CLP_MODES.items(): - if str(mode_config.query_engine.value) == str(dict_query_engine) and str( - mode_config.storage_engine.value - ) == str(dict_storage_engine): - return mode_name + return True, None - err_msg = ( - "The set of kv-pairs described in `dictionary` does not correspond to any mode of operation" - " for which integration testing is supported." - ) - raise ValueError(err_msg) + +def _get_running_mode(package_instance: PackageInstance) -> str: + shared_config_dict = _load_shared_config(package_instance.shared_config_file_path) + return get_mode_name_from_dict(shared_config_dict) From 27b7f5b18c650712ab26811fce711d1d4647498f Mon Sep 17 00:00:00 2001 From: Quinn Date: Sun, 2 Nov 2025 20:34:58 +0000 Subject: [PATCH 06/22] Add comments. --- integration-tests/tests/conftest.py | 9 ++++----- .../tests/fixtures/package_config_fixtures.py | 4 ++-- .../tests/fixtures/package_instance_fixtures.py | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/integration-tests/tests/conftest.py b/integration-tests/tests/conftest.py index 863bdbc7a2..d9891e408e 100644 --- a/integration-tests/tests/conftest.py +++ b/integration-tests/tests/conftest.py @@ -16,10 +16,8 @@ def pytest_configure(config: Config) -> None: # noqa: ARG001 - """ - Returns the /lib/python3/site-packages path. - Raises a clear error if the directory is not usable. - """ + """Adds /lib/python3/site-packages to the path.""" + # Retrieve and validate path to package directory. clp_package_dir = Path(get_env_var("CLP_PACKAGE_DIR")).expanduser().resolve() if not clp_package_dir: err_msg = ( @@ -28,6 +26,7 @@ def pytest_configure(config: Config) -> None: # noqa: ARG001 ) raise RuntimeError(err_msg) + # Construct and validate path to site_packages site_packages = Path(clp_package_dir) / "lib" / "python3" / "site-packages" if not site_packages.is_dir(): err_msg = ( @@ -36,7 +35,7 @@ def pytest_configure(config: Config) -> None: # noqa: ARG001 ) raise RuntimeError(err_msg) - # Sanity check: ensure clp_py_utils exists. + # Sanity check: ensure clp_py_utils exists within site_packages. if not (site_packages / "clp_py_utils").exists(): err_msg = ( f"'clp_py_utils' not found under: '{site_packages}'. These integration tests expect the" diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py index 6b93759e73..e86444eb71 100644 --- a/integration-tests/tests/fixtures/package_config_fixtures.py +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -50,7 +50,7 @@ def clp_text_config( yield run_config - # Delete the temporary config file. + # Remove the temporary config file. logger.info("Removing the temporary config file...") temp_config_file_path.unlink() @@ -87,7 +87,7 @@ def clp_json_config( yield run_config - # Delete the temporary config file. + # Remove the temporary config file. logger.info("Removing the temporary config file...") temp_config_file_path.unlink() diff --git a/integration-tests/tests/fixtures/package_instance_fixtures.py b/integration-tests/tests/fixtures/package_instance_fixtures.py index 17ffdec4fe..49285b5c0e 100644 --- a/integration-tests/tests/fixtures/package_instance_fixtures.py +++ b/integration-tests/tests/fixtures/package_instance_fixtures.py @@ -21,7 +21,7 @@ def clp_text_package( clp_text_config: PackageInstanceConfig, ) -> Iterator[PackageInstance]: - """Fixture that launches a clp-text instance, and gracefully stops it at scope.""" + """Fixture that launches a clp-text instance, and gracefully stops it at scope boundary.""" mode_name = clp_text_config.mode_config.name log_msg = f"Starting up the {mode_name} package..." logger.info(log_msg) @@ -51,7 +51,7 @@ def clp_text_package( def clp_json_package( clp_json_config: PackageInstanceConfig, ) -> Iterator[PackageInstance]: - """Fixture that launches a clp-json instance, and gracefully stops it at scope.""" + """Fixture that launches a clp-json instance, and gracefully stops it at scope boundary.""" mode_name = clp_json_config.mode_config.name log_msg = f"Starting up the {mode_name} package..." logger.info(log_msg) From e041f0e0adc9d9ea4a1c15197faa950dc9ae83b6 Mon Sep 17 00:00:00 2001 From: Quinn Date: Sun, 2 Nov 2025 20:54:04 +0000 Subject: [PATCH 07/22] Streamline logger calls. --- integration-tests/tests/conftest.py | 2 +- .../tests/fixtures/package_config_fixtures.py | 12 ++---- .../fixtures/package_instance_fixtures.py | 38 +++++++++---------- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/integration-tests/tests/conftest.py b/integration-tests/tests/conftest.py index d9891e408e..89749919ae 100644 --- a/integration-tests/tests/conftest.py +++ b/integration-tests/tests/conftest.py @@ -26,7 +26,7 @@ def pytest_configure(config: Config) -> None: # noqa: ARG001 ) raise RuntimeError(err_msg) - # Construct and validate path to site_packages + # Construct and validate path to site_packages. site_packages = Path(clp_package_dir) / "lib" / "python3" / "site-packages" if not site_packages.is_dir(): err_msg = ( diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py index e86444eb71..633394aca0 100644 --- a/integration-tests/tests/fixtures/package_config_fixtures.py +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -27,8 +27,7 @@ def clp_text_config( ) -> Iterator[PackageInstanceConfig]: """Fixture that creates and maintains a config file for clp-text.""" mode_name = "clp-text" - log_msg = f"Creating a temporary config file for the {mode_name} package..." - logger.info(log_msg) + logger.info("Creating a temporary config file for the %s package...", mode_name) mode_config: PackageModeConfig = CLP_MODE_CONFIGS[mode_name] run_config = PackageInstanceConfig( @@ -45,8 +44,7 @@ def clp_text_config( ) object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) - log_msg = f"The temporary config file has been written for the {mode_name} package." - logger.info(log_msg) + logger.info("The temporary config file has been written for the %s package.", mode_name) yield run_config @@ -64,8 +62,7 @@ def clp_json_config( ) -> Iterator[PackageInstanceConfig]: """Fixture that creates and maintains a config file for clp-json.""" mode_name = "clp-json" - log_msg = f"Creating a temporary config file for the {mode_name} package..." - logger.info(log_msg) + logger.info("Creating a temporary config file for the %s package...", mode_name) mode_config: PackageModeConfig = CLP_MODE_CONFIGS[mode_name] run_config = PackageInstanceConfig( @@ -82,8 +79,7 @@ def clp_json_config( ) object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) - log_msg = f"The temporary config file has been written for the {mode_name} package." - logger.info(log_msg) + logger.info("The temporary config file has been written for the %s package.", mode_name) yield run_config diff --git a/integration-tests/tests/fixtures/package_instance_fixtures.py b/integration-tests/tests/fixtures/package_instance_fixtures.py index 49285b5c0e..6eb0985510 100644 --- a/integration-tests/tests/fixtures/package_instance_fixtures.py +++ b/integration-tests/tests/fixtures/package_instance_fixtures.py @@ -23,28 +23,27 @@ def clp_text_package( ) -> Iterator[PackageInstance]: """Fixture that launches a clp-text instance, and gracefully stops it at scope boundary.""" mode_name = clp_text_config.mode_config.name - log_msg = f"Starting up the {mode_name} package..." - logger.info(log_msg) + logger.info("Starting up the %s package...", mode_name) start_clp_package(clp_text_config) instance = PackageInstance(package_instance_config=clp_text_config) instance_id = instance.clp_instance_id - log_msg = ( - f"An instance of the {mode_name} package was started successfully." - f" Its instance ID is '{instance_id}'" + logger.info( + "An instance of the %s package was started successfully. Its instance ID is '%s'", + mode_name, + instance_id, ) - logger.info(log_msg) yield instance - log_msg = f"Now stopping the {mode_name} package with instance ID '{instance_id}'..." - logger.info(log_msg) + logger.info("Now stopping the %s package with instance ID '%s'...", mode_name, instance_id) stop_clp_package(instance) - log_msg = f"The {mode_name} package with instance ID '{instance_id}' was stopped successfully." - logger.info(log_msg) + logger.info( + "The %s package with instance ID '%s' was stopped successfully.", mode_name, instance_id + ) @pytest.fixture @@ -53,25 +52,24 @@ def clp_json_package( ) -> Iterator[PackageInstance]: """Fixture that launches a clp-json instance, and gracefully stops it at scope boundary.""" mode_name = clp_json_config.mode_config.name - log_msg = f"Starting up the {mode_name} package..." - logger.info(log_msg) + logger.info("Starting up the %s package...", mode_name) start_clp_package(clp_json_config) instance = PackageInstance(package_instance_config=clp_json_config) instance_id = instance.clp_instance_id - log_msg = ( - f"An instance of the {mode_name} package was started successfully." - f" Its instance ID is '{instance_id}'" + logger.info( + "An instance of the %s package was started successfully. Its instance ID is '%s'", + mode_name, + instance_id, ) - logger.info(log_msg) yield instance - log_msg = f"Now stopping the {mode_name} package with instance ID '{instance_id}'..." - logger.info(log_msg) + logger.info("Now stopping the %s package with instance ID '%s'...", mode_name, instance_id) stop_clp_package(instance) - log_msg = f"The {mode_name} package with instance ID '{instance_id}' was stopped successfully." - logger.info(log_msg) + logger.info( + "The %s package with instance ID '%s' was stopped successfully.", mode_name, instance_id + ) From 75672fca8955abefccedd849be40a83744997727 Mon Sep 17 00:00:00 2001 From: Quinn Date: Sun, 2 Nov 2025 21:33:16 +0000 Subject: [PATCH 08/22] Deduplicate fixture code. --- .../tests/fixtures/package_config_fixtures.py | 66 +++++++------------ .../fixtures/package_instance_fixtures.py | 61 +++++------------ integration-tests/tests/test_package_start.py | 27 +++----- 3 files changed, 50 insertions(+), 104 deletions(-) diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py index 633394aca0..d56571f36a 100644 --- a/integration-tests/tests/fixtures/package_config_fixtures.py +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -21,21 +21,23 @@ logger = logging.getLogger(__name__) -@pytest.fixture -def clp_text_config( +def _build_package_instance_config( + mode_name: str, package_config: PackageConfig, -) -> Iterator[PackageInstanceConfig]: - """Fixture that creates and maintains a config file for clp-text.""" - mode_name = "clp-text" - logger.info("Creating a temporary config file for the %s package...", mode_name) +) -> PackageInstanceConfig: + """Construct a PackageInstanceConfig for the given `mode_name`.""" + if mode_name not in CLP_MODE_CONFIGS: + err_msg = f"Unknown CLP mode '{mode_name}'. Known modes: {list(CLP_MODE_CONFIGS.keys())}" + raise KeyError(err_msg) + # Find the corresponding PackageModeConfig object and instantiate PackageInstanceConfig. mode_config: PackageModeConfig = CLP_MODE_CONFIGS[mode_name] run_config = PackageInstanceConfig( package_config=package_config, mode_config=mode_config, ) - # Create a temporary config file for the package run. + # Write the temporary config file that the instance will use during the test. mode_kv_dict: dict[str, Any] = get_dict_from_mode_name(mode_name) temp_config_file_path: Path = write_temp_config_file( mode_kv_dict=mode_kv_dict, @@ -44,48 +46,28 @@ def clp_text_config( ) object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) - logger.info("The temporary config file has been written for the %s package.", mode_name) - - yield run_config - - # Remove the temporary config file. - logger.info("Removing the temporary config file...") - - temp_config_file_path.unlink() - - logger.info("The temporary config file has been removed.") + return run_config @pytest.fixture -def clp_json_config( +def clp_config( + request: pytest.FixtureRequest, package_config: PackageConfig, ) -> Iterator[PackageInstanceConfig]: - """Fixture that creates and maintains a config file for clp-json.""" - mode_name = "clp-json" + """ + Parameterized fixture that creates and removes a temporary config file for a mode of operation. + The mode name arrives through request.param from the test's indirect parametrization. + """ + mode_name: str = request.param logger.info("Creating a temporary config file for the %s package...", mode_name) - mode_config: PackageModeConfig = CLP_MODE_CONFIGS[mode_name] - run_config = PackageInstanceConfig( - package_config=package_config, - mode_config=mode_config, - ) - - # Create a temporary config file for the package run. - mode_kv_dict: dict[str, Any] = get_dict_from_mode_name(mode_name) - temp_config_file_path: Path = write_temp_config_file( - mode_kv_dict=mode_kv_dict, - temp_config_dir=package_config.temp_config_dir, - mode_name=mode_name, - ) - object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) + run_config = _build_package_instance_config(mode_name, package_config) logger.info("The temporary config file has been written for the %s package.", mode_name) - yield run_config - - # Remove the temporary config file. - logger.info("Removing the temporary config file...") - - temp_config_file_path.unlink() - - logger.info("The temporary config file has been removed.") + try: + yield run_config + finally: + logger.info("Removing the temporary config file...") + run_config.temp_config_file_path.unlink() + logger.info("The temporary config file has been removed.") diff --git a/integration-tests/tests/fixtures/package_instance_fixtures.py b/integration-tests/tests/fixtures/package_instance_fixtures.py index 6eb0985510..c86bf10c3c 100644 --- a/integration-tests/tests/fixtures/package_instance_fixtures.py +++ b/integration-tests/tests/fixtures/package_instance_fixtures.py @@ -18,16 +18,19 @@ @pytest.fixture -def clp_text_package( - clp_text_config: PackageInstanceConfig, +def clp_package( + clp_config: PackageInstanceConfig, ) -> Iterator[PackageInstance]: - """Fixture that launches a clp-text instance, and gracefully stops it at scope boundary.""" - mode_name = clp_text_config.mode_config.name + """ + Parameterized fixture that starts a CLP instance for the mode supplied to `clp_config`, + and gracefully stops it at scope boundary (fixture teardown). + """ + mode_name = clp_config.mode_config.name logger.info("Starting up the %s package...", mode_name) - start_clp_package(clp_text_config) + start_clp_package(clp_config) - instance = PackageInstance(package_instance_config=clp_text_config) + instance = PackageInstance(package_instance_config=clp_config) instance_id = instance.clp_instance_id logger.info( "An instance of the %s package was started successfully. Its instance ID is '%s'", @@ -35,41 +38,11 @@ def clp_text_package( instance_id, ) - yield instance - - logger.info("Now stopping the %s package with instance ID '%s'...", mode_name, instance_id) - - stop_clp_package(instance) - - logger.info( - "The %s package with instance ID '%s' was stopped successfully.", mode_name, instance_id - ) - - -@pytest.fixture -def clp_json_package( - clp_json_config: PackageInstanceConfig, -) -> Iterator[PackageInstance]: - """Fixture that launches a clp-json instance, and gracefully stops it at scope boundary.""" - mode_name = clp_json_config.mode_config.name - logger.info("Starting up the %s package...", mode_name) - - start_clp_package(clp_json_config) - - instance = PackageInstance(package_instance_config=clp_json_config) - instance_id = instance.clp_instance_id - logger.info( - "An instance of the %s package was started successfully. Its instance ID is '%s'", - mode_name, - instance_id, - ) - - yield instance - - logger.info("Now stopping the %s package with instance ID '%s'...", mode_name, instance_id) - - stop_clp_package(instance) - - logger.info( - "The %s package with instance ID '%s' was stopped successfully.", mode_name, instance_id - ) + try: + yield instance + finally: + logger.info("Now stopping the %s package with instance ID '%s'...", mode_name, instance_id) + stop_clp_package(instance) + logger.info( + "The %s package with instance ID '%s' was stopped successfully.", mode_name, instance_id + ) diff --git a/integration-tests/tests/test_package_start.py b/integration-tests/tests/test_package_start.py index c3a189d341..a3a57e2a38 100644 --- a/integration-tests/tests/test_package_start.py +++ b/integration-tests/tests/test_package_start.py @@ -4,36 +4,27 @@ import pytest +from tests.utils.config import PackageInstance from tests.utils.package_utils import ( + CLP_MODE_CONFIGS, is_package_running, is_running_mode_correct, ) -package_configurations = pytest.mark.parametrize( - "test_package_fixture", - [ - "clp_text_package", - "clp_json_package", - ], -) +TEST_MODES = CLP_MODE_CONFIGS.keys() logger = logging.getLogger(__name__) @pytest.mark.package -@package_configurations -def test_clp_package( - request: pytest.FixtureRequest, - test_package_fixture: str, -) -> None: +@pytest.mark.parametrize("clp_config", TEST_MODES, indirect=True) +def test_clp_package(clp_package: PackageInstance) -> None: """ - Validate that all of the components of the clp package start up successfully. The package is - tested in each of the configurations described in `test_package_fixture`. - - :param request: - :param test_package_fixture: + Validate that all of the components of the CLP package start up successfully for the selected + mode of operation. """ - package_instance = request.getfixturevalue(test_package_fixture) + # Spin up the package by getting the PackageInstance fixture. + package_instance = clp_package mode_name = package_instance.package_instance_config.mode_config.name instance_id = package_instance.clp_instance_id From 2df951523018dfb6f520c5079c13887481431484 Mon Sep 17 00:00:00 2001 From: Quinn Date: Wed, 5 Nov 2025 20:41:19 +0000 Subject: [PATCH 09/22] Change method of clp_config import; temporary, will change again once #1549 is merged. --- integration-tests/pyproject.toml | 4 + integration-tests/tests/conftest.py | 40 +- integration-tests/uv.lock | 627 ++++++++++++++++++++++++++++ 3 files changed, 632 insertions(+), 39 deletions(-) diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index 5c100c2fbb..f515e230eb 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -17,6 +17,7 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ + "clp-py-utils", "mypy>=1.16.0", "ruff>=0.11.12", "pytest>=8.4.1", @@ -72,3 +73,6 @@ isort.order-by-type = false [tool.ruff.format] docstring-code-format = true docstring-code-line-length = 100 + +[tool.uv.sources] +clp-py-utils = { path = "../components/clp-py-utils" } \ No newline at end of file diff --git a/integration-tests/tests/conftest.py b/integration-tests/tests/conftest.py index 89749919ae..1e475b1bd7 100644 --- a/integration-tests/tests/conftest.py +++ b/integration-tests/tests/conftest.py @@ -1,11 +1,4 @@ -"""Pytest config: register fixtures, validate env, adjust paths, and other test setup.""" - -import sys -from pathlib import Path - -from _pytest.config import Config - -from tests.utils.utils import get_env_var +"""Make the fixtures defined in `tests/fixtures/` globally available without imports.""" pytest_plugins = [ "tests.fixtures.integration_test_config", @@ -13,34 +6,3 @@ "tests.fixtures.package_instance_fixtures", "tests.fixtures.package_config_fixtures", ] - - -def pytest_configure(config: Config) -> None: # noqa: ARG001 - """Adds /lib/python3/site-packages to the path.""" - # Retrieve and validate path to package directory. - clp_package_dir = Path(get_env_var("CLP_PACKAGE_DIR")).expanduser().resolve() - if not clp_package_dir: - err_msg = ( - "CLP_PACKAGE_DIR is not set. Point it to the root of the CLP package that contains" - " 'lib/python3/site-packages'." - ) - raise RuntimeError(err_msg) - - # Construct and validate path to site_packages. - site_packages = Path(clp_package_dir) / "lib" / "python3" / "site-packages" - if not site_packages.is_dir(): - err_msg = ( - f"Could not find the 'site-packages' directory at: '{site_packages}'. Verify" - " CLP_PACKAGE_DIR points at the CLP package root." - ) - raise RuntimeError(err_msg) - - # Sanity check: ensure clp_py_utils exists within site_packages. - if not (site_packages / "clp_py_utils").exists(): - err_msg = ( - f"'clp_py_utils' not found under: '{site_packages}'. These integration tests expect the" - " package to contain clp_py_utils." - ) - raise RuntimeError(err_msg) - - sys.path.insert(0, str(site_packages)) diff --git a/integration-tests/uv.lock b/integration-tests/uv.lock index 2444a82944..67104a259a 100644 --- a/integration-tests/uv.lock +++ b/integration-tests/uv.lock @@ -2,6 +2,72 @@ version = 1 revision = 3 requires-python = ">=3.10" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "boto3" +version = "1.40.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/95/db1f23bbc5cf1a9b66cb1828a0940305ea300162ae12c55522c738ab6f0e/boto3-1.40.66.tar.gz", hash = "sha256:f2038d9bac5154da7390c29bfd013546ac96609e7ce5a7f3cb6f99412be3f4c0", size = 111564, upload-time = "2025-11-04T20:28:59.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/c2/3097e2492931b8fdcab47217b917c7964dbc8bfce31f89ace49568ed47f8/boto3-1.40.66-py3-none-any.whl", hash = "sha256:ee4fe21c5301cc0e11cc11a53e71e5ddd82d5fae42b10fa8e5403f3aa06434e3", size = 139361, upload-time = "2025-11-04T20:28:57.146Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/f3/5dae6e3b06493f2ac769c6764543b84fa50a8de3fec1e33252271166b394/botocore-1.40.66.tar.gz", hash = "sha256:e49a55ad54426c4ea853a59ff9d8243023a90c935782d4c287e9b3424883c3fa", size = 14411853, upload-time = "2025-11-04T20:28:48.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/48/43f9335e28351f35a939dce366a3943296f381ecd4660bd1c8d2bb8f3006/botocore-1.40.66-py3-none-any.whl", hash = "sha256:98d5766e17e72110b1d08ab510a8475a6597c59d9560235e2d28ae1a4b043b92", size = 14076509, upload-time = "2025-11-04T20:28:44.233Z" }, +] + +[[package]] +name = "clp-py-utils" +version = "0.5.2.dev0" +source = { directory = "../components/clp-py-utils" } +dependencies = [ + { name = "boto3" }, + { name = "mariadb" }, + { name = "mysql-connector-python" }, + { name = "pydantic" }, + { name = "python-levenshtein" }, + { name = "pyyaml" }, + { name = "result" }, + { name = "sqlalchemy" }, + { name = "strenum" }, +] + +[package.metadata] +requires-dist = [ + { name = "boto3", specifier = ">=1.40.55" }, + { name = "mariadb", specifier = ">=1.0.11,<1.1.dev0" }, + { name = "mysql-connector-python", specifier = ">=9.4.0" }, + { name = "pydantic", specifier = ">=2.12.3" }, + { name = "python-levenshtein", specifier = ">=0.27.1" }, + { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "result", specifier = ">=0.17.0" }, + { name = "sqlalchemy", specifier = ">=2.0.44" }, + { name = "strenum", specifier = ">=0.4.15" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -23,6 +89,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, + { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, + { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -39,6 +166,7 @@ source = { editable = "." } [package.dev-dependencies] dev = [ + { name = "clp-py-utils" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-env" }, @@ -51,6 +179,7 @@ dev = [ [package.metadata.requires-dev] dev = [ + { name = "clp-py-utils", directory = "../components/clp-py-utils" }, { name = "mypy", specifier = ">=1.16.0" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-env", specifier = ">=1.1.5" }, @@ -59,6 +188,118 @@ dev = [ { name = "types-pyyaml", specifier = ">=6.0.12.20240808" }, ] +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "levenshtein" +version = "0.27.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rapidfuzz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/56/dcf68853b062e3b94bdc3d011cc4198779abc5b9dc134146a062920ce2e2/levenshtein-0.27.3.tar.gz", hash = "sha256:1ac326b2c84215795163d8a5af471188918b8797b4953ec87aaba22c9c1f9fc0", size = 393269, upload-time = "2025-11-01T12:14:31.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/07/e8d04ec84fae72f0a75a2c46f897fe2abb82a657707a902a414faa5f8a72/levenshtein-0.27.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d61eff70799fd5e710625da8a13e5adabd62bfd9f70abb9c531af6cad458cd27", size = 171954, upload-time = "2025-11-01T12:12:40.151Z" }, + { url = "https://files.pythonhosted.org/packages/8d/13/606682ad2a7f0c01178cbc1f8de1b53d86e5dd8a03983c8feb8a6f403e76/levenshtein-0.27.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:477efed87edf72ad0d3870038479ed2f63020a42e69c6a38a32a550e51f8e70e", size = 158414, upload-time = "2025-11-01T12:12:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c5/9627e1fc5cbfaff7fbf2e95aaf29340929ff2e92ae2d185b967a36942262/levenshtein-0.27.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ef99b9827d7d1100fc4398ac5522bd56766b894561c0cbdea0a01b93f24e642", size = 133822, upload-time = "2025-11-01T12:12:43.243Z" }, + { url = "https://files.pythonhosted.org/packages/32/88/9e24a51b99b3dd6b3706a94bd258b2254edab5392e92c2e6d9b0773eba8f/levenshtein-0.27.3-cp310-cp310-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9091e8ca9fff6088836abf372f8871fb480e44603defa526e1c3ae2f1d70acc5", size = 114383, upload-time = "2025-11-01T12:12:44.4Z" }, + { url = "https://files.pythonhosted.org/packages/4c/95/9a11eb769bad0583712e2772e90ef92929d4ff4931fbb34efe79a0bff493/levenshtein-0.27.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ffdb2329712c5595eda3532a4f701f87f6c73a0f7aaac240681bf0b54310d63", size = 153061, upload-time = "2025-11-01T12:12:46.215Z" }, + { url = "https://files.pythonhosted.org/packages/b3/86/47387ed38df23ed3a6640032cdca97367eacb2a2d2075d97d6e88f43b40e/levenshtein-0.27.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:35856330eac1b968b45a5abbc4a3d14279bd9d1224be727cb1aac9ac4928a419", size = 1115566, upload-time = "2025-11-01T12:12:47.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/17/ed94dadabdf7e86940f6179238312a6750688f44565a4eb19ae5a87ce8a8/levenshtein-0.27.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:5377e237f6a13f5b0618621cca7992848993470c011716c3ad09cdf19c3b13ab", size = 1007140, upload-time = "2025-11-01T12:12:49.283Z" }, + { url = "https://files.pythonhosted.org/packages/52/25/c971c043aec0994c5600789d2bf4c183e2f389ee21559bb46a06c6f46ec2/levenshtein-0.27.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e30614186eb5c43833b62ae7d893a116b88373eec8cf3f3d62ba51aa5962d8ea", size = 1185316, upload-time = "2025-11-01T12:12:50.849Z" }, + { url = "https://files.pythonhosted.org/packages/3c/54/2a1a1af73470cd6ca0d709efb1786fe4651eee9a3cb5b767903defb4fe9c/levenshtein-0.27.3-cp310-cp310-win32.whl", hash = "sha256:5499342fd6b003bd5abc28790c7b333884838f7fd8c50570a6520bbaf5e2a35b", size = 84312, upload-time = "2025-11-01T12:12:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/10/15/50f508790a7b7e0d6258ec85add62c257ab27ca70e5e8a1bae8350305932/levenshtein-0.27.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e2792730388bec6a85d4d3e3a9b53b8a4b509722bea1a78a39a1a0a7d8f0e13", size = 94376, upload-time = "2025-11-01T12:12:53.361Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/ca3e54e5144695cc8a34601d275fabfc97c2ab9b824cbe0b49a0173a0575/levenshtein-0.27.3-cp310-cp310-win_arm64.whl", hash = "sha256:8a2a274b55562a49c6e9dadb16d05f6c27ffa98906b55d5c122893457ca6e464", size = 87216, upload-time = "2025-11-01T12:12:54.674Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/42e28a86e2f04a2e064faa1eab7d81a35fb111212b508ce7e450f839943d/levenshtein-0.27.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:245b6ffb6e1b0828cafbce35c500cb3265d0962c121d090669f177968c5a2980", size = 172216, upload-time = "2025-11-01T12:12:55.727Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/fe665c8e5d8ebe4266807e43af72db9d4f84d4f513ea86eacca3aaf5f77b/levenshtein-0.27.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f44c98fa23f489eb7b2ad87d5dd24b6a784434bb5edb73f6b0513309c949690", size = 158616, upload-time = "2025-11-01T12:12:56.99Z" }, + { url = "https://files.pythonhosted.org/packages/22/46/9998bc56729444e350c083635b94c3eae97218b8a618cdc89f6825eec08c/levenshtein-0.27.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f5f85a1fc96dfc147bba82b4c67d6346ea26c27ef77a6a9de689118e26dddbe", size = 134222, upload-time = "2025-11-01T12:12:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/19/09/914b3fc22c083728904f8dc7876a2a90a602b4769f27f5320176cbd6f781/levenshtein-0.27.3-cp311-cp311-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:18ceddd38d0e990d2c1c9b72f3e191dace87e2f8f0446207ce9e9cd2bfdfc8a1", size = 114902, upload-time = "2025-11-01T12:12:59.645Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ee/f361bfa5afe24698fb07ae7811e00c2984131023c7688299dea4fd3f2f4c/levenshtein-0.27.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:222b81adca29ee4128183328c6e1b25a48c817d14a008ab49e74be9df963b293", size = 153562, upload-time = "2025-11-01T12:13:00.745Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/614d0ab9777ebb91895ce1c9390ec2f244f53f7ddf7e29f36b0ca33f3841/levenshtein-0.27.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee3769ab6e89c24f901e6b7004100630e86721464d7d0384860a322d7953d3a5", size = 1115732, upload-time = "2025-11-01T12:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/24/d9/f33c4e35399349ec2eb7be53ed49459bf6e59c31668868c89cf6f7964029/levenshtein-0.27.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:03eba8fda9f3f2b4b0760263fa20b20a90ab00cbeeab4d0d9d899b4f77912b0a", size = 1009023, upload-time = "2025-11-01T12:13:03.954Z" }, + { url = "https://files.pythonhosted.org/packages/2e/63/e8803a6d71488334c100afc79a98efc8cf0086ad29ee7f1d083f7f2c584d/levenshtein-0.27.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c870b19e2d5c7bc7f16213cc10312b82d873a4d46e1c6d51857a12ef39a76552", size = 1185850, upload-time = "2025-11-01T12:13:05.341Z" }, + { url = "https://files.pythonhosted.org/packages/09/55/a6a815ef76a6d5f7a2ee4e1edc8e8f1f935b9fa278634cc687af19b86de9/levenshtein-0.27.3-cp311-cp311-win32.whl", hash = "sha256:1987622e9b8ba2ae47dc27469291da1f58462660fa34f4358e9d9c1830fb1355", size = 84375, upload-time = "2025-11-01T12:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/cf4c36ffe91994e772b682ff4c3cb721bd50ac05d4a887baa35f4d3b2268/levenshtein-0.27.3-cp311-cp311-win_amd64.whl", hash = "sha256:a2b2aa81851e01bb09667b07e80c3fbf0f5a7c6ee9cd80caf43cce705e65832a", size = 94598, upload-time = "2025-11-01T12:13:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/92/4b/43e820c3a13033908925eae8614ad7c0be1e5868836770565174012158c0/levenshtein-0.27.3-cp311-cp311-win_arm64.whl", hash = "sha256:a084b335c54def1aef9a594b7163faa44dd00056323808bab783f43d8e4c1395", size = 87133, upload-time = "2025-11-01T12:13:08.701Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8e/3be9d8e0245704e3af5258fb6cb157c3d59902e1351e95edf6ed8a8c0434/levenshtein-0.27.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2de7f095b0ca8e44de9de986ccba661cd0dec3511c751b499e76b60da46805e9", size = 169622, upload-time = "2025-11-01T12:13:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/a6/42/a2b2fda5e8caf6ecd5aac142f946a77574a3961e65da62c12fd7e48e5cb1/levenshtein-0.27.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9b8b29e5d5145a3c958664c85151b1bb4b26e4ca764380b947e6a96a321217c", size = 159183, upload-time = "2025-11-01T12:13:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c4/f083fabbd61c449752df1746533538f4a8629e8811931b52f66e6c4290ad/levenshtein-0.27.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc975465a51b1c5889eadee1a583b81fba46372b4b22df28973e49e8ddb8f54a", size = 133120, upload-time = "2025-11-01T12:13:12.363Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e5/b6421e04cb0629615b8efd6d4d167dd2b1afb5097b87bb83cd992004dcca/levenshtein-0.27.3-cp312-cp312-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:57573ed885118554770979fdee584071b66103f6d50beddeabb54607a1213d81", size = 114988, upload-time = "2025-11-01T12:13:13.486Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/39ee0e8d3028e90178e1031530ccc98563f8f2f0d905ec784669dcf0fa90/levenshtein-0.27.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23aff800a6dd5d91bb3754a6092085aa7ad46b28e497682c155c74f681cfaa2d", size = 153346, upload-time = "2025-11-01T12:13:14.744Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/c0f367bbd260dbd7a4e134fd21f459e0f5eac43deac507952b46a1d8a93a/levenshtein-0.27.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c08a952432b8ad9dccb145f812176db94c52cda732311ddc08d29fd3bf185b0a", size = 1114538, upload-time = "2025-11-01T12:13:15.851Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ef/ae71433f7b4db0bd2af7974785e36cdec899919203fb82e647c5a6109c07/levenshtein-0.27.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3bfcb2d78ab9cc06a1e75da8fcfb7a430fe513d66cfe54c07e50f32805e5e6db", size = 1009734, upload-time = "2025-11-01T12:13:17.212Z" }, + { url = "https://files.pythonhosted.org/packages/27/dc/62c28b812dcb0953fc32ab7adf3d0e814e43c8560bb28d9269a44d874adf/levenshtein-0.27.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7235f6dcb31a217247468295e2dd4c6c1d3ac81629dc5d355d93e1a5f4c185", size = 1185581, upload-time = "2025-11-01T12:13:18.661Z" }, + { url = "https://files.pythonhosted.org/packages/56/e8/2e7ab9c565793220edb8e5432f9a846386a157075bdd032a90e9585bce38/levenshtein-0.27.3-cp312-cp312-win32.whl", hash = "sha256:ea80d70f1d18c161a209be556b9094968627cbaae620e102459ef9c320a98cbb", size = 84660, upload-time = "2025-11-01T12:13:19.87Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/907a1fc8587dc91c40156973e09d106ab064c06eb28dc4700ba0fe54d654/levenshtein-0.27.3-cp312-cp312-win_amd64.whl", hash = "sha256:fbaa1219d9b2d955339a37e684256a861e9274a3fe3a6ee1b8ea8724c3231ed9", size = 94909, upload-time = "2025-11-01T12:13:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d6/e04f0ddf6a71df3cdd1817b71703490ac874601ed460b2af172d3752c321/levenshtein-0.27.3-cp312-cp312-win_arm64.whl", hash = "sha256:2edbaa84f887ea1d9d8e4440af3fdda44769a7855d581c6248d7ee51518402a8", size = 87358, upload-time = "2025-11-01T12:13:22.393Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f2/162e9ea7490b36bbf05776c8e3a8114c75aa78546ddda8e8f36731db3da6/levenshtein-0.27.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e55aa9f9453fd89d4a9ff1f3c4a650b307d5f61a7eed0568a52fbd2ff2eba107", size = 169230, upload-time = "2025-11-01T12:13:23.735Z" }, + { url = "https://files.pythonhosted.org/packages/01/2d/7316ba7f94e3d60e89bd120526bc71e4812866bb7162767a2a10f73f72c5/levenshtein-0.27.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae4d484453c48939ecd01c5c213530c68dd5cd6e5090f0091ef69799ec7a8a9f", size = 158643, upload-time = "2025-11-01T12:13:25.549Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/85433cb1e51c45016f061d96fea3106b6969f700e2cbb56c15de82d0deeb/levenshtein-0.27.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d18659832567ee387b266be390da0de356a3aa6cf0e8bc009b6042d8188e131f", size = 132881, upload-time = "2025-11-01T12:13:26.822Z" }, + { url = "https://files.pythonhosted.org/packages/40/1c/3ce66c9a7da169a43dd89146d69df9dec935e6f86c70c6404f48d1291d2c/levenshtein-0.27.3-cp313-cp313-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027b3d142cc8ea2ab4e60444d7175f65a94dde22a54382b2f7b47cc24936eb53", size = 114650, upload-time = "2025-11-01T12:13:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/73/60/7138e98884ca105c76ef192f5b43165d6eac6f32b432853ebe9f09ee50c9/levenshtein-0.27.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdca6989368cc64f347f0423c528520f12775b812e170a0eb0c10e4c9b0f3ff", size = 153127, upload-time = "2025-11-01T12:13:29.781Z" }, + { url = "https://files.pythonhosted.org/packages/df/8f/664ac8b83026d7d1382866b68babae17e92b7b6ff8dc3c6205c0066b8ce1/levenshtein-0.27.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa00ab389386032b02a1c9050ec3c6aa824d2bbcc692548fdc44a46b71c058c6", size = 1114602, upload-time = "2025-11-01T12:13:31.651Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c8/8905d96cf2d7ed6af7eb39a8be0925ef335729473c1e9d1f56230ecaffc5/levenshtein-0.27.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:691c9003c6c481b899a5c2f72e8ce05a6d956a9668dc75f2a3ce9f4381a76dc6", size = 1008036, upload-time = "2025-11-01T12:13:33.006Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/01c37608121380a6357a297625562adad1c1fc8058d4f62279b735108927/levenshtein-0.27.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12f7fc8bf0c24492fe97905348e020b55b9fc6dbaab7cd452566d1a466cb5e15", size = 1185338, upload-time = "2025-11-01T12:13:34.452Z" }, + { url = "https://files.pythonhosted.org/packages/dd/57/bceab41d40b58dee7927a8d1d18ed3bff7c95c5e530fb60093ce741a8c26/levenshtein-0.27.3-cp313-cp313-win32.whl", hash = "sha256:9f4872e4e19ee48eed39f214eea4eca42e5ef303f8a4a488d8312370674dbf3a", size = 84562, upload-time = "2025-11-01T12:13:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/42/1d/74f1ff589bb687d0cad2bbdceef208dc070f56d1e38a3831da8c00bf13bb/levenshtein-0.27.3-cp313-cp313-win_amd64.whl", hash = "sha256:83aa2422e9a9af2c9d3e56a53e3e8de6bae58d1793628cae48c4282577c5c2c6", size = 94658, upload-time = "2025-11-01T12:13:36.963Z" }, + { url = "https://files.pythonhosted.org/packages/21/3c/22c86d3c8f254141096fd6089d2e9fdf98b1472c7a5d79d36d3557ec2d83/levenshtein-0.27.3-cp313-cp313-win_arm64.whl", hash = "sha256:d4adaf1edbcf38c3f2e290b52f4dcb5c6deff20308c26ef1127a106bc2d23e9f", size = 86929, upload-time = "2025-11-01T12:13:37.997Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bc/9b7cf1b5fa098b86844d42de22549304699deff309c5c9e28b9a3fc4076a/levenshtein-0.27.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:272e24764b8210337b65a1cfd69ce40df5d2de1a3baf1234e7f06d2826ba2e7a", size = 170360, upload-time = "2025-11-01T12:13:39.019Z" }, + { url = "https://files.pythonhosted.org/packages/dc/95/997f2c83bd4712426bf0de8143b5e4403c7ebbafb5d1271983e774de3ae7/levenshtein-0.27.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:329a8e748a4e14d56daaa11f07bce3fde53385d05bad6b3f6dd9ee7802cdc915", size = 159098, upload-time = "2025-11-01T12:13:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/fc/96/123c3316ae2f72c73be4fba9756924af015da4c0e5b12804f5753c0ee511/levenshtein-0.27.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5fea1a9c6b9cc8729e467e2174b4359ff6bac27356bb5f31898e596b4ce133a", size = 136655, upload-time = "2025-11-01T12:13:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/45/72/a3180d437736b1b9eacc3100be655a756deafb91de47c762d40eb45a9d91/levenshtein-0.27.3-cp313-cp313t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3a61aa825819b6356555091d8a575d1235bd9c3753a68316a261af4856c3b487", size = 117511, upload-time = "2025-11-01T12:13:42.647Z" }, + { url = "https://files.pythonhosted.org/packages/61/f9/ba7c546a4b99347938e6661104064ab6a3651c601d59f241ffdc37510ecc/levenshtein-0.27.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51de7a514e8183f0a82f2947d01b014d2391426543b1c076bf5a26328cec4e4", size = 155656, upload-time = "2025-11-01T12:13:44.208Z" }, + { url = "https://files.pythonhosted.org/packages/42/cd/5edd6e1e02c3e47c8121761756dd0f85f816b636f25509118b687e6b0f96/levenshtein-0.27.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53cbf726d6e92040c9be7e594d959d496bd62597ea48eba9d96105898acbeafe", size = 1116689, upload-time = "2025-11-01T12:13:45.485Z" }, + { url = "https://files.pythonhosted.org/packages/95/67/25ca0119e0c6ec17226c72638f48ef8887124597ac48ad5da111c0b3a825/levenshtein-0.27.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:191b358afead8561c4fcfed22f83c13bb6c8da5f5789e277f0c5aa1c45ca612f", size = 1003166, upload-time = "2025-11-01T12:13:47.126Z" }, + { url = "https://files.pythonhosted.org/packages/45/64/ab216f3fb3cef1ee7e222665537f9340d828ef84c99409ba31f2ef2a3947/levenshtein-0.27.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ba1318d0635b834b8f0397014a7c43f007e65fce396a47614780c881bdff828b", size = 1189362, upload-time = "2025-11-01T12:13:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/31/58/b150034858de0899a5a222974b6710618ebc0779a0695df070f7ab559a0b/levenshtein-0.27.3-cp313-cp313t-win32.whl", hash = "sha256:8dd9e1db6c3b35567043e155a686e4827c4aa28a594bd81e3eea84d3a1bd5875", size = 86149, upload-time = "2025-11-01T12:13:50.588Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c4/bbe46a11073641450200e6a604b3b62d311166e8061c492612a40e560e85/levenshtein-0.27.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7813ecdac7a6223264ebfea0c8d69959c43d21a99694ef28018d22c4265c2af6", size = 96685, upload-time = "2025-11-01T12:13:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/23/65/30b362ad9bfc1085741776a08b6ddee3f434e9daac2920daaee2e26271bf/levenshtein-0.27.3-cp313-cp313t-win_arm64.whl", hash = "sha256:8f05a0d23d13a6f802c7af595d0e43f5b9b98b6ed390cec7a35cb5d6693b882b", size = 88538, upload-time = "2025-11-01T12:13:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e1/2f705da403f865a5fa3449b155738dc9c53021698fd6926253a9af03180b/levenshtein-0.27.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a6728bfae9a86002f0223576675fc7e2a6e7735da47185a1d13d1eaaa73dd4be", size = 169457, upload-time = "2025-11-01T12:13:53.778Z" }, + { url = "https://files.pythonhosted.org/packages/76/2c/bb6ef359e007fe7b6b3195b68a94f4dd3ecd1885ee337ee8fbd4df55996f/levenshtein-0.27.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8e5037c4a6f97a238e24aad6f98a1e984348b7931b1b04b6bd02bd4f8238150d", size = 158680, upload-time = "2025-11-01T12:13:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/51/7b/de1999f4cf1cfebc3fbbf03a6d58498952d6560d9798af4b0a566e6b6f30/levenshtein-0.27.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6cf5ecf9026bf24cf66ad019c6583f50058fae3e1b3c20e8812455b55d597f1", size = 133167, upload-time = "2025-11-01T12:13:56.426Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/aaa7f3a0a8ae8744b284043653652db3d7d93595517f9ed8158c03287692/levenshtein-0.27.3-cp314-cp314-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9285084bd2fc19adb47dab54ed4a71f57f78fe0d754e4a01e3c75409a25aed24", size = 114530, upload-time = "2025-11-01T12:13:57.883Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/ed422816fb30ffa3bc11597b30d5deca06b4a1388707a04215da73c65b53/levenshtein-0.27.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce3bbbe92172a08b599d79956182c6b7ab6ec8d4adbe7237417a363b968ad87b", size = 153325, upload-time = "2025-11-01T12:13:59.318Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5a/a225477a0bda154f19f1c07a5e35500d631ae25dfd620b479027d79f0d4c/levenshtein-0.27.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9dac48fab9d166ca90e12fb6cf6c7c8eb9c41aacf7136584411e20f7f136f745", size = 1114956, upload-time = "2025-11-01T12:14:00.543Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c4/a1be1040f3cce516a5e2be68453fd0c32ac63b2e9d31f476723fd8002c09/levenshtein-0.27.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d37a83722dc5326c93d17078e926c4732dc4f3488dc017c6839e34cd16af92b7", size = 1007610, upload-time = "2025-11-01T12:14:02.036Z" }, + { url = "https://files.pythonhosted.org/packages/86/d7/6f50e8a307e0c2befd819b481eb3a4c2eacab3dd8101982423003fac8ea3/levenshtein-0.27.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3466cb8294ce586e49dd467560a153ab8d296015c538223f149f9aefd3d9f955", size = 1185379, upload-time = "2025-11-01T12:14:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e5/5d8fb1b3ebd5735f53221bf95c923066bcfc132234925820128f7eee5b47/levenshtein-0.27.3-cp314-cp314-win32.whl", hash = "sha256:c848bf2457b268672b7e9e73b44f18f49856420ac50b2564cf115a6e4ef82688", size = 86328, upload-time = "2025-11-01T12:14:04.74Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/8a9ccbdb4e38bd4d516f2804999dccb8cb4bcb4e33f52851735da0c73ea7/levenshtein-0.27.3-cp314-cp314-win_amd64.whl", hash = "sha256:742633f024362a4ed6ef9d7e75d68f74b041ae738985fcf55a0e6d1d4cade438", size = 96640, upload-time = "2025-11-01T12:14:06.24Z" }, + { url = "https://files.pythonhosted.org/packages/14/86/f9d15919f59f5d92c6baa500315e1fa0143a39d811427b83c54f038267ca/levenshtein-0.27.3-cp314-cp314-win_arm64.whl", hash = "sha256:9eed6851224b19e8d588ddb8eb8a4ae3c2dcabf3d1213985f0b94a67e517b1df", size = 89689, upload-time = "2025-11-01T12:14:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f6/10f44975ae6dc3047b2cd260e3d4c3a5258b8d10690a42904115de24fc51/levenshtein-0.27.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:77de69a345c76227b51a4521cd85442eb3da54c7eb6a06663a20c058fc49e683", size = 170518, upload-time = "2025-11-01T12:14:09.196Z" }, + { url = "https://files.pythonhosted.org/packages/08/07/fa294a145a0c99a814a9a807614962c1ee0f5749ca691645980462027d5d/levenshtein-0.27.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eba2756dc1f5b962b0ff80e49abb2153d5e809cc5e7fa5e85be9410ce474795d", size = 159097, upload-time = "2025-11-01T12:14:10.404Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/24bdf37813fc30f293e53b46022b091144f4737a6a66663d2235b311bb98/levenshtein-0.27.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c8fcb498287e971d84260f67808ff1a06b3f6212d80fea75cf5155db80606ff", size = 136650, upload-time = "2025-11-01T12:14:11.579Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a9/0399c7a190b277cdea3acc801129d9d30da57c3fa79519e7b8c3f080d86c/levenshtein-0.27.3-cp314-cp314t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f067092c67464faab13e00a5c1a80da93baca8955d4d49579861400762e35591", size = 117515, upload-time = "2025-11-01T12:14:12.877Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a4/1c27533e97578b385a4b8079abe8d1ce2e514717c761efbe4bf7bbd0ac2e/levenshtein-0.27.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92415f32c68491203f2855d05eef3277d376182d014cf0859c013c89f277fbbf", size = 155711, upload-time = "2025-11-01T12:14:13.985Z" }, + { url = "https://files.pythonhosted.org/packages/50/35/bbc26638394a72b1e31a685ec251c995ee66a630c7e5c86f98770928b632/levenshtein-0.27.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ef61eeaf1e0a42d7d947978d981fe4b9426b98b3dd8c1582c535f10dee044c3f", size = 1116692, upload-time = "2025-11-01T12:14:15.359Z" }, + { url = "https://files.pythonhosted.org/packages/cd/83/32fcf28b388f8dc6c36b54552b9bae289dab07d43df104893158c834cbcc/levenshtein-0.27.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:103bb2e9049d1aa0d1216dd09c1c9106ecfe7541bbdc1a0490b9357d42eec8f2", size = 1003167, upload-time = "2025-11-01T12:14:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/d1/79/1fbf2877ec4b819f373a32ebe3c48a61ee810693593a6015108b0be97b78/levenshtein-0.27.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a64ddd1986b2a4c468b09544382287315c53585eb067f6e200c337741e057ee", size = 1189417, upload-time = "2025-11-01T12:14:19.081Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/dad4e09f1f7459c64172e48e40ed2baf3aa92d38205bcbd1b4ff00853701/levenshtein-0.27.3-cp314-cp314t-win32.whl", hash = "sha256:957244f27dc284ccb030a8b77b8a00deb7eefdcd70052a4b1d96f375780ae9dc", size = 88144, upload-time = "2025-11-01T12:14:20.667Z" }, + { url = "https://files.pythonhosted.org/packages/c0/61/cd51dc8b8a382e17c559a9812734c3a9afc2dab7d36253516335ee16ae50/levenshtein-0.27.3-cp314-cp314t-win_amd64.whl", hash = "sha256:ccd7eaa6d8048c3ec07c93cfbcdefd4a3ae8c6aca3a370f2023ee69341e5f076", size = 98516, upload-time = "2025-11-01T12:14:21.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/5e/3fb67e882c1fee01ebb7abc1c0a6669e5ff8acd060e93bfe7229e9ce6e4f/levenshtein-0.27.3-cp314-cp314t-win_arm64.whl", hash = "sha256:1d8520b89b7a27bb5aadbcc156715619bcbf556a8ac46ad932470945dca6e1bd", size = 91020, upload-time = "2025-11-01T12:14:22.944Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/21983893d3f40c6990e2e51c02dd48cfca350a36214be90d7c58f5f85896/levenshtein-0.27.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d2d7d22b6117a143f0cf101fe18a3ca90bd949fc33716a42d6165b9768d4a78c", size = 166073, upload-time = "2025-11-01T12:14:24.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/52deb821ebf0cfc61baf7c9ebc5601649cfbfdaaaf156867786d1c5332d5/levenshtein-0.27.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:a55e7a2f317abd28576636e1f840fd268261f447c496a8481a9997a5ce889c59", size = 153629, upload-time = "2025-11-01T12:14:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/60/0c/b72e6e2d16efd57c143785a30370ca50c2e355a9d0d678edb1c024865447/levenshtein-0.27.3-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa5f11952c38186bd4719e936eb4595b3d519218634924928787c36840256c", size = 130242, upload-time = "2025-11-01T12:14:26.926Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b0/0aafad0dab03a58fd507773d3ff94ec13efdd3772ba217f85366213ab7ae/levenshtein-0.27.3-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:559d3588e6766134d95f84f830cf40166360e1769d253f5f83474bff10a24341", size = 150655, upload-time = "2025-11-01T12:14:28.034Z" }, + { url = "https://files.pythonhosted.org/packages/b7/77/42dbcbafe9e0b0eb14cb6b08378c8c3bdc563ee34ee58f62e708e7f8956e/levenshtein-0.27.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:82d40da143c1b9e27adcd34a33dfcc4a0761aa717c5f618b9c6f57dec5d7a958", size = 92370, upload-time = "2025-11-01T12:14:29.143Z" }, +] + +[[package]] +name = "mariadb" +version = "1.0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/0a/ce5724f852f2937c6955bcea09659aa2d85d487df1c9de6711344b71527d/mariadb-1.0.11.zip", hash = "sha256:76916c892bc936c5b0f36e25a1411f651a7b7ce978992ae3a8de7e283efdacbf", size = 85926, upload-time = "2022-04-12T19:33:17.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/4a/6a6b7ad6a7b8156b41d0a6d849debe804f9d8696288ed2c6c31a9654357c/mariadb-1.0.11-cp310-cp310-win32.whl", hash = "sha256:4a583a80059d11f1895a2c93b1b7110948e87f0256da3e3222939a2530f0518e", size = 159743, upload-time = "2022-04-12T19:33:04.712Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a7/10740ceec1ce9d57f4bc3614e55efe2e72ae284e8c8d32eacabfbd7ad6cc/mariadb-1.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:63c5c7cf99335e5c961e1d65a323576c9cb834e1a6a8084a6a8b4ffd85ca6213", size = 177316, upload-time = "2022-04-12T19:33:06.95Z" }, +] + [[package]] name = "mypy" version = "1.18.2" @@ -113,6 +354,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "mysql-connector-python" +version = "9.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/33/b332b001bc8c5ee09255a0d4b09a254da674450edd6a3e5228b245ca82a0/mysql_connector_python-9.5.0.tar.gz", hash = "sha256:92fb924285a86d8c146ebd63d94f9eaefa548da7813bc46271508fdc6cc1d596", size = 12251077, upload-time = "2025-10-22T09:05:45.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/5d/30210fcf7ba98d1e03de0c47a58218ab5313d82f2e01ae53b47f45c36b9d/mysql_connector_python-9.5.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:77d14c9fde90726de22443e8c5ba0912a4ebb632cc1ade52a349dacbac47b140", size = 17579085, upload-time = "2025-10-22T09:01:27.388Z" }, + { url = "https://files.pythonhosted.org/packages/77/92/ea79a0875436665330a81e82b4b73a6d52aebcfb1cf4d97f4ad4bd4dedf5/mysql_connector_python-9.5.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:4d603b55de310b9689bb3cb5e57fe97e98756e36d62f8f308f132f2c724f62b8", size = 18445098, upload-time = "2025-10-22T09:01:29.721Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f2/4578b5093f46985c659035e880e70e8b0bed44d4a59ad4e83df5d49b9c69/mysql_connector_python-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:48ffa71ba748afaae5c45ed9a085a72604368ce611fe81c3fdc146ef60181d51", size = 33660118, upload-time = "2025-10-22T09:01:32.048Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/63135610ae0cee1260ce64874c1ddbf08e7fb560c21a3d9cce88b0ddc266/mysql_connector_python-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:77c71df48293d3c08713ff7087cf483804c8abf41a4bb4aefea7317b752c8e9a", size = 34096212, upload-time = "2025-10-22T09:01:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b1/78dc693552cfbb45076b3638ca4c402fae52209af8f276370d02d78367a0/mysql_connector_python-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:4f8d2d9d586c34dc9508a44d19cf30ccafabbbd12d7f8ab58da3af118636843c", size = 16512395, upload-time = "2025-10-22T09:01:38.602Z" }, + { url = "https://files.pythonhosted.org/packages/05/03/77347d58b0027ce93a41858477e08422e498c6ebc24348b1f725ed7a67ae/mysql_connector_python-9.5.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:653e70cd10cf2d18dd828fae58dff5f0f7a5cf7e48e244f2093314dddf84a4b9", size = 17578984, upload-time = "2025-10-22T09:01:41.213Z" }, + { url = "https://files.pythonhosted.org/packages/a5/bb/0f45c7ee55ebc56d6731a593d85c0e7f25f83af90a094efebfd5be9fe010/mysql_connector_python-9.5.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:5add93f60b3922be71ea31b89bc8a452b876adbb49262561bd559860dae96b3f", size = 18445067, upload-time = "2025-10-22T09:01:43.215Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ec/054de99d4aa50d851a37edca9039280f7194cc1bfd30aab38f5bd6977ebe/mysql_connector_python-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:20950a5e44896c03e3dc93ceb3a5e9b48c9acae18665ca6e13249b3fe5b96811", size = 33668029, upload-time = "2025-10-22T09:01:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/90/a2/e6095dc3a7ad5c959fe4a65681db63af131f572e57cdffcc7816bc84e3ad/mysql_connector_python-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7fdd3205b9242c284019310fa84437f3357b13f598e3f9b5d80d337d4a6406b8", size = 34101687, upload-time = "2025-10-22T09:01:48.462Z" }, + { url = "https://files.pythonhosted.org/packages/9c/88/bc13c33fca11acaf808bd1809d8602d78f5bb84f7b1e7b1a288c383a14fd/mysql_connector_python-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c021d8b0830958b28712c70c53b206b4cf4766948dae201ea7ca588a186605e0", size = 16511749, upload-time = "2025-10-22T09:01:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/02/89/167ebee82f4b01ba7339c241c3cc2518886a2be9f871770a1efa81b940a0/mysql_connector_python-9.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72c2ef9d50b84f3c567c31b3bf30901af740686baa2a4abead5f202e0b7ea61", size = 17581904, upload-time = "2025-10-22T09:01:53.21Z" }, + { url = "https://files.pythonhosted.org/packages/67/46/630ca969ce10b30fdc605d65dab4a6157556d8cc3b77c724f56c2d83cb79/mysql_connector_python-9.5.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bd9ba5a946cfd3b3b2688a75135357e862834b0321ed936fd968049be290872b", size = 18448195, upload-time = "2025-10-22T09:01:55.378Z" }, + { url = "https://files.pythonhosted.org/packages/f6/87/4c421f41ad169d8c9065ad5c46673c7af889a523e4899c1ac1d6bfd37262/mysql_connector_python-9.5.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5ef7accbdf8b5f6ec60d2a1550654b7e27e63bf6f7b04020d5fb4191fb02bc4d", size = 33668638, upload-time = "2025-10-22T09:01:57.896Z" }, + { url = "https://files.pythonhosted.org/packages/a6/01/67cf210d50bfefbb9224b9a5c465857c1767388dade1004c903c8e22a991/mysql_connector_python-9.5.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a6e0a4a0274d15e3d4c892ab93f58f46431222117dba20608178dfb2cc4d5fd8", size = 34102899, upload-time = "2025-10-22T09:02:00.291Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ef/3d1a67d503fff38cc30e11d111cf28f0976987fb175f47b10d44494e1080/mysql_connector_python-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:b6c69cb37600b7e22f476150034e2afbd53342a175e20aea887f8158fc5e3ff6", size = 16512684, upload-time = "2025-10-22T09:02:02.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/18/f221aeac49ce94ac119a427afbd51fe1629d48745b571afc0de49647b528/mysql_connector_python-9.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1f5f7346b0d5edb2e994c1bd77b3f5eed88b0ca368ad6788d1012c7e56d7bf68", size = 17581933, upload-time = "2025-10-22T09:02:04.396Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/14d44db7353350006a12e46d61c3a995bba06acd7547fc78f9bb32611e0c/mysql_connector_python-9.5.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:07bf52591b4215cb4318b4617c327a6d84c31978c11e3255f01a627bcda2618e", size = 18448446, upload-time = "2025-10-22T09:02:06.399Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f5/ab306f292a99bff3544ff44ad53661a031dc1a11e5b1ad64b9e5b5290ef9/mysql_connector_python-9.5.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:8972c1f960b30d487f34f9125ec112ea2b3200bd02c53e5e32ee7a43be6d64c1", size = 33668933, upload-time = "2025-10-22T09:02:08.785Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ee/d146d2642552ebb5811cf551f06aca7da536c80b18fb6c75bdbc29723388/mysql_connector_python-9.5.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:f6d32d7aa514d2f6f8709ba1e018314f82ab2acea2e6af30d04c1906fe9171b9", size = 34103214, upload-time = "2025-10-22T09:02:11.657Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/5e88e5eda1fe58f7d146b73744f691d85dce76fb42e7ce3de53e49911da3/mysql_connector_python-9.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:edd47048eb65c196b28aa9d2c0c6a017d8ca084a9a7041cd317301c829eb5a05", size = 16512689, upload-time = "2025-10-22T09:02:14.167Z" }, + { url = "https://files.pythonhosted.org/packages/14/42/52bef145028af1b8e633eb77773278a04b2cd9f824117209aba093018445/mysql_connector_python-9.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:6effda35df1a96d9a096f04468d40f2324ea36b34d0e9632e81daae8be97b308", size = 17581903, upload-time = "2025-10-22T09:02:16.441Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a6/bd800b42bde86bf2e9468dfabcbd7538c66daff9d1a9fc97d2cc897f96fa/mysql_connector_python-9.5.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:fd057bd042464eedbf5337d1ceea7f2a4ab075a1cf6d1d62ffd5184966a656dd", size = 18448394, upload-time = "2025-10-22T09:02:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/4a/21/a1a3247775d0dfee094499cb915560755eaa1013ac3b03e34a98b0e16e49/mysql_connector_python-9.5.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2797dd7bbefb1d1669d984cfb284ea6b34401bbd9c1b3bf84e646d0bd3a82197", size = 33669845, upload-time = "2025-10-22T09:02:20.966Z" }, + { url = "https://files.pythonhosted.org/packages/58/b7/dcab48349ab8abafd6f40f113101549e0cf107e43dd9c7e1fae79799604b/mysql_connector_python-9.5.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:a5fff063ed48281b7374a4da6b9ef4293d390c153f79b1589ee547ea08c92310", size = 34104103, upload-time = "2025-10-22T09:02:23.469Z" }, + { url = "https://files.pythonhosted.org/packages/21/3a/be129764fe5f5cd89a5aa3f58e7a7471284715f4af71097a980d24ebec0a/mysql_connector_python-9.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:56104693478fd447886c470a6d0558ded0fe2577df44c18232a6af6a2bbdd3e9", size = 17001255, upload-time = "2025-10-22T09:02:25.765Z" }, + { url = "https://files.pythonhosted.org/packages/95/e1/45373c06781340c7b74fe9b88b85278ac05321889a307eaa5be079a997d4/mysql_connector_python-9.5.0-py2.py3-none-any.whl", hash = "sha256:ace137b88eb6fdafa1e5b2e03ac76ce1b8b1844b3a4af1192a02ae7c1a45bdee", size = 479047, upload-time = "2025-10-22T09:02:27.809Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -140,6 +415,139 @@ wheels = [ { 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 = "pydantic" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -180,6 +588,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl", hash = "sha256:d7e5b7198f9b83c795377c09feefa45d56083834e60d04767efd64819fc9da00", size = 6251, upload-time = "2025-10-09T19:15:46.077Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-levenshtein" +version = "0.27.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "levenshtein" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/b4/36eda4188dd19d3cb53d8a8749d7520bd23dfe1c1f44e56ea9dcd0232274/python_levenshtein-0.27.3.tar.gz", hash = "sha256:27dc2d65aeb62a7d6852388f197073296370779286c0860b087357f3b8129a62", size = 12446, upload-time = "2025-11-01T12:54:59.712Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/5b/26e3cca2589252ceabf964ba81514e6f48556553c9c2766e1a0fdceec696/python_levenshtein-0.27.3-py3-none-any.whl", hash = "sha256:5d6168a8e8befb25abf04d2952368a446722be10e8ced218d0dc4fd3703a43a1", size = 9504, upload-time = "2025-11-01T12:54:58.933Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -244,6 +676,105 @@ wheels = [ { 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 = "rapidfuzz" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/d1/0efa42a602ed466d3ca1c462eed5d62015c3fd2a402199e2c4b87aa5aa25/rapidfuzz-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9fcd4d751a4fffa17aed1dde41647923c72c74af02459ad1222e3b0022da3a1", size = 1952376, upload-time = "2025-11-01T11:52:29.175Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/37a169bb28b23850a164e6624b1eb299e1ad73c9e7c218ee15744e68d628/rapidfuzz-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ad73afb688b36864a8d9b7344a9cf6da186c471e5790cbf541a635ee0f457f2", size = 1390903, upload-time = "2025-11-01T11:52:31.239Z" }, + { url = "https://files.pythonhosted.org/packages/3c/91/b37207cbbdb6eaafac3da3f55ea85287b27745cb416e75e15769b7d8abe8/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5fb2d978a601820d2cfd111e2c221a9a7bfdf84b41a3ccbb96ceef29f2f1ac7", size = 1385655, upload-time = "2025-11-01T11:52:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/ca53e518acf43430be61f23b9c5987bd1e01e74fcb7a9ee63e00f597aefb/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d83b8b712fa37e06d59f29a4b49e2e9e8635e908fbc21552fe4d1163db9d2a1", size = 3164708, upload-time = "2025-11-01T11:52:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/df/e1/7667bf2db3e52adb13cb933dd4a6a2efc66045d26fa150fc0feb64c26d61/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:dc8c07801df5206b81ed6bd6c35cb520cf9b6c64b9b0d19d699f8633dc942897", size = 1221106, upload-time = "2025-11-01T11:52:36.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/8a/84d9f2d46a2c8eb2ccae81747c4901fa10fe4010aade2d57ce7b4b8e02ec/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c71ce6d4231e5ef2e33caa952bfe671cb9fd42e2afb11952df9fad41d5c821f9", size = 2406048, upload-time = "2025-11-01T11:52:37.936Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a9/a0b7b7a1b81a020c034eb67c8e23b7e49f920004e295378de3046b0d99e1/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0e38828d1381a0cceb8a4831212b2f673d46f5129a1897b0451c883eaf4a1747", size = 2527020, upload-time = "2025-11-01T11:52:39.657Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/416df7d108b99b4942ba04dd4cf73c45c3aadb3ef003d95cad78b1d12eb9/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da2a007434323904719158e50f3076a4dadb176ce43df28ed14610c773cc9825", size = 4273958, upload-time = "2025-11-01T11:52:41.017Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/b81e041c17cd475002114e0ab8800e4305e60837882cb376a621e520d70f/rapidfuzz-3.14.3-cp310-cp310-win32.whl", hash = "sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9", size = 1725043, upload-time = "2025-11-01T11:52:42.465Z" }, + { url = "https://files.pythonhosted.org/packages/09/6b/64ad573337d81d64bc78a6a1df53a72a71d54d43d276ce0662c2e95a1f35/rapidfuzz-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:37d3c653af15cd88592633e942f5407cb4c64184efab163c40fcebad05f25141", size = 1542273, upload-time = "2025-11-01T11:52:44.005Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5e/faf76e259bc15808bc0b86028f510215c3d755b6c3a3911113079485e561/rapidfuzz-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:cc594bbcd3c62f647dfac66800f307beaee56b22aaba1c005e9c4c40ed733923", size = 814875, upload-time = "2025-11-01T11:52:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" }, + { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" }, + { url = "https://files.pythonhosted.org/packages/ed/69/309d8f3a0bb3031fd9b667174cc4af56000645298af7c2931be5c3d14bb4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e", size = 3178495, upload-time = "2025-11-01T11:52:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/f9c44a99269ea5bf6fd6a40b84e858414b6e241288b9f2b74af470d222b1/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae", size = 1228443, upload-time = "2025-11-01T11:52:54.991Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0a/3b3137abac7f19c9220e14cd7ce993e35071a7655e7ef697785a3edfea1a/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63", size = 2411998, upload-time = "2025-11-01T11:52:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b6/983805a844d44670eaae63831024cdc97ada4e9c62abc6b20703e81e7f9b/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094", size = 2530120, upload-time = "2025-11-01T11:52:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cc/2c97beb2b1be2d7595d805682472f1b1b844111027d5ad89b65e16bdbaaa/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678", size = 4283129, upload-time = "2025-11-01T11:53:00.188Z" }, + { url = "https://files.pythonhosted.org/packages/4d/03/2f0e5e94941045aefe7eafab72320e61285c07b752df9884ce88d6b8b835/rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91", size = 1724224, upload-time = "2025-11-01T11:53:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/5fa23e204435803875daefda73fd61baeabc3c36b8fc0e34c1705aab8c7b/rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5", size = 1544259, upload-time = "2025-11-01T11:53:03.66Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/d657b85fcc615a42661b98ac90ce8e95bd32af474603a105643963749886/rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7", size = 814734, upload-time = "2025-11-01T11:53:05.008Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" }, + { url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" }, + { url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" }, + { url = "https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c", size = 3156759, upload-time = "2025-11-01T11:53:30.777Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/b41eeb4930501cc899d5a9a7b5c9a33d85a670200d7e81658626dcc0ecc0/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e", size = 1222067, upload-time = "2025-11-01T11:53:32.334Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7d/6d9abb4ffd1027c6ed837b425834f3bed8344472eb3a503ab55b3407c721/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10", size = 2394775, upload-time = "2025-11-01T11:53:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/4f3ab4c401c5a55364da1ffff8cc879fc97b4e5f4fa96033827da491a973/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41", size = 2526123, upload-time = "2025-11-01T11:53:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4b/54f804975376a328f57293bd817c12c9036171d15cf7292032e3f5820b2d/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0", size = 4262874, upload-time = "2025-11-01T11:53:37.866Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b6/958db27d8a29a50ee6edd45d33debd3ce732e7209183a72f57544cd5fe22/rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63", size = 1707972, upload-time = "2025-11-01T11:53:39.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/75/fde1f334b0cec15b5946d9f84d73250fbfcc73c236b4bc1b25129d90876b/rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c", size = 1537011, upload-time = "2025-11-01T11:53:40.92Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d7/d83fe001ce599dc7ead57ba1debf923dc961b6bdce522b741e6b8c82f55c/rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0", size = 810744, upload-time = "2025-11-01T11:53:42.723Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/a486369e63ff3c1a58444d16b15c5feb943edd0e6c28a1d7d67cb8946b8f/rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424", size = 1967702, upload-time = "2025-11-01T11:53:44.554Z" }, + { url = "https://files.pythonhosted.org/packages/f1/82/efad25e260b7810f01d6b69122685e355bed78c94a12784bac4e0beb2afb/rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e", size = 1410702, upload-time = "2025-11-01T11:53:46.066Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1a/34c977b860cde91082eae4a97ae503f43e0d84d4af301d857679b66f9869/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8", size = 1382337, upload-time = "2025-11-01T11:53:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/88/74/f50ea0e24a5880a9159e8fd256b84d8f4634c2f6b4f98028bdd31891d907/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519", size = 3165563, upload-time = "2025-11-01T11:53:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7a/e744359404d7737049c26099423fc54bcbf303de5d870d07d2fb1410f567/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a", size = 1214727, upload-time = "2025-11-01T11:53:50.883Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2e/87adfe14ce75768ec6c2b8acd0e05e85e84be4be5e3d283cdae360afc4fe/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897", size = 2403349, upload-time = "2025-11-01T11:53:52.322Z" }, + { url = "https://files.pythonhosted.org/packages/70/17/6c0b2b2bff9c8b12e12624c07aa22e922b0c72a490f180fa9183d1ef2c75/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58", size = 2507596, upload-time = "2025-11-01T11:53:53.835Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d1/87852a7cbe4da7b962174c749a47433881a63a817d04f3e385ea9babcd9e/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f", size = 4273595, upload-time = "2025-11-01T11:53:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/1d0354b7d1771a28fa7fe089bc23acec2bdd3756efa2419f463e3ed80e16/rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204", size = 1757773, upload-time = "2025-11-01T11:53:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0c/71ef356adc29e2bdf74cd284317b34a16b80258fa0e7e242dd92cc1e6d10/rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15", size = 1576797, upload-time = "2025-11-01T11:53:59.455Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d2/0e64fc27bb08d4304aa3d11154eb5480bcf5d62d60140a7ee984dc07468a/rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317", size = 829940, upload-time = "2025-11-01T11:54:01.1Z" }, + { url = "https://files.pythonhosted.org/packages/32/6f/1b88aaeade83abc5418788f9e6b01efefcd1a69d65ded37d89cd1662be41/rapidfuzz-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:442125473b247227d3f2de807a11da6c08ccf536572d1be943f8e262bae7e4ea", size = 1942086, upload-time = "2025-11-01T11:54:02.592Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/b23861347436cb10f46c2bd425489ec462790faaa360a54a7ede5f78de88/rapidfuzz-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ec0c8c0c3d4f97ced46b2e191e883f8c82dbbf6d5ebc1842366d7eff13cd5a6", size = 1386993, upload-time = "2025-11-01T11:54:04.12Z" }, + { url = "https://files.pythonhosted.org/packages/83/86/5d72e2c060aa1fbdc1f7362d938f6b237dff91f5b9fc5dd7cc297e112250/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dc37bc20272f388b8c3a4eba4febc6e77e50a8f450c472def4751e7678f55e4", size = 1379126, upload-time = "2025-11-01T11:54:05.777Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/ef2cee3e4d8b3fc22705ff519f0d487eecc756abdc7c25d53686689d6cf2/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee362e7e79bae940a5e2b3f6d09c6554db6a4e301cc68343886c08be99844f1", size = 3159304, upload-time = "2025-11-01T11:54:07.351Z" }, + { url = "https://files.pythonhosted.org/packages/a0/36/dc5f2f62bbc7bc90be1f75eeaf49ed9502094bb19290dfb4747317b17f12/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:4b39921df948388a863f0e267edf2c36302983459b021ab928d4b801cbe6a421", size = 1218207, upload-time = "2025-11-01T11:54:09.641Z" }, + { url = "https://files.pythonhosted.org/packages/df/7e/8f4be75c1bc62f47edf2bbbe2370ee482fae655ebcc4718ac3827ead3904/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beda6aa9bc44d1d81242e7b291b446be352d3451f8217fcb068fc2933927d53b", size = 2401245, upload-time = "2025-11-01T11:54:11.543Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/f7c92759e1bb188dd05b80d11c630ba59b8d7856657baf454ff56059c2ab/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6a014ba09657abfcfeed64b7d09407acb29af436d7fc075b23a298a7e4a6b41c", size = 2518308, upload-time = "2025-11-01T11:54:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ac/85820f70fed5ecb5f1d9a55f1e1e2090ef62985ef41db289b5ac5ec56e28/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32eeafa3abce138bb725550c0e228fc7eaeec7059aa8093d9cbbec2b58c2371a", size = 4265011, upload-time = "2025-11-01T11:54:15.087Z" }, + { url = "https://files.pythonhosted.org/packages/46/a9/616930721ea9835c918af7cde22bff17f9db3639b0c1a7f96684be7f5630/rapidfuzz-3.14.3-cp314-cp314-win32.whl", hash = "sha256:adb44d996fc610c7da8c5048775b21db60dd63b1548f078e95858c05c86876a3", size = 1742245, upload-time = "2025-11-01T11:54:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f2fa5e9635b1ccafda4accf0e38246003f69982d7c81f2faa150014525a4/rapidfuzz-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:f3d15d8527e2b293e38ce6e437631af0708df29eafd7c9fc48210854c94472f9", size = 1584856, upload-time = "2025-11-01T11:54:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/09e20663917678a6d60d8e0e29796db175b1165e2079830430342d5298be/rapidfuzz-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:576e4b9012a67e0bf54fccb69a7b6c94d4e86a9540a62f1a5144977359133583", size = 833490, upload-time = "2025-11-01T11:54:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/03/1b/6b6084576ba87bf21877c77218a0c97ba98cb285b0c02eaaee3acd7c4513/rapidfuzz-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cec3c0da88562727dd5a5a364bd9efeb535400ff0bfb1443156dd139a1dd7b50", size = 1968658, upload-time = "2025-11-01T11:54:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/fb02a0db80d95704b0a6469cc394e8c38501abf7e1c0b2afe3261d1510c2/rapidfuzz-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d1fa009f8b1100e4880868137e7bf0501422898f7674f2adcd85d5a67f041296", size = 1410742, upload-time = "2025-11-01T11:54:23.863Z" }, + { url = "https://files.pythonhosted.org/packages/a4/72/3fbf12819fc6afc8ec75a45204013b40979d068971e535a7f3512b05e765/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b86daa7419b5e8b180690efd1fdbac43ff19230803282521c5b5a9c83977655", size = 1382810, upload-time = "2025-11-01T11:54:25.571Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/0f1991d59bb7eee28922a00f79d83eafa8c7bfb4e8edebf4af2a160e7196/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7bd1816db05d6c5ffb3a4df0a2b7b56fb8c81ef584d08e37058afa217da91b1", size = 3166349, upload-time = "2025-11-01T11:54:27.195Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/baa958b1989c8f88c78bbb329e969440cf330b5a01a982669986495bb980/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:33da4bbaf44e9755b0ce192597f3bde7372fe2e381ab305f41b707a95ac57aa7", size = 1214994, upload-time = "2025-11-01T11:54:28.821Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a0/cd12ec71f9b2519a3954febc5740291cceabc64c87bc6433afcb36259f3b/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3fecce764cf5a991ee2195a844196da840aba72029b2612f95ac68a8b74946bf", size = 2403919, upload-time = "2025-11-01T11:54:30.393Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ce/019bd2176c1644098eced4f0595cb4b3ef52e4941ac9a5854f209d0a6e16/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ecd7453e02cf072258c3a6b8e930230d789d5d46cc849503729f9ce475d0e785", size = 2508346, upload-time = "2025-11-01T11:54:32.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/f8/be16c68e2c9e6c4f23e8f4adbb7bccc9483200087ed28ff76c5312da9b14/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea188aa00e9bcae8c8411f006a5f2f06c4607a02f24eab0d8dc58566aa911f35", size = 4274105, upload-time = "2025-11-01T11:54:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/5ab148e03f7e6ec8cd220ccf7af74d3aaa4de26dd96df58936beb7cba820/rapidfuzz-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:7ccbf68100c170e9a0581accbe9291850936711548c6688ce3bfb897b8c589ad", size = 1793465, upload-time = "2025-11-01T11:54:35.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/433b2d98e97abd9fff1c470a109b311669f44cdec8d0d5aa250aceaed1fb/rapidfuzz-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9ec02e62ae765a318d6de38df609c57fc6dacc65c0ed1fd489036834fd8a620c", size = 1623491, upload-time = "2025-11-01T11:54:38.085Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/e2176eb94f94892441bce3ddc514c179facb65db245e7ce3356965595b19/rapidfuzz-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e805e52322ae29aa945baf7168b6c898120fbc16d2b8f940b658a5e9e3999253", size = 851487, upload-time = "2025-11-01T11:54:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/c9/33/b5bd6475c7c27164b5becc9b0e3eb978f1e3640fea590dd3dced6006ee83/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23", size = 1888499, upload-time = "2025-11-01T11:54:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/89d65d4db4bb931beade9121bc71ad916b5fa9396e807d11b33731494e8e/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300", size = 1336747, upload-time = "2025-11-01T11:54:43.957Z" }, + { url = "https://files.pythonhosted.org/packages/85/33/cd87d92b23f0b06e8914a61cea6850c6d495ca027f669fab7a379041827a/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede", size = 1352187, upload-time = "2025-11-01T11:54:45.518Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/9d30b4a1ab26aac22fff17d21dec7e9089ccddfe25151d0a8bb57001dc3d/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6", size = 3101472, upload-time = "2025-11-01T11:54:47.255Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/fa2d3e5c29a04ead7eaa731c7cd1f30f9ec3c77b3a578fdf90280797cbcb/rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5", size = 1511361, upload-time = "2025-11-01T11:54:49.057Z" }, +] + +[[package]] +name = "result" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/47/2175be65744aa4d8419c27bd3a7a7d65af5bcad7a4dc6a812c00778754f0/result-0.17.0.tar.gz", hash = "sha256:b73da420c0cb1a3bf741dbd41ff96dedafaad6a1b3ef437a9e33e380bb0d91cf", size = 20180, upload-time = "2024-06-02T16:39:54.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/90/19110ce9374c3db619e2df0816f2c58e4ddc5cdad5f7284cd81d8b30b7cb/result-0.17.0-py3-none-any.whl", hash = "sha256:49fd668b4951ad15800b8ccefd98b6b94effc789607e19c65064b775570933e8", size = 11689, upload-time = "2024-06-02T16:39:52.715Z" }, +] + [[package]] name = "ruff" version = "0.14.1" @@ -270,6 +801,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" }, ] +[[package]] +name = "s3transfer" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/a7/e9ccfa7eecaf34c6f57d8cb0bb7cbdeeff27017cc0f5d0ca90fdde7a7c0d/sqlalchemy-2.0.44-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce", size = 2137282, upload-time = "2025-10-10T15:36:10.965Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/50bc121885bdf10833a4f65ecbe9fe229a3215f4d65a58da8a181734cae3/sqlalchemy-2.0.44-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985", size = 2127322, upload-time = "2025-10-10T15:36:12.428Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/a8573b7230a3ce5ee4b961a2d510d71b43872513647398e595b744344664/sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0", size = 3214772, upload-time = "2025-10-10T15:34:15.09Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/c63d8adb6a7edaf8dcb6f75a2b1e9f8577960a1e489606859c4d73e7d32b/sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e", size = 3214434, upload-time = "2025-10-10T15:47:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a6/243d277a4b54fae74d4797957a7320a5c210c293487f931cbe036debb697/sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749", size = 3155365, upload-time = "2025-10-10T15:34:17.932Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f8/6a39516ddd75429fd4ee5a0d72e4c80639fab329b2467c75f363c2ed9751/sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2", size = 3178910, upload-time = "2025-10-10T15:47:02.346Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/118355d4ad3c39d9a2f5ee4c7304a9665b3571482777357fa9920cd7a6b4/sqlalchemy-2.0.44-cp310-cp310-win32.whl", hash = "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165", size = 2105624, upload-time = "2025-10-10T15:38:15.552Z" }, + { url = "https://files.pythonhosted.org/packages/61/83/6ae5f9466f8aa5d0dcebfff8c9c33b98b27ce23292df3b990454b3d434fd/sqlalchemy-2.0.44-cp310-cp310-win_amd64.whl", hash = "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5", size = 2129240, upload-time = "2025-10-10T15:38:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e3/81/15d7c161c9ddf0900b076b55345872ed04ff1ed6a0666e5e94ab44b0163c/sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd", size = 2140517, upload-time = "2025-10-10T15:36:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d5/4abd13b245c7d91bdf131d4916fd9e96a584dac74215f8b5bc945206a974/sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa", size = 2130738, upload-time = "2025-10-10T15:36:16.91Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3c/8418969879c26522019c1025171cefbb2a8586b6789ea13254ac602986c0/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e", size = 3304145, upload-time = "2025-10-10T15:34:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/94/2d/fdb9246d9d32518bda5d90f4b65030b9bf403a935cfe4c36a474846517cb/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e", size = 3304511, upload-time = "2025-10-10T15:47:05.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/40f2ad1da97d5c83f6c1269664678293d3fe28e90ad17a1093b735420549/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399", size = 3235161, upload-time = "2025-10-10T15:34:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/95/cb/7cf4078b46752dca917d18cf31910d4eff6076e5b513c2d66100c4293d83/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b", size = 3261426, upload-time = "2025-10-10T15:47:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/f8/3b/55c09b285cb2d55bdfa711e778bdffdd0dc3ffa052b0af41f1c5d6e582fa/sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3", size = 2105392, upload-time = "2025-10-10T15:38:20.051Z" }, + { url = "https://files.pythonhosted.org/packages/c7/23/907193c2f4d680aedbfbdf7bf24c13925e3c7c292e813326c1b84a0b878e/sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5", size = 2130293, upload-time = "2025-10-10T15:38:21.601Z" }, + { url = "https://files.pythonhosted.org/packages/62/c4/59c7c9b068e6813c898b771204aad36683c96318ed12d4233e1b18762164/sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", size = 2139675, upload-time = "2025-10-10T16:03:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ae/eeb0920537a6f9c5a3708e4a5fc55af25900216bdb4847ec29cfddf3bf3a/sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", size = 2127726, upload-time = "2025-10-10T16:03:35.934Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d5/2ebbabe0379418eda8041c06b0b551f213576bfe4c2f09d77c06c07c8cc5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", size = 3327603, upload-time = "2025-10-10T15:35:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/5aa65852dadc24b7d8ae75b7efb8d19303ed6ac93482e60c44a585930ea5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", size = 3337842, upload-time = "2025-10-10T15:43:45.431Z" }, + { url = "https://files.pythonhosted.org/packages/41/92/648f1afd3f20b71e880ca797a960f638d39d243e233a7082c93093c22378/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", size = 3264558, upload-time = "2025-10-10T15:35:29.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/e27d7ee61a10f74b17740918e23cbc5bc62011b48282170dc4c66da8ec0f/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", size = 3301570, upload-time = "2025-10-10T15:43:48.407Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3d/3116a9a7b63e780fb402799b6da227435be878b6846b192f076d2f838654/sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", size = 2103447, upload-time = "2025-10-10T15:03:21.678Z" }, + { url = "https://files.pythonhosted.org/packages/25/83/24690e9dfc241e6ab062df82cc0df7f4231c79ba98b273fa496fb3dd78ed/sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", size = 2130912, upload-time = "2025-10-10T15:03:24.656Z" }, + { url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" }, + { url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, +] + +[[package]] +name = "strenum" +version = "0.4.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ad/430fb60d90e1d112a62ff57bdd1f286ec73a2a0331272febfddd21f330e1/StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff", size = 23384, upload-time = "2023-06-29T22:02:58.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/69/297302c5f5f59c862faa31e6cb9a4cd74721cd1e052b38e464c5b402df8b/StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659", size = 8851, upload-time = "2023-06-29T22:02:56.947Z" }, +] + [[package]] name = "tomli" version = "2.3.0" @@ -336,3 +942,24 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { 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 = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] From 63f3ae4e5c8d1e098f04adff0b093b13ffd75a59 Mon Sep 17 00:00:00 2001 From: Quinn Date: Wed, 5 Nov 2025 21:11:36 +0000 Subject: [PATCH 10/22] Generalize code so that it's better equipped to handle future CLP_MODE_CONFIGS other than clp-text and clp-json --- integration-tests/tests/utils/config.py | 17 ++--- .../tests/utils/package_utils.py | 65 ++++++++++--------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index 078bacf8d8..0372a98e05 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -1,13 +1,15 @@ """Define all python classes used in `integration-tests`.""" +from __future__ import annotations + import re +from collections.abc import Callable from dataclasses import dataclass, field, InitVar from pathlib import Path +from typing import TYPE_CHECKING -from clp_py_utils.clp_config import ( - QueryEngine, - StorageEngine, -) +if TYPE_CHECKING: + from clp_py_utils.clp_config import CLPConfig from tests.utils.utils import ( unlink, @@ -81,7 +83,7 @@ def __post_init__(self, temp_config_dir_init: Path | None) -> None: validate_dir_exists(clp_package_dir) # Check for required package script directories - required_dirs = ["bin", "etc", "lib", "sbin"] + required_dirs = ["etc", "sbin"] missing_dirs = [d for d in required_dirs if not (clp_package_dir / d).is_dir()] if len(missing_dirs) > 0: err_msg = ( @@ -112,11 +114,10 @@ def stop_script_path(self) -> Path: @dataclass(frozen=True) class PackageModeConfig: - """Defines details related to a package instance's mode of operation.""" + """Builds a fully formed CLPConfig for a named mode.""" name: str - storage_engine: StorageEngine - query_engine: QueryEngine + build_config: Callable[[], CLPConfig] @dataclass(frozen=True) diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index 462e50a368..211cbde097 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -7,6 +7,7 @@ import yaml from clp_py_utils.clp_config import ( + CLPConfig, COMPRESSION_SCHEDULER_COMPONENT_NAME, COMPRESSION_WORKER_COMPONENT_NAME, DB_COMPONENT_NAME, @@ -55,13 +56,21 @@ def _to_container_basename(name: str) -> str: CLP_MODE_CONFIGS: dict[str, PackageModeConfig] = { "clp-text": PackageModeConfig( name="clp-text", - storage_engine=StorageEngine.CLP, - query_engine=QueryEngine.CLP, + build_config=lambda: CLPConfig( + package=Package( + storage_engine=StorageEngine.CLP, + query_engine=QueryEngine.CLP, + ), + ), ), "clp-json": PackageModeConfig( name="clp-json", - storage_engine=StorageEngine.CLP_S, - query_engine=QueryEngine.CLP_S, + build_config=lambda: CLPConfig( + package=Package( + storage_engine=StorageEngine.CLP_S, + query_engine=QueryEngine.CLP_S, + ), + ), ), } @@ -77,31 +86,23 @@ def get_dict_from_mode_name(mode_name: str) -> dict[str, Any]: def get_mode_name_from_dict(dictionary: dict[str, Any]) -> str: - """Returns the name of the mode of operation described by the contents of `dictionary`.""" - package_dict = dictionary.get("package") - if not isinstance(package_dict, dict): - err_msg = "`dictionary` does not carry any mapping for 'package'." - raise TypeError(err_msg) - - dict_query_engine = package_dict.get("query_engine") - dict_storage_engine = package_dict.get("storage_engine") - if dict_query_engine is None or dict_storage_engine is None: - err_msg = ( - "`dictionary` must specify both 'package.query_engine' and 'package.storage_engine'." - ) - raise ValueError(err_msg) - - for mode_name, mode_config in CLP_MODE_CONFIGS.items(): - if str(mode_config.query_engine.value) == str(dict_query_engine) and str( - mode_config.storage_engine.value - ) == str(dict_storage_engine): - return mode_name + """Returns the mode name for a parsed CLPConfig.""" + try: + cfg = CLPConfig.model_validate(dictionary) + except Exception as err: + err_msg = f"Shared config failed validation: {err}" + raise ValueError(err_msg) from err - err_msg = ( - "The set of kv-pairs described in `dictionary` does not correspond to any mode of operation" - " for which integration testing is supported." - ) - raise ValueError(err_msg) + key = (cfg.package.storage_engine, cfg.package.query_engine) + mode_lookup: dict[tuple[StorageEngine, QueryEngine], str] = { + (StorageEngine.CLP, QueryEngine.CLP): "clp-text", + (StorageEngine.CLP_S, QueryEngine.CLP_S): "clp-json", + } + try: + return mode_lookup[key] + except KeyError: + err_msg = f"Unsupported storage/query engine pair: {key[0].value}, {key[1].value}" + raise ValueError(err_msg) from None def write_temp_config_file( @@ -130,10 +131,10 @@ def write_temp_config_file( def _build_dict_from_config(mode_config: PackageModeConfig) -> dict[str, Any]: - storage_engine = mode_config.storage_engine - query_engine = mode_config.query_engine - package_model = Package(storage_engine=storage_engine, query_engine=query_engine) - return {"package": package_model.model_dump()} + """Build a validated config dict using clp_config.CLPCconfig.""" + clp_config = mode_config.build_config() + config_dict: dict[str, Any] = clp_config.dump_to_primitive_dict() + return config_dict def _load_shared_config(path: Path) -> dict[str, Any]: From 0ff94651309379f0a7c1145b242348bea9a2ebfa Mon Sep 17 00:00:00 2001 From: Quinn Date: Thu, 6 Nov 2025 17:27:02 +0000 Subject: [PATCH 11/22] Combine PackageConfig, PackageModeConfig, and PackageInstanceConfig into one class. --- integration-tests/pyproject.toml | 2 +- .../tests/fixtures/integration_test_config.py | 11 ---- .../tests/fixtures/package_config_fixtures.py | 49 +++++++-------- .../fixtures/package_instance_fixtures.py | 31 +++++---- integration-tests/tests/test_package_start.py | 2 +- integration-tests/tests/utils/config.py | 48 +++++++------- .../tests/utils/package_utils.py | 63 ++++++++----------- 7 files changed, 85 insertions(+), 121 deletions(-) diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index f515e230eb..c2f41d3913 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -75,4 +75,4 @@ docstring-code-format = true docstring-code-line-length = 100 [tool.uv.sources] -clp-py-utils = { path = "../components/clp-py-utils" } \ No newline at end of file +clp-py-utils = { path = "../components/clp-py-utils" } diff --git a/integration-tests/tests/fixtures/integration_test_config.py b/integration-tests/tests/fixtures/integration_test_config.py index dacfd42371..7bca44fe05 100644 --- a/integration-tests/tests/fixtures/integration_test_config.py +++ b/integration-tests/tests/fixtures/integration_test_config.py @@ -7,7 +7,6 @@ from tests.utils.config import ( CoreConfig, IntegrationTestConfig, - PackageConfig, ) from tests.utils.utils import get_env_var @@ -20,16 +19,6 @@ def core_config() -> CoreConfig: ) -@pytest.fixture(scope="session") -def package_config() -> PackageConfig: - """Fixture that provides a PackageConfig shared across tests.""" - return PackageConfig( - clp_package_dir=Path(get_env_var("CLP_PACKAGE_DIR")).expanduser().resolve(), - test_root_dir=Path(get_env_var("CLP_BUILD_DIR")).expanduser().resolve() - / "integration-tests", - ) - - @pytest.fixture(scope="session") def integration_test_config() -> IntegrationTestConfig: """Fixture that provides an IntegrationTestConfig shared across tests.""" diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py index d56571f36a..9eabc81b91 100644 --- a/integration-tests/tests/fixtures/package_config_fixtures.py +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -7,67 +7,64 @@ import pytest -from tests.utils.config import ( - PackageConfig, - PackageInstanceConfig, - PackageModeConfig, -) +from tests.utils.config import PackageConfig from tests.utils.package_utils import ( CLP_MODE_CONFIGS, get_dict_from_mode_name, write_temp_config_file, ) +from tests.utils.utils import get_env_var logger = logging.getLogger(__name__) -def _build_package_instance_config( - mode_name: str, - package_config: PackageConfig, -) -> PackageInstanceConfig: - """Construct a PackageInstanceConfig for the given `mode_name`.""" +def _build_package_config_for_mode(mode_name: str) -> PackageConfig: + """Construct a PackageConfig for the given `mode_name`.""" if mode_name not in CLP_MODE_CONFIGS: - err_msg = f"Unknown CLP mode '{mode_name}'. Known modes: {list(CLP_MODE_CONFIGS.keys())}" + err_msg = f"Unknown CLP mode '{mode_name}'." raise KeyError(err_msg) - # Find the corresponding PackageModeConfig object and instantiate PackageInstanceConfig. - mode_config: PackageModeConfig = CLP_MODE_CONFIGS[mode_name] - run_config = PackageInstanceConfig( - package_config=package_config, - mode_config=mode_config, + clp_package_dir = Path(get_env_var("CLP_PACKAGE_DIR")).expanduser().resolve() + test_root_dir = Path(get_env_var("CLP_BUILD_DIR")).expanduser().resolve() / "integration-tests" + + build_config = CLP_MODE_CONFIGS[mode_name] + package_config = PackageConfig( + clp_package_dir=clp_package_dir, + test_root_dir=test_root_dir, + mode_name=mode_name, + build_config=build_config, ) - # Write the temporary config file that the instance will use during the test. + # Write the temporary config file that the package will use. mode_kv_dict: dict[str, Any] = get_dict_from_mode_name(mode_name) temp_config_file_path: Path = write_temp_config_file( mode_kv_dict=mode_kv_dict, temp_config_dir=package_config.temp_config_dir, mode_name=mode_name, ) - object.__setattr__(run_config, "temp_config_file_path", temp_config_file_path) + object.__setattr__(package_config, "temp_config_file_path", temp_config_file_path) - return run_config + return package_config @pytest.fixture def clp_config( request: pytest.FixtureRequest, - package_config: PackageConfig, -) -> Iterator[PackageInstanceConfig]: +) -> Iterator[PackageConfig]: """ - Parameterized fixture that creates and removes a temporary config file for a mode of operation. - The mode name arrives through request.param from the test's indirect parametrization. + Parameterized fixture that creates and maintains a PackageConfig object for a specific mode of + operation. """ mode_name: str = request.param logger.info("Creating a temporary config file for the %s package...", mode_name) - run_config = _build_package_instance_config(mode_name, package_config) + package_config = _build_package_config_for_mode(mode_name) logger.info("The temporary config file has been written for the %s package.", mode_name) try: - yield run_config + yield package_config finally: logger.info("Removing the temporary config file...") - run_config.temp_config_file_path.unlink() + package_config.temp_config_file_path.unlink() logger.info("The temporary config file has been removed.") diff --git a/integration-tests/tests/fixtures/package_instance_fixtures.py b/integration-tests/tests/fixtures/package_instance_fixtures.py index c86bf10c3c..20867d8e50 100644 --- a/integration-tests/tests/fixtures/package_instance_fixtures.py +++ b/integration-tests/tests/fixtures/package_instance_fixtures.py @@ -6,8 +6,8 @@ import pytest from tests.utils.config import ( + PackageConfig, PackageInstance, - PackageInstanceConfig, ) from tests.utils.package_utils import ( start_clp_package, @@ -19,30 +19,27 @@ @pytest.fixture def clp_package( - clp_config: PackageInstanceConfig, + clp_config: PackageConfig, ) -> Iterator[PackageInstance]: """ - Parameterized fixture that starts a CLP instance for the mode supplied to `clp_config`, - and gracefully stops it at scope boundary (fixture teardown). + Parameterized fixture that starts a instance of the CLP package in the configuration described + in PackageConfig, and gracefully stops it at teardown. """ - mode_name = clp_config.mode_config.name + mode_name = clp_config.mode_name logger.info("Starting up the %s package...", mode_name) start_clp_package(clp_config) - instance = PackageInstance(package_instance_config=clp_config) - instance_id = instance.clp_instance_id - logger.info( - "An instance of the %s package was started successfully. Its instance ID is '%s'", - mode_name, - instance_id, - ) - try: + instance = PackageInstance(package_config=clp_config) + instance_id = instance.clp_instance_id + logger.info( + "An instance of the %s package was started successfully. Its instance ID is '%s'", + mode_name, + instance_id, + ) yield instance finally: - logger.info("Now stopping the %s package with instance ID '%s'...", mode_name, instance_id) + logger.info("Now stopping the %s package...", mode_name) stop_clp_package(instance) - logger.info( - "The %s package with instance ID '%s' was stopped successfully.", mode_name, instance_id - ) + logger.info("The %s package was stopped successfully.", mode_name) diff --git a/integration-tests/tests/test_package_start.py b/integration-tests/tests/test_package_start.py index a3a57e2a38..02ce62a739 100644 --- a/integration-tests/tests/test_package_start.py +++ b/integration-tests/tests/test_package_start.py @@ -25,7 +25,7 @@ def test_clp_package(clp_package: PackageInstance) -> None: """ # Spin up the package by getting the PackageInstance fixture. package_instance = clp_package - mode_name = package_instance.package_instance_config.mode_config.name + mode_name = package_instance.package_config.mode_name instance_id = package_instance.clp_instance_id # Ensure that all package components are running. diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index 0372a98e05..73c999991f 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -72,10 +72,19 @@ class PackageConfig: #: Root directory for package tests output. test_root_dir: Path - temp_config_dir_init: InitVar[Path | None] = None + #: Name of the mode of operation represented in this config. + mode_name: str + + #: The CLPConfig object corresponding to this mode of operation. + build_config: Callable[[], CLPConfig] + #: Directory to store any cached package config files. + temp_config_dir_init: InitVar[Path | None] = None temp_config_dir: Path = field(init=False, repr=True) + #: The location of the constructed temporary config file for this package. + temp_config_file_path: Path = field(init=False, repr=True) + def __post_init__(self, temp_config_dir_init: Path | None) -> None: """Validates the values specified at init, and initialises attributes.""" # Validate that the CLP package directory exists and contains all required directories. @@ -101,6 +110,13 @@ def __post_init__(self, temp_config_dir_init: Path | None) -> None: self.test_root_dir.mkdir(parents=True, exist_ok=True) self.temp_config_dir.mkdir(parents=True, exist_ok=True) + # Initialize temp_config_file_path placeholder. The file will be written by the fixture. + object.__setattr__( + self, + "temp_config_file_path", + self.temp_config_dir / f"clp-config-{self.mode_name}.yml", + ) + @property def start_script_path(self) -> Path: """:return: The absolute path to the package start script.""" @@ -112,39 +128,17 @@ def stop_script_path(self) -> Path: return self.clp_package_dir / "sbin" / "stop-clp.sh" -@dataclass(frozen=True) -class PackageModeConfig: - """Builds a fully formed CLPConfig for a named mode.""" - - name: str - build_config: Callable[[], CLPConfig] - - -@dataclass(frozen=True) -class PackageInstanceConfig: - """Metadata for the clp-config.yml file used to configure a clp package instance.""" - - #: The PackageConfig object corresponding to this package run. - package_config: PackageConfig - - #: The PackageModeConfig object describing this config file's configuration. - mode_config: PackageModeConfig - - #: The location of the configfile used during this package run. - temp_config_file_path: Path = field(init=False, repr=True) - - @dataclass(frozen=True) class PackageInstance: """Metadata for a run of the clp package.""" #: - package_instance_config: PackageInstanceConfig + package_config: PackageConfig - #: + #: The location of the logging directory within the running package. clp_log_dir: Path = field(init=False, repr=True) - #: + #: The instance ID of the running package. clp_instance_id: str = field(init=False, repr=True) #: The path to the .clp-config.yml file constructed by the package during spin-up. @@ -153,7 +147,7 @@ class PackageInstance: def __post_init__(self) -> None: """Validates the values specified at init, and initialises attributes.""" # Set clp_log_dir and validate that it exists. - clp_log_dir = self.package_instance_config.package_config.clp_package_dir / "var" / "log" + clp_log_dir = self.package_config.clp_package_dir / "var" / "log" validate_dir_exists(clp_log_dir) object.__setattr__(self, "clp_log_dir", clp_log_dir) diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index 211cbde097..e93a8d33d3 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -2,6 +2,7 @@ import shutil import subprocess +from collections.abc import Callable from pathlib import Path from typing import Any @@ -25,9 +26,8 @@ ) from tests.utils.config import ( + PackageConfig, PackageInstance, - PackageInstanceConfig, - PackageModeConfig, ) from tests.utils.docker_utils import ( inspect_container_state, @@ -53,23 +53,17 @@ def _to_container_basename(name: str) -> str: _to_container_basename(GARBAGE_COLLECTOR_COMPONENT_NAME), ] -CLP_MODE_CONFIGS: dict[str, PackageModeConfig] = { - "clp-text": PackageModeConfig( - name="clp-text", - build_config=lambda: CLPConfig( - package=Package( - storage_engine=StorageEngine.CLP, - query_engine=QueryEngine.CLP, - ), +CLP_MODE_CONFIGS: dict[str, Callable[[], CLPConfig]] = { + "clp-text": lambda: CLPConfig( + package=Package( + storage_engine=StorageEngine.CLP, + query_engine=QueryEngine.CLP, ), ), - "clp-json": PackageModeConfig( - name="clp-json", - build_config=lambda: CLPConfig( - package=Package( - storage_engine=StorageEngine.CLP_S, - query_engine=QueryEngine.CLP_S, - ), + "clp-json": lambda: CLPConfig( + package=Package( + storage_engine=StorageEngine.CLP_S, + query_engine=QueryEngine.CLP_S, ), ), } @@ -82,7 +76,9 @@ def get_dict_from_mode_name(mode_name: str) -> dict[str, Any]: err_msg = f"Unsupported mode: {mode_name}" raise ValueError(err_msg) - return _build_dict_from_config(mode_config) + clp_config = mode_config() + ret_dict: dict[str, Any] = clp_config.dump_to_primitive_dict() + return ret_dict def get_mode_name_from_dict(dictionary: dict[str, Any]) -> str: @@ -130,13 +126,6 @@ def write_temp_config_file( return temp_config_file_path -def _build_dict_from_config(mode_config: PackageModeConfig) -> dict[str, Any]: - """Build a validated config dict using clp_config.CLPCconfig.""" - clp_config = mode_config.build_config() - config_dict: dict[str, Any] = clp_config.dump_to_primitive_dict() - return config_dict - - def _load_shared_config(path: Path) -> dict[str, Any]: try: with path.open("r", encoding="utf-8") as file: @@ -155,29 +144,27 @@ def _load_shared_config(path: Path) -> dict[str, Any]: return shared_config_dict -def start_clp_package(run_config: PackageInstanceConfig) -> None: - """Starts an instance of the clp package.""" - start_script_path = run_config.package_config.start_script_path +def start_clp_package(cfg: PackageConfig) -> None: + """Start an instance of the CLP package.""" + start_script_path = cfg.start_script_path try: # fmt: off start_cmd = [ str(start_script_path), "--config", - str(run_config.temp_config_file_path) + str(cfg.temp_config_file_path) ] # fmt: on subprocess.run(start_cmd, check=True) except Exception as e: - err_msg = f"Failed to start an instance of the {run_config.mode_config.name} package." + err_msg = f"Failed to start an instance of the {cfg.mode_name} package." raise RuntimeError(err_msg) from e -def stop_clp_package( - instance: PackageInstance, -) -> None: - """Stops an instance of the clp package.""" - run_config = instance.package_instance_config - stop_script_path = run_config.package_config.stop_script_path +def stop_clp_package(instance: PackageInstance) -> None: + """Stop an instance of the CLP package.""" + cfg = instance.package_config + stop_script_path = cfg.stop_script_path try: # fmt: off stop_cmd = [ @@ -186,7 +173,7 @@ def stop_clp_package( # fmt: on subprocess.run(stop_cmd, check=True) except Exception as e: - err_msg = f"Failed to stop an instance of the {run_config.mode_config.name} package." + err_msg = f"Failed to stop an instance of the {cfg.mode_name} package." raise RuntimeError(err_msg) from e @@ -233,7 +220,7 @@ def is_running_mode_correct(package_instance: PackageInstance) -> tuple[bool, st mismatch. """ running_mode = _get_running_mode(package_instance) - intended_mode = package_instance.package_instance_config.mode_config.name + intended_mode = package_instance.package_config.mode_name if running_mode != intended_mode: return ( False, From cdcfaeefe30beab144dd24fa1ff4a12cc95d330b Mon Sep 17 00:00:00 2001 From: Quinn Date: Thu, 6 Nov 2025 18:39:34 +0000 Subject: [PATCH 12/22] Employ a 'CLPConfig'-centric approach rather than 'dict'-centric. --- .../tests/fixtures/package_config_fixtures.py | 7 +- .../fixtures/package_instance_fixtures.py | 7 +- integration-tests/tests/utils/config.py | 11 +- .../tests/utils/package_utils.py | 124 ++++++++---------- 4 files changed, 72 insertions(+), 77 deletions(-) diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py index 9eabc81b91..c6d421941a 100644 --- a/integration-tests/tests/fixtures/package_config_fixtures.py +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -3,14 +3,13 @@ import logging from collections.abc import Iterator from pathlib import Path -from typing import Any import pytest from tests.utils.config import PackageConfig from tests.utils.package_utils import ( CLP_MODE_CONFIGS, - get_dict_from_mode_name, + get_clp_config_from_mode, write_temp_config_file, ) from tests.utils.utils import get_env_var @@ -36,9 +35,9 @@ def _build_package_config_for_mode(mode_name: str) -> PackageConfig: ) # Write the temporary config file that the package will use. - mode_kv_dict: dict[str, Any] = get_dict_from_mode_name(mode_name) + clp_config_obj = get_clp_config_from_mode(mode_name) temp_config_file_path: Path = write_temp_config_file( - mode_kv_dict=mode_kv_dict, + clp_config=clp_config_obj, temp_config_dir=package_config.temp_config_dir, mode_name=mode_name, ) diff --git a/integration-tests/tests/fixtures/package_instance_fixtures.py b/integration-tests/tests/fixtures/package_instance_fixtures.py index 20867d8e50..582eb3b5df 100644 --- a/integration-tests/tests/fixtures/package_instance_fixtures.py +++ b/integration-tests/tests/fixtures/package_instance_fixtures.py @@ -1,6 +1,7 @@ """Fixtures that start and stop CLP package instances for integration tests.""" import logging +import subprocess from collections.abc import Iterator import pytest @@ -41,5 +42,9 @@ def clp_package( yield instance finally: logger.info("Now stopping the %s package...", mode_name) - stop_clp_package(instance) + if instance is not None: + stop_clp_package(instance) + else: + # This means setup failed after start; fall back to calling stop script directly + subprocess.run([str(clp_config.stop_script_path)], check=True) logger.info("The %s package was stopped successfully.", mode_name) diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index 73c999991f..9fbf8ae266 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -11,6 +11,11 @@ if TYPE_CHECKING: from clp_py_utils.clp_config import CLPConfig +from clp_py_utils.clp_config import ( + CLP_DEFAULT_LOG_DIRECTORY_PATH, + CLP_SHARED_CONFIG_FILENAME, +) + from tests.utils.utils import ( unlink, validate_dir_exists, @@ -158,7 +163,11 @@ def __post_init__(self) -> None: object.__setattr__(self, "clp_instance_id", clp_instance_id) # Set shared_config_file_path after validating it. - shared_config_file_path = self.clp_log_dir / ".clp-config.yml" + shared_config_file_path = ( + self.package_config.clp_package_dir + / CLP_DEFAULT_LOG_DIRECTORY_PATH + / CLP_SHARED_CONFIG_FILENAME + ) validate_file_exists(shared_config_file_path) object.__setattr__(self, "shared_config_file_path", shared_config_file_path) diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index e93a8d33d3..99f45de57c 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -24,6 +24,7 @@ StorageEngine, WEBUI_COMPONENT_NAME, ) +from pydantic import ValidationError from tests.utils.config import ( PackageConfig, @@ -69,102 +70,78 @@ def _to_container_basename(name: str) -> str: } -def get_dict_from_mode_name(mode_name: str) -> dict[str, Any]: - """Returns the dictionary that describes the operation of `mode_name`.""" - mode_config = CLP_MODE_CONFIGS.get(mode_name) - if mode_config is None: +def get_clp_config_from_mode(mode_name: str) -> CLPConfig: + """Return a CLPConfig object corresponding to the given `mode_name`.""" + try: + config = CLP_MODE_CONFIGS[mode_name] + except KeyError as err: err_msg = f"Unsupported mode: {mode_name}" - raise ValueError(err_msg) - - clp_config = mode_config() - ret_dict: dict[str, Any] = clp_config.dump_to_primitive_dict() - return ret_dict + raise ValueError(err_msg) from err + return config() -def get_mode_name_from_dict(dictionary: dict[str, Any]) -> str: - """Returns the mode name for a parsed CLPConfig.""" +def _load_shared_config(path: Path) -> dict[str, Any]: try: - cfg = CLPConfig.model_validate(dictionary) - except Exception as err: - err_msg = f"Shared config failed validation: {err}" + with path.open("r", encoding="utf-8") as file: + shared_config_dict = yaml.safe_load(file) + except yaml.YAMLError as err: + err_msg = f"Invalid YAML in shared config {path}: {err}" + raise ValueError(err_msg) from err + except OSError as err: + err_msg = f"Cannot read shared config {path}: {err}" raise ValueError(err_msg) from err - key = (cfg.package.storage_engine, cfg.package.query_engine) - mode_lookup: dict[tuple[StorageEngine, QueryEngine], str] = { - (StorageEngine.CLP, QueryEngine.CLP): "clp-text", - (StorageEngine.CLP_S, QueryEngine.CLP_S): "clp-json", - } - try: - return mode_lookup[key] - except KeyError: - err_msg = f"Unsupported storage/query engine pair: {key[0].value}, {key[1].value}" - raise ValueError(err_msg) from None + if not isinstance(shared_config_dict, dict): + err_msg = f"Shared config {path} must be a mapping at the top level." + raise TypeError(err_msg) + + return shared_config_dict def write_temp_config_file( - mode_kv_dict: dict[str, Any], + clp_config: CLPConfig, temp_config_dir: Path, mode_name: str, ) -> Path: """ - Writes a temporary config file to `temp_config_dir`. Returns the path to the temporary file on - success. + Writes a temporary config file to `temp_config_dir` for a CLPCongig object. Returns the path to + the temporary file on success. """ - if not isinstance(mode_kv_dict, dict): - err_msg = "`mode_kv_dict` must be a mapping." - raise TypeError(err_msg) - temp_config_dir.mkdir(parents=True, exist_ok=True) temp_config_filename = f"clp-config-{mode_name}.yml" temp_config_file_path = temp_config_dir / temp_config_filename + payload: dict[str, Any] = clp_config.dump_to_primitive_dict() + tmp_path = temp_config_file_path.with_suffix(temp_config_file_path.suffix + ".tmp") with tmp_path.open("w", encoding="utf-8") as f: - yaml.safe_dump(mode_kv_dict, f, sort_keys=False) + yaml.safe_dump(payload, f, sort_keys=False) tmp_path.replace(temp_config_file_path) return temp_config_file_path -def _load_shared_config(path: Path) -> dict[str, Any]: - try: - with path.open("r", encoding="utf-8") as file: - shared_config_dict = yaml.safe_load(file) - except yaml.YAMLError as err: - err_msg = f"Invalid YAML in shared config {path}: {err}" - raise ValueError(err_msg) from err - except OSError as err: - err_msg = f"Cannot read shared config {path}: {err}" - raise ValueError(err_msg) from err - - if not isinstance(shared_config_dict, dict): - err_msg = f"Shared config {path} must be a mapping at the top level." - raise TypeError(err_msg) - - return shared_config_dict - - -def start_clp_package(cfg: PackageConfig) -> None: +def start_clp_package(package_config: PackageConfig) -> None: """Start an instance of the CLP package.""" - start_script_path = cfg.start_script_path + start_script_path = package_config.start_script_path try: # fmt: off start_cmd = [ str(start_script_path), "--config", - str(cfg.temp_config_file_path) + str(package_config.temp_config_file_path) ] # fmt: on subprocess.run(start_cmd, check=True) except Exception as e: - err_msg = f"Failed to start an instance of the {cfg.mode_name} package." + err_msg = f"Failed to start an instance of the {package_config.mode_name} package." raise RuntimeError(err_msg) from e def stop_clp_package(instance: PackageInstance) -> None: """Stop an instance of the CLP package.""" - cfg = instance.package_config - stop_script_path = cfg.stop_script_path + package_config = instance.package_config + stop_script_path = package_config.stop_script_path try: # fmt: off stop_cmd = [ @@ -173,7 +150,7 @@ def stop_clp_package(instance: PackageInstance) -> None: # fmt: on subprocess.run(stop_cmd, check=True) except Exception as e: - err_msg = f"Failed to stop an instance of the {cfg.mode_name} package." + err_msg = f"Failed to stop an instance of the {package_config.mode_name} package." raise RuntimeError(err_msg) from e @@ -215,22 +192,27 @@ def is_package_running(package_instance: PackageInstance) -> tuple[bool, str | N def is_running_mode_correct(package_instance: PackageInstance) -> tuple[bool, str | None]: """ - Checks if the mode described in the shared config file matches the mode described in - `mode_config` of `package_instance`. Returns `True` if correct, `False` with message on - mismatch. + Checks if the mode described in the shared config file of `package_instance` is accurate with + respect to its `mode_name`. Returns `True` if correct, `False` with message on mismatch. """ - running_mode = _get_running_mode(package_instance) - intended_mode = package_instance.package_config.mode_name - if running_mode != intended_mode: + shared_config_dict = _load_shared_config(package_instance.shared_config_file_path) + try: + running_config = CLPConfig.model_validate(shared_config_dict) + except ValidationError as err: + err_msg = f"Shared config failed validation: {err}" + raise ValueError(err_msg) from err + + intended_config = get_clp_config_from_mode(package_instance.package_config.mode_name) + + # TODO: when there are more modes, the following two lines should be reassessed, as checking the + # engines may not be enough. + running = (running_config.package.storage_engine, running_config.package.query_engine) + intended = (intended_config.package.storage_engine, intended_config.package.query_engine) + + if running != intended: return ( False, - f"Mode mismatch: the package is running in {running_mode}, but it should be running in" - f" {intended_mode}.", + f"Mode mismatch: the package is running in {running[0].value}, {running[1].value}," + f" but it should be running in {intended[0].value}, {intended[1].value}.", ) - return True, None - - -def _get_running_mode(package_instance: PackageInstance) -> str: - shared_config_dict = _load_shared_config(package_instance.shared_config_file_path) - return get_mode_name_from_dict(shared_config_dict) From 53e7343539c816c11e7060d743741838fda3e1c2 Mon Sep 17 00:00:00 2001 From: Quinn Date: Thu, 6 Nov 2025 20:34:39 +0000 Subject: [PATCH 13/22] Store list of required components for each mode --- .../tests/fixtures/package_config_fixtures.py | 7 +- integration-tests/tests/utils/config.py | 3 + .../tests/utils/package_utils.py | 67 ++++++++++++------- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py index c6d421941a..7f937754ff 100644 --- a/integration-tests/tests/fixtures/package_config_fixtures.py +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -9,7 +9,6 @@ from tests.utils.config import PackageConfig from tests.utils.package_utils import ( CLP_MODE_CONFIGS, - get_clp_config_from_mode, write_temp_config_file, ) from tests.utils.utils import get_env_var @@ -26,16 +25,18 @@ def _build_package_config_for_mode(mode_name: str) -> PackageConfig: clp_package_dir = Path(get_env_var("CLP_PACKAGE_DIR")).expanduser().resolve() test_root_dir = Path(get_env_var("CLP_BUILD_DIR")).expanduser().resolve() / "integration-tests" - build_config = CLP_MODE_CONFIGS[mode_name] + build_config = CLP_MODE_CONFIGS[mode_name][0] + required_components = CLP_MODE_CONFIGS[mode_name][1] package_config = PackageConfig( clp_package_dir=clp_package_dir, test_root_dir=test_root_dir, mode_name=mode_name, build_config=build_config, + component_list=required_components, ) # Write the temporary config file that the package will use. - clp_config_obj = get_clp_config_from_mode(mode_name) + clp_config_obj = build_config() temp_config_file_path: Path = write_temp_config_file( clp_config=clp_config_obj, temp_config_dir=package_config.temp_config_dir, diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index 9fbf8ae266..357fbc3635 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -83,6 +83,9 @@ class PackageConfig: #: The CLPConfig object corresponding to this mode of operation. build_config: Callable[[], CLPConfig] + #: The list of containerized CLP components that this package needs. + component_list: list[str] + #: Directory to store any cached package config files. temp_config_dir_init: InitVar[Path | None] = None temp_config_dir: Path = field(init=False, repr=True) diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index 99f45de57c..5c682e2e5f 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -40,32 +40,48 @@ def _to_container_basename(name: str) -> str: return name.replace("_", "-") -CLP_COMPONENT_BASENAMES = [ - _to_container_basename(DB_COMPONENT_NAME), - _to_container_basename(QUEUE_COMPONENT_NAME), - _to_container_basename(REDIS_COMPONENT_NAME), - _to_container_basename(RESULTS_CACHE_COMPONENT_NAME), - _to_container_basename(COMPRESSION_SCHEDULER_COMPONENT_NAME), - _to_container_basename(QUERY_SCHEDULER_COMPONENT_NAME), - _to_container_basename(COMPRESSION_WORKER_COMPONENT_NAME), - _to_container_basename(QUERY_WORKER_COMPONENT_NAME), - _to_container_basename(REDUCER_COMPONENT_NAME), - _to_container_basename(WEBUI_COMPONENT_NAME), - _to_container_basename(GARBAGE_COLLECTOR_COMPONENT_NAME), -] - -CLP_MODE_CONFIGS: dict[str, Callable[[], CLPConfig]] = { - "clp-text": lambda: CLPConfig( - package=Package( - storage_engine=StorageEngine.CLP, - query_engine=QueryEngine.CLP, +CLP_MODE_CONFIGS: dict[str, tuple[Callable[[], CLPConfig], list[str]]] = { + "clp-text": ( + lambda: CLPConfig( + package=Package( + storage_engine=StorageEngine.CLP, + query_engine=QueryEngine.CLP, + ), ), + [ + _to_container_basename(DB_COMPONENT_NAME), + _to_container_basename(QUEUE_COMPONENT_NAME), + _to_container_basename(REDIS_COMPONENT_NAME), + _to_container_basename(REDUCER_COMPONENT_NAME), + _to_container_basename(RESULTS_CACHE_COMPONENT_NAME), + _to_container_basename(COMPRESSION_SCHEDULER_COMPONENT_NAME), + _to_container_basename(QUERY_SCHEDULER_COMPONENT_NAME), + _to_container_basename(COMPRESSION_WORKER_COMPONENT_NAME), + _to_container_basename(QUERY_WORKER_COMPONENT_NAME), + _to_container_basename(WEBUI_COMPONENT_NAME), + _to_container_basename(GARBAGE_COLLECTOR_COMPONENT_NAME), + ], ), - "clp-json": lambda: CLPConfig( - package=Package( - storage_engine=StorageEngine.CLP_S, - query_engine=QueryEngine.CLP_S, + "clp-json": ( + lambda: CLPConfig( + package=Package( + storage_engine=StorageEngine.CLP_S, + query_engine=QueryEngine.CLP_S, + ), ), + [ + _to_container_basename(DB_COMPONENT_NAME), + _to_container_basename(QUEUE_COMPONENT_NAME), + _to_container_basename(REDIS_COMPONENT_NAME), + _to_container_basename(REDUCER_COMPONENT_NAME), + _to_container_basename(RESULTS_CACHE_COMPONENT_NAME), + _to_container_basename(COMPRESSION_SCHEDULER_COMPONENT_NAME), + _to_container_basename(QUERY_SCHEDULER_COMPONENT_NAME), + _to_container_basename(COMPRESSION_WORKER_COMPONENT_NAME), + _to_container_basename(QUERY_WORKER_COMPONENT_NAME), + _to_container_basename(WEBUI_COMPONENT_NAME), + _to_container_basename(GARBAGE_COLLECTOR_COMPONENT_NAME), + ], ), } @@ -73,7 +89,7 @@ def _to_container_basename(name: str) -> str: def get_clp_config_from_mode(mode_name: str) -> CLPConfig: """Return a CLPConfig object corresponding to the given `mode_name`.""" try: - config = CLP_MODE_CONFIGS[mode_name] + config = CLP_MODE_CONFIGS[mode_name][0] except KeyError as err: err_msg = f"Unsupported mode: {mode_name}" raise ValueError(err_msg) from err @@ -167,7 +183,8 @@ def is_package_running(package_instance: PackageInstance) -> tuple[bool, str | N instance_id = package_instance.clp_instance_id problems: list[str] = [] - for component in CLP_COMPONENT_BASENAMES: + required_components = package_instance.package_config.component_list + for component in required_components: prefix = f"clp-package-{instance_id}-{component}-" candidates = list_prefixed_containers(docker_bin, prefix) From 796ffe3e712b07550fa5b7729ab5790dafc0e86f Mon Sep 17 00:00:00 2001 From: Quinn Date: Thu, 6 Nov 2025 21:23:37 +0000 Subject: [PATCH 14/22] Generalize the method by which the mode of operation is determined. --- .../tests/utils/package_utils.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index 5c682e2e5f..1e86e9c735 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -120,7 +120,7 @@ def write_temp_config_file( mode_name: str, ) -> Path: """ - Writes a temporary config file to `temp_config_dir` for a CLPCongig object. Returns the path to + Writes a temporary config file to `temp_config_dir` for a CLPConfig object. Returns the path to the temporary file on success. """ temp_config_dir.mkdir(parents=True, exist_ok=True) @@ -207,6 +207,21 @@ def is_package_running(package_instance: PackageInstance) -> tuple[bool, str | N return True, None +def _compute_mode_signature(config: CLPConfig) -> tuple[Any, ...]: + """Constructs a signature that captures the mode-defining aspects of a CLPConfig object.""" + return ( + config.logs_input.type, + config.package.storage_engine.value, + config.package.storage_engine.value, + config.mcp_server is not None, + config.presto is not None, + config.archive_output.storage.type, + config.stream_output.storage.type, + config.aws_config_directory is not None, + config.get_deployment_type(), + ) + + def is_running_mode_correct(package_instance: PackageInstance) -> tuple[bool, str | None]: """ Checks if the mode described in the shared config file of `package_instance` is accurate with @@ -221,15 +236,13 @@ def is_running_mode_correct(package_instance: PackageInstance) -> tuple[bool, st intended_config = get_clp_config_from_mode(package_instance.package_config.mode_name) - # TODO: when there are more modes, the following two lines should be reassessed, as checking the - # engines may not be enough. - running = (running_config.package.storage_engine, running_config.package.query_engine) - intended = (intended_config.package.storage_engine, intended_config.package.query_engine) + running_signature = _compute_mode_signature(running_config) + intended_signature = _compute_mode_signature(intended_config) - if running != intended: + if running_signature != intended_signature: return ( False, - f"Mode mismatch: the package is running in {running[0].value}, {running[1].value}," - f" but it should be running in {intended[0].value}, {intended[1].value}.", + "Mode mismatch: running configuration does not match intended configuration.", ) + return True, None From bae12def4ae00c0016b94394ab88b4fdc13d360e Mon Sep 17 00:00:00 2001 From: Quinn Date: Thu, 6 Nov 2025 21:39:00 +0000 Subject: [PATCH 15/22] Move all mode-related utilities to their own file. --- .../tests/fixtures/package_config_fixtures.py | 6 +- integration-tests/tests/test_package_start.py | 2 +- .../tests/utils/clp_mode_utils.py | 97 +++++++++++++++++++ .../tests/utils/package_utils.py | 95 +----------------- 4 files changed, 103 insertions(+), 97 deletions(-) create mode 100644 integration-tests/tests/utils/clp_mode_utils.py diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py index 7f937754ff..365b5f93ab 100644 --- a/integration-tests/tests/fixtures/package_config_fixtures.py +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -6,11 +6,9 @@ import pytest +from tests.utils.clp_mode_utils import CLP_MODE_CONFIGS from tests.utils.config import PackageConfig -from tests.utils.package_utils import ( - CLP_MODE_CONFIGS, - write_temp_config_file, -) +from tests.utils.package_utils import write_temp_config_file from tests.utils.utils import get_env_var logger = logging.getLogger(__name__) diff --git a/integration-tests/tests/test_package_start.py b/integration-tests/tests/test_package_start.py index 02ce62a739..581ccf08e9 100644 --- a/integration-tests/tests/test_package_start.py +++ b/integration-tests/tests/test_package_start.py @@ -4,9 +4,9 @@ import pytest +from tests.utils.clp_mode_utils import CLP_MODE_CONFIGS from tests.utils.config import PackageInstance from tests.utils.package_utils import ( - CLP_MODE_CONFIGS, is_package_running, is_running_mode_correct, ) diff --git a/integration-tests/tests/utils/clp_mode_utils.py b/integration-tests/tests/utils/clp_mode_utils.py new file mode 100644 index 0000000000..49a050a9f0 --- /dev/null +++ b/integration-tests/tests/utils/clp_mode_utils.py @@ -0,0 +1,97 @@ +"""Provides utilities related to the user-level configurations of CLP's operating modes.""" + +from collections.abc import Callable +from typing import Any + +from clp_py_utils.clp_config import ( + CLPConfig, + COMPRESSION_SCHEDULER_COMPONENT_NAME, + COMPRESSION_WORKER_COMPONENT_NAME, + DB_COMPONENT_NAME, + GARBAGE_COLLECTOR_COMPONENT_NAME, + Package, + QUERY_SCHEDULER_COMPONENT_NAME, + QUERY_WORKER_COMPONENT_NAME, + QueryEngine, + QUEUE_COMPONENT_NAME, + REDIS_COMPONENT_NAME, + REDUCER_COMPONENT_NAME, + RESULTS_CACHE_COMPONENT_NAME, + StorageEngine, + WEBUI_COMPONENT_NAME, +) + + +def _to_container_basename(name: str) -> str: + return name.replace("_", "-") + + +CLP_MODE_CONFIGS: dict[str, tuple[Callable[[], CLPConfig], list[str]]] = { + "clp-text": ( + lambda: CLPConfig( + package=Package( + storage_engine=StorageEngine.CLP, + query_engine=QueryEngine.CLP, + ), + ), + [ + _to_container_basename(DB_COMPONENT_NAME), + _to_container_basename(QUEUE_COMPONENT_NAME), + _to_container_basename(REDIS_COMPONENT_NAME), + _to_container_basename(REDUCER_COMPONENT_NAME), + _to_container_basename(RESULTS_CACHE_COMPONENT_NAME), + _to_container_basename(COMPRESSION_SCHEDULER_COMPONENT_NAME), + _to_container_basename(QUERY_SCHEDULER_COMPONENT_NAME), + _to_container_basename(COMPRESSION_WORKER_COMPONENT_NAME), + _to_container_basename(QUERY_WORKER_COMPONENT_NAME), + _to_container_basename(WEBUI_COMPONENT_NAME), + _to_container_basename(GARBAGE_COLLECTOR_COMPONENT_NAME), + ], + ), + "clp-json": ( + lambda: CLPConfig( + package=Package( + storage_engine=StorageEngine.CLP_S, + query_engine=QueryEngine.CLP_S, + ), + ), + [ + _to_container_basename(DB_COMPONENT_NAME), + _to_container_basename(QUEUE_COMPONENT_NAME), + _to_container_basename(REDIS_COMPONENT_NAME), + _to_container_basename(REDUCER_COMPONENT_NAME), + _to_container_basename(RESULTS_CACHE_COMPONENT_NAME), + _to_container_basename(COMPRESSION_SCHEDULER_COMPONENT_NAME), + _to_container_basename(QUERY_SCHEDULER_COMPONENT_NAME), + _to_container_basename(COMPRESSION_WORKER_COMPONENT_NAME), + _to_container_basename(QUERY_WORKER_COMPONENT_NAME), + _to_container_basename(WEBUI_COMPONENT_NAME), + _to_container_basename(GARBAGE_COLLECTOR_COMPONENT_NAME), + ], + ), +} + + +def compute_mode_signature(config: CLPConfig) -> tuple[Any, ...]: + """Constructs a signature that captures the mode-defining aspects of a CLPConfig object.""" + return ( + config.logs_input.type, + config.package.storage_engine.value, + config.package.storage_engine.value, + config.mcp_server is not None, + config.presto is not None, + config.archive_output.storage.type, + config.stream_output.storage.type, + config.aws_config_directory is not None, + config.get_deployment_type(), + ) + + +def get_clp_config_from_mode(mode_name: str) -> CLPConfig: + """Return a CLPConfig object corresponding to the given `mode_name`.""" + try: + config = CLP_MODE_CONFIGS[mode_name][0] + except KeyError as err: + err_msg = f"Unsupported mode: {mode_name}" + raise ValueError(err_msg) from err + return config() diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index 1e86e9c735..c35be77b71 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -2,30 +2,16 @@ import shutil import subprocess -from collections.abc import Callable from pathlib import Path from typing import Any import yaml from clp_py_utils.clp_config import ( CLPConfig, - COMPRESSION_SCHEDULER_COMPONENT_NAME, - COMPRESSION_WORKER_COMPONENT_NAME, - DB_COMPONENT_NAME, - GARBAGE_COLLECTOR_COMPONENT_NAME, - Package, - QUERY_SCHEDULER_COMPONENT_NAME, - QUERY_WORKER_COMPONENT_NAME, - QueryEngine, - QUEUE_COMPONENT_NAME, - REDIS_COMPONENT_NAME, - REDUCER_COMPONENT_NAME, - RESULTS_CACHE_COMPONENT_NAME, - StorageEngine, - WEBUI_COMPONENT_NAME, ) from pydantic import ValidationError +from tests.utils.clp_mode_utils import compute_mode_signature, get_clp_config_from_mode from tests.utils.config import ( PackageConfig, PackageInstance, @@ -36,66 +22,6 @@ ) -def _to_container_basename(name: str) -> str: - return name.replace("_", "-") - - -CLP_MODE_CONFIGS: dict[str, tuple[Callable[[], CLPConfig], list[str]]] = { - "clp-text": ( - lambda: CLPConfig( - package=Package( - storage_engine=StorageEngine.CLP, - query_engine=QueryEngine.CLP, - ), - ), - [ - _to_container_basename(DB_COMPONENT_NAME), - _to_container_basename(QUEUE_COMPONENT_NAME), - _to_container_basename(REDIS_COMPONENT_NAME), - _to_container_basename(REDUCER_COMPONENT_NAME), - _to_container_basename(RESULTS_CACHE_COMPONENT_NAME), - _to_container_basename(COMPRESSION_SCHEDULER_COMPONENT_NAME), - _to_container_basename(QUERY_SCHEDULER_COMPONENT_NAME), - _to_container_basename(COMPRESSION_WORKER_COMPONENT_NAME), - _to_container_basename(QUERY_WORKER_COMPONENT_NAME), - _to_container_basename(WEBUI_COMPONENT_NAME), - _to_container_basename(GARBAGE_COLLECTOR_COMPONENT_NAME), - ], - ), - "clp-json": ( - lambda: CLPConfig( - package=Package( - storage_engine=StorageEngine.CLP_S, - query_engine=QueryEngine.CLP_S, - ), - ), - [ - _to_container_basename(DB_COMPONENT_NAME), - _to_container_basename(QUEUE_COMPONENT_NAME), - _to_container_basename(REDIS_COMPONENT_NAME), - _to_container_basename(REDUCER_COMPONENT_NAME), - _to_container_basename(RESULTS_CACHE_COMPONENT_NAME), - _to_container_basename(COMPRESSION_SCHEDULER_COMPONENT_NAME), - _to_container_basename(QUERY_SCHEDULER_COMPONENT_NAME), - _to_container_basename(COMPRESSION_WORKER_COMPONENT_NAME), - _to_container_basename(QUERY_WORKER_COMPONENT_NAME), - _to_container_basename(WEBUI_COMPONENT_NAME), - _to_container_basename(GARBAGE_COLLECTOR_COMPONENT_NAME), - ], - ), -} - - -def get_clp_config_from_mode(mode_name: str) -> CLPConfig: - """Return a CLPConfig object corresponding to the given `mode_name`.""" - try: - config = CLP_MODE_CONFIGS[mode_name][0] - except KeyError as err: - err_msg = f"Unsupported mode: {mode_name}" - raise ValueError(err_msg) from err - return config() - - def _load_shared_config(path: Path) -> dict[str, Any]: try: with path.open("r", encoding="utf-8") as file: @@ -207,21 +133,6 @@ def is_package_running(package_instance: PackageInstance) -> tuple[bool, str | N return True, None -def _compute_mode_signature(config: CLPConfig) -> tuple[Any, ...]: - """Constructs a signature that captures the mode-defining aspects of a CLPConfig object.""" - return ( - config.logs_input.type, - config.package.storage_engine.value, - config.package.storage_engine.value, - config.mcp_server is not None, - config.presto is not None, - config.archive_output.storage.type, - config.stream_output.storage.type, - config.aws_config_directory is not None, - config.get_deployment_type(), - ) - - def is_running_mode_correct(package_instance: PackageInstance) -> tuple[bool, str | None]: """ Checks if the mode described in the shared config file of `package_instance` is accurate with @@ -236,8 +147,8 @@ def is_running_mode_correct(package_instance: PackageInstance) -> tuple[bool, st intended_config = get_clp_config_from_mode(package_instance.package_config.mode_name) - running_signature = _compute_mode_signature(running_config) - intended_signature = _compute_mode_signature(intended_config) + running_signature = compute_mode_signature(running_config) + intended_signature = compute_mode_signature(intended_config) if running_signature != intended_signature: return ( From 9c9ef741e5f6c55e53e967f4cb702998f1620130 Mon Sep 17 00:00:00 2001 From: Quinn Date: Thu, 6 Nov 2025 21:40:31 +0000 Subject: [PATCH 16/22] Minor error. --- integration-tests/tests/utils/clp_mode_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/tests/utils/clp_mode_utils.py b/integration-tests/tests/utils/clp_mode_utils.py index 49a050a9f0..992eec8c36 100644 --- a/integration-tests/tests/utils/clp_mode_utils.py +++ b/integration-tests/tests/utils/clp_mode_utils.py @@ -77,7 +77,7 @@ def compute_mode_signature(config: CLPConfig) -> tuple[Any, ...]: return ( config.logs_input.type, config.package.storage_engine.value, - config.package.storage_engine.value, + config.package.query_engine.value, config.mcp_server is not None, config.presto is not None, config.archive_output.storage.type, From 0f3dcd526b54203e91e125c637939f53f73ea518 Mon Sep 17 00:00:00 2001 From: Quinn Date: Thu, 6 Nov 2025 21:53:02 +0000 Subject: [PATCH 17/22] Move load_yaml_to_dict to general utilities file. --- .../tests/utils/package_utils.py | 21 ++------------ integration-tests/tests/utils/utils.py | 29 ++++++++++++++++++- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index c35be77b71..68106c233e 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -20,24 +20,7 @@ inspect_container_state, list_prefixed_containers, ) - - -def _load_shared_config(path: Path) -> dict[str, Any]: - try: - with path.open("r", encoding="utf-8") as file: - shared_config_dict = yaml.safe_load(file) - except yaml.YAMLError as err: - err_msg = f"Invalid YAML in shared config {path}: {err}" - raise ValueError(err_msg) from err - except OSError as err: - err_msg = f"Cannot read shared config {path}: {err}" - raise ValueError(err_msg) from err - - if not isinstance(shared_config_dict, dict): - err_msg = f"Shared config {path} must be a mapping at the top level." - raise TypeError(err_msg) - - return shared_config_dict +from tests.utils.utils import load_yaml_to_dict def write_temp_config_file( @@ -138,7 +121,7 @@ def is_running_mode_correct(package_instance: PackageInstance) -> tuple[bool, st Checks if the mode described in the shared config file of `package_instance` is accurate with respect to its `mode_name`. Returns `True` if correct, `False` with message on mismatch. """ - shared_config_dict = _load_shared_config(package_instance.shared_config_file_path) + shared_config_dict = load_yaml_to_dict(package_instance.shared_config_file_path) try: running_config = CLPConfig.model_validate(shared_config_dict) except ValidationError as err: diff --git a/integration-tests/tests/utils/utils.py b/integration-tests/tests/utils/utils.py index 3594b34b64..075359e589 100644 --- a/integration-tests/tests/utils/utils.py +++ b/integration-tests/tests/utils/utils.py @@ -5,7 +5,9 @@ import subprocess from pathlib import Path from tempfile import NamedTemporaryFile -from typing import IO +from typing import Any, IO + +import yaml def get_env_var(var_name: str) -> str: @@ -51,6 +53,31 @@ def is_json_file_structurally_equal(json_fp1: Path, json_fp2: Path) -> bool: return is_dir_tree_content_equal(Path(temp_file_1.name), Path(temp_file_2.name)) +def load_yaml_to_dict(path: Path) -> dict[str, Any]: + """ + :param path: + :return: The dict constructed from parsing the content of the YAML file at `path`. + :raise: ValueError if there is invalid YAML in target file. + :raise: ValueError if the target file can't be read. + :raise: TypeError if the target file has no top-level mapping. + """ + try: + with path.open("r", encoding="utf-8") as file: + target_dict = yaml.safe_load(file) + except yaml.YAMLError as err: + err_msg = f"Invalid YAML in target file {path}: {err}" + raise ValueError(err_msg) from err + except OSError as err: + err_msg = f"Cannot read target file {path}: {err}" + raise ValueError(err_msg) from err + + if not isinstance(target_dict, dict): + err_msg = f"Target file {path} must have a top-level mapping." + raise TypeError(err_msg) + + return target_dict + + def unlink(rm_path: Path, force: bool = True) -> None: """ Remove a file or directory at `path`. From f9865a88fe9dff0a6d2cf8ccf9f1df1f503ceda4 Mon Sep 17 00:00:00 2001 From: Quinn Date: Thu, 6 Nov 2025 22:43:32 +0000 Subject: [PATCH 18/22] Add taskfile tasks for package tests. --- taskfiles/tests/integration.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/taskfiles/tests/integration.yaml b/taskfiles/tests/integration.yaml index 7fc7304a57..bb1c83da90 100644 --- a/taskfiles/tests/integration.yaml +++ b/taskfiles/tests/integration.yaml @@ -24,3 +24,13 @@ tasks: CLP_CORE_BINS_DIR: "{{.G_CORE_COMPONENT_BUILD_DIR}}" CLP_PACKAGE_DIR: "{{.G_PACKAGE_BUILD_DIR}}" cmd: "uv run python -m pytest -m core" + + package: + deps: + - task: "::package" + dir: "{{.G_INTEGRATION_TESTS_DIR}}" + env: + CLP_BUILD_DIR: "{{.G_BUILD_DIR}}" + CLP_CORE_BINS_DIR: "{{.G_CORE_COMPONENT_BUILD_DIR}}" + CLP_PACKAGE_DIR: "{{.G_PACKAGE_BUILD_DIR}}" + cmd: "uv run python -m pytest -m package" From 865e9edc1d1dc8063652716cddd947b98de75627 Mon Sep 17 00:00:00 2001 From: Quinn Date: Fri, 7 Nov 2025 16:08:58 +0000 Subject: [PATCH 19/22] Address rabbit comments. --- integration-tests/pyproject.toml | 5 +- .../tests/fixtures/package_config_fixtures.py | 16 +------ integration-tests/tests/utils/config.py | 47 +++++++++++++------ .../tests/utils/package_utils.py | 26 ---------- 4 files changed, 38 insertions(+), 56 deletions(-) diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index c2f41d3913..b95a42a4bb 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -28,7 +28,6 @@ dev = [ [tool.mypy] strict = true -ignore_missing_imports = true # Additional output pretty = true @@ -36,6 +35,10 @@ show_error_code_links = true show_error_context = true show_error_end = true +[[tool.mypy.overrides]] +module = ["clp_py_utils.*"] +ignore_missing_imports = true + [tool.ruff] line-length = 100 diff --git a/integration-tests/tests/fixtures/package_config_fixtures.py b/integration-tests/tests/fixtures/package_config_fixtures.py index 365b5f93ab..f5c0dfe5f8 100644 --- a/integration-tests/tests/fixtures/package_config_fixtures.py +++ b/integration-tests/tests/fixtures/package_config_fixtures.py @@ -8,7 +8,6 @@ from tests.utils.clp_mode_utils import CLP_MODE_CONFIGS from tests.utils.config import PackageConfig -from tests.utils.package_utils import write_temp_config_file from tests.utils.utils import get_env_var logger = logging.getLogger(__name__) @@ -22,10 +21,10 @@ def _build_package_config_for_mode(mode_name: str) -> PackageConfig: clp_package_dir = Path(get_env_var("CLP_PACKAGE_DIR")).expanduser().resolve() test_root_dir = Path(get_env_var("CLP_BUILD_DIR")).expanduser().resolve() / "integration-tests" - build_config = CLP_MODE_CONFIGS[mode_name][0] required_components = CLP_MODE_CONFIGS[mode_name][1] - package_config = PackageConfig( + + return PackageConfig( clp_package_dir=clp_package_dir, test_root_dir=test_root_dir, mode_name=mode_name, @@ -33,17 +32,6 @@ def _build_package_config_for_mode(mode_name: str) -> PackageConfig: component_list=required_components, ) - # Write the temporary config file that the package will use. - clp_config_obj = build_config() - temp_config_file_path: Path = write_temp_config_file( - clp_config=clp_config_obj, - temp_config_dir=package_config.temp_config_dir, - mode_name=mode_name, - ) - object.__setattr__(package_config, "temp_config_file_path", temp_config_file_path) - - return package_config - @pytest.fixture def clp_config( diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index 357fbc3635..001d5bbe80 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -6,7 +6,9 @@ from collections.abc import Callable from dataclasses import dataclass, field, InitVar from pathlib import Path -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING + +import yaml if TYPE_CHECKING: from clp_py_utils.clp_config import CLPConfig @@ -87,19 +89,16 @@ class PackageConfig: component_list: list[str] #: Directory to store any cached package config files. - temp_config_dir_init: InitVar[Path | None] = None temp_config_dir: Path = field(init=False, repr=True) #: The location of the constructed temporary config file for this package. temp_config_file_path: Path = field(init=False, repr=True) - def __post_init__(self, temp_config_dir_init: Path | None) -> None: + def __post_init__(self) -> None: """Validates the values specified at init, and initialises attributes.""" # Validate that the CLP package directory exists and contains all required directories. clp_package_dir = self.clp_package_dir validate_dir_exists(clp_package_dir) - - # Check for required package script directories required_dirs = ["etc", "sbin"] missing_dirs = [d for d in required_dirs if not (clp_package_dir / d).is_dir()] if len(missing_dirs) > 0: @@ -110,20 +109,18 @@ def __post_init__(self, temp_config_dir_init: Path | None) -> None: raise ValueError(err_msg) # Initialize and create required cache directory for package tests. - if temp_config_dir_init is not None: - object.__setattr__(self, "temp_config_dir", temp_config_dir_init) - else: - object.__setattr__(self, "temp_config_dir", self.test_root_dir / "config-cache") - + object.__setattr__(self, "temp_config_dir", self.test_root_dir / "temp_config_files") self.test_root_dir.mkdir(parents=True, exist_ok=True) self.temp_config_dir.mkdir(parents=True, exist_ok=True) - # Initialize temp_config_file_path placeholder. The file will be written by the fixture. - object.__setattr__( - self, - "temp_config_file_path", - self.temp_config_dir / f"clp-config-{self.mode_name}.yml", + # Write the temporary config file that the package will use. + clp_config_obj = self.build_config() + temp_config_file_path: Path = self._write_temp_config_file( + clp_config=clp_config_obj, + temp_config_dir=self.temp_config_dir, + mode_name=self.mode_name, ) + object.__setattr__(self, "temp_config_file_path", temp_config_file_path) @property def start_script_path(self) -> Path: @@ -135,6 +132,26 @@ def stop_script_path(self) -> Path: """:return: The absolute path to the package stop script.""" return self.clp_package_dir / "sbin" / "stop-clp.sh" + @staticmethod + def _write_temp_config_file( + clp_config: CLPConfig, + temp_config_dir: Path, + mode_name: str, + ) -> Path: + """Writes a temporary config file to `temp_config_dir` for a CLPConfig object.""" + temp_config_dir.mkdir(parents=True, exist_ok=True) + temp_config_filename = f"clp-config-{mode_name}.yml" + temp_config_file_path = temp_config_dir / temp_config_filename + + payload: dict[str, Any] = clp_config.dump_to_primitive_dict() + + tmp_path = temp_config_file_path.with_suffix(temp_config_file_path.suffix + ".tmp") + with tmp_path.open("w", encoding="utf-8") as f: + yaml.safe_dump(payload, f, sort_keys=False) + tmp_path.replace(temp_config_file_path) + + return temp_config_file_path + @dataclass(frozen=True) class PackageInstance: diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index 68106c233e..2c7da02c89 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -2,10 +2,7 @@ import shutil import subprocess -from pathlib import Path -from typing import Any -import yaml from clp_py_utils.clp_config import ( CLPConfig, ) @@ -23,29 +20,6 @@ from tests.utils.utils import load_yaml_to_dict -def write_temp_config_file( - clp_config: CLPConfig, - temp_config_dir: Path, - mode_name: str, -) -> Path: - """ - Writes a temporary config file to `temp_config_dir` for a CLPConfig object. Returns the path to - the temporary file on success. - """ - temp_config_dir.mkdir(parents=True, exist_ok=True) - temp_config_filename = f"clp-config-{mode_name}.yml" - temp_config_file_path = temp_config_dir / temp_config_filename - - payload: dict[str, Any] = clp_config.dump_to_primitive_dict() - - tmp_path = temp_config_file_path.with_suffix(temp_config_file_path.suffix + ".tmp") - with tmp_path.open("w", encoding="utf-8") as f: - yaml.safe_dump(payload, f, sort_keys=False) - tmp_path.replace(temp_config_file_path) - - return temp_config_file_path - - def start_clp_package(package_config: PackageConfig) -> None: """Start an instance of the CLP package.""" start_script_path = package_config.start_script_path From 09d52006b50b11ef99e11e70a5ef5a108dc3b246 Mon Sep 17 00:00:00 2001 From: Quinn Date: Fri, 7 Nov 2025 16:14:29 +0000 Subject: [PATCH 20/22] Add mariadb_config install for linting purposes (will be unecessary after #1549). --- .github/workflows/clp-lint.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/clp-lint.yaml b/.github/workflows/clp-lint.yaml index 03045716f6..1d97737e74 100644 --- a/.github/workflows/clp-lint.yaml +++ b/.github/workflows/clp-lint.yaml @@ -43,6 +43,30 @@ jobs: name: "Install coreutils (for md5sum)" run: "brew install coreutils" + # This is a necessary dependency for the Python mariadb package which is a transitive + # dependency of `lint:py-check`. + - name: "Install MariaDB Connector/C" + shell: "bash" + run: |- + case "${{ matrix.os }}" in + ubuntu-24.04) + sudo apt-get update + sudo apt-get install -y libmariadb-dev + MARIADB_CONFIG_PATH="$(command -v mariadb_config)" + ;; + macos-15) + brew update + brew install mariadb-connector-c + PREFIX="$(brew --prefix mariadb-connector-c)" + MARIADB_CONFIG_PATH="$PREFIX/bin/mariadb_config" + ;; + *) + # Control flow should not reach here + exit 1 + ;; + esac + echo "MARIADB_CONFIG=$MARIADB_CONFIG_PATH" >> "$GITHUB_ENV" + - name: "Lint .js files" shell: "bash" run: "task lint:check-js" From 5fffc324d87f2ce687ef4f39892aa70b76d693e2 Mon Sep 17 00:00:00 2001 From: Quinn Date: Fri, 7 Nov 2025 16:31:57 +0000 Subject: [PATCH 21/22] Lint YAML. --- taskfiles/tests/integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taskfiles/tests/integration.yaml b/taskfiles/tests/integration.yaml index bb1c83da90..6f02327c3b 100644 --- a/taskfiles/tests/integration.yaml +++ b/taskfiles/tests/integration.yaml @@ -24,7 +24,7 @@ tasks: CLP_CORE_BINS_DIR: "{{.G_CORE_COMPONENT_BUILD_DIR}}" CLP_PACKAGE_DIR: "{{.G_PACKAGE_BUILD_DIR}}" cmd: "uv run python -m pytest -m core" - + package: deps: - task: "::package" From e348be04de84afe38a12346503dae8a0c5708593 Mon Sep 17 00:00:00 2001 From: Quinn Date: Fri, 7 Nov 2025 18:58:48 +0000 Subject: [PATCH 22/22] Adapt code after #1549 merge. --- integration-tests/pyproject.toml | 5 ----- integration-tests/tests/utils/config.py | 8 ++------ integration-tests/uv.lock | 3 +-- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index 593e103848..315336e2b0 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -23,7 +23,6 @@ clp = [ "job-orchestration", ] dev = [ - "clp-py-utils", "mypy>=1.16.0", "ruff>=0.11.12", "pytest>=8.4.1", @@ -41,10 +40,6 @@ show_error_code_links = true show_error_context = true show_error_end = true -[[tool.mypy.overrides]] -module = ["clp_py_utils.*"] -ignore_missing_imports = true - [tool.ruff] line-length = 100 diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index 001d5bbe80..e2bc7552be 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -6,16 +6,12 @@ from collections.abc import Callable from dataclasses import dataclass, field, InitVar from pathlib import Path -from typing import Any, TYPE_CHECKING import yaml - -if TYPE_CHECKING: - from clp_py_utils.clp_config import CLPConfig - from clp_py_utils.clp_config import ( CLP_DEFAULT_LOG_DIRECTORY_PATH, CLP_SHARED_CONFIG_FILENAME, + CLPConfig, ) from tests.utils.utils import ( @@ -143,7 +139,7 @@ def _write_temp_config_file( temp_config_filename = f"clp-config-{mode_name}.yml" temp_config_file_path = temp_config_dir / temp_config_filename - payload: dict[str, Any] = clp_config.dump_to_primitive_dict() + payload = clp_config.dump_to_primitive_dict() # type: ignore[no-untyped-call] tmp_path = temp_config_file_path.with_suffix(temp_config_file_path.suffix + ".tmp") with tmp_path.open("w", encoding="utf-8") as f: diff --git a/integration-tests/uv.lock b/integration-tests/uv.lock index f64921e79c..c6cea155dc 100644 --- a/integration-tests/uv.lock +++ b/integration-tests/uv.lock @@ -879,7 +879,6 @@ clp = [ { name = "job-orchestration" }, ] dev = [ - { name = "clp-py-utils" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-env" }, @@ -898,12 +897,12 @@ clp = [ { name = "job-orchestration", directory = "../components/job-orchestration" }, ] dev = [ - { name = "clp-py-utils", directory = "../components/clp-py-utils" }, { name = "mypy", specifier = ">=1.16.0" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-env", specifier = ">=1.1.5" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "ruff", specifier = ">=0.11.12" }, + { name = "types-pyyaml", specifier = ">=6.0.12.20240808" }, ] [[package]]