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]]