CORS Fixed + black and ruff fixes

This commit is contained in:
Yûki VACHOT 2025-12-01 02:55:47 +01:00
parent 5e2f6078a1
commit 3844ffc207
30 changed files with 85 additions and 114 deletions

View file

@ -465,16 +465,6 @@ geoguessr-mcp/
└── Dockerfile └── Dockerfile
``` ```
### MCP Inspector Tool Test
Tool to inspect MCP server and test connectivity:
Using the [Inspector](https://github.com/modelcontextprotocol/inspector) tool from the MCP project.
Via Docker:
```bash
docker run --rm -p 6274:6274 -p 6277:6277 -e HOST=0.0.0.0 ghcr.io/modelcontextprotocol/inspector:latest
```
## 🤝 Contributing ## 🤝 Contributing
Contributions are welcome! Please: Contributions are welcome! Please:

View file

@ -4,8 +4,8 @@ GeoGuessr API Endpoints Registry.
Centralized endpoint definitions with metadata for dynamic discovery and routing. Centralized endpoint definitions with metadata for dynamic discovery and routing.
""" """
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable, Optional
from ..config import settings from ..config import settings
@ -19,7 +19,7 @@ class EndpointInfo:
description: str = "" description: str = ""
auth_required: bool = True auth_required: bool = True
use_game_server: bool = False use_game_server: bool = False
params_builder: Optional[Callable[..., dict]] = None params_builder: Callable[..., dict] | None = None
class Endpoints: class Endpoints:

View file

@ -10,7 +10,6 @@ Classes:
""" """
import logging import logging
from typing import Optional
import httpx import httpx
@ -45,7 +44,7 @@ class GeoGuessrClient:
async def _get_authenticated_client( async def _get_authenticated_client(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> httpx.AsyncClient: ) -> httpx.AsyncClient:
""" """
Get an authenticated HTTP client. Get an authenticated HTTP client.
@ -79,9 +78,9 @@ class GeoGuessrClient:
async def request( async def request(
self, self,
endpoint: EndpointInfo, endpoint: EndpointInfo,
session_token: Optional[str] = None, session_token: str | None = None,
params: Optional[dict] = None, params: dict | None = None,
json_data: Optional[dict] = None, json_data: dict | None = None,
**kwargs, **kwargs,
) -> DynamicResponse: ) -> DynamicResponse:
""" """
@ -154,8 +153,8 @@ class GeoGuessrClient:
async def get( async def get(
self, self,
endpoint: EndpointInfo, endpoint: EndpointInfo,
session_token: Optional[str] = None, session_token: str | None = None,
params: Optional[dict] = None, params: dict | None = None,
**kwargs, **kwargs,
) -> DynamicResponse: ) -> DynamicResponse:
"""Make a GET request.""" """Make a GET request."""
@ -164,8 +163,8 @@ class GeoGuessrClient:
async def post( async def post(
self, self,
endpoint: EndpointInfo, endpoint: EndpointInfo,
session_token: Optional[str] = None, session_token: str | None = None,
json_data: Optional[dict] = None, json_data: dict | None = None,
**kwargs, **kwargs,
) -> DynamicResponse: ) -> DynamicResponse:
"""Make a POST request.""" """Make a POST request."""
@ -174,9 +173,9 @@ class GeoGuessrClient:
async def get_raw( async def get_raw(
self, self,
path: str, path: str,
session_token: Optional[str] = None, session_token: str | None = None,
use_game_server: bool = False, use_game_server: bool = False,
params: Optional[dict] = None, params: dict | None = None,
) -> DynamicResponse: ) -> DynamicResponse:
""" """
Make a raw GET request to any path. Make a raw GET request to any path.

View file

@ -7,11 +7,10 @@ where each API key can have its own GeoGuessr session.
import asyncio import asyncio
import logging import logging
from typing import Optional
from ..config import settings
from .session import SessionManager, UserSession from .session import SessionManager, UserSession
from .user_context import UserContext from .user_context import UserContext
from ..config import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -146,7 +145,7 @@ class MultiUserSessionManager:
return True return True
async def get_session_for_api_key(self, api_key: str) -> Optional[UserSession]: async def get_session_for_api_key(self, api_key: str) -> UserSession | None:
""" """
Get the active session for a specific API key. Get the active session for a specific API key.

View file

@ -7,12 +7,11 @@ the authenticated user making the request.
""" """
from contextvars import ContextVar from contextvars import ContextVar
from typing import Optional
from .user_context import UserContext from .user_context import UserContext
# Context variable to store the current user context # Context variable to store the current user context
_current_user_context: ContextVar[Optional[UserContext]] = ContextVar( _current_user_context: ContextVar[UserContext | None] = ContextVar(
"current_user_context", default=None "current_user_context", default=None
) )
@ -30,7 +29,7 @@ def set_current_user_context(context: UserContext) -> None:
_current_user_context.set(context) _current_user_context.set(context)
def get_current_user_context() -> Optional[UserContext]: def get_current_user_context() -> UserContext | None:
""" """
Get the current user context. Get the current user context.

View file

@ -7,7 +7,6 @@ import logging
import secrets import secrets
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Optional
import httpx import httpx
@ -25,7 +24,7 @@ class UserSession:
username: str username: str
email: str email: str
created_at: datetime = field(default_factory=datetime.now) created_at: datetime = field(default_factory=datetime.now)
expires_at: Optional[datetime] = None expires_at: datetime | None = None
def is_valid(self) -> bool: def is_valid(self) -> bool:
"""Check if the session is still valid.""" """Check if the session is still valid."""
@ -37,10 +36,10 @@ class UserSession:
class SessionManager: class SessionManager:
"""Manages user sessions for the MCP server.""" """Manages user sessions for the MCP server."""
def __init__(self, default_cookie: Optional[str] = None): def __init__(self, default_cookie: str | None = None):
self._sessions: dict[str, UserSession] = {} self._sessions: dict[str, UserSession] = {}
self._user_sessions: dict[str, str] = {} self._user_sessions: dict[str, str] = {}
self._default_cookie: Optional[str] = default_cookie or settings.DEFAULT_NCFA_COOKIE self._default_cookie: str | None = default_cookie or settings.DEFAULT_NCFA_COOKIE
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
@staticmethod @staticmethod
@ -111,7 +110,7 @@ class SessionManager:
return session_token, session return session_token, session
@staticmethod @staticmethod
def _extract_ncfa_cookie(response: httpx.Response) -> Optional[str]: def _extract_ncfa_cookie(response: httpx.Response) -> str | None:
"""Extract _ncfa cookie from response.""" """Extract _ncfa cookie from response."""
# Try cookies jar first # Try cookies jar first
for cookie in response.cookies.jar: for cookie in response.cookies.jar:
@ -160,7 +159,7 @@ class SessionManager:
return True return True
return False return False
async def get_session(self, session_token: Optional[str] = None) -> Optional[UserSession]: async def get_session(self, session_token: str | None = None) -> UserSession | None:
""" """
Get a session by token or return default if available. Get a session by token or return default if available.
@ -203,7 +202,7 @@ class SessionManager:
logger.info("Default NCFA cookie updated") logger.info("Default NCFA cookie updated")
@staticmethod @staticmethod
async def validate_cookie(cookie: str) -> Optional[dict]: async def validate_cookie(cookie: str) -> dict | None:
""" """
Validate a cookie by making a test request. Validate a cookie by making a test request.

View file

@ -6,7 +6,6 @@ is making a request and their associated GeoGuessr session.
""" """
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
from .session import UserSession from .session import UserSession
@ -23,7 +22,7 @@ class UserContext:
api_key: str api_key: str
"""The API key used to authenticate this request""" """The API key used to authenticate this request"""
session: Optional[UserSession] = None session: UserSession | None = None
"""The GeoGuessr session for this user (if logged in)""" """The GeoGuessr session for this user (if logged in)"""
@property @property
@ -41,7 +40,7 @@ class UserContext:
return f"User-{hash(self.api_key) % 10000:04d}" return f"User-{hash(self.api_key) % 10000:04d}"
@property @property
def ncfa_cookie(self) -> Optional[str]: def ncfa_cookie(self) -> str | None:
"""Get the NCFA cookie for this user.""" """Get the NCFA cookie for this user."""
if self.session: if self.session:
return self.session.ncfa_cookie return self.session.ncfa_cookie

View file

@ -2,7 +2,6 @@
import os import os
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional
@dataclass @dataclass
@ -18,7 +17,7 @@ class Settings:
GEOGUESSR_DOMAIN_NAME: str = "geoguessr.com" GEOGUESSR_DOMAIN_NAME: str = "geoguessr.com"
GEOGUESSR_API_URL: str = "https://www.geoguessr.com/api" GEOGUESSR_API_URL: str = "https://www.geoguessr.com/api"
GAME_SERVER_URL: str = "https://game-server.geoguessr.com/api" GAME_SERVER_URL: str = "https://game-server.geoguessr.com/api"
DEFAULT_NCFA_COOKIE: Optional[str] = field( DEFAULT_NCFA_COOKIE: str | None = field(
default_factory=lambda: os.getenv("GEOGUESSR_NCFA_COOKIE") default_factory=lambda: os.getenv("GEOGUESSR_NCFA_COOKIE")
) )
@ -37,7 +36,7 @@ class Settings:
MCP_AUTH_ENABLED: bool = field( MCP_AUTH_ENABLED: bool = field(
default_factory=lambda: os.getenv("MCP_AUTH_ENABLED", "false").lower() == "true" default_factory=lambda: os.getenv("MCP_AUTH_ENABLED", "false").lower() == "true"
) )
MCP_API_KEYS: Optional[str] = field(default_factory=lambda: os.getenv("MCP_API_KEYS")) MCP_API_KEYS: str | None = field(default_factory=lambda: os.getenv("MCP_API_KEYS"))
# Logging Configuration # Logging Configuration
LOG_LEVEL: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO")) LOG_LEVEL: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO"))

View file

@ -6,7 +6,6 @@ and attaches user context for multi-user support.
""" """
import logging import logging
from typing import Optional
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request from starlette.requests import Request
@ -26,7 +25,7 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
Authorization header with a Bearer token matching one of the configured API keys. Authorization header with a Bearer token matching one of the configured API keys.
""" """
def __init__(self, app, valid_api_keys: Optional[set[str]] = None): def __init__(self, app, valid_api_keys: set[str] | None = None):
super().__init__(app) super().__init__(app)
self.valid_api_keys = valid_api_keys or settings.get_api_keys() self.valid_api_keys = valid_api_keys or settings.get_api_keys()
self.enabled = settings.MCP_AUTH_ENABLED self.enabled = settings.MCP_AUTH_ENABLED

View file

@ -1,7 +1,6 @@
"""Achievement-related data models.""" """Achievement-related data models."""
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
@dataclass @dataclass
@ -12,9 +11,9 @@ class Achievement:
name: str name: str
description: str = "" description: str = ""
unlocked: bool = False unlocked: bool = False
unlocked_at: Optional[str] = None unlocked_at: str | None = None
progress: float = 0.0 progress: float = 0.0
icon_url: Optional[str] = None icon_url: str | None = None
@classmethod @classmethod
def from_api_response(cls, data: dict) -> "Achievement": def from_api_response(cls, data: dict) -> "Achievement":

View file

@ -1,7 +1,6 @@
"""DailyChallenge-related data models.""" """DailyChallenge-related data models."""
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional
@dataclass @dataclass
@ -13,7 +12,7 @@ class DailyChallenge:
date: str = "" date: str = ""
time_limit: int = 0 time_limit: int = 0
completed: bool = False completed: bool = False
score: Optional[int] = None score: int | None = None
raw_data: dict = field(default_factory=dict) raw_data: dict = field(default_factory=dict)
@classmethod @classmethod

View file

@ -1,7 +1,6 @@
"""Game-related data models.""" """Game-related data models."""
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional
from .round_guess import RoundGuess from .round_guess import RoundGuess
@ -15,7 +14,7 @@ class Game:
mode: str mode: str
total_score: int total_score: int
rounds: list[RoundGuess] = field(default_factory=list) rounds: list[RoundGuess] = field(default_factory=list)
created_at: Optional[str] = None created_at: str | None = None
finished: bool = False finished: bool = False
raw_data: dict = field(default_factory=dict) raw_data: dict = field(default_factory=dict)

View file

@ -1,7 +1,6 @@
"""RoundGuess-related data models.""" """RoundGuess-related data models."""
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
@dataclass @dataclass
@ -12,8 +11,8 @@ class RoundGuess:
score: int score: int
distance_meters: float distance_meters: float
time_seconds: int time_seconds: int
lat: Optional[float] = None lat: float | None = None
lng: Optional[float] = None lng: float | None = None
country: str = "" country: str = ""
@classmethod @classmethod

View file

@ -1,7 +1,6 @@
"""UserProfile-related data models.""" """UserProfile-related data models."""
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional
@dataclass @dataclass
@ -16,7 +15,7 @@ class UserProfile:
created: str = "" created: str = ""
is_verified: bool = False is_verified: bool = False
is_pro: bool = False is_pro: bool = False
avatar_url: Optional[str] = None avatar_url: str | None = None
raw_data: dict = field(default_factory=dict) raw_data: dict = field(default_factory=dict)
@classmethod @classmethod

View file

@ -12,9 +12,9 @@ Classes:
""" """
import asyncio import asyncio
import contextlib
import logging import logging
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Optional
import httpx import httpx
@ -119,14 +119,14 @@ class EndpointMonitor:
def __init__( def __init__(
self, self,
registry: Optional[SchemaRegistry] = None, registry: SchemaRegistry | None = None,
ncfa_cookie: Optional[str] = None, ncfa_cookie: str | None = None,
): ):
self.registry = registry or schema_registry self.registry = registry or schema_registry
self.ncfa_cookie = ncfa_cookie or settings.DEFAULT_NCFA_COOKIE self.ncfa_cookie = ncfa_cookie or settings.DEFAULT_NCFA_COOKIE
self.results: list[MonitoringResult] = [] self.results: list[MonitoringResult] = []
self._running = False self._running = False
self._task: Optional[asyncio.Task] = None self._task: asyncio.Task | None = None
async def check_endpoint( async def check_endpoint(
self, self,
@ -286,10 +286,8 @@ class EndpointMonitor:
self._running = False self._running = False
if self._task: if self._task:
self._task.cancel() self._task.cancel()
try: with contextlib.suppress(asyncio.CancelledError):
await self._task await self._task
except asyncio.CancelledError:
pass
logger.info("Stopped periodic monitoring") logger.info("Stopped periodic monitoring")
async def _monitoring_loop(self) -> None: async def _monitoring_loop(self) -> None:

View file

@ -8,7 +8,6 @@ an endpoint, including its availability, response time, and any errors encounter
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Optional
@dataclass @dataclass
@ -20,5 +19,5 @@ class MonitoringResult:
response_code: int response_code: int
response_time_ms: float response_time_ms: float
schema_changed: bool schema_changed: bool
error_message: Optional[str] = None error_message: str | None = None
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC)) timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))

View file

@ -11,7 +11,7 @@ schema information.
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any, Optional from typing import Any
from .schema_field import SchemaField from .schema_field import SchemaField
@ -29,8 +29,8 @@ class EndpointSchema:
schema_hash: str = "" schema_hash: str = ""
response_code: int = 200 response_code: int = 200
is_available: bool = True is_available: bool = True
error_message: Optional[str] = None error_message: str | None = None
sample_response: Optional[dict] = None sample_response: dict | None = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""Convert to dictionary for serialization.""" """Convert to dictionary for serialization."""

View file

@ -6,7 +6,7 @@ a schema field, including its name, type, and other relevant metadata.
""" """
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Optional from typing import Any
@dataclass @dataclass
@ -16,6 +16,6 @@ class SchemaField:
name: str name: str
field_type: str field_type: str
nullable: bool = False nullable: bool = False
nested_schema: Optional[dict] = None nested_schema: dict | None = None
example_value: Any = None example_value: Any = None
description: str = "" description: str = ""

View file

@ -15,7 +15,7 @@ import logging
import tempfile import tempfile
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any
from .endpoint_schema import EndpointSchema from .endpoint_schema import EndpointSchema
from .schema_detector import SchemaDetector from .schema_detector import SchemaDetector
@ -32,7 +32,7 @@ class SchemaRegistry:
to track changes over time and adapt automatically. to track changes over time and adapt automatically.
""" """
def __init__(self, cache_dir: Optional[str] = None): def __init__(self, cache_dir: str | None = None):
self.cache_dir = Path(cache_dir or settings.SCHEMA_CACHE_DIR) self.cache_dir = Path(cache_dir or settings.SCHEMA_CACHE_DIR)
# Try to create the cache directory, fall back to temp if permission denied # Try to create the cache directory, fall back to temp if permission denied
@ -187,7 +187,7 @@ class SchemaRegistry:
) )
self._save_schemas() self._save_schemas()
def get_schema(self, endpoint: str) -> Optional[EndpointSchema]: def get_schema(self, endpoint: str) -> EndpointSchema | None:
"""Get the current schema for an endpoint.""" """Get the current schema for an endpoint."""
return self.schemas.get(endpoint) return self.schemas.get(endpoint)

View file

@ -7,7 +7,6 @@ dynamic data handling and LLM-friendly output formatting.
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional
from .game_service import GameService from .game_service import GameService
from .profile_service import ProfileService from .profile_service import ProfileService
@ -61,8 +60,8 @@ class AnalysisService:
def __init__( def __init__(
self, self,
client: GeoGuessrClient, client: GeoGuessrClient,
game_service: Optional[GameService] = None, game_service: GameService | None = None,
profile_service: Optional[ProfileService] = None, profile_service: ProfileService | None = None,
): ):
self.client = client self.client = client
self.game_service = game_service or GameService(client) self.game_service = game_service or GameService(client)
@ -155,7 +154,7 @@ class AnalysisService:
async def analyze_recent_games( async def analyze_recent_games(
self, self,
count: int = 10, count: int = 10,
session_token: Optional[str] = None, session_token: str | None = None,
) -> dict: ) -> dict:
""" """
Analyze recent games and provide statistics summary. Analyze recent games and provide statistics summary.
@ -181,7 +180,7 @@ class AnalysisService:
async def get_performance_summary( async def get_performance_summary(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> dict: ) -> dict:
""" """
Get a comprehensive performance summary. Get a comprehensive performance summary.
@ -246,7 +245,7 @@ class AnalysisService:
async def get_strategy_recommendations( async def get_strategy_recommendations(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> dict: ) -> dict:
""" """
Generate strategy recommendations based on performance analysis. Generate strategy recommendations based on performance analysis.

View file

@ -5,9 +5,8 @@ Handles game history, details, and competitive data with dynamic schema support.
""" """
import logging import logging
from typing import Optional
from ..api import Endpoints, DynamicResponse, GeoGuessrClient from ..api import DynamicResponse, Endpoints, GeoGuessrClient
from ..models import DailyChallenge, Game, SeasonStats from ..models import DailyChallenge, Game, SeasonStats
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -22,7 +21,7 @@ class GameService:
async def get_game_details( async def get_game_details(
self, self,
game_token: str, game_token: str,
session_token: Optional[str] = None, session_token: str | None = None,
) -> tuple[Game, DynamicResponse]: ) -> tuple[Game, DynamicResponse]:
""" """
Get details for a specific game. Get details for a specific game.
@ -45,7 +44,7 @@ class GameService:
async def get_unfinished_games( async def get_unfinished_games(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> DynamicResponse: ) -> DynamicResponse:
"""Get list of unfinished games.""" """Get list of unfinished games."""
return await self.client.get(Endpoints.GAMES.GET_UNFINISHED_GAMES, session_token) return await self.client.get(Endpoints.GAMES.GET_UNFINISHED_GAMES, session_token)
@ -53,7 +52,7 @@ class GameService:
async def get_streak_game( async def get_streak_game(
self, self,
game_token: str, game_token: str,
session_token: Optional[str] = None, session_token: str | None = None,
) -> DynamicResponse: ) -> DynamicResponse:
"""Get streak game details.""" """Get streak game details."""
endpoint = Endpoints.GAMES.get_streak_game(game_token) endpoint = Endpoints.GAMES.get_streak_game(game_token)
@ -63,7 +62,7 @@ class GameService:
self, self,
count: int = 10, count: int = 10,
page: int = 0, page: int = 0,
session_token: Optional[str] = None, session_token: str | None = None,
) -> DynamicResponse: ) -> DynamicResponse:
""" """
Get the activity feed. Get the activity feed.
@ -82,7 +81,7 @@ class GameService:
async def get_recent_games( async def get_recent_games(
self, self,
count: int = 10, count: int = 10,
session_token: Optional[str] = None, session_token: str | None = None,
) -> list[Game]: ) -> list[Game]:
""" """
Get recent games from the activity feed. Get recent games from the activity feed.
@ -122,7 +121,7 @@ class GameService:
async def get_season_stats( async def get_season_stats(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> tuple[SeasonStats, DynamicResponse]: ) -> tuple[SeasonStats, DynamicResponse]:
"""Get active season statistics.""" """Get active season statistics."""
response = await self.client.get( response = await self.client.get(
@ -138,7 +137,7 @@ class GameService:
async def get_daily_challenge( async def get_daily_challenge(
self, self,
day: str = "today", day: str = "today",
session_token: Optional[str] = None, session_token: str | None = None,
) -> tuple[DailyChallenge, DynamicResponse]: ) -> tuple[DailyChallenge, DynamicResponse]:
""" """
Get daily challenge. Get daily challenge.
@ -162,7 +161,7 @@ class GameService:
async def get_battle_royale( async def get_battle_royale(
self, self,
game_id: str, game_id: str,
session_token: Optional[str] = None, session_token: str | None = None,
) -> DynamicResponse: ) -> DynamicResponse:
"""Get battle royale game details.""" """Get battle royale game details."""
endpoint = Endpoints.GAME_SERVER.get_battle_royale(game_id) endpoint = Endpoints.GAME_SERVER.get_battle_royale(game_id)
@ -171,7 +170,7 @@ class GameService:
async def get_duel( async def get_duel(
self, self,
duel_id: str, duel_id: str,
session_token: Optional[str] = None, session_token: str | None = None,
) -> DynamicResponse: ) -> DynamicResponse:
"""Get duel game details.""" """Get duel game details."""
endpoint = Endpoints.GAME_SERVER.get_duel(duel_id) endpoint = Endpoints.GAME_SERVER.get_duel(duel_id)
@ -179,7 +178,7 @@ class GameService:
async def get_tournaments( async def get_tournaments(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> DynamicResponse: ) -> DynamicResponse:
"""Get tournament information.""" """Get tournament information."""
return await self.client.get(Endpoints.GAME_SERVER.GET_TOURNAMENTS, session_token) return await self.client.get(Endpoints.GAME_SERVER.GET_TOURNAMENTS, session_token)

View file

@ -6,9 +6,8 @@ dynamic schema adaptation.
""" """
import logging import logging
from typing import Optional
from ..api import DynamicResponse, GeoGuessrClient, Endpoints from ..api import DynamicResponse, Endpoints, GeoGuessrClient
from ..models import Achievement, UserProfile, UserStats from ..models import Achievement, UserProfile, UserStats
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -22,7 +21,7 @@ class ProfileService:
async def get_profile( async def get_profile(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> tuple[UserProfile, DynamicResponse]: ) -> tuple[UserProfile, DynamicResponse]:
""" """
Get current user's profile. Get current user's profile.
@ -40,7 +39,7 @@ class ProfileService:
async def get_stats( async def get_stats(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> tuple[UserStats, DynamicResponse]: ) -> tuple[UserStats, DynamicResponse]:
""" """
Get user statistics. Get user statistics.
@ -58,7 +57,7 @@ class ProfileService:
async def get_extended_stats( async def get_extended_stats(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> DynamicResponse: ) -> DynamicResponse:
""" """
Get extended statistics. Get extended statistics.
@ -69,7 +68,7 @@ class ProfileService:
async def get_achievements( async def get_achievements(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> tuple[list[Achievement], DynamicResponse]: ) -> tuple[list[Achievement], DynamicResponse]:
""" """
Get user achievements. Get user achievements.
@ -96,7 +95,7 @@ class ProfileService:
async def get_public_profile( async def get_public_profile(
self, self,
user_id: str, user_id: str,
session_token: Optional[str] = None, session_token: str | None = None,
) -> tuple[UserProfile, DynamicResponse]: ) -> tuple[UserProfile, DynamicResponse]:
"""Get another user's public profile.""" """Get another user's public profile."""
endpoint = Endpoints.PROFILES.get_public_profile(user_id) endpoint = Endpoints.PROFILES.get_public_profile(user_id)
@ -110,14 +109,14 @@ class ProfileService:
async def get_user_maps( async def get_user_maps(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> DynamicResponse: ) -> DynamicResponse:
"""Get user's custom maps.""" """Get user's custom maps."""
return await self.client.get(Endpoints.PROFILES.GET_USER_MAPS, session_token) return await self.client.get(Endpoints.PROFILES.GET_USER_MAPS, session_token)
async def get_comprehensive_profile( async def get_comprehensive_profile(
self, self,
session_token: Optional[str] = None, session_token: str | None = None,
) -> dict: ) -> dict:
""" """
Get a comprehensive profile combining multiple endpoints. Get a comprehensive profile combining multiple endpoints.

View file

@ -99,7 +99,7 @@ def register_game_tools(mcp: FastMCP, game_service: GameService):
"summary": { "summary": {
"total_score": sum(g.total_score for g in games), "total_score": sum(g.total_score for g in games),
"average_score": sum(g.total_score for g in games) / len(games) if games else 0, "average_score": sum(g.total_score for g in games) / len(games) if games else 0,
"maps_played": list(set(g.map_name for g in games)), "maps_played": list({g.map_name for g in games}),
}, },
} }

View file

@ -9,7 +9,7 @@ from geoguessr_mcp.api import GeoGuessrClient
from geoguessr_mcp.api.dynamic_response import DynamicResponse from geoguessr_mcp.api.dynamic_response import DynamicResponse
from geoguessr_mcp.auth import SessionManager, UserSession from geoguessr_mcp.auth import SessionManager, UserSession
from geoguessr_mcp.config import settings from geoguessr_mcp.config import settings
from geoguessr_mcp.models import RoundGuess, Game from geoguessr_mcp.models import Game, RoundGuess
from geoguessr_mcp.services import AnalysisService, GameService, ProfileService from geoguessr_mcp.services import AnalysisService, GameService, ProfileService

View file

@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import httpx import httpx
import pytest import pytest
from geoguessr_mcp.api import DynamicResponse, GeoGuessrClient, EndpointInfo, Endpoints from geoguessr_mcp.api import DynamicResponse, EndpointInfo, Endpoints, GeoGuessrClient
from geoguessr_mcp.config import settings from geoguessr_mcp.config import settings

View file

@ -187,7 +187,7 @@ class TestAuthenticationFlow:
assert "expired_user" not in session_manager._user_sessions assert "expired_user" not in session_manager._user_sessions
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_default_cookie_fallback(self, session_manager): async def test_default_cookie_fallback(self):
"""Test falling back to default cookie when no session exists.""" """Test falling back to default cookie when no session exists."""
# Create manager with default cookie # Create manager with default cookie
manager_with_default = SessionManager(default_cookie="default_test_cookie") manager_with_default = SessionManager(default_cookie="default_test_cookie")

View file

@ -26,8 +26,8 @@ class TestMultiUserSessionManager:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_user_context_reuses_existing_manager(self, manager): async def test_get_user_context_reuses_existing_manager(self, manager):
"""Test that getting context for existing API key reuses the same manager.""" """Test that getting context for existing API key reuses the same manager."""
context1 = await manager.get_user_context("existing_key") await manager.get_user_context("existing_key")
context2 = await manager.get_user_context("existing_key") await manager.get_user_context("existing_key")
# Should use the same manager instance # Should use the same manager instance
assert manager._user_managers["existing_key"] is manager._user_managers["existing_key"] assert manager._user_managers["existing_key"] is manager._user_managers["existing_key"]
@ -36,9 +36,9 @@ class TestMultiUserSessionManager:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_multiple_api_keys_get_separate_managers(self, manager): async def test_multiple_api_keys_get_separate_managers(self, manager):
"""Test that different API keys get separate session managers.""" """Test that different API keys get separate session managers."""
context1 = await manager.get_user_context("key1") await manager.get_user_context("key1")
context2 = await manager.get_user_context("key2") await manager.get_user_context("key2")
context3 = await manager.get_user_context("key3") await manager.get_user_context("key3")
assert len(manager._user_managers) == 3 assert len(manager._user_managers) == 3
assert manager._user_managers["key1"] is not manager._user_managers["key2"] assert manager._user_managers["key1"] is not manager._user_managers["key2"]
@ -61,7 +61,7 @@ class TestMultiUserSessionManager:
assert session is None assert session is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_login_user_creates_manager_if_not_exists(self, manager): async def test_login_user_creates_manager_if_not_exists(self):
"""Test that login_user creates a manager if it doesn't exist.""" """Test that login_user creates a manager if it doesn't exist."""
# This test requires mocking the HTTP client for GeoGuessr API # This test requires mocking the HTTP client for GeoGuessr API
# We'll mark it as a placeholder for now # We'll mark it as a placeholder for now

View file

@ -1,10 +1,9 @@
"""Tests for UserContext class.""" """Tests for UserContext class."""
import pytest from datetime import UTC, datetime, timedelta
from geoguessr_mcp.auth.session import UserSession from geoguessr_mcp.auth.session import UserSession
from geoguessr_mcp.auth.user_context import UserContext from geoguessr_mcp.auth.user_context import UserContext
from datetime import datetime, timedelta, UTC
class TestUserContext: class TestUserContext:

View file

@ -11,7 +11,7 @@ feeds, recent games, season statistics, and daily challenges.
import pytest import pytest
from geoguessr_mcp.models import Game, SeasonStats, DailyChallenge from geoguessr_mcp.models import DailyChallenge, Game, SeasonStats
class TestGameService: class TestGameService:

View file

@ -9,7 +9,7 @@ operations.
import pytest import pytest
from geoguessr_mcp.models import UserProfile, UserStats, Achievement from geoguessr_mcp.models import Achievement, UserProfile, UserStats
class TestProfileService: class TestProfileService: