From a8aa416ecbe0f9948d93e248ce3474c72661e53b Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Sat, 28 Feb 2026 19:12:40 +0100 Subject: [PATCH] refactor(backend): migrate type checking from mypy to pyright MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace mypy>=1.8.0 with pyright>=1.1.390. Remove all [tool.mypy] and [tool.pydantic-mypy] sections from pyproject.toml and add pyrightconfig.json (standard mode, SQLAlchemy false-positive rules suppressed globally). Fixes surfaced by pyright: - Remove unreachable except AuthError clauses in login/login_oauth (same class as AuthenticationError) - Fix Pydantic v2 list Field: min_items/max_items โ†’ min_length/max_length - Split OAuthProviderConfig TypedDict into required + optional(email_url) inheritance - Move JWTError/ExpiredSignatureError from lazy try-block imports to module level - Add timezone-aware guard to UserSession.is_expired to match sibling models - Fix is_active: bool โ†’ bool | None in three organization repo signatures - Initialize search_filter = None before conditional block (possibly unbound fix) - Add bool() casts to model is_expired and repo is_active/is_superuser returns - Restructure except (JWTError, Exception) into separate except clauses --- backend/Makefile | 8 +- backend/app/api/routes/admin.py | 2 +- backend/app/api/routes/auth.py | 6 - backend/app/api/routes/oauth_provider.py | 2 +- backend/app/core/exceptions.py | 4 +- .../app/models/oauth_authorization_code.py | 2 +- backend/app/models/oauth_provider_token.py | 2 +- backend/app/models/user_session.py | 6 +- backend/app/repositories/organization.py | 11 +- backend/app/repositories/user.py | 4 +- backend/app/schemas/organizations.py | 2 +- .../app/services/oauth_provider_service.py | 15 +-- backend/app/services/oauth_service.py | 13 +- backend/app/utils/test_utils.py | 4 +- backend/pyproject.toml | 116 +----------------- backend/pyrightconfig.json | 23 ++++ backend/uv.lock | 66 +++------- 17 files changed, 85 insertions(+), 201 deletions(-) create mode 100644 backend/pyrightconfig.json diff --git a/backend/Makefile b/backend/Makefile index fc86162..7418132 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -14,7 +14,7 @@ help: @echo " make lint-fix - Run Ruff linter with auto-fix" @echo " make format - Format code with Ruff" @echo " make format-check - Check if code is formatted" - @echo " make type-check - Run mypy type checking" + @echo " make type-check - Run pyright type checking" @echo " make validate - Run all checks (lint + format + types)" @echo "" @echo "Testing:" @@ -63,8 +63,8 @@ format-check: @uv run ruff format --check app/ tests/ type-check: - @echo "๐Ÿ”Ž Running mypy type checking..." - @uv run mypy app/ + @echo "๐Ÿ”Ž Running pyright type checking..." + @uv run pyright app/ validate: lint format-check type-check @echo "โœ… All quality checks passed!" @@ -127,7 +127,7 @@ clean: @echo "๐Ÿงน Cleaning up..." @find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true @find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true + @find . -type d -name ".pyright" -exec rm -rf {} + 2>/dev/null || true @find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true @find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true @find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true diff --git a/backend/app/api/routes/admin.py b/backend/app/api/routes/admin.py index f1cfad1..ce7f122 100755 --- a/backend/app/api/routes/admin.py +++ b/backend/app/api/routes/admin.py @@ -65,7 +65,7 @@ class BulkUserAction(BaseModel): action: BulkAction = Field(..., description="Action to perform on selected users") user_ids: list[UUID] = Field( - ..., min_items=1, max_items=100, description="List of user IDs (max 100)" + ..., min_length=1, max_length=100, description="List of user IDs (max 100)" ) diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index a884d66..7eca77c 100755 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -183,9 +183,6 @@ async def login( # Handle specific authentication errors like inactive accounts logger.warning(f"Authentication failed: {e!s}") raise AuthError(message=str(e), error_code=ErrorCode.INVALID_CREDENTIALS) - except AuthError: - # Re-raise custom auth exceptions without modification - raise except Exception as e: # Handle unexpected errors logger.error(f"Unexpected error during login: {e!s}", exc_info=True) @@ -232,9 +229,6 @@ async def login_oauth( except AuthenticationError as e: logger.warning(f"OAuth authentication failed: {e!s}") raise AuthError(message=str(e), error_code=ErrorCode.INVALID_CREDENTIALS) - except AuthError: - # Re-raise custom auth exceptions without modification - raise except Exception as e: logger.error(f"Unexpected error during OAuth login: {e!s}", exc_info=True) raise DatabaseError( diff --git a/backend/app/api/routes/oauth_provider.py b/backend/app/api/routes/oauth_provider.py index 024cc14..dd61d27 100644 --- a/backend/app/api/routes/oauth_provider.py +++ b/backend/app/api/routes/oauth_provider.py @@ -655,7 +655,7 @@ async def introspect( ) except Exception as e: logger.warning(f"Token introspection error: {e}") - return OAuthTokenIntrospectionResponse(active=False) + return OAuthTokenIntrospectionResponse(active=False) # pyright: ignore[reportCallIssue] # ============================================================================ diff --git a/backend/app/core/exceptions.py b/backend/app/core/exceptions.py index 0b45b09..8e2bc61 100644 --- a/backend/app/core/exceptions.py +++ b/backend/app/core/exceptions.py @@ -222,7 +222,7 @@ async def http_exception_handler(request: Request, exc: HTTPException) -> JSONRe ) error_response = ErrorResponse( - errors=[ErrorDetail(code=error_code, message=str(exc.detail))] + errors=[ErrorDetail(code=error_code, message=str(exc.detail), field=None)] ) return JSONResponse( @@ -254,7 +254,7 @@ async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONR message = f"{type(exc).__name__}: {exc!s}" error_response = ErrorResponse( - errors=[ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message=message)] + errors=[ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message=message, field=None)] ) return JSONResponse( diff --git a/backend/app/models/oauth_authorization_code.py b/backend/app/models/oauth_authorization_code.py index 63c4863..f92fa9e 100755 --- a/backend/app/models/oauth_authorization_code.py +++ b/backend/app/models/oauth_authorization_code.py @@ -92,7 +92,7 @@ class OAuthAuthorizationCode(Base, UUIDMixin, TimestampMixin): # Handle both timezone-aware and naive datetimes from DB if expires_at.tzinfo is None: expires_at = expires_at.replace(tzinfo=UTC) - return now > expires_at + return bool(now > expires_at) @property def is_valid(self) -> bool: diff --git a/backend/app/models/oauth_provider_token.py b/backend/app/models/oauth_provider_token.py index c238fc8..bb5c6ff 100755 --- a/backend/app/models/oauth_provider_token.py +++ b/backend/app/models/oauth_provider_token.py @@ -99,7 +99,7 @@ class OAuthProviderRefreshToken(Base, UUIDMixin, TimestampMixin): # Handle both timezone-aware and naive datetimes from DB if expires_at.tzinfo is None: expires_at = expires_at.replace(tzinfo=UTC) - return now > expires_at + return bool(now > expires_at) @property def is_valid(self) -> bool: diff --git a/backend/app/models/user_session.py b/backend/app/models/user_session.py index 3bd9df9..703b5a1 100644 --- a/backend/app/models/user_session.py +++ b/backend/app/models/user_session.py @@ -76,7 +76,11 @@ class UserSession(Base, UUIDMixin, TimestampMixin): """Check if session has expired.""" from datetime import datetime - return self.expires_at < datetime.now(UTC) + now = datetime.now(UTC) + expires_at = self.expires_at + if expires_at.tzinfo is None: + expires_at = expires_at.replace(tzinfo=UTC) + return bool(expires_at < now) def to_dict(self): """Convert session to dictionary for serialization.""" diff --git a/backend/app/repositories/organization.py b/backend/app/repositories/organization.py index fa3f002..827a47f 100644 --- a/backend/app/repositories/organization.py +++ b/backend/app/repositories/organization.py @@ -174,6 +174,7 @@ class OrganizationRepository( if is_active is not None: query = query.where(Organization.is_active == is_active) + search_filter = None if search: search_filter = or_( Organization.name.ilike(f"%{search}%"), @@ -185,7 +186,7 @@ class OrganizationRepository( count_query = select(func.count(Organization.id)) if is_active is not None: count_query = count_query.where(Organization.is_active == is_active) - if search: + if search_filter is not None: count_query = count_query.where(search_filter) count_result = await db.execute(count_query) @@ -333,7 +334,7 @@ class OrganizationRepository( organization_id: UUID, skip: int = 0, limit: int = 100, - is_active: bool = True, + is_active: bool | None = True, ) -> tuple[list[dict[str, Any]], int]: """Get members of an organization with user details.""" try: @@ -387,7 +388,7 @@ class OrganizationRepository( raise async def get_user_organizations( - self, db: AsyncSession, *, user_id: UUID, is_active: bool = True + self, db: AsyncSession, *, user_id: UUID, is_active: bool | None = True ) -> list[Organization]: """Get all organizations a user belongs to.""" try: @@ -410,7 +411,7 @@ class OrganizationRepository( raise async def get_user_organizations_with_details( - self, db: AsyncSession, *, user_id: UUID, is_active: bool = True + self, db: AsyncSession, *, user_id: UUID, is_active: bool | None = True ) -> list[dict[str, Any]]: """Get user's organizations with role and member count in SINGLE QUERY.""" try: @@ -476,7 +477,7 @@ class OrganizationRepository( ) user_org = result.scalar_one_or_none() - return user_org.role if user_org else None + return user_org.role if user_org else None # pyright: ignore[reportReturnType] except Exception as e: logger.error(f"Error getting user role in org: {e!s}") raise diff --git a/backend/app/repositories/user.py b/backend/app/repositories/user.py index d4877f3..850cf0d 100644 --- a/backend/app/repositories/user.py +++ b/backend/app/repositories/user.py @@ -256,11 +256,11 @@ class UserRepository(BaseRepository[User, UserCreate, UserUpdate]): def is_active(self, user: User) -> bool: """Check if user is active.""" - return user.is_active + return bool(user.is_active) def is_superuser(self, user: User) -> bool: """Check if user is a superuser.""" - return user.is_superuser + return bool(user.is_superuser) # Singleton instance diff --git a/backend/app/schemas/organizations.py b/backend/app/schemas/organizations.py index b71e91e..5848ddf 100644 --- a/backend/app/schemas/organizations.py +++ b/backend/app/schemas/organizations.py @@ -48,7 +48,7 @@ class OrganizationCreate(OrganizationBase): """Schema for creating a new organization.""" name: str = Field(..., min_length=1, max_length=255) - slug: str = Field(..., min_length=1, max_length=255) + slug: str = Field(..., min_length=1, max_length=255) # pyright: ignore[reportIncompatibleVariableOverride] class OrganizationUpdate(BaseModel): diff --git a/backend/app/services/oauth_provider_service.py b/backend/app/services/oauth_provider_service.py index 03ccce2..933634a 100755 --- a/backend/app/services/oauth_provider_service.py +++ b/backend/app/services/oauth_provider_service.py @@ -25,7 +25,8 @@ from datetime import UTC, datetime, timedelta from typing import Any from uuid import UUID -from jose import jwt +from jose import JWTError, jwt +from jose.exceptions import ExpiredSignatureError from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings @@ -677,8 +678,6 @@ async def revoke_token( # Try as access token (JWT) if token_type_hint != "refresh_token": try: - from jose.exceptions import JWTError - payload = jwt.decode( token, settings.SECRET_KEY, @@ -700,7 +699,9 @@ async def revoke_token( f"Revoked refresh token via access token JTI {jti[:8]}..." ) return True - except (JWTError, Exception): # noqa: S110 - Intentional: invalid JWT not an error + except JWTError: + pass + except Exception: # noqa: S110 - Intentional: invalid JWT not an error pass return False @@ -791,8 +792,6 @@ async def introspect_token( # Try as access token (JWT) first if token_type_hint != "refresh_token": try: - from jose.exceptions import ExpiredSignatureError, JWTError - payload = jwt.decode( token, settings.SECRET_KEY, @@ -823,7 +822,9 @@ async def introspect_token( } except ExpiredSignatureError: return {"active": False} - except (JWTError, Exception): # noqa: S110 - Intentional: invalid JWT falls through to refresh token check + except JWTError: + pass + except Exception: # noqa: S110 - Intentional: invalid JWT falls through to refresh token check pass # Try as refresh token diff --git a/backend/app/services/oauth_service.py b/backend/app/services/oauth_service.py index f023586..da83aed 100644 --- a/backend/app/services/oauth_service.py +++ b/backend/app/services/oauth_service.py @@ -39,19 +39,22 @@ from app.schemas.oauth import ( logger = logging.getLogger(__name__) -class OAuthProviderConfig(TypedDict, total=False): - """Type definition for OAuth provider configuration.""" - +class _OAuthProviderConfigRequired(TypedDict): name: str icon: str authorize_url: str token_url: str userinfo_url: str - email_url: str # Optional, GitHub-only scopes: list[str] supports_pkce: bool +class OAuthProviderConfig(_OAuthProviderConfigRequired, total=False): + """Type definition for OAuth provider configuration.""" + + email_url: str # Optional, GitHub-only + + # Provider configurations OAUTH_PROVIDERS: dict[str, OAuthProviderConfig] = { "google": { @@ -485,7 +488,7 @@ class OAuthService: # GitHub requires separate request for email if provider == "github" and not user_info.get("email"): email_resp = await client.get( - config["email_url"], + config["email_url"], # pyright: ignore[reportTypedDictNotRequiredAccess] headers=headers, ) email_resp.raise_for_status() diff --git a/backend/app/utils/test_utils.py b/backend/app/utils/test_utils.py index fa89d59..a5c2907 100644 --- a/backend/app/utils/test_utils.py +++ b/backend/app/utils/test_utils.py @@ -65,10 +65,10 @@ async def setup_async_test_db(): async with test_engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) - AsyncTestingSessionLocal = sessionmaker( + AsyncTestingSessionLocal = sessionmaker( # pyright: ignore[reportCallIssue] autocommit=False, autoflush=False, - bind=test_engine, + bind=test_engine, # pyright: ignore[reportArgumentType] expire_on_commit=False, class_=AsyncSession, ) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 67f4324..61eb254 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -72,7 +72,7 @@ dev = [ # Development tools "ruff>=0.8.0", # All-in-one: linting, formatting, import sorting - "mypy>=1.8.0", # Type checking + "pyright>=1.1.390", # Type checking ] # E2E testing with real PostgreSQL (requires Docker) @@ -185,120 +185,6 @@ indent-style = "space" skip-magic-trailing-comma = false line-ending = "lf" -# ============================================================================ -# mypy Configuration - Type Checking -# ============================================================================ -[tool.mypy] -python_version = "3.12" -warn_return_any = false # SQLAlchemy queries return Any - overly strict -warn_unused_configs = true -disallow_untyped_defs = false # Gradual typing - enable later -disallow_incomplete_defs = false -check_untyped_defs = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_no_return = true -strict_equality = true -ignore_missing_imports = false -explicit_package_bases = true -namespace_packages = true - -# Pydantic plugin for better validation -plugins = ["pydantic.mypy"] - -# Per-module options -[[tool.mypy.overrides]] -module = "alembic.*" -ignore_errors = true - -[[tool.mypy.overrides]] -module = "app.alembic.*" -ignore_errors = true - -[[tool.mypy.overrides]] -module = "sqlalchemy.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "fastapi_utils.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "slowapi.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "jose.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "passlib.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pydantic_settings.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "fastapi.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "apscheduler.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "starlette.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "authlib.*" -ignore_missing_imports = true - -# SQLAlchemy ORM models - Column descriptors cause type confusion -[[tool.mypy.overrides]] -module = "app.models.*" -disable_error_code = ["assignment", "arg-type", "return-value"] - -# CRUD operations - Generic ModelType and SQLAlchemy Result issues -[[tool.mypy.overrides]] -module = "app.crud.*" -disable_error_code = ["attr-defined", "assignment", "arg-type", "return-value"] - -# API routes - SQLAlchemy Column to Pydantic schema conversions -[[tool.mypy.overrides]] -module = "app.api.routes.*" -disable_error_code = ["arg-type", "call-arg", "call-overload", "assignment"] - -# API dependencies - Similar SQLAlchemy Column issues -[[tool.mypy.overrides]] -module = "app.api.dependencies.*" -disable_error_code = ["arg-type"] - -# FastAPI exception handlers have correct signatures despite mypy warnings -[[tool.mypy.overrides]] -module = "app.main" -disable_error_code = ["arg-type"] - -# Auth service - SQLAlchemy Column issues -[[tool.mypy.overrides]] -module = "app.services.auth_service" -disable_error_code = ["assignment", "arg-type"] - -# Test utils - Testing patterns -[[tool.mypy.overrides]] -module = "app.utils.auth_test_utils" -disable_error_code = ["assignment", "arg-type"] - -# ============================================================================ -# Pydantic mypy plugin configuration -# ============================================================================ -[tool.pydantic-mypy] -init_forbid_extra = true -init_typed = true -warn_required_dynamic_aliases = true - # ============================================================================ # Pytest Configuration # ============================================================================ diff --git a/backend/pyrightconfig.json b/backend/pyrightconfig.json new file mode 100644 index 0000000..21effe9 --- /dev/null +++ b/backend/pyrightconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["app"], + "exclude": ["app/alembic"], + "pythonVersion": "3.12", + "venvPath": ".", + "venv": ".venv", + "typeCheckingMode": "standard", + "reportMissingImports": true, + "reportMissingTypeStubs": false, + "reportUnknownMemberType": false, + "reportUnknownVariableType": false, + "reportUnknownArgumentType": false, + "reportUnknownParameterType": false, + "reportUnknownLambdaType": false, + "reportReturnType": true, + "reportUnusedImport": false, + "reportGeneralTypeIssues": false, + "reportAttributeAccessIssue": false, + "reportArgumentType": false, + "strictListInference": false, + "strictDictionaryInference": false, + "strictSetInference": false +} diff --git a/backend/uv.lock b/backend/uv.lock index 46b988c..600d59f 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -519,7 +519,7 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "freezegun" }, - { name = "mypy" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -546,12 +546,12 @@ requires-dist = [ { name = "fastapi-utils", specifier = "==0.8.0" }, { name = "freezegun", marker = "extra == 'dev'", specifier = "~=1.5.1" }, { name = "httpx", specifier = ">=0.27.0" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" }, { name = "passlib", specifier = "==1.7.4" }, { name = "pillow", specifier = ">=10.3.0" }, { name = "psycopg2-binary", specifier = ">=2.9.9" }, { name = "pydantic", specifier = ">=2.10.6" }, { name = "pydantic-settings", specifier = ">=2.2.1" }, + { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.390" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.5" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, @@ -966,44 +966,12 @@ wheels = [ ] [[package]] -name = "mypy" -version = "1.18.2" +name = "nodeenv" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, -] - -[[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" }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] @@ -1024,15 +992,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, ] -[[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 = "pillow" version = "12.0.0" @@ -1302,6 +1261,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/af/d8bf0959ece9bc4679bd203908c31019556a421d76d8143b0c6871c7f614/pyrate_limiter-3.9.0-py3-none-any.whl", hash = "sha256:77357840c8cf97a36d67005d4e090787043f54000c12c2b414ff65657653e378", size = 33628, upload-time = "2025-07-30T14:36:57.71Z" }, ] +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + [[package]] name = "pytest" version = "8.4.2"