transfermarkt-api 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. transfermarkt_api/__init__.py +23 -0
  2. transfermarkt_api/_version.py +3 -0
  3. transfermarkt_api/cache.py +20 -0
  4. transfermarkt_api/client.py +226 -0
  5. transfermarkt_api/exceptions.py +31 -0
  6. transfermarkt_api/py.typed +0 -0
  7. transfermarkt_api/schemas/__init__.py +24 -0
  8. transfermarkt_api/schemas/achievements.py +25 -0
  9. transfermarkt_api/schemas/base.py +17 -0
  10. transfermarkt_api/schemas/clubs/__init__.py +3 -0
  11. transfermarkt_api/schemas/clubs/listing.py +14 -0
  12. transfermarkt_api/schemas/clubs/players.py +30 -0
  13. transfermarkt_api/schemas/clubs/profile.py +82 -0
  14. transfermarkt_api/schemas/clubs/search.py +19 -0
  15. transfermarkt_api/schemas/coaches/__init__.py +31 -0
  16. transfermarkt_api/schemas/competitions/__init__.py +4 -0
  17. transfermarkt_api/schemas/competitions/clubs.py +15 -0
  18. transfermarkt_api/schemas/competitions/search.py +21 -0
  19. transfermarkt_api/schemas/competitions/seasons.py +15 -0
  20. transfermarkt_api/schemas/competitions/table.py +31 -0
  21. transfermarkt_api/schemas/countries.py +16 -0
  22. transfermarkt_api/schemas/games/__init__.py +83 -0
  23. transfermarkt_api/schemas/players/__init__.py +9 -0
  24. transfermarkt_api/schemas/players/absences.py +22 -0
  25. transfermarkt_api/schemas/players/injuries.py +20 -0
  26. transfermarkt_api/schemas/players/market_value.py +19 -0
  27. transfermarkt_api/schemas/players/matches.py +56 -0
  28. transfermarkt_api/schemas/players/national_career.py +21 -0
  29. transfermarkt_api/schemas/players/profile.py +81 -0
  30. transfermarkt_api/schemas/players/search.py +25 -0
  31. transfermarkt_api/schemas/players/stats.py +24 -0
  32. transfermarkt_api/schemas/players/transfers.py +38 -0
  33. transfermarkt_api/services/__init__.py +0 -0
  34. transfermarkt_api/services/achievements.py +50 -0
  35. transfermarkt_api/services/clubs/__init__.py +0 -0
  36. transfermarkt_api/services/clubs/achievements.py +10 -0
  37. transfermarkt_api/services/clubs/listing.py +22 -0
  38. transfermarkt_api/services/clubs/players.py +88 -0
  39. transfermarkt_api/services/clubs/profile.py +114 -0
  40. transfermarkt_api/services/clubs/search.py +33 -0
  41. transfermarkt_api/services/coaches/__init__.py +0 -0
  42. transfermarkt_api/services/coaches/profile.py +29 -0
  43. transfermarkt_api/services/coaches/search.py +18 -0
  44. transfermarkt_api/services/competitions/__init__.py +0 -0
  45. transfermarkt_api/services/competitions/clubs.py +30 -0
  46. transfermarkt_api/services/competitions/search.py +61 -0
  47. transfermarkt_api/services/competitions/seasons.py +28 -0
  48. transfermarkt_api/services/competitions/table.py +61 -0
  49. transfermarkt_api/services/countries.py +12 -0
  50. transfermarkt_api/services/games/__init__.py +0 -0
  51. transfermarkt_api/services/games/game.py +136 -0
  52. transfermarkt_api/services/players/__init__.py +0 -0
  53. transfermarkt_api/services/players/absences.py +33 -0
  54. transfermarkt_api/services/players/achievements.py +10 -0
  55. transfermarkt_api/services/players/injuries.py +42 -0
  56. transfermarkt_api/services/players/market_value.py +35 -0
  57. transfermarkt_api/services/players/matches.py +107 -0
  58. transfermarkt_api/services/players/national_career.py +31 -0
  59. transfermarkt_api/services/players/profile.py +122 -0
  60. transfermarkt_api/services/players/search.py +44 -0
  61. transfermarkt_api/services/players/stats.py +74 -0
  62. transfermarkt_api/services/players/transfers.py +89 -0
  63. transfermarkt_api/tfmkt/__init__.py +1 -0
  64. transfermarkt_api/tfmkt/client.py +249 -0
  65. transfermarkt_api/tfmkt/concurrency.py +17 -0
  66. transfermarkt_api/tfmkt/freshness.py +38 -0
  67. transfermarkt_api/tfmkt/reference.py +116 -0
  68. transfermarkt_api-5.0.0.dist-info/METADATA +234 -0
  69. transfermarkt_api-5.0.0.dist-info/RECORD +71 -0
  70. transfermarkt_api-5.0.0.dist-info/WHEEL +4 -0
  71. transfermarkt_api-5.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,16 @@
1
+ from typing import Optional
2
+
3
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
4
+
5
+
6
+ class Country(TransfermarktBaseModel):
7
+ id: int
8
+ name: str
9
+ fifa_code: Optional[str] = None
10
+ confederation_id: Optional[int] = None
11
+ flag_url: Optional[str] = None
12
+ is_historical: bool
13
+
14
+
15
+ class Countries(TransfermarktBaseModel, AuditMixin):
16
+ countries: list[Country]
@@ -0,0 +1,83 @@
1
+ from datetime import datetime
2
+ from typing import Literal, Optional
3
+
4
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
5
+
6
+
7
+ class GameEntity(TransfermarktBaseModel):
8
+ id: str
9
+ name: Optional[str] = None
10
+
11
+
12
+ class GamePlayer(TransfermarktBaseModel):
13
+ id: str
14
+ name: Optional[str] = None
15
+ shirt_number: Optional[int] = None
16
+ position: Optional[str] = None
17
+ is_captain: bool = False
18
+
19
+
20
+ class GameTeamStats(TransfermarktBaseModel):
21
+ possession: Optional[float] = None
22
+ shots: Optional[int] = None
23
+ shots_on_target: Optional[int] = None
24
+ shots_off_target: Optional[int] = None
25
+ shots_blocked: Optional[int] = None
26
+ passes: Optional[int] = None
27
+ accurate_passes: Optional[int] = None
28
+ tackles: Optional[int] = None
29
+ tackles_won: Optional[int] = None
30
+ clearances: Optional[int] = None
31
+ saves: Optional[int] = None
32
+ offsides: Optional[int] = None
33
+ corners: Optional[int] = None
34
+ fouls_committed: Optional[int] = None
35
+ fouls_suffered: Optional[int] = None
36
+ yellow_cards: Optional[int] = None
37
+ second_yellow_cards: Optional[int] = None
38
+ red_cards: Optional[int] = None
39
+ penalties_won: Optional[int] = None
40
+ penalties_saved: Optional[int] = None
41
+ own_goals: Optional[int] = None
42
+
43
+
44
+ class GameSide(TransfermarktBaseModel):
45
+ id: str
46
+ name: Optional[str] = None
47
+ score: Optional[int] = None
48
+ formation: Optional[str] = None
49
+ coach: Optional[GameEntity] = None
50
+ stats: Optional[GameTeamStats] = None
51
+ starting_lineup: list[GamePlayer]
52
+ substitutes: list[GamePlayer]
53
+
54
+
55
+ class GameScore(TransfermarktBaseModel):
56
+ home: int
57
+ away: int
58
+
59
+
60
+ class GameEvent(TransfermarktBaseModel):
61
+ type: Literal["goal", "card", "substitution"]
62
+ minute: Optional[int] = None
63
+ added_time: Optional[int] = None
64
+ club_id: Optional[str] = None
65
+ player: Optional[GameEntity] = None
66
+ related_player: Optional[GameEntity] = None
67
+ card: Optional[Literal["yellow", "secondYellow", "red"]] = None
68
+ score: Optional[GameScore] = None
69
+
70
+
71
+ class Game(TransfermarktBaseModel, AuditMixin):
72
+ id: str
73
+ url: Optional[str] = None
74
+ competition: GameEntity
75
+ season_id: Optional[str] = None
76
+ season: Optional[str] = None
77
+ match_day: Optional[int] = None
78
+ date: Optional[datetime] = None
79
+ stadium: Optional[GameEntity] = None
80
+ attendance: Optional[int] = None
81
+ home: GameSide
82
+ away: GameSide
83
+ events: list[GameEvent]
@@ -0,0 +1,9 @@
1
+ from transfermarkt_api.schemas.players.absences import PlayerAbsences as PlayerAbsences
2
+ from transfermarkt_api.schemas.players.injuries import PlayerInjuries as PlayerInjuries
3
+ from transfermarkt_api.schemas.players.market_value import PlayerMarketValue as PlayerMarketValue
4
+ from transfermarkt_api.schemas.players.matches import PlayerMatches as PlayerMatches
5
+ from transfermarkt_api.schemas.players.national_career import PlayerNationalCareer as PlayerNationalCareer
6
+ from transfermarkt_api.schemas.players.profile import PlayerProfile as PlayerProfile
7
+ from transfermarkt_api.schemas.players.search import PlayerSearch as PlayerSearch
8
+ from transfermarkt_api.schemas.players.stats import PlayerStats as PlayerStats
9
+ from transfermarkt_api.schemas.players.transfers import PlayerTransfers as PlayerTransfers
@@ -0,0 +1,22 @@
1
+ from datetime import date
2
+ from typing import Optional
3
+
4
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
5
+
6
+
7
+ class Absence(TransfermarktBaseModel):
8
+ season: Optional[str] = None
9
+ reason: Optional[str] = None
10
+ competition_id: Optional[str] = None
11
+ competition_name: Optional[str] = None
12
+ from_date: Optional[date] = None
13
+ until_date: Optional[date] = None
14
+ days: Optional[int] = None
15
+ games_missed: Optional[int] = None
16
+
17
+
18
+ class PlayerAbsences(TransfermarktBaseModel, AuditMixin):
19
+ id: str
20
+ page_number: int
21
+ last_page_number: int
22
+ absences: list[Absence]
@@ -0,0 +1,20 @@
1
+ from datetime import date
2
+ from typing import Optional
3
+
4
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
5
+
6
+
7
+ class Injury(TransfermarktBaseModel):
8
+ season: Optional[str] = None
9
+ injury: Optional[str] = None
10
+ from_date: Optional[date] = None
11
+ until_date: Optional[date] = None
12
+ days: Optional[int] = None
13
+ games_missed: Optional[int] = None
14
+
15
+
16
+ class PlayerInjuries(TransfermarktBaseModel, AuditMixin):
17
+ id: str
18
+ page_number: int
19
+ last_page_number: int
20
+ injuries: list[Injury]
@@ -0,0 +1,19 @@
1
+ from datetime import date
2
+ from typing import Optional
3
+
4
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
5
+
6
+
7
+ class MarketValueHistory(TransfermarktBaseModel):
8
+ age: Optional[int] = None
9
+ date: date
10
+ club_id: Optional[str] = None
11
+ club_name: Optional[str] = None
12
+ market_value: Optional[int] = None
13
+
14
+
15
+ class PlayerMarketValue(TransfermarktBaseModel, AuditMixin):
16
+ id: str
17
+ market_value: Optional[int] = None
18
+ market_value_history: list[MarketValueHistory]
19
+ ranking: Optional[dict[str, int]] = None
@@ -0,0 +1,56 @@
1
+ from datetime import datetime
2
+ from typing import Literal, Optional
3
+
4
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
5
+
6
+
7
+ class PlayerMatchEntity(TransfermarktBaseModel):
8
+ id: str
9
+ name: Optional[str] = None
10
+
11
+
12
+ class PlayerMatch(TransfermarktBaseModel):
13
+ game_id: str
14
+ date: Optional[datetime] = None
15
+ season: Optional[str] = None
16
+ competition: PlayerMatchEntity
17
+ match_day: Optional[int] = None
18
+ club: PlayerMatchEntity
19
+ opponent: PlayerMatchEntity
20
+ venue: Optional[Literal["home", "away", "neutral"]] = None
21
+ club_goals: Optional[int] = None
22
+ opponent_goals: Optional[int] = None
23
+ participation: Optional[str] = None
24
+ shirt_number: Optional[int] = None
25
+ is_captain: bool = False
26
+ position: Optional[str] = None
27
+ is_starting: bool = False
28
+ substituted_in_minute: Optional[int] = None
29
+ substituted_out_minute: Optional[int] = None
30
+ minutes_played: int = 0
31
+ team_points: Optional[int] = None
32
+ goals: int = 0
33
+ assists: int = 0
34
+ yellow_card: bool = False
35
+ second_yellow_card: bool = False
36
+ red_card: bool = False
37
+ own_goals: Optional[int] = None
38
+ penalty_goals: Optional[int] = None
39
+ penalties_missed: Optional[int] = None
40
+ penalties_saved: Optional[int] = None
41
+ shots: Optional[int] = None
42
+ shots_on_target: Optional[int] = None
43
+ passes: Optional[int] = None
44
+ accurate_passes: Optional[int] = None
45
+ tackles: Optional[int] = None
46
+ tackles_won: Optional[int] = None
47
+ fouls_committed: Optional[int] = None
48
+ fouls_suffered: Optional[int] = None
49
+ offsides: Optional[int] = None
50
+
51
+
52
+ class PlayerMatches(TransfermarktBaseModel, AuditMixin):
53
+ id: str
54
+ page_number: int
55
+ last_page_number: int
56
+ matches: list[PlayerMatch]
@@ -0,0 +1,21 @@
1
+ from datetime import date
2
+ from typing import Optional
3
+
4
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
5
+
6
+
7
+ class NationalTeam(TransfermarktBaseModel):
8
+ id: str
9
+ name: Optional[str] = None
10
+ appearances: Optional[int] = None
11
+ goals: Optional[int] = None
12
+ shirt_number: Optional[int] = None
13
+ is_captain: Optional[bool] = None
14
+ debut: Optional[date] = None
15
+ last_match: Optional[date] = None
16
+ status: Optional[str] = None
17
+
18
+
19
+ class PlayerNationalCareer(TransfermarktBaseModel, AuditMixin):
20
+ id: str
21
+ national_teams: list[NationalTeam]
@@ -0,0 +1,81 @@
1
+ import datetime
2
+ from datetime import date
3
+ from typing import Literal, Optional
4
+
5
+ from pydantic import HttpUrl
6
+
7
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
8
+
9
+
10
+ class PlayerPlaceOfBirth(TransfermarktBaseModel):
11
+ city: Optional[str] = None
12
+ country: Optional[str] = None
13
+
14
+
15
+ class PlayerPosition(TransfermarktBaseModel):
16
+ main: Optional[str] = None
17
+ group: Optional[str] = None
18
+ other: list[str] = []
19
+
20
+
21
+ class PlayerClub(TransfermarktBaseModel):
22
+ id: Optional[str] = None
23
+ name: Optional[str] = None
24
+ joined: Optional[date] = None
25
+ is_captain: Optional[bool] = None
26
+ contract_expires: Optional[date] = None
27
+ last_contract_renewal: Optional[date] = None
28
+ contract_option: Optional[str] = None
29
+ last_club_id: Optional[str] = None
30
+ last_club_name: Optional[str] = None
31
+
32
+
33
+ class PlayerNationalTeam(TransfermarktBaseModel):
34
+ id: str
35
+ name: Optional[str] = None
36
+ shirt_number: Optional[str] = None
37
+ is_captain: Optional[bool] = None
38
+ debut: Optional[date] = None
39
+
40
+
41
+ class PlayerDatedValue(TransfermarktBaseModel):
42
+ value: int
43
+ date: Optional[datetime.date] = None
44
+
45
+
46
+ class PlayerMarketValueDetails(TransfermarktBaseModel):
47
+ last_updated: Optional[date] = None
48
+ trend: Optional[Literal["increased", "decreased", "unchanged"]] = None
49
+ previous: Optional[PlayerDatedValue] = None
50
+ highest: Optional[PlayerDatedValue] = None
51
+
52
+
53
+ class PlayerAgent(TransfermarktBaseModel):
54
+ name: Optional[str] = None
55
+ url: Optional[str] = None
56
+
57
+
58
+ class PlayerProfile(TransfermarktBaseModel, AuditMixin):
59
+ id: str
60
+ url: Optional[HttpUrl] = None
61
+ name: str
62
+ full_name: Optional[str] = None
63
+ name_in_home_country: Optional[str] = None
64
+ image_url: Optional[HttpUrl] = None
65
+ date_of_birth: Optional[date] = None
66
+ date_of_death: Optional[date] = None
67
+ place_of_birth: PlayerPlaceOfBirth
68
+ age: Optional[int] = None
69
+ height: Optional[int] = None
70
+ citizenship: list[str]
71
+ is_retired: bool
72
+ retired_since: Optional[date] = None
73
+ position: PlayerPosition
74
+ foot: Optional[str] = None
75
+ shirt_number: Optional[str] = None
76
+ club: PlayerClub
77
+ national_team: Optional[PlayerNationalTeam] = None
78
+ market_value: Optional[int] = None
79
+ market_value_details: Optional[PlayerMarketValueDetails] = None
80
+ agent: Optional[PlayerAgent] = None
81
+ outfitter: Optional[str] = None
@@ -0,0 +1,25 @@
1
+ from typing import Optional
2
+
3
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
4
+
5
+
6
+ class PlayerSearchClub(TransfermarktBaseModel):
7
+ id: str
8
+ name: Optional[str] = None
9
+
10
+
11
+ class PlayerSearchResult(TransfermarktBaseModel):
12
+ id: str
13
+ name: Optional[str] = None
14
+ position: Optional[str] = None
15
+ club: Optional[PlayerSearchClub] = None
16
+ age: Optional[int] = None
17
+ nationalities: list[str]
18
+ market_value: Optional[int] = None
19
+
20
+
21
+ class PlayerSearch(TransfermarktBaseModel, AuditMixin):
22
+ query: str
23
+ page_number: int
24
+ last_page_number: int
25
+ results: list[PlayerSearchResult]
@@ -0,0 +1,24 @@
1
+ from typing import Optional
2
+
3
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
4
+
5
+
6
+ class PlayerStat(TransfermarktBaseModel):
7
+ competition_id: str
8
+ competition_name: Optional[str] = None
9
+ season_id: str
10
+ club_id: str
11
+ appearances: int
12
+ goals: int
13
+ assists: int
14
+ yellow_cards: int
15
+ second_yellow_cards: int
16
+ red_cards: int
17
+ minutes_played: int
18
+ goals_conceded: int
19
+ clean_sheets: int
20
+
21
+
22
+ class PlayerStats(TransfermarktBaseModel, AuditMixin):
23
+ id: str
24
+ stats: list[PlayerStat]
@@ -0,0 +1,38 @@
1
+ import datetime
2
+ from typing import Literal, Optional
3
+
4
+ from transfermarkt_api.schemas.base import AuditMixin, TransfermarktBaseModel
5
+
6
+
7
+ class PlayerTransferLeague(TransfermarktBaseModel):
8
+ id: str
9
+ name: Optional[str] = None
10
+
11
+
12
+ class PlayerTransferClub(TransfermarktBaseModel):
13
+ id: str
14
+ name: Optional[str] = None
15
+ country_id: Optional[str] = None
16
+ country_name: Optional[str] = None
17
+ league: Optional[PlayerTransferLeague] = None
18
+
19
+
20
+ class PlayerTransfer(TransfermarktBaseModel):
21
+ id: str
22
+ club_from: PlayerTransferClub
23
+ club_to: PlayerTransferClub
24
+ date: Optional[datetime.date] = None
25
+ upcoming: bool
26
+ transfer_type: Optional[Literal["transfer", "freeTransfer", "internal", "loan", "endOfLoan"]] = None
27
+ season: Optional[str] = None
28
+ market_value: Optional[int] = None
29
+ fee: Optional[int] = None
30
+ age: Optional[int] = None
31
+ contract_until: Optional[datetime.date] = None
32
+ remaining_contract_days: Optional[int] = None
33
+
34
+
35
+ class PlayerTransfers(TransfermarktBaseModel, AuditMixin):
36
+ id: str
37
+ transfers: list[PlayerTransfer]
38
+ youth_clubs: list[str] = []
File without changes
@@ -0,0 +1,50 @@
1
+ from transfermarkt_api.tfmkt import TfmktClient
2
+
3
+
4
+ async def group_achievements(tfmkt: TfmktClient, data: dict) -> list[dict]:
5
+ """
6
+ Group upstream achievement records (one per title won) into titles with a count and per-season details.
7
+
8
+ Titles follow upstream's display order; details are most recent season first. Player records carry the club and
9
+ competition of each title; club records carry only the season, so those fields are null.
10
+ """
11
+ records_by_title: dict[int, list[dict]] = {}
12
+ for record in data.get("achievements") or []:
13
+ records_by_title.setdefault(record["id"], []).append(record)
14
+
15
+ aggregated = {entry["id"]: entry for entry in data.get("aggregated") or []}
16
+ titles = sorted(
17
+ records_by_title,
18
+ key=lambda title_id: ((aggregated.get(title_id) or {}).get("order", 10_000), title_id),
19
+ )
20
+
21
+ details = [record.get("details") or {} for records in records_by_title.values() for record in records]
22
+ clubs = await tfmkt.clubs(d.get("clubId") for d in details if d.get("clubId") not in (None, "", "0"))
23
+ competitions = await tfmkt.competitions(d.get("competitionId") for d in details if d.get("competitionId"))
24
+
25
+ def entity(entities: dict, entity_id: str | int | None) -> dict | None:
26
+ """Return `{id, name}` for a referenced club or competition, or None when there is no reference."""
27
+ if entity_id in (None, "", "0"):
28
+ return None
29
+ return {"id": str(entity_id), "name": entities.get(str(entity_id), {}).get("name")}
30
+
31
+ return [
32
+ {
33
+ "title": (
34
+ (aggregated.get(title_id) or {}).get("name") or records_by_title[title_id][0].get("name") or ""
35
+ ).strip(),
36
+ "count": len(records_by_title[title_id]),
37
+ "details": [
38
+ {
39
+ "season": {
40
+ "id": str(record["seasonId"]) if record.get("seasonId") is not None else None,
41
+ "name": (record.get("season") or {}).get("display"),
42
+ },
43
+ "club": entity(clubs, (record.get("details") or {}).get("clubId")),
44
+ "competition": entity(competitions, (record.get("details") or {}).get("competitionId")),
45
+ }
46
+ for record in sorted(records_by_title[title_id], key=lambda r: r.get("seasonId") or 0, reverse=True)
47
+ ],
48
+ }
49
+ for title_id in titles
50
+ ]
File without changes
@@ -0,0 +1,10 @@
1
+ from transfermarkt_api.services.achievements import group_achievements
2
+ from transfermarkt_api.tfmkt import TfmktClient
3
+ from transfermarkt_api.tfmkt.concurrency import gather
4
+
5
+
6
+ async def get_club_achievements(tfmkt: TfmktClient, club_id: str) -> dict:
7
+ """Club titles, including participations, with the seasons they were won."""
8
+ # the achievement route answers 200 with no data for unknown clubs; the club lookup gives a proper 404
9
+ _, data = await gather(tfmkt.club(club_id), tfmkt.club_achievements(club_id))
10
+ return {"id": club_id, "achievements": await group_achievements(tfmkt, data)}
@@ -0,0 +1,22 @@
1
+ from transfermarkt_api.exceptions import NotFound, UpstreamError
2
+ from transfermarkt_api.tfmkt import TfmktClient
3
+ from transfermarkt_api.tfmkt.reference import get_reference
4
+
5
+
6
+ async def list_clubs(tfmkt: TfmktClient, country_id: int) -> dict:
7
+ """List the clubs available in the upstream country directory, preserving its order."""
8
+ reference = await get_reference(tfmkt)
9
+ country_name = reference.country_name(country_id)
10
+ if country_name is None:
11
+ raise NotFound(f"Country not found: {country_id}")
12
+
13
+ directory = await tfmkt.country_clubs(country_id)
14
+ if not isinstance(directory, dict) or not isinstance(directory.get("clubIds"), list):
15
+ raise UpstreamError(f"Unexpected upstream payload for /country/{country_id}/club")
16
+ club_ids = list(dict.fromkeys(str(club_id) for club_id in directory["clubIds"]))
17
+ clubs = await tfmkt.clubs(club_ids)
18
+ return {
19
+ "countryId": country_id,
20
+ "countryName": country_name,
21
+ "clubs": [{"id": club_id, "name": clubs.get(club_id, {}).get("name")} for club_id in club_ids],
22
+ }
@@ -0,0 +1,88 @@
1
+ from typing import Optional
2
+
3
+ from transfermarkt_api.tfmkt import TfmktClient
4
+ from transfermarkt_api.tfmkt.concurrency import gather
5
+ from transfermarkt_api.tfmkt.reference import (
6
+ current_assignment,
7
+ date_of_birth,
8
+ get_reference,
9
+ height_cm,
10
+ market_value_of,
11
+ nationality_ids,
12
+ )
13
+
14
+
15
+ async def get_club_players(tfmkt: TfmktClient, club_id: str, season_id: Optional[str] = None) -> dict:
16
+ """
17
+ Squad of a club for a season (current squad when `season_id` is omitted).
18
+
19
+ Player details come from today's player records. `signedFrom` (and `joinedOn` for past seasons) come from the
20
+ latest transfer into this club up to that season. For a past season, other time-dependent fields (age, contract,
21
+ market value) are null rather than today's values, and `currentClub` names where the player is now.
22
+ Every squad member is returned, even if its player record is missing upstream.
23
+ """
24
+ squad, reference = await gather(tfmkt.club_squad(club_id, season_id), get_reference(tfmkt))
25
+ members = squad.get("squad") or []
26
+ player_ids = [str(m["playerId"]) for m in members]
27
+ is_current = all(m.get("type") == "current" for m in members)
28
+
29
+ players, histories = await gather(
30
+ tfmkt.players(player_ids),
31
+ gather(*(tfmkt.player_transfer_history(i) for i in player_ids)),
32
+ )
33
+ max_season = int(season_id) if season_id and not is_current else None
34
+ arrivals = {i: latest_arrival(history, club_id, max_season) for i, history in zip(player_ids, histories)}
35
+ assignments = {i: current_assignment(players.get(i, {})) for i in player_ids}
36
+ squad_members = {str(member["playerId"]): member for member in members}
37
+ clubs = await tfmkt.clubs(
38
+ [a["transferSource"]["clubId"] for a in arrivals.values() if a]
39
+ + ([] if is_current else [a["clubId"] for a in assignments.values() if a]),
40
+ )
41
+
42
+ result = []
43
+ for player_id in player_ids:
44
+ player = players.get(player_id, {})
45
+ attributes = player.get("attributes") or {}
46
+ assignment = assignments[player_id]
47
+ arrival = arrivals[player_id]
48
+ entry = {
49
+ "id": player_id,
50
+ "name": player.get("name"),
51
+ "imageUrl": player.get("portraitUrl"),
52
+ "shirtNumber": squad_members[player_id].get("shirtNumber"),
53
+ "isCaptain": squad_members[player_id].get("isCaptain"),
54
+ "position": (attributes.get("position") or {}).get("name"),
55
+ "dateOfBirth": date_of_birth(player),
56
+ "nationality": reference.country_names(*nationality_ids(player)),
57
+ "height": height_cm(player),
58
+ "foot": (attributes.get("preferredFoot") or {}).get("name"),
59
+ "signedFrom": clubs.get(str(arrival["transferSource"]["clubId"]), {}).get("name") if arrival else None,
60
+ }
61
+ if is_current:
62
+ in_this_club = assignment is not None and str(assignment.get("clubId")) == str(club_id)
63
+ entry |= {
64
+ "age": (player.get("lifeDates") or {}).get("age"),
65
+ "joinedOn": assignment.get("start") if assignment is not None and in_this_club else None,
66
+ "contract": attributes.get("contractUntil"),
67
+ "marketValue": market_value_of((player.get("marketValueDetails") or {}).get("current")),
68
+ }
69
+ else:
70
+ entry |= {
71
+ "joinedOn": (arrival["details"].get("date") or "")[:10] or None if arrival else None,
72
+ "currentClub": clubs.get(str(assignment["clubId"]), {}).get("name") if assignment else None,
73
+ }
74
+ result.append(entry)
75
+
76
+ return {"id": club_id, "players": result}
77
+
78
+
79
+ def latest_arrival(history: Optional[dict], club_id: str, max_season: Optional[int]) -> Optional[dict]:
80
+ """The most recent completed transfer into `club_id`, optionally limited to seasons up to `max_season`."""
81
+ records = ((history or {}).get("history") or {}).get("terminated") or []
82
+ arrivals = [
83
+ record
84
+ for record in records
85
+ if str(record["transferDestination"]["clubId"]) == str(club_id)
86
+ and (max_season is None or (record["details"].get("seasonId") or 0) <= max_season)
87
+ ]
88
+ return max(arrivals, key=lambda record: record["details"].get("date") or "", default=None)