Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: implement pydantic schemas #80

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app/api/endpoints/clubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@

from fastapi import APIRouter

from app.schemas import clubs as schemas
from app.services.clubs.players import TransfermarktClubPlayers
from app.services.clubs.profile import TransfermarktClubProfile
from app.services.clubs.search import TransfermarktClubSearch

router = APIRouter()


@router.get("/search/{club_name}")
@router.get("/search/{club_name}", response_model=schemas.ClubSearch, response_model_exclude_none=True)
def search_clubs(club_name: str, page_number: Optional[int] = 1) -> dict:
tfmkt = TransfermarktClubSearch(query=club_name, page_number=page_number)
found_clubs = tfmkt.search_clubs()
return found_clubs
Comment on lines +13 to 17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Based on the gathered information, I can now provide the final response:

Schema structure matches the response but needs field name adjustments

The ClubSearch schema and actual response have slight field name mismatches:

  • Response uses pageNumber but schema expects page_number
  • Response uses lastPageNumber but schema expects last_page_number

These inconsistencies need to be fixed either in the schema or the service response to ensure proper validation.

🔗 Analysis chain

Verify schema compatibility with the search response

The response model configuration looks good, but let's verify the schema structure matches the actual response.


Let me gather more information about the actual response structure from TransfermarktClubSearch.


Let me check one more thing to verify the actual structure of the response being returned.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify ClubSearch schema exists and matches response structure

# Check if the schema file exists and contains required fields
ast-grep --pattern 'class ClubSearch' -l 'app/schemas/clubs/search.py'

# Check the schema structure
rg -A 10 'class ClubSearch' 'app/schemas/clubs/search.py'

# Verify fields used in TransfermarktClubSearch
rg -A 5 'def search_clubs' 'app/services/clubs/search.py'

Length of output: 905


Script:

#!/bin/bash
# Check the implementation of TransfermarktClubSearch.search_clubs
ast-grep --pattern 'def search_clubs' --lang python

# Check for any return statements in search_clubs
rg -A 2 'return.*search' app/services/clubs/search.py

# Look for the structure being returned
rg -B 5 -A 15 'class TransfermarktClubSearch' app/services/clubs/search.py

Length of output: 1177


Script:

#!/bin/bash
# Check the full implementation of search_clubs method
rg -B 2 -A 20 'def search_clubs' app/services/clubs/search.py

# Also check if there are any tests that verify the response structure
fd -e py -E 'venv' test_clubs search_clubs | xargs rg -l 'test.*search.*club'

Length of output: 917



@router.get("/{club_id}/profile")
@router.get("/{club_id}/profile", response_model=schemas.ClubProfile, response_model_exclude_defaults=True)
def get_club_profile(club_id: str) -> dict:
tfmkt = TransfermarktClubProfile(club_id=club_id)
club_profile = tfmkt.get_club_profile()
return club_profile


@router.get("/{club_id}/players")
@router.get("/{club_id}/players", response_model=schemas.ClubPlayers, response_model_exclude_defaults=True)
def get_club_players(club_id: str, season_id: Optional[str] = None) -> dict:
tfmkt = TransfermarktClubPlayers(club_id=club_id, season_id=season_id)
club_players = tfmkt.get_club_players()
Expand Down
9 changes: 5 additions & 4 deletions app/api/endpoints/competitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@

from fastapi import APIRouter

from app.schemas import competitions as schemas
from app.services.competitions.clubs import TransfermarktCompetitionClubs
from app.services.competitions.search import TransfermarktCompetitionSearch

router = APIRouter()


@router.get("/search/{competition_name}")
def search_competitions(competition_name: str, page_number: Optional[int] = 1) -> dict:
@router.get("/search/{competition_name}", response_model=schemas.CompetitionSearch)
def search_competitions(competition_name: str, page_number: Optional[int] = 1):
tfmkt = TransfermarktCompetitionSearch(query=competition_name, page_number=page_number)
competitions = tfmkt.search_competitions()
return competitions


@router.get("/{competition_id}/clubs")
def get_competition_clubs(competition_id: str, season_id: Optional[str] = None) -> dict:
@router.get("/{competition_id}/clubs", response_model=schemas.CompetitionClubs)
def get_competition_clubs(competition_id: str, season_id: Optional[str] = None):
tfmkt = TransfermarktCompetitionClubs(competition_id=competition_id, season_id=season_id)
competition_clubs = tfmkt.get_competition_clubs()
return competition_clubs
17 changes: 9 additions & 8 deletions app/api/endpoints/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from fastapi import APIRouter

from app.schemas import players as schemas
from app.services.players.achievements import TransfermarktPlayerAchievements
from app.services.players.injuries import TransfermarktPlayerInjuries
from app.services.players.jersey_numbers import TransfermarktPlayerJerseyNumbers
Expand All @@ -14,56 +15,56 @@
router = APIRouter()


@router.get("/search/{player_name}")
@router.get("/search/{player_name}", response_model=schemas.PlayerSearch, response_model_exclude_none=True)
def search_players(player_name: str, page_number: Optional[int] = 1):
tfmkt = TransfermarktPlayerSearch(query=player_name, page_number=page_number)
found_players = tfmkt.search_players()
return found_players


@router.get("/{player_id}/profile")
@router.get("/{player_id}/profile", response_model=schemas.PlayerProfile, response_model_exclude_none=True)
def get_player_profile(player_id: str):
tfmkt = TransfermarktPlayerProfile(player_id=player_id)
player_info = tfmkt.get_player_profile()
return player_info


@router.get("/{player_id}/market_value")
@router.get("/{player_id}/market_value", response_model=schemas.PlayerMarketValue, response_model_exclude_none=True)
def get_player_market_value(player_id: str):
tfmkt = TransfermarktPlayerMarketValue(player_id=player_id)
player_market_value = tfmkt.get_player_market_value()
return player_market_value


@router.get("/{player_id}/transfers")
@router.get("/{player_id}/transfers", response_model=schemas.PlayerTransfers, response_model_exclude_none=True)
def get_player_transfers(player_id: str):
tfmkt = TransfermarktPlayerTransfers(player_id=player_id)
player_market_value = tfmkt.get_player_transfers()
return player_market_value
Comment on lines +39 to 43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix variable name inconsistency

The variable name player_market_value seems to be a copy-paste error and should match the transfer context.

 @router.get("/{player_id}/transfers", response_model=schemas.PlayerTransfers, response_model_exclude_none=True)
 def get_player_transfers(player_id: str):
     tfmkt = TransfermarktPlayerTransfers(player_id=player_id)
-    player_market_value = tfmkt.get_player_transfers()
-    return player_market_value
+    player_transfers = tfmkt.get_player_transfers()
+    return player_transfers
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@router.get("/{player_id}/transfers", response_model=schemas.PlayerTransfers, response_model_exclude_none=True)
def get_player_transfers(player_id: str):
tfmkt = TransfermarktPlayerTransfers(player_id=player_id)
player_market_value = tfmkt.get_player_transfers()
return player_market_value
@router.get("/{player_id}/transfers", response_model=schemas.PlayerTransfers, response_model_exclude_none=True)
def get_player_transfers(player_id: str):
tfmkt = TransfermarktPlayerTransfers(player_id=player_id)
player_transfers = tfmkt.get_player_transfers()
return player_transfers



@router.get("/{player_id}/jersey_numbers")
@router.get("/{player_id}/jersey_numbers", response_model=schemas.PlayerJerseyNumbers, response_model_exclude_none=True)
def get_player_jersey_numbers(player_id: str):
tfmkt = TransfermarktPlayerJerseyNumbers(player_id=player_id)
player_jerseynumbers = tfmkt.get_player_jersey_numbers()
return player_jerseynumbers


@router.get("/{player_id}/stats")
@router.get("/{player_id}/stats", response_model=schemas.PlayerStats, response_model_exclude_none=True)
def get_player_stats(player_id: str):
tfmkt = TransfermarktPlayerStats(player_id=player_id)
player_stats = tfmkt.get_player_stats()
return player_stats


@router.get("/{player_id}/injuries")
@router.get("/{player_id}/injuries", response_model=schemas.PlayerInjuries, response_model_exclude_none=True)
def get_player_injuries(player_id: str, page_number: Optional[int] = 1):
tfmkt = TransfermarktPlayerInjuries(player_id=player_id, page_number=page_number)
players_injuries = tfmkt.get_player_injuries()
return players_injuries


@router.get("/{player_id}/achievements")
@router.get("/{player_id}/achievements", response_model=schemas.PlayerAchievements, response_model_exclude_none=True)
def get_player_achievements(player_id: str):
tfmkt = TransfermarktPlayerAchievements(player_id=player_id)
player_achievements = tfmkt.get_player_achievements()
Expand Down
Empty file added app/schemas/__init__.py
Empty file.
82 changes: 82 additions & 0 deletions app/schemas/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from datetime import datetime

from dateutil import parser
from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic.alias_generators import to_camel


class IDMixin(BaseModel):
id: str


class AuditMixin(BaseModel):
updated_at: datetime = Field(default_factory=datetime.now)


class TransfermarktBaseModel(BaseModel):
model_config = ConfigDict(alias_generator=to_camel)

@field_validator(
"date_of_birth",
"joined_on",
"contract",
"founded_on",
"members_date",
"from_date",
"until_date",
"date",
"contract_expires",
"joined",
"retired_since",
mode="before",
check_fields=False,
)
def parse_str_to_date(cls, v: str):
try:
return parser.parse(v).date() if v else None
except parser.ParserError:
return None
Comment on lines +35 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix exception handling in parse_str_to_date method

The dateutil.parser.parse function raises a ValueError when parsing fails, not parser.ParserError. Updating the exception handling ensures that parsing errors are correctly caught and handled.

Apply this diff to fix the exception handling:

 def parse_str_to_date(cls, v: str):
     try:
         return parser.parse(v).date() if v else None
-    except parser.ParserError:
+    except ValueError:
         return None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
return parser.parse(v).date() if v else None
except parser.ParserError:
return None
try:
return parser.parse(v).date() if v else None
except ValueError:
return None


@field_validator(
"current_market_value",
"current_transfer_record",
"market_value",
"mean_market_value",
"members",
"total_market_value",
"age",
"goals",
"assists",
"yellow_cards",
"red_cards",
"minutes_played",
mode="before",
check_fields=False,
)
def parse_str_to_int(cls, v: str):
parsed_value = (
(
v.replace("bn", "000000000")
.replace("m", "000000")
.replace("k", "000")
.replace("€", "")
.replace(".", "")
.replace("+", "")
.replace(".", "")
.replace("'", "")
)
if v and any(char.isdigit() for char in v)
else None
)
return int(parsed_value) if parsed_value else None
Comment on lines +57 to +71
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve parsing of decimal values in parse_str_to_int

The current implementation may incorrectly parse values with decimal points (e.g., "10.5m"). To accurately handle decimal values, consider retaining the decimal point and converting the string to a float before converting it to an integer.

Apply this diff to improve the parsing logic:

 def parse_str_to_int(cls, v: str):
     parsed_value = (
         (
             v.replace("bn", "e9")
             .replace("m", "e6")
             .replace("k", "e3")
             .replace("€", "")
             .replace("+", "")
             .replace("'", "")
         )
         if v and any(char.isdigit() for char in v)
         else None
     )
-    return int(parsed_value) if parsed_value else None
+    return int(float(parsed_value)) if parsed_value else None

This approach uses scientific notation to handle decimal values accurately.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
parsed_value = (
(
v.replace("bn", "000000000")
.replace("m", "000000")
.replace("k", "000")
.replace("€", "")
.replace(".", "")
.replace("+", "")
.replace(".", "")
.replace("'", "")
)
if v and any(char.isdigit() for char in v)
else None
)
return int(parsed_value) if parsed_value else None
parsed_value = (
(
v.replace("bn", "e9")
.replace("m", "e6")
.replace("k", "e3")
.replace("€", "")
.replace("+", "")
.replace("'", "")
)
if v and any(char.isdigit() for char in v)
else None
)
return int(float(parsed_value)) if parsed_value else None


@field_validator("height", mode="before", check_fields=False)
def parse_height(cls, v: str):
if not any(char.isdigit() for char in v):
return None
return int(v.replace(",", "").replace("m", ""))
Comment on lines +75 to +77
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle decimal values in parse_height correctly

Converting height strings like "1.78m" directly to int will cause a ValueError due to the decimal point. To accurately represent heights, use float instead of int.

Apply this diff to fix the conversion:

 def parse_height(cls, v: str):
     if not any(char.isdigit() for char in v):
         return None
-    return int(v.replace(",", "").replace("m", ""))
+    return float(v.replace(",", "").replace("m", ""))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not any(char.isdigit() for char in v):
return None
return int(v.replace(",", "").replace("m", ""))
if not any(char.isdigit() for char in v):
return None
return float(v.replace(",", "").replace("m", ""))


@field_validator("days", mode="before", check_fields=False)
def parse_days(cls, v: str):
days = "".join(filter(str.isdigit, v))
return int(days) if days else None
3 changes: 3 additions & 0 deletions app/schemas/clubs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from app.schemas.clubs.players import ClubPlayers as ClubPlayers
from app.schemas.clubs.profile import ClubProfile as ClubProfile
from app.schemas.clubs.search import ClubSearch as ClubSearch
25 changes: 25 additions & 0 deletions app/schemas/clubs/players.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from datetime import date
from typing import Optional

from app.schemas.base import TransfermarktBaseModel


class ClubPlayer(TransfermarktBaseModel):
name: str
position: str
date_of_birth: date
age: int
nationality: list[str]
current_club: Optional[str] = None
height: Optional[int] = None
foot: Optional[str] = None
joined_on: Optional[date] = None
joined: Optional[str] = None
signed_from: Optional[str] = None
contract: Optional[date] = None
market_value: Optional[int] = None
status: Optional[str] = ""
Comment on lines +7 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add field constraints and documentation.

The schema would benefit from:

  1. Field descriptions using docstrings or Field(..., description="...")
  2. Validation constraints (e.g., age ranges, valid positions)
  3. Unit specifications for height and market_value

Consider applying these improvements:

 class ClubPlayer(TransfermarktBaseModel):
-    name: str
-    position: str
-    date_of_birth: date
-    age: int
-    nationality: list[str]
-    current_club: Optional[str] = None
-    height: Optional[int] = None
-    foot: Optional[str] = None
-    joined_on: Optional[date] = None
-    joined: Optional[str] = None
-    signed_from: Optional[str] = None
-    contract: Optional[date] = None
-    market_value: Optional[int] = None
-    status: Optional[str] = ""
+    name: str = Field(..., description="Player's full name")
+    position: str = Field(..., description="Player's position on the field",
+                         regex="^(Goalkeeper|Defender|Midfielder|Forward)$")
+    date_of_birth: date = Field(..., description="Player's date of birth")
+    age: int = Field(..., description="Player's age", ge=0, le=100)
+    nationality: list[str] = Field(..., min_items=1, description="List of player's nationalities")
+    current_club: Optional[str] = Field(None, description="Current club name")
+    height: Optional[int] = Field(None, description="Player's height in centimeters", gt=0, lt=300)
+    foot: Optional[str] = Field(None, description="Player's preferred foot",
+                               regex="^(Left|Right|Both)$")
+    joined_on: Optional[date] = Field(None, description="Date when player joined the club")
+    joined: Optional[str] = Field(None, description="Season or transfer window when player joined")
+    signed_from: Optional[str] = Field(None, description="Previous club name")
+    contract: Optional[date] = Field(None, description="Contract end date")
+    market_value: Optional[int] = Field(None, description="Market value in euros")
+    status: Optional[str] = Field(None, description="Player's current status")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class ClubPlayer(TransfermarktBaseModel):
name: str
position: str
date_of_birth: date
age: int
nationality: list[str]
current_club: Optional[str] = None
height: Optional[int] = None
foot: Optional[str] = None
joined_on: Optional[date] = None
joined: Optional[str] = None
signed_from: Optional[str] = None
contract: Optional[date] = None
market_value: Optional[int] = None
status: Optional[str] = ""
class ClubPlayer(TransfermarktBaseModel):
name: str = Field(..., description="Player's full name")
position: str = Field(..., description="Player's position on the field",
regex="^(Goalkeeper|Defender|Midfielder|Forward)$")
date_of_birth: date = Field(..., description="Player's date of birth")
age: int = Field(..., description="Player's age", ge=0, le=100)
nationality: list[str] = Field(..., min_items=1, description="List of player's nationalities")
current_club: Optional[str] = Field(None, description="Current club name")
height: Optional[int] = Field(None, description="Player's height in centimeters", gt=0, lt=300)
foot: Optional[str] = Field(None, description="Player's preferred foot",
regex="^(Left|Right|Both)$")
joined_on: Optional[date] = Field(None, description="Date when player joined the club")
joined: Optional[str] = Field(None, description="Season or transfer window when player joined")
signed_from: Optional[str] = Field(None, description="Previous club name")
contract: Optional[date] = Field(None, description="Contract end date")
market_value: Optional[int] = Field(None, description="Market value in euros")
status: Optional[str] = Field(None, description="Player's current status")



class ClubPlayers(TransfermarktBaseModel):
players: list[ClubPlayer]
47 changes: 47 additions & 0 deletions app/schemas/clubs/profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from datetime import date
from typing import Optional

from app.schemas.base import TransfermarktBaseModel


class ClubSquad(TransfermarktBaseModel):
size: int
average_age: float
foreigners: int
national_team_players: int

Comment on lines +7 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add field validation and documentation

The schema should include:

  1. Field constraints to ensure non-negative values
  2. Documentation for the class and fields

Consider applying these improvements:

 class ClubSquad(TransfermarktBaseModel):
+    """Represents the squad statistics of a club."""
-    size: int
-    average_age: float
-    foreigners: int
-    national_team_players: int
+    size: int = Field(ge=0, description="Total number of players in the squad")
+    average_age: float = Field(ge=0, description="Average age of all squad players")
+    foreigners: int = Field(ge=0, description="Number of foreign players in the squad")
+    national_team_players: int = Field(ge=0, description="Number of national team players in the squad")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class ClubSquad(TransfermarktBaseModel):
size: int
average_age: float
foreigners: int
national_team_players: int
class ClubSquad(TransfermarktBaseModel):
"""Represents the squad statistics of a club."""
size: int = Field(ge=0, description="Total number of players in the squad")
average_age: float = Field(ge=0, description="Average age of all squad players")
foreigners: int = Field(ge=0, description="Number of foreign players in the squad")
national_team_players: int = Field(ge=0, description="Number of national team players in the squad")


class ClubLeague(TransfermarktBaseModel):
id: str
name: str
country_id: str
country_name: str
tier: str


class ClubProfile(TransfermarktBaseModel):
url: str
name: str
official_name: str
image: str
legal_form: Optional[str] = None
address_line_1: str
address_line_2: str
address_line_3: str
tel: str
fax: str
website: str
founded_on: date
members: Optional[int] = None
members_date: Optional[date] = None
other_sports: Optional[list[str]] = None
colors: Optional[list[str]] = []
stadium_name: str
stadium_seats: int
current_transfer_record: int
current_market_value: int
confederation: Optional[str] = None
fifa_world_ranking: Optional[str] = None
squad: ClubSquad
league: ClubLeague
historical_crests: Optional[list[str]] = []
Comment on lines +22 to +47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix inconsistent defaults and add field validation

There are several areas that need attention:

  1. Inconsistent default values between colors and other_sports lists
  2. Missing validation for URLs, phone numbers, and other formatted fields
  3. Missing documentation

Apply these fixes:

 class ClubProfile(TransfermarktBaseModel):
+    """Represents detailed profile information of a club."""
     url: str
-    name: str
-    official_name: str
-    image: str
+    name: str = Field(min_length=1, description="Common name of the club")
+    official_name: str = Field(min_length=1, description="Legal/official name of the club")
+    image: HttpUrl = Field(description="URL to the club's primary crest/logo")
     legal_form: Optional[str] = None
     address_line_1: str
     address_line_2: str
     address_line_3: str
-    tel: str
-    fax: str
-    website: str
+    tel: str = Field(pattern=r'^\+?[\d\s-]+$', description="Contact telephone number")
+    fax: str = Field(pattern=r'^\+?[\d\s-]+$', description="Contact fax number")
+    website: HttpUrl = Field(description="Official club website URL")
     founded_on: date
     members: Optional[int] = None
     members_date: Optional[date] = None
-    other_sports: Optional[list[str]] = None
-    colors: Optional[list[str]] = []
+    other_sports: Optional[list[str]] = Field(default_factory=list, description="List of other sports departments")
+    colors: Optional[list[str]] = Field(default_factory=list, description="Club's official colors")
     stadium_name: str
-    stadium_seats: int
-    current_transfer_record: int
-    current_market_value: int
+    stadium_seats: int = Field(ge=0, description="Stadium capacity")
+    current_transfer_record: int = Field(description="Highest transfer fee (can be negative for sales)")
+    current_market_value: int = Field(ge=0, description="Current total market value of the club")
     confederation: Optional[str] = None
     fifa_world_ranking: Optional[str] = None
     squad: ClubSquad
     league: ClubLeague
-    historical_crests: Optional[list[str]] = []
+    historical_crests: Optional[list[HttpUrl]] = Field(default_factory=list, description="URLs to historical club crests")

Don't forget to add the necessary import:

 from datetime import date
 from typing import Optional
+from pydantic import Field, HttpUrl
 
 from app.schemas.base import TransfermarktBaseModel
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class ClubProfile(TransfermarktBaseModel):
url: str
name: str
official_name: str
image: str
legal_form: Optional[str] = None
address_line_1: str
address_line_2: str
address_line_3: str
tel: str
fax: str
website: str
founded_on: date
members: Optional[int] = None
members_date: Optional[date] = None
other_sports: Optional[list[str]] = None
colors: Optional[list[str]] = []
stadium_name: str
stadium_seats: int
current_transfer_record: int
current_market_value: int
confederation: Optional[str] = None
fifa_world_ranking: Optional[str] = None
squad: ClubSquad
league: ClubLeague
historical_crests: Optional[list[str]] = []
class ClubProfile(TransfermarktBaseModel):
"""Represents detailed profile information of a club."""
url: str
name: str = Field(min_length=1, description="Common name of the club")
official_name: str = Field(min_length=1, description="Legal/official name of the club")
image: HttpUrl = Field(description="URL to the club's primary crest/logo")
legal_form: Optional[str] = None
address_line_1: str
address_line_2: str
address_line_3: str
tel: str = Field(pattern=r'^\+?[\d\s-]+$', description="Contact telephone number")
fax: str = Field(pattern=r'^\+?[\d\s-]+$', description="Contact fax number")
website: HttpUrl = Field(description="Official club website URL")
founded_on: date
members: Optional[int] = None
members_date: Optional[date] = None
other_sports: Optional[list[str]] = Field(default_factory=list, description="List of other sports departments")
colors: Optional[list[str]] = Field(default_factory=list, description="Club's official colors")
stadium_name: str
stadium_seats: int = Field(ge=0, description="Stadium capacity")
current_transfer_record: int = Field(description="Highest transfer fee (can be negative for sales)")
current_market_value: int = Field(ge=0, description="Current total market value of the club")
confederation: Optional[str] = None
fifa_world_ranking: Optional[str] = None
squad: ClubSquad
league: ClubLeague
historical_crests: Optional[list[HttpUrl]] = Field(default_factory=list, description="URLs to historical club crests")

18 changes: 18 additions & 0 deletions app/schemas/clubs/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Optional

from app.schemas.base import AuditMixin, IDMixin, TransfermarktBaseModel


class ClubSearchResult(TransfermarktBaseModel, IDMixin):
url: str
name: str
country: str
squad: int
market_value: Optional[int] = None


class ClubSearch(TransfermarktBaseModel, AuditMixin):
query: str
page_number: int
last_page_number: int
results: list[ClubSearchResult]
2 changes: 2 additions & 0 deletions app/schemas/competitions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from app.schemas.competitions.clubs import CompetitionClubs as CompetitionClubs
from app.schemas.competitions.search import CompetitionSearch as CompetitionSearch
12 changes: 12 additions & 0 deletions app/schemas/competitions/clubs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from app.schemas.base import AuditMixin, IDMixin, TransfermarktBaseModel


class CompetitionClub(TransfermarktBaseModel, IDMixin):
name: str


class CompetitionClubs(TransfermarktBaseModel, IDMixin, AuditMixin):
id: str
name: str
season_id: str
clubs: list[CompetitionClub]
Comment on lines +8 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove redundant ID field and enhance field validations.

  1. The id field appears redundant as it's already provided by IDMixin
  2. Consider adding constraints for season_id and other fields
 class CompetitionClubs(TransfermarktBaseModel, IDMixin, AuditMixin):
+    """Represents a collection of clubs participating in a competition for a specific season."""
-    id: str
-    name: str
-    season_id: str
-    clubs: list[CompetitionClub]
+    name: str = Field(
+        min_length=1,
+        max_length=100,
+        description="The name of the competition"
+    )
+    season_id: str = Field(
+        regex="^[0-9]{4}$",
+        description="The season year identifier"
+    )
+    clubs: list[CompetitionClub] = Field(
+        min_items=1,
+        description="List of clubs participating in the competition"
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class CompetitionClubs(TransfermarktBaseModel, IDMixin, AuditMixin):
id: str
name: str
season_id: str
clubs: list[CompetitionClub]
class CompetitionClubs(TransfermarktBaseModel, IDMixin, AuditMixin):
"""Represents a collection of clubs participating in a competition for a specific season."""
name: str = Field(
min_length=1,
max_length=100,
description="The name of the competition"
)
season_id: str = Field(
regex="^[0-9]{4}$",
description="The season year identifier"
)
clubs: list[CompetitionClub] = Field(
min_items=1,
description="List of clubs participating in the competition"
)

20 changes: 20 additions & 0 deletions app/schemas/competitions/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Optional

from app.schemas.base import AuditMixin, IDMixin, TransfermarktBaseModel


class CompetitionSearchResult(TransfermarktBaseModel, IDMixin):
name: str
country: str
clubs: int
players: int
total_market_value: Optional[int] = None
mean_market_value: Optional[int] = None
continent: Optional[str] = None


class CompetitionSearch(TransfermarktBaseModel, AuditMixin):
query: str
page_number: int
last_page_number: int
results: list[CompetitionSearchResult]
8 changes: 8 additions & 0 deletions app/schemas/players/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from app.schemas.players.achievements import PlayerAchievements as PlayerAchievements
from app.schemas.players.injuries import PlayerInjuries as PlayerInjuries
from app.schemas.players.jersey_numbers import PlayerJerseyNumbers as PlayerJerseyNumbers
from app.schemas.players.market_value import PlayerMarketValue as PlayerMarketValue
from app.schemas.players.profile import PlayerProfile as PlayerProfile
from app.schemas.players.search import PlayerSearch as PlayerSearch
from app.schemas.players.stats import PlayerStats as PlayerStats
from app.schemas.players.transfers import PlayerTransfers as PlayerTransfers
24 changes: 24 additions & 0 deletions app/schemas/players/achievements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Optional

from app.schemas.base import AuditMixin, IDMixin, TransfermarktBaseModel


class AchievementDetail(TransfermarktBaseModel):
id: Optional[str] = None
name: str


class AchievementDetails(TransfermarktBaseModel):
competition: Optional[AchievementDetail] = None
season: AchievementDetail
club: Optional[AchievementDetail] = None


class PlayerAchievement(TransfermarktBaseModel):
title: str
count: int
details: list[AchievementDetails]


class PlayerAchievements(TransfermarktBaseModel, IDMixin, AuditMixin):
achievements: list[PlayerAchievement]
Loading
Loading