CORS Fixed + black and ruff fixes
This commit is contained in:
parent
5e2f6078a1
commit
3844ffc207
30 changed files with 85 additions and 114 deletions
10
README.md
10
README.md
|
|
@ -465,16 +465,6 @@ geoguessr-mcp/
|
|||
└── 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
|
||||
|
||||
Contributions are welcome! Please:
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ GeoGuessr API Endpoints Registry.
|
|||
Centralized endpoint definitions with metadata for dynamic discovery and routing.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Optional
|
||||
|
||||
from ..config import settings
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ class EndpointInfo:
|
|||
description: str = ""
|
||||
auth_required: bool = True
|
||||
use_game_server: bool = False
|
||||
params_builder: Optional[Callable[..., dict]] = None
|
||||
params_builder: Callable[..., dict] | None = None
|
||||
|
||||
|
||||
class Endpoints:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ Classes:
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
|
|
@ -45,7 +44,7 @@ class GeoGuessrClient:
|
|||
|
||||
async def _get_authenticated_client(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> httpx.AsyncClient:
|
||||
"""
|
||||
Get an authenticated HTTP client.
|
||||
|
|
@ -79,9 +78,9 @@ class GeoGuessrClient:
|
|||
async def request(
|
||||
self,
|
||||
endpoint: EndpointInfo,
|
||||
session_token: Optional[str] = None,
|
||||
params: Optional[dict] = None,
|
||||
json_data: Optional[dict] = None,
|
||||
session_token: str | None = None,
|
||||
params: dict | None = None,
|
||||
json_data: dict | None = None,
|
||||
**kwargs,
|
||||
) -> DynamicResponse:
|
||||
"""
|
||||
|
|
@ -154,8 +153,8 @@ class GeoGuessrClient:
|
|||
async def get(
|
||||
self,
|
||||
endpoint: EndpointInfo,
|
||||
session_token: Optional[str] = None,
|
||||
params: Optional[dict] = None,
|
||||
session_token: str | None = None,
|
||||
params: dict | None = None,
|
||||
**kwargs,
|
||||
) -> DynamicResponse:
|
||||
"""Make a GET request."""
|
||||
|
|
@ -164,8 +163,8 @@ class GeoGuessrClient:
|
|||
async def post(
|
||||
self,
|
||||
endpoint: EndpointInfo,
|
||||
session_token: Optional[str] = None,
|
||||
json_data: Optional[dict] = None,
|
||||
session_token: str | None = None,
|
||||
json_data: dict | None = None,
|
||||
**kwargs,
|
||||
) -> DynamicResponse:
|
||||
"""Make a POST request."""
|
||||
|
|
@ -174,9 +173,9 @@ class GeoGuessrClient:
|
|||
async def get_raw(
|
||||
self,
|
||||
path: str,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
use_game_server: bool = False,
|
||||
params: Optional[dict] = None,
|
||||
params: dict | None = None,
|
||||
) -> DynamicResponse:
|
||||
"""
|
||||
Make a raw GET request to any path.
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ where each API key can have its own GeoGuessr session.
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from ..config import settings
|
||||
from .session import SessionManager, UserSession
|
||||
from .user_context import UserContext
|
||||
from ..config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -146,7 +145,7 @@ class MultiUserSessionManager:
|
|||
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ the authenticated user making the request.
|
|||
"""
|
||||
|
||||
from contextvars import ContextVar
|
||||
from typing import Optional
|
||||
|
||||
from .user_context import UserContext
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
|
|
@ -30,7 +29,7 @@ def set_current_user_context(context: UserContext) -> None:
|
|||
_current_user_context.set(context)
|
||||
|
||||
|
||||
def get_current_user_context() -> Optional[UserContext]:
|
||||
def get_current_user_context() -> UserContext | None:
|
||||
"""
|
||||
Get the current user context.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import logging
|
|||
import secrets
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
|
|
@ -25,7 +24,7 @@ class UserSession:
|
|||
username: str
|
||||
email: str
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
expires_at: Optional[datetime] = None
|
||||
expires_at: datetime | None = None
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if the session is still valid."""
|
||||
|
|
@ -37,10 +36,10 @@ class UserSession:
|
|||
class SessionManager:
|
||||
"""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._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()
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -111,7 +110,7 @@ class SessionManager:
|
|||
return session_token, session
|
||||
|
||||
@staticmethod
|
||||
def _extract_ncfa_cookie(response: httpx.Response) -> Optional[str]:
|
||||
def _extract_ncfa_cookie(response: httpx.Response) -> str | None:
|
||||
"""Extract _ncfa cookie from response."""
|
||||
# Try cookies jar first
|
||||
for cookie in response.cookies.jar:
|
||||
|
|
@ -160,7 +159,7 @@ class SessionManager:
|
|||
return True
|
||||
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.
|
||||
|
||||
|
|
@ -203,7 +202,7 @@ class SessionManager:
|
|||
logger.info("Default NCFA cookie updated")
|
||||
|
||||
@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.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ is making a request and their associated GeoGuessr session.
|
|||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from .session import UserSession
|
||||
|
||||
|
|
@ -23,7 +22,7 @@ class UserContext:
|
|||
api_key: str
|
||||
"""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)"""
|
||||
|
||||
@property
|
||||
|
|
@ -41,7 +40,7 @@ class UserContext:
|
|||
return f"User-{hash(self.api_key) % 10000:04d}"
|
||||
|
||||
@property
|
||||
def ncfa_cookie(self) -> Optional[str]:
|
||||
def ncfa_cookie(self) -> str | None:
|
||||
"""Get the NCFA cookie for this user."""
|
||||
if self.session:
|
||||
return self.session.ncfa_cookie
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -18,7 +17,7 @@ class Settings:
|
|||
GEOGUESSR_DOMAIN_NAME: str = "geoguessr.com"
|
||||
GEOGUESSR_API_URL: str = "https://www.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")
|
||||
)
|
||||
|
||||
|
|
@ -37,7 +36,7 @@ class Settings:
|
|||
MCP_AUTH_ENABLED: bool = field(
|
||||
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
|
||||
LOG_LEVEL: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO"))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ and attaches user context for multi-user support.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
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.
|
||||
"""
|
||||
|
||||
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)
|
||||
self.valid_api_keys = valid_api_keys or settings.get_api_keys()
|
||||
self.enabled = settings.MCP_AUTH_ENABLED
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"""Achievement-related data models."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -12,9 +11,9 @@ class Achievement:
|
|||
name: str
|
||||
description: str = ""
|
||||
unlocked: bool = False
|
||||
unlocked_at: Optional[str] = None
|
||||
unlocked_at: str | None = None
|
||||
progress: float = 0.0
|
||||
icon_url: Optional[str] = None
|
||||
icon_url: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict) -> "Achievement":
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"""DailyChallenge-related data models."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -13,7 +12,7 @@ class DailyChallenge:
|
|||
date: str = ""
|
||||
time_limit: int = 0
|
||||
completed: bool = False
|
||||
score: Optional[int] = None
|
||||
score: int | None = None
|
||||
raw_data: dict = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"""Game-related data models."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
from .round_guess import RoundGuess
|
||||
|
||||
|
|
@ -15,7 +14,7 @@ class Game:
|
|||
mode: str
|
||||
total_score: int
|
||||
rounds: list[RoundGuess] = field(default_factory=list)
|
||||
created_at: Optional[str] = None
|
||||
created_at: str | None = None
|
||||
finished: bool = False
|
||||
raw_data: dict = field(default_factory=dict)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"""RoundGuess-related data models."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -12,8 +11,8 @@ class RoundGuess:
|
|||
score: int
|
||||
distance_meters: float
|
||||
time_seconds: int
|
||||
lat: Optional[float] = None
|
||||
lng: Optional[float] = None
|
||||
lat: float | None = None
|
||||
lng: float | None = None
|
||||
country: str = ""
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"""UserProfile-related data models."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -16,7 +15,7 @@ class UserProfile:
|
|||
created: str = ""
|
||||
is_verified: bool = False
|
||||
is_pro: bool = False
|
||||
avatar_url: Optional[str] = None
|
||||
avatar_url: str | None = None
|
||||
raw_data: dict = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ Classes:
|
|||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
|
|
@ -119,14 +119,14 @@ class EndpointMonitor:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
registry: Optional[SchemaRegistry] = None,
|
||||
ncfa_cookie: Optional[str] = None,
|
||||
registry: SchemaRegistry | None = None,
|
||||
ncfa_cookie: str | None = None,
|
||||
):
|
||||
self.registry = registry or schema_registry
|
||||
self.ncfa_cookie = ncfa_cookie or settings.DEFAULT_NCFA_COOKIE
|
||||
self.results: list[MonitoringResult] = []
|
||||
self._running = False
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._task: asyncio.Task | None = None
|
||||
|
||||
async def check_endpoint(
|
||||
self,
|
||||
|
|
@ -286,10 +286,8 @@ class EndpointMonitor:
|
|||
self._running = False
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
try:
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self._task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
logger.info("Stopped periodic monitoring")
|
||||
|
||||
async def _monitoring_loop(self) -> None:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ an endpoint, including its availability, response time, and any errors encounter
|
|||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -20,5 +19,5 @@ class MonitoringResult:
|
|||
response_code: int
|
||||
response_time_ms: float
|
||||
schema_changed: bool
|
||||
error_message: Optional[str] = None
|
||||
error_message: str | None = None
|
||||
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ schema information.
|
|||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from .schema_field import SchemaField
|
||||
|
||||
|
|
@ -29,8 +29,8 @@ class EndpointSchema:
|
|||
schema_hash: str = ""
|
||||
response_code: int = 200
|
||||
is_available: bool = True
|
||||
error_message: Optional[str] = None
|
||||
sample_response: Optional[dict] = None
|
||||
error_message: str | None = None
|
||||
sample_response: dict | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary for serialization."""
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ a schema field, including its name, type, and other relevant metadata.
|
|||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -16,6 +16,6 @@ class SchemaField:
|
|||
name: str
|
||||
field_type: str
|
||||
nullable: bool = False
|
||||
nested_schema: Optional[dict] = None
|
||||
nested_schema: dict | None = None
|
||||
example_value: Any = None
|
||||
description: str = ""
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import logging
|
|||
import tempfile
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from .endpoint_schema import EndpointSchema
|
||||
from .schema_detector import SchemaDetector
|
||||
|
|
@ -32,7 +32,7 @@ class SchemaRegistry:
|
|||
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)
|
||||
|
||||
# Try to create the cache directory, fall back to temp if permission denied
|
||||
|
|
@ -187,7 +187,7 @@ class SchemaRegistry:
|
|||
)
|
||||
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."""
|
||||
return self.schemas.get(endpoint)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ dynamic data handling and LLM-friendly output formatting.
|
|||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
from .game_service import GameService
|
||||
from .profile_service import ProfileService
|
||||
|
|
@ -61,8 +60,8 @@ class AnalysisService:
|
|||
def __init__(
|
||||
self,
|
||||
client: GeoGuessrClient,
|
||||
game_service: Optional[GameService] = None,
|
||||
profile_service: Optional[ProfileService] = None,
|
||||
game_service: GameService | None = None,
|
||||
profile_service: ProfileService | None = None,
|
||||
):
|
||||
self.client = client
|
||||
self.game_service = game_service or GameService(client)
|
||||
|
|
@ -155,7 +154,7 @@ class AnalysisService:
|
|||
async def analyze_recent_games(
|
||||
self,
|
||||
count: int = 10,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Analyze recent games and provide statistics summary.
|
||||
|
|
@ -181,7 +180,7 @@ class AnalysisService:
|
|||
|
||||
async def get_performance_summary(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Get a comprehensive performance summary.
|
||||
|
|
@ -246,7 +245,7 @@ class AnalysisService:
|
|||
|
||||
async def get_strategy_recommendations(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Generate strategy recommendations based on performance analysis.
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@ Handles game history, details, and competitive data with dynamic schema support.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from ..api import Endpoints, DynamicResponse, GeoGuessrClient
|
||||
from ..api import DynamicResponse, Endpoints, GeoGuessrClient
|
||||
from ..models import DailyChallenge, Game, SeasonStats
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -22,7 +21,7 @@ class GameService:
|
|||
async def get_game_details(
|
||||
self,
|
||||
game_token: str,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> tuple[Game, DynamicResponse]:
|
||||
"""
|
||||
Get details for a specific game.
|
||||
|
|
@ -45,7 +44,7 @@ class GameService:
|
|||
|
||||
async def get_unfinished_games(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> DynamicResponse:
|
||||
"""Get list of unfinished games."""
|
||||
return await self.client.get(Endpoints.GAMES.GET_UNFINISHED_GAMES, session_token)
|
||||
|
|
@ -53,7 +52,7 @@ class GameService:
|
|||
async def get_streak_game(
|
||||
self,
|
||||
game_token: str,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> DynamicResponse:
|
||||
"""Get streak game details."""
|
||||
endpoint = Endpoints.GAMES.get_streak_game(game_token)
|
||||
|
|
@ -63,7 +62,7 @@ class GameService:
|
|||
self,
|
||||
count: int = 10,
|
||||
page: int = 0,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> DynamicResponse:
|
||||
"""
|
||||
Get the activity feed.
|
||||
|
|
@ -82,7 +81,7 @@ class GameService:
|
|||
async def get_recent_games(
|
||||
self,
|
||||
count: int = 10,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> list[Game]:
|
||||
"""
|
||||
Get recent games from the activity feed.
|
||||
|
|
@ -122,7 +121,7 @@ class GameService:
|
|||
|
||||
async def get_season_stats(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> tuple[SeasonStats, DynamicResponse]:
|
||||
"""Get active season statistics."""
|
||||
response = await self.client.get(
|
||||
|
|
@ -138,7 +137,7 @@ class GameService:
|
|||
async def get_daily_challenge(
|
||||
self,
|
||||
day: str = "today",
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> tuple[DailyChallenge, DynamicResponse]:
|
||||
"""
|
||||
Get daily challenge.
|
||||
|
|
@ -162,7 +161,7 @@ class GameService:
|
|||
async def get_battle_royale(
|
||||
self,
|
||||
game_id: str,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> DynamicResponse:
|
||||
"""Get battle royale game details."""
|
||||
endpoint = Endpoints.GAME_SERVER.get_battle_royale(game_id)
|
||||
|
|
@ -171,7 +170,7 @@ class GameService:
|
|||
async def get_duel(
|
||||
self,
|
||||
duel_id: str,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> DynamicResponse:
|
||||
"""Get duel game details."""
|
||||
endpoint = Endpoints.GAME_SERVER.get_duel(duel_id)
|
||||
|
|
@ -179,7 +178,7 @@ class GameService:
|
|||
|
||||
async def get_tournaments(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> DynamicResponse:
|
||||
"""Get tournament information."""
|
||||
return await self.client.get(Endpoints.GAME_SERVER.GET_TOURNAMENTS, session_token)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@ dynamic schema adaptation.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from ..api import DynamicResponse, GeoGuessrClient, Endpoints
|
||||
from ..api import DynamicResponse, Endpoints, GeoGuessrClient
|
||||
from ..models import Achievement, UserProfile, UserStats
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -22,7 +21,7 @@ class ProfileService:
|
|||
|
||||
async def get_profile(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> tuple[UserProfile, DynamicResponse]:
|
||||
"""
|
||||
Get current user's profile.
|
||||
|
|
@ -40,7 +39,7 @@ class ProfileService:
|
|||
|
||||
async def get_stats(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> tuple[UserStats, DynamicResponse]:
|
||||
"""
|
||||
Get user statistics.
|
||||
|
|
@ -58,7 +57,7 @@ class ProfileService:
|
|||
|
||||
async def get_extended_stats(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> DynamicResponse:
|
||||
"""
|
||||
Get extended statistics.
|
||||
|
|
@ -69,7 +68,7 @@ class ProfileService:
|
|||
|
||||
async def get_achievements(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> tuple[list[Achievement], DynamicResponse]:
|
||||
"""
|
||||
Get user achievements.
|
||||
|
|
@ -96,7 +95,7 @@ class ProfileService:
|
|||
async def get_public_profile(
|
||||
self,
|
||||
user_id: str,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> tuple[UserProfile, DynamicResponse]:
|
||||
"""Get another user's public profile."""
|
||||
endpoint = Endpoints.PROFILES.get_public_profile(user_id)
|
||||
|
|
@ -110,14 +109,14 @@ class ProfileService:
|
|||
|
||||
async def get_user_maps(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> DynamicResponse:
|
||||
"""Get user's custom maps."""
|
||||
return await self.client.get(Endpoints.PROFILES.GET_USER_MAPS, session_token)
|
||||
|
||||
async def get_comprehensive_profile(
|
||||
self,
|
||||
session_token: Optional[str] = None,
|
||||
session_token: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Get a comprehensive profile combining multiple endpoints.
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ def register_game_tools(mcp: FastMCP, game_service: GameService):
|
|||
"summary": {
|
||||
"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,
|
||||
"maps_played": list(set(g.map_name for g in games)),
|
||||
"maps_played": list({g.map_name for g in games}),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from geoguessr_mcp.api import GeoGuessrClient
|
|||
from geoguessr_mcp.api.dynamic_response import DynamicResponse
|
||||
from geoguessr_mcp.auth import SessionManager, UserSession
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||
import httpx
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ class TestAuthenticationFlow:
|
|||
assert "expired_user" not in session_manager._user_sessions
|
||||
|
||||
@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."""
|
||||
# Create manager with default cookie
|
||||
manager_with_default = SessionManager(default_cookie="default_test_cookie")
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ class TestMultiUserSessionManager:
|
|||
@pytest.mark.asyncio
|
||||
async def test_get_user_context_reuses_existing_manager(self, manager):
|
||||
"""Test that getting context for existing API key reuses the same manager."""
|
||||
context1 = await manager.get_user_context("existing_key")
|
||||
context2 = await manager.get_user_context("existing_key")
|
||||
await manager.get_user_context("existing_key")
|
||||
await manager.get_user_context("existing_key")
|
||||
|
||||
# Should use the same manager instance
|
||||
assert manager._user_managers["existing_key"] is manager._user_managers["existing_key"]
|
||||
|
|
@ -36,9 +36,9 @@ class TestMultiUserSessionManager:
|
|||
@pytest.mark.asyncio
|
||||
async def test_multiple_api_keys_get_separate_managers(self, manager):
|
||||
"""Test that different API keys get separate session managers."""
|
||||
context1 = await manager.get_user_context("key1")
|
||||
context2 = await manager.get_user_context("key2")
|
||||
context3 = await manager.get_user_context("key3")
|
||||
await manager.get_user_context("key1")
|
||||
await manager.get_user_context("key2")
|
||||
await manager.get_user_context("key3")
|
||||
|
||||
assert len(manager._user_managers) == 3
|
||||
assert manager._user_managers["key1"] is not manager._user_managers["key2"]
|
||||
|
|
@ -61,7 +61,7 @@ class TestMultiUserSessionManager:
|
|||
assert session is None
|
||||
|
||||
@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."""
|
||||
# This test requires mocking the HTTP client for GeoGuessr API
|
||||
# We'll mark it as a placeholder for now
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
"""Tests for UserContext class."""
|
||||
|
||||
import pytest
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from geoguessr_mcp.auth.session import UserSession
|
||||
from geoguessr_mcp.auth.user_context import UserContext
|
||||
from datetime import datetime, timedelta, UTC
|
||||
|
||||
|
||||
class TestUserContext:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ feeds, recent games, season statistics, and daily challenges.
|
|||
|
||||
import pytest
|
||||
|
||||
from geoguessr_mcp.models import Game, SeasonStats, DailyChallenge
|
||||
from geoguessr_mcp.models import DailyChallenge, Game, SeasonStats
|
||||
|
||||
|
||||
class TestGameService:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ operations.
|
|||
|
||||
import pytest
|
||||
|
||||
from geoguessr_mcp.models import UserProfile, UserStats, Achievement
|
||||
from geoguessr_mcp.models import Achievement, UserProfile, UserStats
|
||||
|
||||
|
||||
class TestProfileService:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue