Rework data models: reorganized and extended models for UserProfile, Game, and others, added new models (RoundGuess, UserStats, Achievement, SeasonStats, DailyChallenge), and updated __init__.py.

This commit is contained in:
Yûki VACHOT 2025-11-29 00:03:31 +01:00
parent cfe4a641a6
commit 6548f11884
11 changed files with 298 additions and 73 deletions

View file

@ -0,0 +1,29 @@
"""Achievement-related data models."""
from dataclasses import dataclass
from typing import Optional
@dataclass
class Achievement:
"""Represents a user achievement."""
id: str
name: str
description: str = ""
unlocked: bool = False
unlocked_at: Optional[str] = None
progress: float = 0.0
icon_url: Optional[str] = None
@classmethod
def from_api_response(cls, data: dict) -> "Achievement":
"""Create Achievement from API response."""
return cls(
id=data.get("id", data.get("achievementId", "")),
name=data.get("name", data.get("title", "")),
description=data.get("description", ""),
unlocked=data.get("unlocked", data.get("achieved", False)),
unlocked_at=data.get("unlockedAt", data.get("achievedAt")),
progress=data.get("progress", 1.0 if data.get("unlocked") else 0.0),
icon_url=data.get("icon", data.get("imageUrl")),
)

View file

@ -0,0 +1,29 @@
"""DailyChallenge-related data models."""
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class DailyChallenge:
"""Daily challenge information."""
token: str
map_name: str = ""
date: str = ""
time_limit: int = 0
completed: bool = False
score: Optional[int] = None
raw_data: dict = field(default_factory=dict)
@classmethod
def from_api_response(cls, data: dict) -> "DailyChallenge":
"""Create DailyChallenge from API response."""
return cls(
token=data.get("token", data.get("challengeToken", "")),
map_name=data.get("map", {}).get("name", "") if isinstance(data.get("map"), dict) else "",
date=data.get("date", data.get("day", "")),
time_limit=data.get("timeLimit", 0),
completed=data.get("completed", data.get("played", False)),
score=data.get("score"),
raw_data=data,
)

View file

@ -0,0 +1,63 @@
"""Game-related data models."""
from dataclasses import dataclass, field
from typing import Optional
from .RoundGuess import RoundGuess
@dataclass
class Game:
"""Represents a complete game."""
token: str
map_name: str
mode: str
total_score: int
rounds: list[RoundGuess] = field(default_factory=list)
created_at: Optional[str] = None
finished: bool = False
raw_data: dict = field(default_factory=dict)
@classmethod
def from_api_response(cls, data: dict) -> "Game":
"""Create Game from API response."""
rounds = []
guesses = data.get("player", {}).get("guesses", [])
if not guesses:
guesses = data.get("rounds", data.get("guesses", []))
for i, guess_data in enumerate(guesses):
rounds.append(RoundGuess.from_api_response(guess_data, i + 1))
map_data = data.get("map", {})
map_name = map_data.get("name", "Unknown") if isinstance(map_data, dict) else str(map_data)
return cls(
token=data.get("token", data.get("gameToken", data.get("id", ""))),
map_name=map_name,
mode=data.get("type", data.get("gameType", data.get("mode", "Unknown"))),
total_score=sum(r.score for r in rounds),
rounds=rounds,
created_at=data.get("created", data.get("createdAt")),
finished=data.get("state") == "finished" or data.get("finished", False),
raw_data=data,
)
def to_dict(self) -> dict:
"""Convert to dictionary."""
return {
"token": self.token,
"map_name": self.map_name,
"mode": self.mode,
"total_score": self.total_score,
"rounds": [
{
"round": r.round_number,
"score": r.score,
"distance_m": round(r.distance_meters, 1),
"time_s": r.time_seconds,
}
for r in self.rounds
],
"finished": self.finished,
}

View file

@ -0,0 +1,29 @@
"""RoundGuess-related data models."""
from dataclasses import dataclass
from typing import Optional
@dataclass
class RoundGuess:
"""Represents a single round guess in a game."""
round_number: int
score: int
distance_meters: float
time_seconds: int
lat: Optional[float] = None
lng: Optional[float] = None
country: str = ""
@classmethod
def from_api_response(cls, data: dict, round_num: int = 0) -> "RoundGuess":
"""Create RoundGuess from API response."""
return cls(
round_number=round_num,
score=data.get("roundScoreInPoints", data.get("score", 0)),
distance_meters=data.get("distanceInMeters", data.get("distance", 0)),
time_seconds=data.get("time", data.get("timeInSeconds", 0)),
lat=data.get("lat", data.get("latitude")),
lng=data.get("lng", data.get("longitude")),
country=data.get("country", ""),
)

View file

@ -0,0 +1,30 @@
"""SeasonStats-related data models."""
from dataclasses import dataclass, field
@dataclass
class SeasonStats:
"""Competitive season statistics."""
season_id: str
season_name: str = ""
rank: int = 0
rating: int = 0
games_played: int = 0
wins: int = 0
division: str = ""
raw_data: dict = field(default_factory=dict)
@classmethod
def from_api_response(cls, data: dict) -> "SeasonStats":
"""Create SeasonStats from API response."""
return cls(
season_id=data.get("seasonId", data.get("id", "")),
season_name=data.get("seasonName", data.get("name", "")),
rank=data.get("rank", data.get("position", 0)),
rating=data.get("rating", data.get("elo", data.get("score", 0))),
games_played=data.get("gamesPlayed", data.get("games", 0)),
wins=data.get("wins", 0),
division=data.get("division", data.get("tier", "")),
raw_data=data,
)

View file

@ -0,0 +1,49 @@
"""UserProfile-related data models."""
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class UserProfile:
"""User profile information."""
id: str
nick: str
email: str = ""
country: str = ""
level: int = 0
created: str = ""
is_verified: bool = False
is_pro: bool = False
avatar_url: Optional[str] = None
raw_data: dict = field(default_factory=dict)
@classmethod
def from_api_response(cls, data: dict) -> "UserProfile":
"""Create UserProfile from API response with dynamic field mapping."""
return cls(
id=data.get("id", ""),
nick=data.get("nick", data.get("username", "")),
email=data.get("email", ""),
country=data.get("country", data.get("countryCode", "")),
level=data.get("level", data.get("xpLevel", 0)),
created=data.get("created", data.get("createdAt", "")),
is_verified=data.get("isVerified", data.get("verified", False)),
is_pro=data.get("isPro", data.get("isProUser", False)),
avatar_url=data.get("pin", {}).get("url") if isinstance(data.get("pin"), dict) else None,
raw_data=data,
)
def to_dict(self) -> dict:
"""Convert to dictionary."""
return {
"id": self.id,
"nick": self.nick,
"email": self.email,
"country": self.country,
"level": self.level,
"created": self.created,
"is_verified": self.is_verified,
"is_pro": self.is_pro,
"avatar_url": self.avatar_url,
}

View file

@ -0,0 +1,50 @@
"""UserStats-related data models."""
from dataclasses import dataclass, field
@dataclass
class UserStats:
"""User statistics from various endpoints."""
games_played: int = 0
rounds_played: int = 0
total_score: int = 0
average_score: float = 0.0
perfect_games: int = 0
win_rate: float = 0.0
streak_best: int = 0
explorer_progress: float = 0.0
raw_data: dict = field(default_factory=dict)
@classmethod
def from_api_response(cls, data: dict) -> "UserStats":
"""Create UserStats from API response with dynamic field mapping."""
# Handle different response formats
games = data.get("gamesPlayed", data.get("totalGames", data.get("games", 0)))
rounds = data.get("roundsPlayed", data.get("totalRounds", 0))
score = data.get("totalScore", data.get("score", 0))
return cls(
games_played=games,
rounds_played=rounds,
total_score=score,
average_score=data.get("averageScore", score / games if games > 0 else 0),
perfect_games=data.get("perfectGames", data.get("fiveKs", 0)),
win_rate=data.get("winRate", data.get("winPercentage", 0.0)),
streak_best=data.get("bestStreak", data.get("countryStreakBest", 0)),
explorer_progress=data.get("explorerProgress", 0.0),
raw_data=data,
)
def to_dict(self) -> dict:
"""Convert to dictionary."""
return {
"games_played": self.games_played,
"rounds_played": self.rounds_played,
"total_score": self.total_score,
"average_score": round(self.average_score, 2),
"perfect_games": self.perfect_games,
"win_rate": round(self.win_rate, 4),
"streak_best": self.streak_best,
"explorer_progress": round(self.explorer_progress, 2),
}

View file

@ -0,0 +1,19 @@
"""Data models for GeoGuessr."""
from .UserProfile import UserProfile
from .UserStats import UserStats
from .RoundGuess import RoundGuess
from Game import Game
from Achievement import Achievement
from SeasonStats import SeasonStats
from DailyChallenge import DailyChallenge
__all__ = [
"UserProfile",
"UserStats",
"RoundGuess",
"Game",
"Achievement",
"SeasonStats",
"DailyChallenge",
]

View file

@ -1,44 +0,0 @@
"""Game-related data models."""
from dataclasses import dataclass
from typing import List
@dataclass
class RoundGuess:
"""Represents a single round guess."""
score: int
distance_meters: int
time_seconds: int
@dataclass
class Game:
"""Represents a complete game."""
token: str
map_name: str
mode: str
total_score: int
rounds: List[RoundGuess]
@classmethod
def from_api_response(cls, data: dict) -> "Game":
"""Create Game from API response."""
rounds = [
RoundGuess(
score=r.get("roundScoreInPoints", 0),
distance_meters=r.get("distanceInMeters", 0),
time_seconds=r.get("time", 0),
)
for r in data.get("player", {}).get("guesses", [])
]
return cls(
token=data["token"],
map_name=data.get("map", {}).get("name", "Unknown"),
mode=data.get("type", "Unknown"),
total_score=sum(r.score for r in rounds),
rounds=rounds,
)

View file

@ -1,29 +0,0 @@
"""Profile-related data models."""
from dataclasses import dataclass
@dataclass
class UserProfile:
"""User profile information."""
id: str
nick: str
email: str
country: str
level: int
created: str
is_verified: bool
@classmethod
def from_api_response(cls, data: dict) -> "UserProfile":
"""Create UserProfile from API response."""
return cls(
id=data["id"],
nick=data["nick"],
email=data.get("email", ""),
country=data.get("country", ""),
level=data.get("level", 0),
created=data.get("created", ""),
is_verified=data.get("isVerified", False),
)