Coverage for models/clubs.py: 63.33%

80 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2026-04-12 14:12 +0000

1""" 

2Pydantic models for clubs and teams data structure. 

3Used for parsing and validating clubs.json and API requests. 

4""" 

5 

6import re 

7import unicodedata 

8 

9from pydantic import BaseModel, Field, field_validator 

10 

11 

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 

17 

18 

19class TeamData(BaseModel): 

20 """Model for team data in clubs.json.""" 

21 

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) 

27 

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 

36 

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 

41 

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) 

46 

47 

48class ClubData(BaseModel): 

49 """Model for club data in clubs.json.""" 

50 

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 ) 

64 

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 

72 

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 

80 

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 

86 

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 

90 

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 

98 

99 

100class Club(BaseModel): 

101 """Model for creating a new club via API.""" 

102 

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 

110 

111 

112class ClubWithTeams(BaseModel): 

113 """Model for returning a club with its teams.""" 

114 

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 

123 

124 

125def load_clubs_from_json(clubs_json: list[dict]) -> list[ClubData]: 

126 """ 

127 Parse clubs.json data into Pydantic models. 

128 

129 Args: 

130 clubs_json: Raw JSON data (list of dicts) 

131 

132 Returns: 

133 List of validated ClubData models 

134 

135 Raises: 

136 ValidationError: If data doesn't match expected structure 

137 """ 

138 return [ClubData(**club) for club in clubs_json]