From 6cccd92b3b58e773d4aaa375ed8fc29160be5e11 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Wed, 5 Nov 2025 15:19:55 +0100 Subject: [PATCH] build: Add type checking --- .github/workflows/test.yml | 3 ++ nextcloud_mcp_server/app.py | 2 +- .../auth/progressive_token_verifier.py | 11 ++++-- .../auth/refresh_token_storage.py | 6 ++-- .../auth/scope_authorization.py | 21 ++++++----- nextcloud_mcp_server/auth/token_broker.py | 2 +- nextcloud_mcp_server/auth/token_exchange.py | 5 ++- nextcloud_mcp_server/auth/token_verifier.py | 3 +- nextcloud_mcp_server/client/calendar.py | 30 +++++++++------- nextcloud_mcp_server/client/contacts.py | 4 +-- .../document_processors/tesseract.py | 2 +- .../document_processors/unstructured.py | 10 +++--- nextcloud_mcp_server/server/cookbook.py | 6 ++-- nextcloud_mcp_server/server/notes.py | 2 +- nextcloud_mcp_server/server/oauth_tools.py | 8 ++--- pyproject.toml | 1 + uv.lock | 35 +++++++++++++++++++ 17 files changed, 104 insertions(+), 47 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7826747..77fbdd1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,9 @@ jobs: - name: Linting run: | uv run --frozen ruff check + - name: Linting + run: | + uv run --frozen ty check -- nextcloud_mcp_server integration-test: diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index 62a8fe6..227deb3 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -818,7 +818,7 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): return allowed_tools # Replace the tool manager's list_tools method - mcp._tool_manager.list_tools = list_tools_filtered + mcp._tool_manager.list_tools = list_tools_filtered # type: ignore[method-assign] logger.info( "Dynamic tool filtering enabled for OAuth mode (JWT and Bearer tokens)" ) diff --git a/nextcloud_mcp_server/auth/progressive_token_verifier.py b/nextcloud_mcp_server/auth/progressive_token_verifier.py index 91e4aaf..a7af61b 100644 --- a/nextcloud_mcp_server/auth/progressive_token_verifier.py +++ b/nextcloud_mcp_server/auth/progressive_token_verifier.py @@ -34,7 +34,7 @@ class ProgressiveConsentTokenVerifier: def __init__( self, - token_storage: RefreshTokenStorage, + token_storage: RefreshTokenStorage | None, token_broker: Optional[TokenBrokerService] = None, oidc_discovery_url: Optional[str] = None, nextcloud_host: Optional[str] = None, @@ -80,7 +80,7 @@ class ProgressiveConsentTokenVerifier: # Create token broker if not provided if token_broker: self.token_broker = token_broker - elif self.encryption_key: + elif self.encryption_key and token_storage and self.nextcloud_host: self.token_broker = TokenBrokerService( storage=token_storage, oidc_discovery_url=self.oidc_discovery_url, @@ -89,7 +89,12 @@ class ProgressiveConsentTokenVerifier: ) else: self.token_broker = None - logger.warning("Token broker not available - encryption key missing") + if not self.encryption_key: + logger.warning("Token broker not available - encryption key missing") + elif not token_storage: + logger.warning("Token broker not available - token storage missing") + elif not self.nextcloud_host: + logger.warning("Token broker not available - nextcloud host missing") async def verify_token(self, token: str) -> Optional[AccessToken]: """ diff --git a/nextcloud_mcp_server/auth/refresh_token_storage.py b/nextcloud_mcp_server/auth/refresh_token_storage.py index bc5486e..7337d40 100644 --- a/nextcloud_mcp_server/auth/refresh_token_storage.py +++ b/nextcloud_mcp_server/auth/refresh_token_storage.py @@ -25,7 +25,7 @@ import logging import os import time from pathlib import Path -from typing import Optional +from typing import Any, Optional import aiosqlite from cryptography.fernet import Fernet @@ -283,7 +283,7 @@ class RefreshTokenStorage: ) async def store_user_profile( - self, user_id: str, profile_data: dict[str, any] + self, user_id: str, profile_data: dict[str, Any] ) -> None: """ Store user profile data (cached from IdP userinfo endpoint). @@ -314,7 +314,7 @@ class RefreshTokenStorage: logger.debug(f"Cached user profile for {user_id}") - async def get_user_profile(self, user_id: str) -> Optional[dict[str, any]]: + async def get_user_profile(self, user_id: str) -> Optional[dict[str, Any]]: """ Retrieve cached user profile data. diff --git a/nextcloud_mcp_server/auth/scope_authorization.py b/nextcloud_mcp_server/auth/scope_authorization.py index 27ac1f7..4b6ed2a 100644 --- a/nextcloud_mcp_server/auth/scope_authorization.py +++ b/nextcloud_mcp_server/auth/scope_authorization.py @@ -3,7 +3,7 @@ import logging import os from functools import wraps -from typing import Callable +from typing import Any, Callable from mcp.server.auth.middleware.auth_context import get_access_token from mcp.server.auth.provider import AccessToken @@ -88,15 +88,18 @@ def require_scopes(*required_scopes: str): ScopeAuthorizationError: If required scopes are not present in the access token """ - def decorator(func: Callable): + def decorator(func: Callable) -> Callable: # Store scope requirements as function metadata for dynamic filtering - func._required_scopes = list(required_scopes) # type: ignore + func._required_scopes = list(required_scopes) # type: ignore[attr-defined] + + # Get function name for logging (works for any callable) + func_name = getattr(func, "__name__", repr(func)) # Find which parameter receives the Context (FastMCP injects it by name) context_param_name = find_context_parameter(func) @wraps(func) - async def wrapper(*args, **kwargs): + async def wrapper(*args: Any, **kwargs: Any) -> Any: # Extract context from kwargs (where FastMCP injected it) ctx: Context | None = ( kwargs.get(context_param_name) if context_param_name else None @@ -106,7 +109,7 @@ def require_scopes(*required_scopes: str): # No context parameter found - likely BasicAuth mode # In BasicAuth mode, all operations are allowed logger.debug( - f"No context parameter for {func.__name__} - allowing (BasicAuth mode)" + f"No context parameter for {func_name} - allowing (BasicAuth mode)" ) return await func(*args, **kwargs) @@ -119,7 +122,7 @@ def require_scopes(*required_scopes: str): # Not in OAuth mode (BasicAuth or no auth) # In BasicAuth mode, all operations are allowed logger.debug( - f"No access token present for {func.__name__} - allowing (BasicAuth mode)" + f"No access token present for {func_name} - allowing (BasicAuth mode)" ) return await func(*args, **kwargs) @@ -172,7 +175,7 @@ def require_scopes(*required_scopes: str): if not has_nextcloud_scopes: error_msg = ( - f"Access denied to {func.__name__}: " + f"Access denied to {func_name}: " f"Nextcloud resource access not provisioned. " f"Please run the 'provision_nextcloud_access' tool first." ) @@ -183,7 +186,7 @@ def require_scopes(*required_scopes: str): missing_scopes = required_scopes_set - token_scopes if missing_scopes: error_msg = ( - f"Access denied to {func.__name__}: " + f"Access denied to {func_name}: " f"Missing required scopes: {', '.join(sorted(missing_scopes))}. " f"Token has scopes: {', '.join(sorted(token_scopes)) if token_scopes else 'none'}" ) @@ -192,7 +195,7 @@ def require_scopes(*required_scopes: str): # All required scopes present - allow execution logger.debug( - f"Scope authorization passed for {func.__name__}: {required_scopes}" + f"Scope authorization passed for {func_name}: {required_scopes}" ) return await func(*args, **kwargs) diff --git a/nextcloud_mcp_server/auth/token_broker.py b/nextcloud_mcp_server/auth/token_broker.py index e69c354..152163c 100644 --- a/nextcloud_mcp_server/auth/token_broker.py +++ b/nextcloud_mcp_server/auth/token_broker.py @@ -68,7 +68,7 @@ class TokenCache: logger.debug(f"Using cached token for user {user_id}") return token - async def set(self, user_id: str, token: str, expires_in: int = None): + async def set(self, user_id: str, token: str, expires_in: int | None = None): """Store token in cache.""" async with self._lock: # Use provided expiry or default TTL diff --git a/nextcloud_mcp_server/auth/token_exchange.py b/nextcloud_mcp_server/auth/token_exchange.py index 54aa344..2ded73e 100644 --- a/nextcloud_mcp_server/auth/token_exchange.py +++ b/nextcloud_mcp_server/auth/token_exchange.py @@ -114,7 +114,8 @@ class TokenExchangeService: if not self.oidc_discovery_url: # Fallback to Nextcloud OIDC if no discovery URL self.oidc_discovery_url = urljoin( - self.nextcloud_host, "/.well-known/openid-configuration" + self.nextcloud_host, # type: ignore[arg-type] + "/.well-known/openid-configuration", ) try: @@ -363,6 +364,7 @@ class TokenExchangeService: True if provisioned, False otherwise """ await self._ensure_storage() + assert self.storage is not None # _ensure_storage() ensures this token_data = await self.storage.get_refresh_token(user_id) return token_data is not None @@ -376,6 +378,7 @@ class TokenExchangeService: Refresh token if found, None otherwise """ await self._ensure_storage() + assert self.storage is not None # _ensure_storage() ensures this token_data = await self.storage.get_refresh_token(user_id) if token_data: return token_data.get("refresh_token") diff --git a/nextcloud_mcp_server/auth/token_verifier.py b/nextcloud_mcp_server/auth/token_verifier.py index 300177e..cc94eeb 100644 --- a/nextcloud_mcp_server/auth/token_verifier.py +++ b/nextcloud_mcp_server/auth/token_verifier.py @@ -165,6 +165,7 @@ class NextcloudTokenVerifier(TokenVerifier): """ try: # Get signing key from JWKS + assert self._jwks_client is not None # Caller should check before calling signing_key = self._jwks_client.get_signing_key_from_jwt(token) # Verify and decode JWT @@ -257,7 +258,7 @@ class NextcloudTokenVerifier(TokenVerifier): try: # Introspection requires client authentication response = await self._client.post( - self.introspection_uri, + self.introspection_uri, # type: ignore data={"token": token}, auth=(self.client_id, self.client_secret), ) diff --git a/nextcloud_mcp_server/client/calendar.py b/nextcloud_mcp_server/client/calendar.py index dc79c84..7e36178 100644 --- a/nextcloud_mcp_server/client/calendar.py +++ b/nextcloud_mcp_server/client/calendar.py @@ -100,7 +100,7 @@ class CalendarClient: # Use custom PROPFIND with CalendarServer namespace (cs:) for calendar-color. # caldav library's nsmap lacks "CS" namespace, and its CalendarColor uses # Apple iCal namespace which Nextcloud doesn't recognize. - from lxml import etree + from lxml import etree # type: ignore[import-untyped] propfind_body = """ @@ -261,11 +261,12 @@ class CalendarClient: result = [] for event in events: await event.load(only_if_unloaded=True) - event_dict = self._parse_ical_event(event.data) - if event_dict: - event_dict["href"] = str(event.url) - event_dict["etag"] = "" - result.append(event_dict) + if event.data: + event_dict = self._parse_ical_event(event.data) + if event_dict: + event_dict["href"] = str(event.url) + event_dict["etag"] = "" + result.append(event_dict) if len(result) >= limit: break @@ -314,8 +315,8 @@ class CalendarClient: await event.load(only_if_unloaded=True) # Merge updates into existing iCal data - updated_ical = self._merge_ical_properties(event.data, event_data, event_uid) - event.data = updated_ical + updated_ical = self._merge_ical_properties # type: ignore[arg-type](event.data, event_data, event_uid) + event.data = updated_ical # type: ignore[misc] await event.save() @@ -349,7 +350,7 @@ class CalendarClient: event = await calendar.event_by_uid(event_uid) await event.load(only_if_unloaded=True) - event_data = self._parse_ical_event(event.data) + event_data = self._parse_ical_event(event.data) if event.data else None # type: ignore[arg-type] if not event_data: raise ValueError(f"Failed to parse event data for {event_uid}") @@ -416,7 +417,10 @@ class CalendarClient: # Only load if data not already present from REPORT response # This avoids 404 errors for virtual calendars (e.g., Deck boards) await todo.load(only_if_unloaded=True) - todo_dict = self._parse_ical_todo(todo.data) + if todo.data: + todo_dict = self._parse_ical_todo(todo.data) # type: ignore[arg-type] + else: + continue if todo_dict: todo_dict["href"] = str(todo.url) todo_dict["etag"] = "" @@ -470,12 +474,14 @@ class CalendarClient: await todo.load(only_if_unloaded=True) logger.debug( - f"Loaded todo {todo_uid}, current data length: {len(todo.data)}" + f"Loaded todo {todo_uid}, current data length: {len(todo.data)}" # type: ignore ) # Merge updates into existing iCal data updated_ical = self._merge_ical_todo_properties( - todo.data, todo_data, todo_uid + todo.data, # type: ignore[arg-type] + todo_data, + todo_uid, ) logger.debug(f"Merged iCal data length: {len(updated_ical)}") logger.debug(f"Updated iCal content:\n{updated_ical}") diff --git a/nextcloud_mcp_server/client/contacts.py b/nextcloud_mcp_server/client/contacts.py index 042a84a..4c7408d 100644 --- a/nextcloud_mcp_server/client/contacts.py +++ b/nextcloud_mcp_server/client/contacts.py @@ -124,7 +124,7 @@ class ContactsClient(BaseNextcloudClient): carddav_path = self._get_carddav_base_path() url = f"{carddav_path}/{addressbook}/{uid}.vcf" - contact = Contact(fn=contact_data.get("fn"), uid=uid) + contact = Contact(fn=contact_data.get("fn"), uid=uid) # type: ignore if "email" in contact_data: contact.email = [{"value": contact_data["email"], "type": ["HOME"]}] if "tel" in contact_data: @@ -174,7 +174,7 @@ class ContactsClient(BaseNextcloudClient): ) else: # Fallback to creating new vCard if we couldn't get existing - contact = Contact(fn=contact_data.get("fn"), uid=uid) + contact = Contact(fn=contact_data.get("fn"), uid=uid) # type: ignore if "email" in contact_data: contact.email = [{"value": contact_data["email"], "type": ["HOME"]}] if "tel" in contact_data: diff --git a/nextcloud_mcp_server/document_processors/tesseract.py b/nextcloud_mcp_server/document_processors/tesseract.py index d912868..890862f 100644 --- a/nextcloud_mcp_server/document_processors/tesseract.py +++ b/nextcloud_mcp_server/document_processors/tesseract.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) try: import io - import pytesseract + import pytesseract # type: ignore from PIL import Image TESSERACT_AVAILABLE = True diff --git a/nextcloud_mcp_server/document_processors/unstructured.py b/nextcloud_mcp_server/document_processors/unstructured.py index de434c1..85077a6 100644 --- a/nextcloud_mcp_server/document_processors/unstructured.py +++ b/nextcloud_mcp_server/document_processors/unstructured.py @@ -112,10 +112,10 @@ class UnstructuredProcessor(DocumentProcessor): f"Processing document with unstructured... ({elapsed}s elapsed)" ) try: - await progress_callback( - progress=float(elapsed), - total=None, # Unknown total duration - message=message, + await progress_callback( # type: ignore + progress=float(elapsed), # type: ignore + total=None, # Unknown total duration # type: ignore + message=message, # type: ignore ) logger.debug(f"Progress update sent: {elapsed}s elapsed") except Exception as e: @@ -293,7 +293,7 @@ class UnstructuredProcessor(DocumentProcessor): self._run_progress_poller, stop_event, progress_callback, start_time ) - return result + return result # type: ignore async def health_check(self) -> bool: """Check if Unstructured API is available. diff --git a/nextcloud_mcp_server/server/cookbook.py b/nextcloud_mcp_server/server/cookbook.py index 5b7c8d8..3b8487d 100644 --- a/nextcloud_mcp_server/server/cookbook.py +++ b/nextcloud_mcp_server/server/cookbook.py @@ -191,7 +191,7 @@ def configure_cookbook_tools(mcp: FastMCP): recipe_yield: int | None = None, category: str | None = None, keywords: str | None = None, - ctx: Context = None, + ctx: Context = None, # type: ignore ) -> CreateRecipeResponse: """Create a new recipe. @@ -271,7 +271,7 @@ def configure_cookbook_tools(mcp: FastMCP): recipe_yield: int | None = None, category: str | None = None, keywords: str | None = None, - ctx: Context = None, + ctx: Context = None, # type: ignore ) -> UpdateRecipeResponse: """Update an existing recipe. @@ -544,7 +544,7 @@ def configure_cookbook_tools(mcp: FastMCP): folder: str | None = None, update_interval: int | None = None, print_image: bool | None = None, - ctx: Context = None, + ctx: Context = None, # type: ignore ) -> ReindexResponse: """Set Cookbook app configuration. diff --git a/nextcloud_mcp_server/server/notes.py b/nextcloud_mcp_server/server/notes.py index d4080dd..17de067 100644 --- a/nextcloud_mcp_server/server/notes.py +++ b/nextcloud_mcp_server/server/notes.py @@ -331,7 +331,7 @@ def configure_notes_tools(mcp: FastMCP): content, mime_type = await client.webdav.get_note_attachment( note_id=note_id, filename=attachment_filename ) - return { + return { # type: ignore "uri": f"nc://Notes/{note_id}/attachments/{attachment_filename}", "mimeType": mime_type, "data": content, diff --git a/nextcloud_mcp_server/server/oauth_tools.py b/nextcloud_mcp_server/server/oauth_tools.py index 26a609f..e18847a 100644 --- a/nextcloud_mcp_server/server/oauth_tools.py +++ b/nextcloud_mcp_server/server/oauth_tools.py @@ -163,7 +163,7 @@ async def provision_nextcloud_access( if not user_id: # Get the authorization token from context if hasattr(ctx, "authorization") and ctx.authorization: - token = ctx.authorization.token + token = ctx.authorization.token # type: ignore # Decode token to get user info try: import jwt @@ -304,7 +304,7 @@ async def revoke_nextcloud_access( # Get user ID from context if not provided if not user_id: user_id = ( - ctx.context.get("user_id", "default_user") + ctx.context.get("user_id", "default_user") # type: ignore if hasattr(ctx, "context") else "default_user" ) @@ -334,7 +334,7 @@ async def revoke_nextcloud_access( "OIDC_DISCOVERY_URL", f"{os.getenv('NEXTCLOUD_HOST')}/.well-known/openid-configuration", ), - nextcloud_host=os.getenv("NEXTCLOUD_HOST"), + nextcloud_host=os.getenv("NEXTCLOUD_HOST"), # type: ignore encryption_key=encryption_key, ) @@ -382,7 +382,7 @@ async def check_provisioning_status( # Get user ID from context if not provided if not user_id: user_id = ( - ctx.context.get("user_id", "default_user") + ctx.context.get("user_id", "default_user") # type: ignore if hasattr(ctx, "context") else "default_user" ) diff --git a/pyproject.toml b/pyproject.toml index ac40a4b..d5f0f6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,7 @@ dev = [ "pytest-timeout>=2.3.1", "ruff>=0.11.13", "reportlab>=4.0.0", + "ty>=0.0.1a25", ] [project.scripts] diff --git a/uv.lock b/uv.lock index 2264397..5e159a4 100644 --- a/uv.lock +++ b/uv.lock @@ -501,6 +501,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, @@ -510,6 +512,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, @@ -519,6 +523,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, @@ -526,6 +532,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] @@ -994,6 +1002,7 @@ dev = [ { name = "pytest-timeout" }, { name = "reportlab" }, { name = "ruff" }, + { name = "ty" }, ] [package.metadata] @@ -1023,6 +1032,7 @@ dev = [ { name = "pytest-timeout", specifier = ">=2.3.1" }, { name = "reportlab", specifier = ">=4.0.0" }, { name = "ruff", specifier = ">=0.11.13" }, + { name = "ty", specifier = ">=0.0.1a25" }, ] [[package]] @@ -1954,6 +1964,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "ty" +version = "0.0.1a25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/6b/e73bc3c1039ea72936158a08313155a49e5aa5e7db5205a149fe516a4660/ty-0.0.1a25.tar.gz", hash = "sha256:5550b24b9dd0e0f8b4b2c1f0fcc608a55d0421dd67b6c364bc7bf25762334511", size = 4403670, upload-time = "2025-10-29T19:40:23.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/3b/4457231238a2eeb04cba4ba7cc33d735be68ee46ca40a98ae30e187de864/ty-0.0.1a25-py3-none-linux_armv6l.whl", hash = "sha256:d35b2c1f94a014a22875d2745aa0432761d2a9a8eb7212630d5caf547daeef6d", size = 8878803, upload-time = "2025-10-29T19:39:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/a328713dd310018fc7a381693d8588185baa2fdae913e01a6839187215df/ty-0.0.1a25-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:192edac94675a468bac7f6e04687a77a64698e4e1fe01f6a048bf9b6dde5b703", size = 8695667, upload-time = "2025-10-29T19:39:45.179Z" }, + { url = "https://files.pythonhosted.org/packages/22/e8/5707939118992ced2bf5385adc3ede7723c1b717b07ad14c495eea1e47b4/ty-0.0.1a25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:949523621f336e01bc7d687b7bd08fe838edadbdb6563c2c057ed1d264e820cf", size = 8159012, upload-time = "2025-10-29T19:39:47.011Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fb/ff313aa71602225cd78f1bce3017713d6d1b1c1e0fa8101ead4594a60d95/ty-0.0.1a25-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f78f621458c05e59e890061021198197f29a7b51a33eda82bbb036e7ed73d7", size = 8433675, upload-time = "2025-10-29T19:39:48.443Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8d/cc7e7fb57215a15b575a43ed042bdd92971871e0decec1b26d2e7d969465/ty-0.0.1a25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9656fca8062a2c6709c30d76d662c96d2e7dbfee8f70e55ec6b6afd67b5d447", size = 8668456, upload-time = "2025-10-29T19:39:50.412Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6d/d7bf5909ed2dcdcbc1e2ca7eea80929893e2d188d9c36b3fcb2b36532ff6/ty-0.0.1a25-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9f3bbf523b49935bbd76e230408d858dce0d614f44f5807bbbd0954f64e0f01", size = 9023543, upload-time = "2025-10-29T19:39:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b8/72bcefb4be32e5a84f0b21de2552f16cdb4cae3eb271ac891c8199c26b1a/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f13ea9815f4a54a0a303ca7bf411b0650e3c2a24fc6c7889ffba2c94f5e97a6a", size = 9700013, upload-time = "2025-10-29T19:39:57.283Z" }, + { url = "https://files.pythonhosted.org/packages/90/0d/cf7e794b840cf6b0bbecb022e593c543f85abad27a582241cf2095048cb1/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eab6e33ebe202a71a50c3d5a5580e3bc1a85cda3ffcdc48cec3f1c693b7a873b", size = 9372574, upload-time = "2025-10-29T19:40:04.532Z" }, + { url = "https://files.pythonhosted.org/packages/1e/71/2d35e7d51b48eabd330e2f7b7e0bce541cbd95950c4d2f780e85f3366af1/ty-0.0.1a25-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6b9a31da43424cdab483703a54a561b93aabba84630788505329fc5294a9c62", size = 9535726, upload-time = "2025-10-29T19:40:06.548Z" }, + { url = "https://files.pythonhosted.org/packages/57/d3/01ecc23bbd8f3e0dfbcf9172d06d84e88155c5f416f1491137e8066fd859/ty-0.0.1a25-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a90d897a7c1a5ae9b41a4c7b0a42262a06361476ad88d783dbedd7913edadbc", size = 9003380, upload-time = "2025-10-29T19:40:08.683Z" }, + { url = "https://files.pythonhosted.org/packages/de/f9/cde9380d8a1a6ca61baeb9aecb12cbec90d489aa929be55cd78ad5c2ccd9/ty-0.0.1a25-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:93c7e7ab2859af0f866d34d27f4ae70dd4fb95b847387f082de1197f9f34e068", size = 8401833, upload-time = "2025-10-29T19:40:10.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/39/0acf3625b0c495011795a391016b572f97a812aca1d67f7a76621fdb9ebf/ty-0.0.1a25-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a247061bd32bae3865a236d7f8b6c9916c80995db30ae1600999010f90623a9", size = 8706761, upload-time = "2025-10-29T19:40:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/25/73/7de1648f3563dd9d416d36ab5f1649bfd7b47a179135027f31d44b89a246/ty-0.0.1a25-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1711dd587eccf04fd50c494dc39babe38f4cb345bc3901bf1d8149cac570e979", size = 8792426, upload-time = "2025-10-29T19:40:14.553Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8a/b6e761a65eac7acd10b2e452f49b2d8ae0ea163ca36bb6b18b2dadae251b/ty-0.0.1a25-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f4c9b0cf7995e2e3de9bab4d066063dea92019f2f62673b7574e3612643dd35", size = 9103991, upload-time = "2025-10-29T19:40:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/9324ae947fcc4322470326cf8276a3fc2f08dc82adec1de79d963fdf7af5/ty-0.0.1a25-py3-none-win32.whl", hash = "sha256:168fc8aee396d617451acc44cd28baffa47359777342836060c27aa6f37e2445", size = 8387095, upload-time = "2025-10-29T19:40:18.368Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2b/cb12cbc7db1ba310aa7b1de9b4e018576f653105993736c086ee67d2ec02/ty-0.0.1a25-py3-none-win_amd64.whl", hash = "sha256:a2fad3d8e92bb4d57a8872a6f56b1aef54539d36f23ebb01abe88ac4338efafb", size = 9059225, upload-time = "2025-10-29T19:40:20.278Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c1/f6be8cdd0bf387c1d8ee9d14bb299b7b5d2c0532f550a6693216a32ec0c5/ty-0.0.1a25-py3-none-win_arm64.whl", hash = "sha256:dde2962d448ed87c48736e9a4bb13715a4cced705525e732b1c0dac1d4c66e3d", size = 8536832, upload-time = "2025-10-29T19:40:22.014Z" }, +] + [[package]] name = "typer" version = "0.19.2"