diff --git a/.github/workflows/confluence-importer.yaml b/.github/workflows/confluence-importer.yaml index 2ff53602..2c0d1a56 100644 --- a/.github/workflows/confluence-importer.yaml +++ b/.github/workflows/confluence-importer.yaml @@ -41,6 +41,8 @@ jobs: run: uv sync --dev - name: Lint with ruff run: uv run ruff check . + - name: Lint with mypy + run: uv run mypy . test: runs-on: ubuntu-latest diff --git a/services/confluence-importer/confluence_importer/c4.py b/services/confluence-importer/confluence_importer/c4.py index c687c5d5..42322d89 100644 --- a/services/confluence-importer/confluence_importer/c4.py +++ b/services/confluence-importer/confluence_importer/c4.py @@ -1,5 +1,6 @@ """Module for interacting with the C4 API to manage Confluence content.""" +from typing import TypedDict import requests from confluence_importer.logger import logger @@ -19,13 +20,13 @@ def clear_previous_ingests() -> None: for index, item in enumerate(files): num_items = len(files) - file_name = item.get("fileName") + file_name = item["fileName"] is_confluence_page_file = file_name.startswith("confluence_page_") and file_name.endswith(".md") if is_confluence_page_file: try: - delete_confluence_page(item.get("id")) + delete_confluence_page(item["id"]) except Exception as e: deletion_counter["error"] += 1 logger.error( @@ -58,7 +59,7 @@ def clear_previous_ingests() -> None: ) -def delete_confluence_page(file_id): +def delete_confluence_page(file_id: int) -> None: """Deletes a file from the C4 bucket by its ID. Args: @@ -67,7 +68,14 @@ def delete_confluence_page(file_id): requests.delete(f"{c4_base_url}/api/buckets/{bucket_id}/files/{file_id}", headers={"x-api-key": config.c4_token}) -def fetch_bucket_files_list(): +class C4BucketFileItem(TypedDict): + """TypedDict representing a file item in the C4 bucket.""" + + id: int + fileName: str + + +def fetch_bucket_files_list() -> list[C4BucketFileItem]: """Fetches the list of all files in the C4 bucket. Returns: @@ -76,14 +84,14 @@ def fetch_bucket_files_list(): page = 1 batch_size = 50 - items: list[str] = [] + items: list[C4BucketFileItem] = [] while True: logger.debug("Fetching partial list of files from c4 ", bucket_id=bucket_id, page=page) response = requests.get(f"{c4_base_url}/api/buckets/{bucket_id}/files", headers={"x-api-key": config.c4_token}) - total = response.json().get("total") - items_in_page = response.json().get("items") + total = response.json().get("total", 0) + items_in_page = response.json().get("items", []) items.extend(items_in_page) diff --git a/services/confluence-importer/confluence_importer/config.py b/services/confluence-importer/confluence_importer/config.py index 0a16aa0d..6e0f11ed 100644 --- a/services/confluence-importer/confluence_importer/config.py +++ b/services/confluence-importer/confluence_importer/config.py @@ -23,4 +23,4 @@ class Config(BaseSettings): c4_token: str -config = Config() +config = Config() # type: ignore[call-arg] diff --git a/services/confluence-importer/confluence_importer/confluence.py b/services/confluence-importer/confluence_importer/confluence.py index 5be38aa7..9375f98e 100644 --- a/services/confluence-importer/confluence_importer/confluence.py +++ b/services/confluence-importer/confluence_importer/confluence.py @@ -10,7 +10,7 @@ confluence_url = config.confluence_url -confluence_api = Confluence(url=confluence_url, token=config.confluence_token) +confluence_api = Confluence(url=confluence_url, token=config.confluence_token) # type: ignore[no-untyped-call] @dataclass @@ -32,7 +32,7 @@ def get_page(page_id: int) -> ConfluencePage: Returns: A ConfluencePage dataclass containing the page information and content as HTML """ - page = confluence_api.get_page_by_id(page_id, expand="body.storage,history.lastUpdated") + page = confluence_api.get_page_by_id(page_id, expand="body.storage,history.lastUpdated") # type: ignore[no-untyped-call] return ConfluencePage( page_id, @@ -67,7 +67,7 @@ def get_pages_for_space(space_key: str) -> Generator[ConfluencePage]: content_type="page", expand="body.storage,history.lastUpdated", status="current", - ) + ) # type: ignore[no-untyped-call] len_result = 0 for r in result: diff --git a/services/confluence-importer/main.py b/services/confluence-importer/main.py index 7e131027..691fc407 100644 --- a/services/confluence-importer/main.py +++ b/services/confluence-importer/main.py @@ -1,5 +1,6 @@ """Main module for the Confluence to C4 synchronization process.""" +from dataclasses import dataclass from confluence_importer import confluence from confluence_importer.c4 import clear_previous_ingests, import_confluence_page from confluence_importer.markdown import html_to_markdown @@ -11,7 +12,15 @@ page_ids = config.confluence_page_ids_to_import -def process_confluence_spaces(page_import_counter): +@dataclass +class PageImportCounter: + """Data class to track the number of successful and failed imports.""" + + error: int = 0 + success: int = 0 + + +def process_confluence_spaces(page_import_counter: PageImportCounter) -> None: """Processes all Confluence spaces specified in the configuration. Fetches all pages from each space and imports them into C4. @@ -29,10 +38,10 @@ def process_confluence_spaces(page_import_counter): try: page_markdown = html_to_markdown(page) import_confluence_page(page.id, page_markdown) - page_import_counter["success"] += 1 + page_import_counter.success += 1 logger.info("Import Confluence page", space_key=space_key, page_id=page.id, page_count=f"{index}") except Exception as e: - page_import_counter["error"] += 1 + page_import_counter.error += 1 logger.error( "Error importing Confluence page", error=str(e), @@ -45,7 +54,7 @@ def process_confluence_spaces(page_import_counter): logger.info("Import of all Confluence Spaces completed") -def process_individual_pages(page_import_counter): +def process_individual_pages(page_import_counter: PageImportCounter) -> None: """Processes individual Confluence pages specified in the configuration. Fetches each page by ID and imports it into C4. @@ -61,10 +70,10 @@ def process_individual_pages(page_import_counter): page = confluence.get_page(page_id) page_markdown = html_to_markdown(page) import_confluence_page(page_id, page_markdown) - page_import_counter["success"] += 1 + page_import_counter.success += 1 logger.info("Import Confluence page", page_id=page_id, progress=f"{index + 1}/{num_pages}") except Exception as e: - page_import_counter["error"] += 1 + page_import_counter.error += 1 logger.error( "Error importing Confluence page", error=str(e), page_id=page_id, progress=f"{index + 1}/{num_pages}" ) @@ -72,7 +81,7 @@ def process_individual_pages(page_import_counter): logger.info("Import of individual Confluence pages completed") -def log_final_results(page_import_counter): +def log_final_results(page_import_counter: PageImportCounter) -> None: """Logs the final results of the import process. Outputs either a success message or an error message based on the import counter. @@ -80,7 +89,7 @@ def log_final_results(page_import_counter): Args: page_import_counter: Dictionary containing counts of successful and failed imports """ - if page_import_counter["error"] > 0: + if page_import_counter.error > 0: logger.error( "Synchronization Confluence to c4 completed with errors! See log for more information.", page_import_counter=page_import_counter, @@ -89,7 +98,7 @@ def log_final_results(page_import_counter): logger.info("Synchronization Confluence to c4 completed.", page_import_counter) -def main(): +def main() -> None: """Main entry point for the Confluence to C4 synchronization process. Orchestrates the entire import process: @@ -102,7 +111,8 @@ def main(): clear_previous_ingests() - page_import_counter = {"error": 0, "success": 0} + page_import_counter = PageImportCounter() + process_confluence_spaces(page_import_counter) process_individual_pages(page_import_counter) log_final_results(page_import_counter) diff --git a/services/confluence-importer/pyproject.toml b/services/confluence-importer/pyproject.toml index a1892a15..de3018cc 100644 --- a/services/confluence-importer/pyproject.toml +++ b/services/confluence-importer/pyproject.toml @@ -24,7 +24,18 @@ convention = "google" [dependency-groups] dev = [ + "mypy>=1.17.1", + "pip>=25.2", "pytest>=8.4.1", "pytest-mock>=3.14.1", "ruff>=0.12.7", ] + +[tool.mypy] +strict = true +install_types = true +non_interactive = true +ignore_missing_imports = true +exclude = [ + ".cache", + ] diff --git a/services/confluence-importer/tests/test_c4.py b/services/confluence-importer/tests/test_c4.py index 82ea41cd..3096ea83 100644 --- a/services/confluence-importer/tests/test_c4.py +++ b/services/confluence-importer/tests/test_c4.py @@ -13,7 +13,7 @@ class TestC4: """Tests for the c4 module functionality.""" - def test_delete_confluence_page(self, mocker: MockerFixture): + def test_delete_confluence_page(self, mocker: MockerFixture) -> None: """Test that delete_confluence_page correctly calls the C4 API. Args: @@ -24,17 +24,17 @@ def test_delete_confluence_page(self, mocker: MockerFixture): mocker.patch("confluence_importer.c4.c4_base_url", "http://test-url") mocker.patch("confluence_importer.c4.bucket_id", "test-bucket") mocker.patch("confluence_importer.c4.config.c4_token", "test-token") - file_id = "test-file-id" + file_id = 23 # act delete_confluence_page(file_id) # assert mock_requests.delete.assert_called_once_with( - "http://test-url/api/buckets/test-bucket/files/test-file-id", headers={"x-api-key": "test-token"} + "http://test-url/api/buckets/test-bucket/files/23", headers={"x-api-key": "test-token"} ) - def test_fetch_bucket_files_list_single_page(self, mocker: MockerFixture): + def test_fetch_bucket_files_list_single_page(self, mocker: MockerFixture) -> None: """Test that fetch_bucket_files_list correctly handles a single page of results. Args: @@ -51,8 +51,8 @@ def test_fetch_bucket_files_list_single_page(self, mocker: MockerFixture): mock_response.json.return_value = { "total": 2, "items": [ - {"id": "file1", "fileName": "confluence_page_1.md"}, - {"id": "file2", "fileName": "confluence_page_2.md"}, + {"id": 1, "fileName": "confluence_page_1.md"}, + {"id": 2, "fileName": "confluence_page_2.md"}, ], } mock_requests.get.return_value = mock_response @@ -65,11 +65,11 @@ def test_fetch_bucket_files_list_single_page(self, mocker: MockerFixture): "http://test-url/api/buckets/test-bucket/files", headers={"x-api-key": "test-token"} ) assert len(result) == 2 - assert result[0]["id"] == "file1" - assert result[1]["id"] == "file2" + assert result[0]["id"] == 1 + assert result[1]["id"] == 2 mock_logger.info.assert_called_once() - def test_fetch_bucket_files_list_multiple_pages(self, mocker: MockerFixture): + def test_fetch_bucket_files_list_multiple_pages(self, mocker: MockerFixture) -> None: """Test that fetch_bucket_files_list correctly handles multiple pages of results. Args: @@ -86,13 +86,13 @@ def test_fetch_bucket_files_list_multiple_pages(self, mocker: MockerFixture): first_response.json.return_value = { "total": 3, "items": [ - {"id": "file1", "fileName": "confluence_page_1.md"}, - {"id": "file2", "fileName": "confluence_page_2.md"}, + {"id": 1, "fileName": "confluence_page_1.md"}, + {"id": 2, "fileName": "confluence_page_2.md"}, ], } second_response = mocker.MagicMock() - second_response.json.return_value = {"total": 3, "items": [{"id": "file3", "fileName": "confluence_page_3.md"}]} + second_response.json.return_value = {"total": 3, "items": [{"id": 3, "fileName": "confluence_page_3.md"}]} mock_requests.get.return_value = first_response @@ -104,10 +104,10 @@ def test_fetch_bucket_files_list_multiple_pages(self, mocker: MockerFixture): "http://test-url/api/buckets/test-bucket/files", headers={"x-api-key": "test-token"} ) assert len(result) == 2 - assert result[0]["id"] == "file1" - assert result[1]["id"] == "file2" + assert result[0]["id"] == 1 + assert result[1]["id"] == 2 - def test_import_confluence_page_success(self, mocker: MockerFixture): + def test_import_confluence_page_success(self, mocker: MockerFixture) -> None: """Test that import_confluence_page correctly handles successful API responses. Args: @@ -140,7 +140,7 @@ def test_import_confluence_page_success(self, mocker: MockerFixture): mock_logger.debug.assert_called_once() mock_logger.error.assert_not_called() - def test_import_confluence_page_error(self, mocker: MockerFixture): + def test_import_confluence_page_error(self, mocker: MockerFixture) -> None: """Test that import_confluence_page correctly handles error API responses. Args: @@ -173,7 +173,7 @@ def test_import_confluence_page_error(self, mocker: MockerFixture): mock_logger.debug.assert_not_called() mock_logger.error.assert_called_once() - def test_clear_previous_ingests(self, mocker: MockerFixture): + def test_clear_previous_ingests(self, mocker: MockerFixture) -> None: """Test that clear_previous_ingests correctly deletes Confluence pages from C4. Args: @@ -183,9 +183,9 @@ def test_clear_previous_ingests(self, mocker: MockerFixture): mock_fetch_bucket_files = mocker.patch( "confluence_importer.c4.fetch_bucket_files_list", return_value=[ - {"id": "file1", "fileName": "confluence_page_1.md"}, - {"id": "file2", "fileName": "other_file.txt"}, - {"id": "file3", "fileName": "confluence_page_2.md"}, + {"id": 1, "fileName": "confluence_page_1.md"}, + {"id": 2, "fileName": "other_file.txt"}, + {"id": 3, "fileName": "confluence_page_2.md"}, ], ) mock_delete_confluence_page = mocker.patch("confluence_importer.c4.delete_confluence_page") @@ -198,11 +198,31 @@ def test_clear_previous_ingests(self, mocker: MockerFixture): # assert mock_fetch_bucket_files.assert_called_once() assert mock_delete_confluence_page.call_count == 2 - mock_delete_confluence_page.assert_any_call("file1") - mock_delete_confluence_page.assert_any_call("file3") + mock_delete_confluence_page.assert_any_call(1) + mock_delete_confluence_page.assert_any_call(3) mock_logger.info.assert_called() - def test_clear_previous_ingests_with_error(self, mocker: MockerFixture): + def test_clear_previous_ingests_with_empty_list(self, mocker: MockerFixture) -> None: + """Test that clear_previous_ingests works correctly with empty bucket files list. + + Args: + mocker: Pytest fixture for mocking + """ + # arrange + mock_fetch_bucket_files = mocker.patch("confluence_importer.c4.fetch_bucket_files_list", return_value=[]) + mock_delete_confluence_page = mocker.patch("confluence_importer.c4.delete_confluence_page") + mock_logger = mocker.patch("confluence_importer.c4.logger") + mocker.patch("confluence_importer.c4.bucket_id", "test-bucket") + + # act + clear_previous_ingests() + + # assert + mock_fetch_bucket_files.assert_called_once() + mock_delete_confluence_page.assert_not_called() + mock_logger.info.assert_called() + + def test_clear_previous_ingests_with_error(self, mocker: MockerFixture) -> None: """Test that clear_previous_ingests correctly handles errors during deletion. Args: @@ -212,13 +232,13 @@ def test_clear_previous_ingests_with_error(self, mocker: MockerFixture): mock_fetch_bucket_files = mocker.patch( "confluence_importer.c4.fetch_bucket_files_list", return_value=[ - {"id": "file1", "fileName": "confluence_page_1.md"}, - {"id": "file2", "fileName": "confluence_page_2.md"}, + {"id": 1, "fileName": "confluence_page_1.md"}, + {"id": 2, "fileName": "confluence_page_2.md"}, ], ) - def delete_side_effect(file_id): - if file_id == "file2": + def delete_side_effect(file_id: int) -> None: + if file_id == 2: raise Exception("Delete failed") mock_delete_confluence_page = mocker.patch( diff --git a/services/confluence-importer/tests/test_confluence.py b/services/confluence-importer/tests/test_confluence.py index 2fca5434..80607b09 100644 --- a/services/confluence-importer/tests/test_confluence.py +++ b/services/confluence-importer/tests/test_confluence.py @@ -8,7 +8,7 @@ class TestConfluence: """Tests for the Confluence module functionality.""" - def test_get_page(self, mocker: MockerFixture): + def test_get_page(self, mocker: MockerFixture) -> None: """Test that get_page correctly retrieves and parses a Confluence page. Args: @@ -38,7 +38,7 @@ def test_get_page(self, mocker: MockerFixture): assert result.url == f"{confluence_url}/rest/api/content/123456" assert result.html_content == "

Test Page

" - def test_get_pages_for_space(self, mocker: MockerFixture): + def test_get_pages_for_space(self, mocker: MockerFixture) -> None: """Test that get_pages_for_space correctly retrieves and parses pages from a Confluence space. Args: @@ -48,13 +48,13 @@ def test_get_pages_for_space(self, mocker: MockerFixture): space_key = "TEST" mock_pages = [ { - "id": "123456", + "id": 123456, "history": {"lastUpdated": {"when": "2025-07-29T13:56:00.000Z"}}, "_links": {"webui": "/rest/api/content/123456"}, "body": {"storage": {"value": "

Test Page 1

"}}, }, { - "id": "789012", + "id": 789012, "history": {"lastUpdated": {"when": "2025-07-30T10:15:00.000Z"}}, "_links": {"webui": "/rest/api/content/789012"}, "body": {"storage": {"value": "

Test Page 2

"}}, @@ -85,18 +85,18 @@ def test_get_pages_for_space(self, mocker: MockerFixture): assert len(results) == 2 assert isinstance(results[0], ConfluencePage) - assert results[0].id == "123456" + assert results[0].id == 123456 assert results[0].last_updated == "2025-07-29T13:56:00.000Z" assert results[0].url == f"{confluence_url}/rest/api/content/123456" assert results[0].html_content == "

Test Page 1

" assert isinstance(results[1], ConfluencePage) - assert results[1].id == "789012" + assert results[1].id == 789012 assert results[1].last_updated == "2025-07-30T10:15:00.000Z" assert results[1].url == f"{confluence_url}/rest/api/content/789012" assert results[1].html_content == "

Test Page 2

" - def test_get_pages_for_space_pagination(self, mocker: MockerFixture): + def test_get_pages_for_space_pagination(self, mocker: MockerFixture) -> None: """Test that get_pages_for_space correctly handles pagination of results. Args: @@ -107,7 +107,7 @@ def test_get_pages_for_space_pagination(self, mocker: MockerFixture): first_batch = [ { - "id": f"{i}", + "id": i, "history": {"lastUpdated": {"when": "2025-07-29T13:56:00.000Z"}}, "_links": {"webui": f"/rest/api/content/{i}"}, "body": {"storage": {"value": f"

Page {i}

"}}, @@ -117,7 +117,7 @@ def test_get_pages_for_space_pagination(self, mocker: MockerFixture): second_batch = [ { - "id": f"{i + 100}", + "id": i + 100, "history": {"lastUpdated": {"when": "2025-07-30T10:15:00.000Z"}}, "_links": {"webui": f"/rest/api/content/{i + 100}"}, "body": {"storage": {"value": f"

Page {i + 100}

"}}, @@ -157,7 +157,7 @@ def test_get_pages_for_space_pagination(self, mocker: MockerFixture): } assert len(results) == 150 - assert results[0].id == "0" - assert results[99].id == "99" - assert results[100].id == "100" - assert results[149].id == "149" + assert results[0].id == 0 + assert results[99].id == 99 + assert results[100].id == 100 + assert results[149].id == 149 diff --git a/services/confluence-importer/tests/test_markdown.py b/services/confluence-importer/tests/test_markdown.py index 6d7fcd92..13b2e1f2 100644 --- a/services/confluence-importer/tests/test_markdown.py +++ b/services/confluence-importer/tests/test_markdown.py @@ -8,7 +8,7 @@ @pytest.fixture -def sample_confluence_page(): +def sample_confluence_page() -> ConfluencePage: """Fixture that returns a sample ConfluencePage object for testing.""" return ConfluencePage( id=12345, @@ -21,7 +21,7 @@ def sample_confluence_page(): class MockDocumentConverterResult: """Mock class for MarkItDown conversion result.""" - def __init__(self, text_content): + def __init__(self, text_content: str) -> None: """Initialize the mock converter result. Args: @@ -33,7 +33,7 @@ def __init__(self, text_content): class TestHtmlToMarkdown: """Tests for the HTML to Markdown conversion functionality.""" - def test_conversion(self, sample_confluence_page, mocker: MockerFixture): + def test_conversion(self, sample_confluence_page: ConfluencePage, mocker: MockerFixture) -> None: """Test that html_to_markdown correctly converts HTML to Markdown with frontmatter. Args: diff --git a/services/confluence-importer/uv.lock b/services/confluence-importer/uv.lock index 6489a28d..3baace31 100644 --- a/services/confluence-importer/uv.lock +++ b/services/confluence-importer/uv.lock @@ -121,6 +121,8 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "mypy" }, + { name = "pip" }, { name = "pytest" }, { name = "pytest-mock" }, { name = "ruff" }, @@ -138,6 +140,8 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "mypy", specifier = ">=1.17.1" }, + { name = "pip", specifier = ">=25.2" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-mock", specifier = ">=3.14.1" }, { name = "ruff", specifier = ">=0.12.7" }, @@ -269,6 +273,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "mypy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, + { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, + { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, + { 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/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +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 = "numpy" version = "2.3.2" @@ -360,6 +399,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pip" +version = "25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/16/650289cd3f43d5a2fadfd98c68bd1e1e7f2550a1a5326768cddfbcedb2c5/pip-25.2.tar.gz", hash = "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2", size = 1840021, upload-time = "2025-07-30T21:50:15.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/3f/945ef7ab14dc4f9d7f40288d2df998d1837ee0888ec3659c813487572faa/pip-25.2-py3-none-any.whl", hash = "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717", size = 1752557, upload-time = "2025-07-30T21:50:13.323Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -525,27 +582,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" }, - { url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" }, - { url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" }, - { url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" }, - { url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" }, - { url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" }, - { url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" }, - { url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" }, - { url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" }, - { url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" }, - { url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" }, +version = "0.12.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" }, + { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" }, + { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" }, ] [[package]]