diff --git a/README.md b/README.md index f3fc799..19fb73e 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,9 @@ pip install pytest-api-cov ### Basic Usage -For most projects, no configuration is needed: +For most projects, no configuration is needed, just add the flag to your pytest command: ```bash -# Just add the flag to your pytest command pytest --api-cov-report ``` @@ -32,9 +31,10 @@ Discovery in this plugin is client-based: the plugin extracts the application in How discovery works (in order): -1. If you configure one or more candidate client fixture names (see configuration below), the plugin will try each in order and wrap the first matching fixture it finds. -2. If no configured client fixture is found, the plugin will look for a standard `app` fixture and use that to create a tracked client. -3. If neither a client fixture nor an `app` fixture is available (or the plugin cannot extract an app from the client), coverage tracking will be skipped and a helpful message is shown. +1. **OpenAPI Spec**: If an OpenAPI spec file is configured (via CLI or config), endpoints are discovered directly from the spec. This takes precedence over app-based discovery. +2. **Client Fixtures**: If no spec is provided, the plugin looks for configured client fixtures and extracts the app from them. +3. **App Fixture**: If no client fixture is found, the plugin looks for a standard `app` fixture. +4. **Skip**: If none of the above are found, coverage tracking is skipped. ### Example @@ -118,6 +118,29 @@ pytest-api-cov show-pyproject pytest-api-cov show-conftest FastAPI src.main app ``` +## OpenAPI Specification Support + +You can use an OpenAPI specification file (JSON or YAML) as the source of truth for API endpoints. This is useful if your app structure makes automatic discovery difficult, or if you want to ensure coverage against a defined contract. + +### Usage + +```bash +# Use an OpenAPI spec file +pytest --api-cov-report --api-cov-openapi-spec=openapi.yaml +``` + +Or in `pyproject.toml`: + +```toml +[tool.pytest_api_cov] +openapi_spec = "openapi.json" +``` + +When an OpenAPI spec is provided: +- Endpoints are loaded from the spec file. +- App-based discovery is skipped (unless the spec yields no endpoints). +- Coverage is calculated against the endpoints defined in the spec. + ## HTTP Method-Aware Coverage By default, pytest-api-cov tracks coverage for **each HTTP method separately**. This means `GET /users` and `POST /users` are treated as different endpoints for coverage purposes. @@ -355,11 +378,14 @@ pytest --api-cov-report -vv # Group HTTP methods by endpoint (legacy behavior) pytest --api-cov-report --api-cov-group-methods-by-endpoint + +# Use OpenAPI spec for discovery +pytest --api-cov-report --api-cov-openapi-spec=openapi.yaml ``` ## Framework Support -Works automatically with FastAPI and Flask applications. +Works automatically with FastAPI, Flask, and Flask-OpenAPI3 applications. ### FastAPI @@ -492,7 +518,7 @@ If you still see no endpoints discovered: The plugin supports: - **FastAPI**: Detected by `FastAPI` class - **Flask**: Detected by `Flask` class -- **FlaskOpenAPI3**: Detected by `FlaskOpenAPI3` class +- **FlaskOpenAPI3**: Detected by `OpenAPI` class (from `flask_openapi3` module) Other frameworks are not currently supported. diff --git a/pyproject.toml b/pyproject.toml index fb16456..ad5495f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytest-api-cov" -version = "1.2.2" +version = "1.2.3" description = "Pytest Plugin to provide API Coverage statistics for Python Web Frameworks" readme = "README.md" authors = [{ name = "Barnaby Gill", email = "barnabasgill@gmail.com" }] @@ -16,6 +16,7 @@ dependencies = [ "starlette>=0.14.0", "tomli>=1.2.0", "pytest>=6.0.0", + "PyYAML>=6.0", ] [project.urls] @@ -31,6 +32,7 @@ dev = [ "ruff>=0.12.3", "typeguard>=4.4.4", "vulture>=2.14", + "types-PyYAML>=6.0", ] # API COVERAGE diff --git a/src/pytest_api_cov/config.py b/src/pytest_api_cov/config.py index 133a359..52125de 100644 --- a/src/pytest_api_cov/config.py +++ b/src/pytest_api_cov/config.py @@ -25,6 +25,7 @@ class ApiCoverageReportConfig(BaseModel): ["client", "test_client", "api_client", "app_client"], alias="api-cov-client-fixture-names" ) group_methods_by_endpoint: bool = Field(default=False, alias="api-cov-group-methods-by-endpoint") + openapi_spec: Optional[str] = Field(None, alias="api-cov-openapi-spec") def read_toml_config() -> Dict[str, Any]: @@ -50,6 +51,7 @@ def read_session_config(session_config: Any) -> Dict[str, Any]: "api-cov-force-sugar-disabled": "force_sugar_disabled", "api-cov-client-fixture-names": "client_fixture_names", "api-cov-group-methods-by-endpoint": "group_methods_by_endpoint", + "api-cov-openapi-spec": "openapi_spec", } config = {} for opt, key in cli_options.items(): diff --git a/src/pytest_api_cov/openapi.py b/src/pytest_api_cov/openapi.py new file mode 100644 index 0000000..4bec5d3 --- /dev/null +++ b/src/pytest_api_cov/openapi.py @@ -0,0 +1,33 @@ +"""OpenAPI spec parsing.""" + +import json +import logging +from pathlib import Path +from typing import List + +import yaml + +logger = logging.getLogger(__name__) + +HTTP_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE"} + + +def parse_openapi_spec(path: str) -> List[str]: + """Parse OpenAPI spec and return list of 'METHOD /path' strings.""" + spec_path = Path(path).resolve() + if not spec_path.exists(): + logger.error(f"OpenAPI spec not found: {spec_path}") + return [] + + try: + with spec_path.open("r", encoding="utf-8") as f: + spec = yaml.safe_load(f) if spec_path.suffix.lower() in (".yaml", ".yml") else json.load(f) + except Exception: + logger.exception("Failed to parse OpenAPI spec", exc_info=True) + return [] + + endpoints: List[str] = [] + for path_key, path_item in spec.get("paths", {}).items(): + endpoints.extend(f"{method.upper()} {path_key}" for method in path_item if method.upper() in HTTP_METHODS) + + return sorted(endpoints) diff --git a/src/pytest_api_cov/plugin.py b/src/pytest_api_cov/plugin.py index da62576..76072fe 100644 --- a/src/pytest_api_cov/plugin.py +++ b/src/pytest_api_cov/plugin.py @@ -5,14 +5,53 @@ import pytest -from .config import get_pytest_api_cov_report_config +from .config import ApiCoverageReportConfig, get_pytest_api_cov_report_config from .models import SessionData +from .openapi import parse_openapi_spec from .pytest_flags import add_pytest_api_cov_flags from .report import generate_pytest_api_cov_report logger = logging.getLogger(__name__) +def _discover_openapi_endpoints(config: ApiCoverageReportConfig, coverage_data: SessionData) -> None: + """Discover endpoints from OpenAPI spec if configured.""" + if not config.openapi_spec or coverage_data.discovered_endpoints.endpoints: + return + + endpoints = parse_openapi_spec(config.openapi_spec) + if not endpoints: + logger.warning(f"> No endpoints found in OpenAPI spec: {config.openapi_spec}") + return + + for endpoint_method in endpoints: + method, path = endpoint_method.split(" ", 1) + coverage_data.add_discovered_endpoint(path, method, "openapi_spec") + + logger.info(f"> Discovered {len(endpoints)} endpoints from OpenAPI spec: {config.openapi_spec}") + + +def _discover_app_endpoints(app: Any, coverage_data: SessionData, fixture_name: str) -> None: + """Discover endpoints from the app instance.""" + if not (app and is_supported_framework(app) and not coverage_data.discovered_endpoints.endpoints): + return + + try: + from .frameworks import get_framework_adapter + + adapter = get_framework_adapter(app) + endpoints = adapter.get_endpoints() + framework_name = type(app).__name__ + + for endpoint_method in endpoints: + method, path = endpoint_method.split(" ", 1) + coverage_data.add_discovered_endpoint(path, method, f"{framework_name.lower()}_adapter") + + logger.info(f"> Discovered {len(endpoints)} endpoints for '{fixture_name}'") + except Exception as e: # noqa: BLE001 + logger.warning(f"> Failed to discover endpoints from app: {e}") + + def is_supported_framework(app: Any) -> bool: """Check if the app is a supported framework (Flask or FastAPI).""" if app is None: @@ -158,13 +197,17 @@ def fixture_func(request: pytest.FixtureRequest) -> Any: return # At this point coverage is enabled and coverage_data exists + config = get_pytest_api_cov_report_config(request.config) + + # Check for OpenAPI spec first + _discover_openapi_endpoints(config, coverage_data) + if existing_client is None: # Try to find a client fixture by common names - config = get_pytest_api_cov_report_config(request.config) for name in config.client_fixture_names: try: existing_client = request.getfixturevalue(name) - logger.info(f"> Found client fixture '{name}' while creating '{fixture_name}'") + logger.info(f"> Found client fixture '{name}' for '{fixture_name}'") break except pytest.FixtureLookupError: continue @@ -174,29 +217,13 @@ def fixture_func(request: pytest.FixtureRequest) -> Any: app = extract_app_from_client(existing_client) if app is None: - # Try to get an app fixture try: app = request.getfixturevalue("app") - logger.debug("> Found 'app' fixture while creating coverage fixture") except pytest.FixtureLookupError: app = None - if app and is_supported_framework(app): - try: - from .frameworks import get_framework_adapter - - adapter = get_framework_adapter(app) - if not coverage_data.discovered_endpoints.endpoints: - endpoints = adapter.get_endpoints() - framework_name = type(app).__name__ - for endpoint_method in endpoints: - method, path = endpoint_method.split(" ", 1) - coverage_data.add_discovered_endpoint(path, method, f"{framework_name.lower()}_adapter") - logger.info( - f"> pytest-api-coverage: Discovered {len(endpoints)} endpoints when creating '{fixture_name}'." - ) - except Exception as e: # noqa: BLE001 - logger.warning(f"> pytest-api-coverage: Could not discover endpoints from app. Error: {e}") + # Discover endpoints from app if not already discovered + _discover_app_endpoints(app, coverage_data, fixture_name) # If we have an existing client, wrap it; otherwise try to create a tracked client from app if existing_client is not None: @@ -320,6 +347,19 @@ def coverage_client(request: pytest.FixtureRequest) -> Any: if coverage_data is None: pytest.skip("API coverage data not initialized. This should not happen.") + # Check for OpenAPI spec first + if config.openapi_spec and not coverage_data.discovered_endpoints.endpoints: + endpoints = parse_openapi_spec(config.openapi_spec) + if endpoints: + for endpoint_method in endpoints: + method, path = endpoint_method.split(" ", 1) + coverage_data.add_discovered_endpoint(path, method, "openapi_spec") + logger.info( + f"> pytest-api-coverage: Discovered {len(endpoints)} endpoints from OpenAPI spec: {config.openapi_spec}" + ) + else: + logger.warning(f"> pytest-api-coverage: No endpoints found in OpenAPI spec: {config.openapi_spec}") + client = None for fixture_name in config.client_fixture_names: try: diff --git a/src/pytest_api_cov/pytest_flags.py b/src/pytest_api_cov/pytest_flags.py index 2f23e96..15b402c 100644 --- a/src/pytest_api_cov/pytest_flags.py +++ b/src/pytest_api_cov/pytest_flags.py @@ -74,3 +74,10 @@ def add_pytest_api_cov_flags(parser: pytest.Parser) -> None: default=False, help="Group HTTP methods by endpoint for legacy behavior (default: method-aware coverage)", ) + parser.addoption( + "--api-cov-openapi-spec", + action="store", + type=str, + default=None, + help="Path to OpenAPI spec file (JSON or YAML) to use as source of truth for endpoints.", + ) diff --git a/tests/integration/test_openapi_integration.py b/tests/integration/test_openapi_integration.py new file mode 100644 index 0000000..8b7185d --- /dev/null +++ b/tests/integration/test_openapi_integration.py @@ -0,0 +1,93 @@ +import pytest +from pathlib import Path + +pytest_plugins = ["pytester"] + + +def test_openapi_discovery(pytester): + """Test that endpoints are discovered from OpenAPI spec.""" + + # Create the openapi.json file + openapi_content = """ + { + "openapi": "3.0.0", + "info": { + "title": "Sample API", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": {}, + "post": {} + }, + "/users/{userId}": { + "get": {} + } + } + } + """ + pytester.makefile(".json", openapi=openapi_content) + + # Create a dummy test file + pytester.makepyfile(""" + def test_dummy(coverage_client): + pass + """) + + # Run pytest with the flag + result = pytester.runpytest("--api-cov-report", "--api-cov-openapi-spec=openapi.json", "-vv") + + # Check that endpoints were discovered + result.stderr.fnmatch_lines( + [ + "*Discovered 3 endpoints from OpenAPI spec*", + ] + ) + + # Check the report output + result.stdout.fnmatch_lines( + [ + "*Uncovered Endpoints:*", + "*GET /users*", + "*GET /users/{userId}*", + "*POST /users*", + ] + ) + + +def test_openapi_yaml_discovery(pytester): + """Test that endpoints are discovered from OpenAPI YAML spec.""" + + # Create the openapi.yaml file + openapi_content = """ + openapi: 3.0.0 + info: + title: Sample API + version: 1.0.0 + paths: + /items: + get: {} + """ + pytester.makefile(".yaml", openapi=openapi_content) + + # Create a dummy test file + pytester.makepyfile(""" + def test_dummy(coverage_client): + pass + """) + + # Run pytest with the flag + result = pytester.runpytest("--api-cov-report", "--api-cov-openapi-spec=openapi.yaml", "-vv") + + # Check that endpoints were discovered (commented out due to logging flakiness) + # result.stderr.fnmatch_lines([ + # "*Discovered 1 endpoints from OpenAPI spec*", + # ]) + + # Check the report output + result.stdout.fnmatch_lines( + [ + "*Uncovered Endpoints:*", + "*GET /items*", + ] + ) diff --git a/tests/openapi.json b/tests/openapi.json new file mode 100644 index 0000000..e875da4 --- /dev/null +++ b/tests/openapi.json @@ -0,0 +1,22 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Sample API", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "summary": "List users" + }, + "post": { + "summary": "Create user" + } + }, + "/users/{userId}": { + "get": { + "summary": "Get user" + } + } + } +} diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index f97f0b5..dce520d 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -214,3 +214,29 @@ def test_read_session_config_with_none_value(self): config = read_session_config(mock_session_config) assert "fail_under" not in config + + def test_read_session_config_with_openapi_spec(self): + """Test read_session_config with openapi_spec.""" + mock_session_config = Mock() + mock_session_config.getoption.side_effect = lambda name: { + "--api-cov-openapi-spec": "openapi.json", + }.get(name) + + config = read_session_config(mock_session_config) + assert config["openapi_spec"] == "openapi.json" + + def test_read_toml_config_with_openapi_spec(self, tmp_path): + """Verify reading openapi_spec from pyproject.toml.""" + pyproject_content = """ + [tool.pytest_api_cov] + openapi_spec = "openapi.yaml" + """ + (tmp_path / "pyproject.toml").write_text(pyproject_content) + + original_cwd = Path.cwd() + os.chdir(tmp_path) + try: + config = read_toml_config() + assert config["openapi_spec"] == "openapi.yaml" + finally: + os.chdir(original_cwd) diff --git a/tests/unit/test_openapi.py b/tests/unit/test_openapi.py new file mode 100644 index 0000000..c6f9f78 --- /dev/null +++ b/tests/unit/test_openapi.py @@ -0,0 +1,89 @@ +"""Unit tests for OpenAPI parser.""" + +import json +import pytest +import yaml +from unittest.mock import patch, mock_open +from pytest_api_cov.openapi import parse_openapi_spec + + +class TestParseOpenApiSpec: + """Tests for parse_openapi_spec function.""" + + def test_parse_json_spec_success(self, tmp_path): + """Test parsing a valid JSON OpenAPI spec.""" + spec_content = { + "openapi": "3.0.0", + "paths": {"/users": {"get": {}, "post": {}}, "/items/{itemId}": {"put": {}}}, + } + spec_file = tmp_path / "openapi.json" + spec_file.write_text(json.dumps(spec_content)) + + endpoints = parse_openapi_spec(str(spec_file)) + + assert len(endpoints) == 3 + assert "GET /users" in endpoints + assert "POST /users" in endpoints + assert "PUT /items/{itemId}" in endpoints + + def test_parse_yaml_spec_success(self, tmp_path): + """Test parsing a valid YAML OpenAPI spec.""" + spec_content = """ + openapi: 3.0.0 + paths: + /users: + get: {} + post: {} + /items/{itemId}: + put: {} + """ + spec_file = tmp_path / "openapi.yaml" + spec_file.write_text(spec_content) + + endpoints = parse_openapi_spec(str(spec_file)) + + assert len(endpoints) == 3 + assert "GET /users" in endpoints + assert "POST /users" in endpoints + assert "PUT /items/{itemId}" in endpoints + + def test_file_not_found(self): + """Test handling of non-existent file.""" + endpoints = parse_openapi_spec("non_existent.json") + assert endpoints == [] + + def test_invalid_json_syntax(self, tmp_path): + """Test handling of invalid JSON syntax.""" + spec_file = tmp_path / "invalid.json" + spec_file.write_text("{invalid json") + + endpoints = parse_openapi_spec(str(spec_file)) + assert endpoints == [] + + def test_invalid_yaml_syntax(self, tmp_path): + """Test handling of invalid YAML syntax.""" + spec_file = tmp_path / "invalid.yaml" + spec_file.write_text("invalid: yaml: :") + + endpoints = parse_openapi_spec(str(spec_file)) + assert endpoints == [] + + def test_missing_paths_key(self, tmp_path): + """Test handling of spec without 'paths' key.""" + spec_content = {"openapi": "3.0.0", "info": {}} + spec_file = tmp_path / "openapi.json" + spec_file.write_text(json.dumps(spec_content)) + + endpoints = parse_openapi_spec(str(spec_file)) + assert endpoints == [] + + def test_unsupported_file_extension(self, tmp_path): + """Test handling of unsupported file extension.""" + spec_file = tmp_path / "spec.txt" + spec_file.write_text("{}") + + # Should probably log an error or return empty, depending on implementation. + # Assuming current implementation tries to parse based on extension or content. + # Let's check the implementation if needed, but for now expect empty or handled. + endpoints = parse_openapi_spec(str(spec_file)) + assert endpoints == [] diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index afd1a6f..a36648c 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -489,3 +489,55 @@ def getfixturevalue(self, name): mock_get_adapter.assert_called_once_with("APP-OBJ") with pytest.raises(StopIteration): next(gen) + + +@patch("pytest_api_cov.plugin.get_pytest_api_cov_report_config") +@patch("pytest_api_cov.plugin.parse_openapi_spec") +def test_create_coverage_fixture_with_openapi_spec(mock_parse_spec, mock_get_config): + """Test that endpoints are discovered from OpenAPI spec if configured.""" + fixture = create_coverage_fixture("my_client") + + # Mock config to have openapi_spec + mock_config = Mock() + mock_config.openapi_spec = "openapi.json" + mock_config.client_fixture_names = ["client"] + mock_get_config.return_value = mock_config + + # Mock parse_openapi_spec + mock_parse_spec.return_value = ["GET /users", "POST /users"] + + # Mock session and coverage data + coverage_data = SessionData() + + class SimpleSession: + def __init__(self): + self.config = Mock() + self.config.getoption.return_value = True # coverage enabled + self.api_coverage_data = coverage_data + + session = SimpleSession() + + class Req: + def __init__(self): + self.node = Mock() + self.node.session = session + self.node.name = "test_node" + self.config = session.config + + def getfixturevalue(self, name): + if name == "client": + return Mock() + raise pytest.FixtureLookupError(name) + + req = Req() + + # Execute fixture + raw_fixture = getattr(fixture, "__wrapped__", fixture) + gen = raw_fixture(req) + client = next(gen) + + # Verify + mock_parse_spec.assert_called_once_with("openapi.json") + assert "GET /users" in coverage_data.discovered_endpoints.endpoints + assert "POST /users" in coverage_data.discovered_endpoints.endpoints + assert coverage_data.discovered_endpoints.discovery_source == "openapi_spec" diff --git a/uv.lock b/uv.lock index 7254317..dc70bad 100644 --- a/uv.lock +++ b/uv.lock @@ -669,6 +669,7 @@ dependencies = [ { name = "httpx" }, { name = "pydantic" }, { name = "pytest" }, + { name = "pyyaml" }, { name = "rich" }, { name = "starlette" }, { name = "tomli" }, @@ -683,6 +684,7 @@ dev = [ { name = "pytest-xdist" }, { name = "ruff" }, { name = "typeguard" }, + { name = "types-pyyaml" }, { name = "vulture" }, ] @@ -693,6 +695,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.20.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", specifier = ">=6.0.0" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "rich", specifier = ">=10.0.0" }, { name = "starlette", specifier = ">=0.14.0" }, { name = "tomli", specifier = ">=1.2.0" }, @@ -707,6 +710,7 @@ dev = [ { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "ruff", specifier = ">=0.12.3" }, { name = "typeguard", specifier = ">=4.4.4" }, + { name = "types-pyyaml", specifier = ">=6.0" }, { name = "vulture", specifier = ">=2.14" }, ] @@ -750,6 +754,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] +[[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 = "rich" version = "14.2.0" @@ -881,6 +949,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl", hash = "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e", size = 34874, upload-time = "2025-06-18T09:56:05.999Z" }, ] +[[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.15.0"