Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,15 @@ ignore = []
python_version = "3.14"
mypy_path = "src"
explicit_package_bases = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true

# Overrides must stay last: any key after a [[...overrides]] table belongs to it.
[[tool.mypy.overrides]]
# APScheduler 3.x ships no type stubs.
module = "apscheduler.*"
ignore_missing_imports = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand Down
7 changes: 4 additions & 3 deletions backend/src/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Lifespan events for startup/shutdown
"""

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any

Expand Down Expand Up @@ -51,7 +52,7 @@


@asynccontextmanager
async def lifespan(app: FastAPI):
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""
Application lifespan manager.

Expand Down Expand Up @@ -201,7 +202,7 @@ async def lifespan(app: FastAPI):


@app.get("/", tags=["root"])
async def root():
async def root() -> dict[str, Any]:
"""
Root endpoint.

Expand All @@ -217,7 +218,7 @@ async def root():


@app.get("/health", tags=["root"])
async def health_check():
async def health_check() -> dict[str, str]:
"""
Simple health check endpoint.

Expand Down
2 changes: 1 addition & 1 deletion backend/src/api/routers/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ async def clear_logs(
async def export_logs(
notification_service: NotificationServiceDep,
format: str = Query(default="json", description="Export format (json or csv)"),
):
) -> StreamingResponse:
"""
Export all activity logs.

Expand Down
2 changes: 1 addition & 1 deletion backend/src/api/routers/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


@router.websocket("/events")
async def websocket_endpoint(websocket: WebSocket):
async def websocket_endpoint(websocket: WebSocket) -> None:
"""
WebSocket endpoint for real-time event streaming.

Expand Down
3 changes: 2 additions & 1 deletion backend/src/api/schemas/giveaway.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

from datetime import datetime
from typing import Any

from pydantic import BaseModel, Field, field_serializer

Expand Down Expand Up @@ -166,7 +167,7 @@ class GiveawayResponse(GiveawayBase):
)

@field_serializer('end_time', 'discovered_at', 'entered_at', 'won_at', 'eligibility_checked_at')
def serialize_datetime(self, dt: datetime | None, _info) -> str | None:
def serialize_datetime(self, dt: datetime | None, _info: Any) -> str | None:
"""Serialize datetime with UTC timezone suffix."""
if dt is None:
return None
Expand Down
8 changes: 4 additions & 4 deletions backend/src/api/schemas/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from datetime import datetime

from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, Field, ValidationInfo, field_validator


class SettingsBase(BaseModel):
Expand Down Expand Up @@ -137,15 +137,15 @@ class SettingsBase(BaseModel):

@field_validator("entry_delay_max")
@classmethod
def validate_delay_range(cls, v, info):
def validate_delay_range(cls, v: int, info: ValidationInfo) -> int:
"""Validate that entry_delay_max >= entry_delay_min."""
if "entry_delay_min" in info.data and v < info.data["entry_delay_min"]:
raise ValueError("entry_delay_max must be >= entry_delay_min")
return v

@field_validator("autojoin_stop_at")
@classmethod
def validate_point_thresholds(cls, v, info):
def validate_point_thresholds(cls, v: int, info: ValidationInfo) -> int:
"""Validate that autojoin_stop_at <= autojoin_start_at."""
if "autojoin_start_at" in info.data and v > info.data["autojoin_start_at"]:
raise ValueError("autojoin_stop_at must be <= autojoin_start_at")
Expand Down Expand Up @@ -375,7 +375,7 @@ class SteamGiftsCredentials(BaseModel):

@field_validator("phpsessid")
@classmethod
def validate_phpsessid(cls, v):
def validate_phpsessid(cls, v: str) -> str:
"""Validate PHPSESSID is not empty after stripping."""
if not v or not v.strip():
raise ValueError("phpsessid cannot be empty")
Expand Down
2 changes: 1 addition & 1 deletion backend/src/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class EventManager:
active_connections: Set of currently connected WebSocket clients
"""

def __init__(self):
def __init__(self) -> None:
"""Initialize EventManager with empty connection set."""
self.active_connections: set[WebSocket] = set()

Expand Down
2 changes: 1 addition & 1 deletion backend/src/db/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async def init_db() -> None:

from alembic import command

def run_migrations():
def run_migrations() -> None:
# Get the directory where alembic.ini is located
src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
alembic_ini = os.path.join(src_dir, "alembic.ini")
Expand Down
8 changes: 4 additions & 4 deletions backend/src/repositories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ async def get_all(
result = await self.session.execute(query)
return list(result.scalars().all())

async def create(self, **kwargs) -> ModelType:
async def create(self, **kwargs: Any) -> ModelType:
"""
Create and persist a new record.

Expand All @@ -130,7 +130,7 @@ async def create(self, **kwargs) -> ModelType:
await self.session.flush() # Flush to get auto-generated fields
return instance

async def update(self, id_value: Any, **kwargs) -> ModelType | None:
async def update(self, id_value: Any, **kwargs: Any) -> ModelType | None:
"""
Update an existing record by primary key.

Expand Down Expand Up @@ -247,7 +247,7 @@ async def bulk_create(self, items: list[dict[str, Any]]) -> list[ModelType]:
await self.session.flush()
return instances

async def filter_by(self, **kwargs) -> list[ModelType]:
async def filter_by(self, **kwargs: Any) -> list[ModelType]:
"""
Filter records by field values.

Expand All @@ -267,7 +267,7 @@ async def filter_by(self, **kwargs) -> list[ModelType]:
result = await self.session.execute(query)
return list(result.scalars().all())

async def get_one_or_none(self, **kwargs) -> ModelType | None:
async def get_one_or_none(self, **kwargs: Any) -> ModelType | None:
"""
Get a single record matching the filter criteria.

Expand Down
5 changes: 3 additions & 2 deletions backend/src/repositories/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

from datetime import timedelta
from typing import Any

from sqlalchemy import or_, select
from sqlalchemy.ext.asyncio import AsyncSession
Expand Down Expand Up @@ -67,7 +68,7 @@ async def get_by_app_id(self, app_id: int) -> Game | None:
"""
return await self.get_by_id(app_id)

async def get_by_ids(self, app_ids) -> dict[int, Game]:
async def get_by_ids(self, app_ids: list[int]) -> dict[int, Game]:
"""
Batch-fetch games by Steam App ID, keyed by id.

Expand Down Expand Up @@ -270,7 +271,7 @@ async def bulk_mark_refreshed(self, app_ids: list[int]) -> None:
for app_id in app_ids:
await self.update(app_id, last_refreshed_at=now)

async def create_or_update(self, app_id: int, **kwargs) -> Game:
async def create_or_update(self, app_id: int, **kwargs: Any) -> Game:
"""
Create a new game or update existing one.

Expand Down
3 changes: 2 additions & 1 deletion backend/src/repositories/giveaway.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

from datetime import datetime, timedelta
from typing import Any

from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
Expand Down Expand Up @@ -798,7 +799,7 @@ async def get_stats_since(self, since: datetime) -> dict:
}

async def create_or_update_by_code(
self, code: str, **kwargs
self, code: str, **kwargs: Any
) -> Giveaway:
"""
Create new giveaway or update existing by code (upsert).
Expand Down
4 changes: 3 additions & 1 deletion backend/src/repositories/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"""


from typing import Any

from sqlalchemy.ext.asyncio import AsyncSession

from models.settings import Settings
Expand Down Expand Up @@ -71,7 +73,7 @@ async def get_settings(self) -> Settings:

return settings

async def update_settings(self, **kwargs) -> Settings:
async def update_settings(self, **kwargs: Any) -> Settings:
"""
Update the singleton settings record.

Expand Down
3 changes: 2 additions & 1 deletion backend/src/services/eligibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from dataclasses import dataclass
from datetime import datetime
from typing import Any

# === Reason codes (stored on Giveaway.eligibility_reason) ===
ELIGIBLE = "eligible"
Expand Down Expand Up @@ -68,7 +69,7 @@ def needs_game_data(self) -> bool:
)


def evaluate_eligibility(giveaway, game, criteria: EligibilityCriteria, now: datetime) -> str:
def evaluate_eligibility(giveaway: Any, game: Any, criteria: EligibilityCriteria, now: datetime) -> str:
"""Return the reason code describing this giveaway's autojoin outcome.

Conditions are checked in a fixed precedence so that a multi-failure giveaway
Expand Down
Loading
Loading