Coverage for models/clubs.py: 63.33%
80 statements
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-13 11:37 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-13 11:37 +0000
1"""
2Pydantic models for clubs and teams data structure.
3Used for parsing and validating clubs.json and API requests.
4"""
6import re
7import unicodedata
9from pydantic import BaseModel, Field, field_validator
12def club_name_to_slug(name: str) -> str:
13 """Convert club name to filename slug: 'Inter Miami CF' -> 'inter-miami-cf'"""
14 name = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii")
15 name = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
16 return name
19class TeamData(BaseModel):
20 """Model for team data in clubs.json."""
22 team_name: str
23 league: str
24 division: str | None = None
25 conference: str | None = None
26 age_groups: list[str] | None = Field(default=None)
28 @field_validator("league")
29 @classmethod
30 def validate_league(cls, v: str) -> str:
31 """Validate league is a known value."""
32 valid_leagues = {"Homegrown", "Academy"}
33 if v not in valid_leagues:
34 raise ValueError(f"League must be one of {valid_leagues}, got {v}")
35 return v
37 @property
38 def division_or_conference(self) -> str | None:
39 """Get division or conference (whichever is set)."""
40 return self.division or self.conference
42 @property
43 def is_complete(self) -> bool:
44 """Check if team has all required data (division/conference and age_groups)."""
45 return bool(self.division_or_conference and self.age_groups)
48class ClubData(BaseModel):
49 """Model for club data in clubs.json."""
51 club_name: str
52 location: str
53 website: str = ""
54 logo_url: str = ""
55 primary_color: str = ""
56 secondary_color: str = ""
57 instagram: str = ""
58 teams: list[TeamData]
59 is_pro_academy: bool = Field(
60 default=False,
61 description="True if this club is a professional academy (e.g., MLS Pro Academy). "
62 "All teams in the club inherit this designation.",
63 )
65 @field_validator("website")
66 @classmethod
67 def validate_website(cls, v: str) -> str:
68 """Validate website is a URL or empty."""
69 if v and not v.startswith(("http://", "https://")):
70 raise ValueError(f"Website must be a valid URL or empty, got {v}")
71 return v
73 @field_validator("logo_url")
74 @classmethod
75 def validate_logo_url(cls, v: str) -> str:
76 """Validate logo_url is a URL or empty."""
77 if v and not v.startswith(("http://", "https://")):
78 raise ValueError(f"logo_url must be a valid URL or empty, got {v}")
79 return v
81 @field_validator("primary_color", "secondary_color")
82 @classmethod
83 def validate_color(cls, v: str) -> str:
84 """Validate color is a hex code or empty."""
85 import re
87 if v and not re.match(r"^#[0-9a-fA-F]{3,8}$", v):
88 raise ValueError(f"Color must be a hex code (e.g., #FF0000) or empty, got {v}")
89 return v
91 @field_validator("instagram")
92 @classmethod
93 def validate_instagram(cls, v: str) -> str:
94 """Validate instagram is a URL or empty."""
95 if v and not v.startswith(("http://", "https://")):
96 raise ValueError(f"Instagram must be a valid URL or empty, got {v}")
97 return v
100class Club(BaseModel):
101 """Model for creating a new club via API."""
103 name: str
104 city: str
105 website: str | None = None
106 description: str | None = None
107 logo_url: str | None = None
108 primary_color: str | None = None
109 secondary_color: str | None = None
112class ClubWithTeams(BaseModel):
113 """Model for returning a club with its teams."""
115 id: int
116 name: str
117 city: str
118 website: str | None = None
119 description: str | None = None
120 is_active: bool = True
121 teams: list[dict] = [] # Teams belonging to this club
122 team_count: int = 0 # Number of teams in this club
125def load_clubs_from_json(clubs_json: list[dict]) -> list[ClubData]:
126 """
127 Parse clubs.json data into Pydantic models.
129 Args:
130 clubs_json: Raw JSON data (list of dicts)
132 Returns:
133 List of validated ClubData models
135 Raises:
136 ValidationError: If data doesn't match expected structure
137 """
138 return [ClubData(**club) for club in clubs_json]