datafc 2.1.0__tar.gz → 2.3.0__tar.gz

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 (57) hide show
  1. {datafc-2.1.0 → datafc-2.3.0}/PKG-INFO +41 -8
  2. {datafc-2.1.0 → datafc-2.3.0}/README.md +40 -7
  3. {datafc-2.1.0 → datafc-2.3.0}/datafc/__init__.py +1 -1
  4. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/aio.py +30 -5
  5. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_match_data.py +25 -3
  6. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_past_matches_data.py +2 -2
  7. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_standings_data.py +6 -3
  8. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_team_match_history_data.py +108 -108
  9. {datafc-2.1.0 → datafc-2.3.0}/datafc/utils/_config.py +20 -0
  10. {datafc-2.1.0 → datafc-2.3.0}/datafc/utils/_validate.py +8 -7
  11. {datafc-2.1.0 → datafc-2.3.0}/datafc.egg-info/PKG-INFO +41 -8
  12. {datafc-2.1.0 → datafc-2.3.0}/pyproject.toml +1 -1
  13. {datafc-2.1.0 → datafc-2.3.0}/LICENSE +0 -0
  14. {datafc-2.1.0 → datafc-2.3.0}/datafc/exceptions.py +0 -0
  15. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/__init__.py +0 -0
  16. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/_parsers.py +0 -0
  17. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_average_positions_data.py +0 -0
  18. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_coordinates_data.py +0 -0
  19. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_goal_networks_data.py +0 -0
  20. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_incidents_data.py +0 -0
  21. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_league_player_stats_data.py +0 -0
  22. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_lineups_data.py +0 -0
  23. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_match_details_data.py +0 -0
  24. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_match_h2h_data.py +0 -0
  25. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_match_odds_data.py +0 -0
  26. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_match_stats_data.py +0 -0
  27. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_momentum_data.py +0 -0
  28. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_player_career_stats_data.py +0 -0
  29. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_player_data.py +0 -0
  30. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_player_match_log_data.py +0 -0
  31. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_player_national_team_data.py +0 -0
  32. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_player_stats_data.py +0 -0
  33. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_player_transfers_data.py +0 -0
  34. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_pregame_form_data.py +0 -0
  35. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_referee_stats_data.py +0 -0
  36. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_search_data.py +0 -0
  37. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_season_rounds_data.py +0 -0
  38. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_seasons_data.py +0 -0
  39. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_shots_data.py +0 -0
  40. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_squad_data.py +0 -0
  41. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_substitutions_data.py +0 -0
  42. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_team_data.py +0 -0
  43. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_team_stats_data.py +0 -0
  44. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_team_transfers_data.py +0 -0
  45. {datafc-2.1.0 → datafc-2.3.0}/datafc/sofascore/fetch_upcoming_matches_data.py +0 -0
  46. {datafc-2.1.0 → datafc-2.3.0}/datafc/utils/__init__.py +0 -0
  47. {datafc-2.1.0 → datafc-2.3.0}/datafc/utils/_async_client.py +0 -0
  48. {datafc-2.1.0 → datafc-2.3.0}/datafc/utils/_cache.py +0 -0
  49. {datafc-2.1.0 → datafc-2.3.0}/datafc/utils/_client.py +0 -0
  50. {datafc-2.1.0 → datafc-2.3.0}/datafc/utils/_helpers.py +0 -0
  51. {datafc-2.1.0 → datafc-2.3.0}/datafc/utils/_save_files.py +0 -0
  52. {datafc-2.1.0 → datafc-2.3.0}/datafc/utils/_tournament_info.py +0 -0
  53. {datafc-2.1.0 → datafc-2.3.0}/datafc.egg-info/SOURCES.txt +0 -0
  54. {datafc-2.1.0 → datafc-2.3.0}/datafc.egg-info/dependency_links.txt +0 -0
  55. {datafc-2.1.0 → datafc-2.3.0}/datafc.egg-info/requires.txt +0 -0
  56. {datafc-2.1.0 → datafc-2.3.0}/datafc.egg-info/top_level.txt +0 -0
  57. {datafc-2.1.0 → datafc-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datafc
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: Fetch, process, and export structured football data.
5
5
  Author-email: Uraz Akgül <urazdev@gmail.com>
6
6
  License: MIT
@@ -24,7 +24,7 @@ Requires-Dist: pytest>=8.0; extra == "dev"
24
24
  Requires-Dist: pytest-mock>=3.12; extra == "dev"
25
25
  Dynamic: license-file
26
26
 
27
- # datafc v2.1.0
27
+ # datafc v2.3.0
28
28
 
29
29
  ## Overview
30
30
 
@@ -530,15 +530,34 @@ ucl_df = match_data(
530
530
  tournament_type="uefa",
531
531
  tournament_stage="round_of_16",
532
532
  )
533
+
534
+ # World Cup knockout stages — week_number not needed:
535
+ wc_df = match_data(
536
+ tournament_id=16,
537
+ season_id=58210,
538
+ tournament_type="world_cup",
539
+ tournament_stage="round_of_16",
540
+ )
541
+
542
+ # World Cup group stage — week_number required:
543
+ wc_group_df = match_data(
544
+ tournament_id=16,
545
+ season_id=58210,
546
+ week_number=1,
547
+ tournament_type="world_cup",
548
+ tournament_stage="group_stage_week",
549
+ )
533
550
  ```
534
551
 
535
552
  Parameters:
536
553
 
537
554
  - `tournament_id` (int)
538
555
  - `season_id` (int)
539
- - `week_number` (int)
540
- - `tournament_type` (str, optional): `"uefa"` for UEFA competitions. `None` assumes a domestic league.
541
- - `tournament_stage` (str, optional): Required when `tournament_type="uefa"`. Options: `preliminary_semifinals`, `preliminary_final`, `qualification_round`, `qualification_playoff`, `group_stage_week`, `playoff_round`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
556
+ - `week_number` (int, optional): Required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Not needed for other `world_cup` stages.
557
+ - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup. `None` assumes a domestic league.
558
+ - `tournament_stage` (str, optional): Required when `tournament_type` is set.
559
+ - `"uefa"` options: `preliminary_semifinals`, `preliminary_final`, `qualification_round`, `qualification_playoff`, `group_stage_week`, `playoff_round`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
560
+ - `"world_cup"` options: `group_stage_week`, `round_of_32`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
542
561
 
543
562
  Columns: `country`, `tournament`, `season`, `week`, `game_id`, `home_team`, `home_team_id`, `away_team`, `away_team_id`, `injury_time_1`, `injury_time_2`, `start_timestamp`, `status`, `home_score_current`, `home_score_display`, `home_score_period1`, `home_score_period2`, `home_score_normaltime`, `away_score_current`, `away_score_display`, `away_score_period1`, `away_score_period2`, `away_score_normaltime`.
544
563
 
@@ -775,9 +794,9 @@ Parameters:
775
794
 
776
795
  - `tournament_id` (int)
777
796
  - `season_id` (int)
778
- - `week_number` (int)
779
- - `tournament_type` (str, optional): `"uefa"` for UEFA competitions.
780
- - `tournament_stage` (str, optional): Required when `tournament_type="uefa"`. Same options as `match_data`.
797
+ - `week_number` (int, optional): Required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Not needed for other `world_cup` stages.
798
+ - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup.
799
+ - `tournament_stage` (str, optional): Required when `tournament_type` is set. Same options as `match_data`.
781
800
 
782
801
  Same columns as `match_data`.
783
802
 
@@ -897,6 +916,20 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
897
916
 
898
917
  ## Changelog
899
918
 
919
+ ### v2.3.0
920
+
921
+ - **Fixed `match_data` for World Cup knockout stages across all seasons.** Round numbers are now resolved automatically from the API instead of being hardcoded, so older seasons work correctly.
922
+ - **Fixed `standings_data` for tournament-format competitions.** Calling this function for World Cup, Euro, or similar tournaments no longer raises an error. Only the available categories are returned.
923
+
924
+ ---
925
+
926
+ ### v2.2.0
927
+
928
+ - Added `tournament_type="world_cup"` support to `match_data` and `past_matches_data` for FIFA World Cup competitions. Knockout stage rounds are fixed internally; only `group_stage_week` requires `week_number`.
929
+ - `week_number` is now optional (`None` by default). It is required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Omitting it when required raises `InvalidParameterError`.
930
+
931
+ ---
932
+
900
933
  ### v2.1.0
901
934
 
902
935
  - Added `team_match_history_data`: fetches the complete match history for a single team across all competitions using `team_id` directly (no standings dependency).
@@ -1,4 +1,4 @@
1
- # datafc v2.1.0
1
+ # datafc v2.3.0
2
2
 
3
3
  ## Overview
4
4
 
@@ -504,15 +504,34 @@ ucl_df = match_data(
504
504
  tournament_type="uefa",
505
505
  tournament_stage="round_of_16",
506
506
  )
507
+
508
+ # World Cup knockout stages — week_number not needed:
509
+ wc_df = match_data(
510
+ tournament_id=16,
511
+ season_id=58210,
512
+ tournament_type="world_cup",
513
+ tournament_stage="round_of_16",
514
+ )
515
+
516
+ # World Cup group stage — week_number required:
517
+ wc_group_df = match_data(
518
+ tournament_id=16,
519
+ season_id=58210,
520
+ week_number=1,
521
+ tournament_type="world_cup",
522
+ tournament_stage="group_stage_week",
523
+ )
507
524
  ```
508
525
 
509
526
  Parameters:
510
527
 
511
528
  - `tournament_id` (int)
512
529
  - `season_id` (int)
513
- - `week_number` (int)
514
- - `tournament_type` (str, optional): `"uefa"` for UEFA competitions. `None` assumes a domestic league.
515
- - `tournament_stage` (str, optional): Required when `tournament_type="uefa"`. Options: `preliminary_semifinals`, `preliminary_final`, `qualification_round`, `qualification_playoff`, `group_stage_week`, `playoff_round`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
530
+ - `week_number` (int, optional): Required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Not needed for other `world_cup` stages.
531
+ - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup. `None` assumes a domestic league.
532
+ - `tournament_stage` (str, optional): Required when `tournament_type` is set.
533
+ - `"uefa"` options: `preliminary_semifinals`, `preliminary_final`, `qualification_round`, `qualification_playoff`, `group_stage_week`, `playoff_round`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
534
+ - `"world_cup"` options: `group_stage_week`, `round_of_32`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
516
535
 
517
536
  Columns: `country`, `tournament`, `season`, `week`, `game_id`, `home_team`, `home_team_id`, `away_team`, `away_team_id`, `injury_time_1`, `injury_time_2`, `start_timestamp`, `status`, `home_score_current`, `home_score_display`, `home_score_period1`, `home_score_period2`, `home_score_normaltime`, `away_score_current`, `away_score_display`, `away_score_period1`, `away_score_period2`, `away_score_normaltime`.
518
537
 
@@ -749,9 +768,9 @@ Parameters:
749
768
 
750
769
  - `tournament_id` (int)
751
770
  - `season_id` (int)
752
- - `week_number` (int)
753
- - `tournament_type` (str, optional): `"uefa"` for UEFA competitions.
754
- - `tournament_stage` (str, optional): Required when `tournament_type="uefa"`. Same options as `match_data`.
771
+ - `week_number` (int, optional): Required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Not needed for other `world_cup` stages.
772
+ - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup.
773
+ - `tournament_stage` (str, optional): Required when `tournament_type` is set. Same options as `match_data`.
755
774
 
756
775
  Same columns as `match_data`.
757
776
 
@@ -871,6 +890,20 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
871
890
 
872
891
  ## Changelog
873
892
 
893
+ ### v2.3.0
894
+
895
+ - **Fixed `match_data` for World Cup knockout stages across all seasons.** Round numbers are now resolved automatically from the API instead of being hardcoded, so older seasons work correctly.
896
+ - **Fixed `standings_data` for tournament-format competitions.** Calling this function for World Cup, Euro, or similar tournaments no longer raises an error. Only the available categories are returned.
897
+
898
+ ---
899
+
900
+ ### v2.2.0
901
+
902
+ - Added `tournament_type="world_cup"` support to `match_data` and `past_matches_data` for FIFA World Cup competitions. Knockout stage rounds are fixed internally; only `group_stage_week` requires `week_number`.
903
+ - `week_number` is now optional (`None` by default). It is required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Omitting it when required raises `InvalidParameterError`.
904
+
905
+ ---
906
+
874
907
  ### v2.1.0
875
908
 
876
909
  - Added `team_match_history_data`: fetches the complete match history for a single team across all competitions using `team_id` directly (no standings dependency).
@@ -1,4 +1,4 @@
1
- __version__ = "2.1.0"
1
+ __version__ = "2.3.0"
2
2
 
3
3
  from .sofascore import *
4
4
  from .exceptions import (
@@ -35,7 +35,7 @@ import pandas as pd
35
35
 
36
36
  from datafc.utils._async_client import AsyncSofascoreClient
37
37
  from datafc.utils._cache import DiskCache
38
- from datafc.utils._config import API_URLS, WWW_URLS
38
+ from datafc.utils._config import API_URLS, WWW_URLS, WORLD_CUP_KNOCKOUT_SLUGS
39
39
  from datafc.utils._validate import validate_source, validate_df, build_tournament_url
40
40
  from datafc.utils._save_files import save_json, save_excel
41
41
  from datafc.utils._tournament_info import resolve_tournament_season
@@ -77,7 +77,7 @@ logger = logging.getLogger(__name__)
77
77
  async def match_data(
78
78
  tournament_id: int,
79
79
  season_id: int,
80
- week_number: int,
80
+ week_number: Optional[int] = None,
81
81
  tournament_type: Optional[str] = None,
82
82
  tournament_stage: Optional[str] = None,
83
83
  data_source: str = "sofascore",
@@ -89,6 +89,28 @@ async def match_data(
89
89
  ) -> pd.DataFrame:
90
90
  """Async version of match_data(). See sync docstring for full parameter docs."""
91
91
  validate_source(data_source)
92
+
93
+ if (
94
+ tournament_type == "world_cup"
95
+ and tournament_stage in WORLD_CUP_KNOCKOUT_SLUGS
96
+ and week_number is None
97
+ ):
98
+ target_slug = WORLD_CUP_KNOCKOUT_SLUGS[tournament_stage]
99
+ rounds_url = (
100
+ f"{API_URLS[data_source]}/api/v1/unique-tournament/{tournament_id}"
101
+ f"/season/{season_id}/rounds"
102
+ )
103
+ async with AsyncSofascoreClient(rate_limit=rate_limit, cache=cache) as client:
104
+ rounds_data = await client.get(rounds_url)
105
+ rounds = rounds_data.get("rounds") or rounds_data.get("currentRounds") or []
106
+ matched = next((r for r in rounds if r.get("slug") == target_slug), None)
107
+ if matched is None:
108
+ raise DataNotAvailableError(
109
+ f"Could not find round with slug '{target_slug}' for "
110
+ f"tournament_id={tournament_id}, season_id={season_id}."
111
+ )
112
+ week_number = matched["round"]
113
+
92
114
  url = build_tournament_url(
93
115
  API_URLS[data_source], tournament_id, season_id, week_number,
94
116
  tournament_type, tournament_stage,
@@ -649,8 +671,11 @@ async def standings_data(
649
671
  f"{API_URLS[data_source]}/api/v1/unique-tournament/{tournament_id}"
650
672
  f"/season/{season_id}/standings/{category}"
651
673
  )
652
- data = await client.get(url)
653
- return parse_standings_rows(data, category, tournament_id, season_id)
674
+ try:
675
+ data = await client.get(url)
676
+ return parse_standings_rows(data, category, tournament_id, season_id)
677
+ except APIError:
678
+ return []
654
679
 
655
680
  async with AsyncSofascoreClient(rate_limit=rate_limit, cache=cache) as client:
656
681
  batches = await asyncio.gather(*[_fetch(client, cat) for cat in ("total", "home", "away")])
@@ -1173,7 +1198,7 @@ async def goal_networks_data(
1173
1198
  async def past_matches_data(
1174
1199
  tournament_id: int,
1175
1200
  season_id: int,
1176
- week_number: int,
1201
+ week_number: Optional[int] = None,
1177
1202
  tournament_type: Optional[str] = None,
1178
1203
  tournament_stage: Optional[str] = None,
1179
1204
  data_source: str = "sofascore",
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Optional
2
2
  import pandas as pd
3
3
  from datafc.utils._client import SofascoreClient
4
4
  from datafc.utils._save_files import save_json, save_excel
5
- from datafc.utils._config import API_URLS
5
+ from datafc.utils._config import API_URLS, WORLD_CUP_KNOCKOUT_SLUGS
6
6
  from datafc.utils._validate import validate_source, build_tournament_url
7
7
  from datafc.sofascore._parsers import parse_match_events
8
8
  from datafc.exceptions import DataNotAvailableError
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
14
14
  def match_data(
15
15
  tournament_id: int,
16
16
  season_id: int,
17
- week_number: int,
17
+ week_number: Optional[int] = None,
18
18
  tournament_type: Optional[str] = None,
19
19
  tournament_stage: Optional[str] = None,
20
20
  data_source: str = "sofascore",
@@ -31,7 +31,7 @@ def match_data(
31
31
  tournament_id: The unique identifier for the tournament.
32
32
  season_id: The unique identifier for the season.
33
33
  week_number: The matchweek number within the season.
34
- tournament_type: The tournament type ('uefa'). If None, assumes league format.
34
+ tournament_type: The tournament type ('uefa', 'world_cup'). If None, assumes league format.
35
35
  tournament_stage: The specific stage of the tournament (e.g., 'group_stage_week', 'round_of_16').
36
36
  data_source: The data source ('sofavpn' or 'sofascore'). Defaults to 'sofascore'.
37
37
  rate_limit: Maximum requests per second. Defaults to 2.0.
@@ -49,6 +49,28 @@ def match_data(
49
49
  APIError: On HTTP errors from the Sofascore API.
50
50
  """
51
51
  validate_source(data_source)
52
+
53
+ if (
54
+ tournament_type == "world_cup"
55
+ and tournament_stage in WORLD_CUP_KNOCKOUT_SLUGS
56
+ and week_number is None
57
+ ):
58
+ target_slug = WORLD_CUP_KNOCKOUT_SLUGS[tournament_stage]
59
+ rounds_url = (
60
+ f"{API_URLS[data_source]}/api/v1/unique-tournament/{tournament_id}"
61
+ f"/season/{season_id}/rounds"
62
+ )
63
+ with SofascoreClient(rate_limit=rate_limit, cache=cache) as client:
64
+ rounds_data = client.get(rounds_url)
65
+ rounds = rounds_data.get("rounds") or rounds_data.get("currentRounds") or []
66
+ matched = next((r for r in rounds if r.get("slug") == target_slug), None)
67
+ if matched is None:
68
+ raise DataNotAvailableError(
69
+ f"Could not find round with slug '{target_slug}' for "
70
+ f"tournament_id={tournament_id}, season_id={season_id}."
71
+ )
72
+ week_number = matched["round"]
73
+
52
74
  url = build_tournament_url(
53
75
  API_URLS[data_source], tournament_id, season_id, week_number,
54
76
  tournament_type, tournament_stage,
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
13
13
  def past_matches_data(
14
14
  tournament_id: int,
15
15
  season_id: int,
16
- week_number: int,
16
+ week_number: Optional[int] = None,
17
17
  tournament_type: Optional[str] = None,
18
18
  tournament_stage: Optional[str] = None,
19
19
  data_source: str = "sofascore",
@@ -30,7 +30,7 @@ def past_matches_data(
30
30
  tournament_id: The unique identifier for the tournament.
31
31
  season_id: The unique identifier for the season.
32
32
  week_number: The matchweek number within the season.
33
- tournament_type: The tournament type ('uefa'). If None, assumes league format.
33
+ tournament_type: The tournament type ('uefa', 'world_cup'). If None, assumes league format.
34
34
  tournament_stage: The specific stage of the tournament (e.g., 'group_stage_week', 'round_of_16').
35
35
  data_source: The data source ('sofavpn' or 'sofascore'). Defaults to 'sofascore'.
36
36
  rate_limit: Maximum requests per second. Defaults to 2.0.
@@ -6,7 +6,7 @@ from datafc.utils._config import API_URLS
6
6
  from datafc.utils._validate import validate_source
7
7
  from datafc.utils._tournament_info import resolve_tournament_season
8
8
  from datafc.sofascore._parsers import parse_standings_rows
9
- from datafc.exceptions import DataNotAvailableError
9
+ from datafc.exceptions import APIError, DataNotAvailableError
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from datafc.utils._cache import DiskCache
@@ -51,8 +51,11 @@ def standings_data(
51
51
  f"{API_URLS[data_source]}/api/v1/unique-tournament/{tournament_id}"
52
52
  f"/season/{season_id}/standings/{category}"
53
53
  )
54
- data = client.get(url)
55
- rows.extend(parse_standings_rows(data, category, tournament_id, season_id))
54
+ try:
55
+ data = client.get(url)
56
+ rows.extend(parse_standings_rows(data, category, tournament_id, season_id))
57
+ except APIError:
58
+ pass
56
59
 
57
60
  result_df = pd.DataFrame(rows)
58
61
  if result_df.empty:
@@ -1,108 +1,108 @@
1
- import logging
2
- from typing import TYPE_CHECKING, Optional
3
- import pandas as pd
4
- from datafc.utils._client import SofascoreClient
5
- from datafc.utils._save_files import save_json, save_excel
6
- from datafc.utils._config import API_URLS
7
- from datafc.utils._validate import validate_source
8
- from datafc.utils._helpers import _cast_int_cols
9
- from datafc.sofascore._parsers import parse_team_match_history_records
10
- from datafc.exceptions import APIError, DataNotAvailableError
11
-
12
- if TYPE_CHECKING:
13
- from datafc.utils._cache import DiskCache
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- def team_match_history_data(
19
- team_id: int,
20
- data_source: str = "sofascore",
21
- rate_limit: float = 2.0,
22
- cache: Optional["DiskCache"] = None,
23
- enable_json_export: bool = False,
24
- enable_excel_export: bool = False,
25
- output_dir: str = ".",
26
- ) -> pd.DataFrame:
27
- """
28
- Fetches the complete match history for a single team across all competitions.
29
-
30
- Paginates through all available history pages until no further pages exist.
31
- The team_id can be obtained from standings_data(), squad_data(), or search_data().
32
-
33
- Args:
34
- team_id: The unique Sofascore identifier for the team.
35
- data_source: The data source ('sofavpn' or 'sofascore'). Defaults to 'sofascore'.
36
- rate_limit: Maximum requests per second. Defaults to 2.0.
37
- cache: Optional DiskCache instance. Cached responses skip the API call.
38
- enable_json_export: If True, saves output as JSON. Defaults to False.
39
- enable_excel_export: If True, saves output as Excel. Defaults to False.
40
- output_dir: Directory for exported files. Defaults to current directory.
41
-
42
- Returns:
43
- Past matches with country, tournament, season, week, home/away team names,
44
- IDs, scores, start timestamp and status; sorted by start_timestamp ascending.
45
-
46
- Raises:
47
- InvalidParameterError: If an invalid data_source is given.
48
- DataNotAvailableError: If no historical match data is found for the team.
49
- APIError: On HTTP errors from the Sofascore API.
50
- """
51
- validate_source(data_source)
52
-
53
- seen_game_ids: set = set()
54
- records = []
55
- page = 0
56
-
57
- with SofascoreClient(rate_limit=rate_limit, cache=cache) as client:
58
- while True:
59
- url = f"{API_URLS[data_source]}/api/v1/team/{team_id}/events/last/{page}"
60
- try:
61
- data = client.get(url)
62
- except APIError as exc:
63
- logger.warning(
64
- "Failed to fetch match history for team_id=%s page=%s: %s",
65
- team_id, page, exc,
66
- )
67
- break
68
-
69
- batch = parse_team_match_history_records(data, seen_game_ids)
70
- records.extend(batch)
71
-
72
- if not data.get("hasNextPage", False):
73
- break
74
- page += 1
75
-
76
- result_df = pd.DataFrame(records)
77
- if result_df.empty:
78
- raise DataNotAvailableError(
79
- f"No historical match data found for team_id={team_id}."
80
- )
81
-
82
- result_df = result_df.sort_values("start_timestamp").reset_index(drop=True)
83
- result_df = _cast_int_cols(
84
- result_df,
85
- "week",
86
- "home_team_id", "away_team_id",
87
- "home_score_period1", "home_score_period2", "home_score_normaltime",
88
- "home_score_display", "home_score_current",
89
- "away_score_period1", "away_score_period2", "away_score_normaltime",
90
- "away_score_display", "away_score_current",
91
- "start_timestamp",
92
- )
93
-
94
- if enable_json_export or enable_excel_export:
95
- first = result_df.iloc[0]
96
- kwargs = dict(
97
- fn_name="team_match_history_data",
98
- data_source=data_source,
99
- country=first.get("country", ""),
100
- tournament=first.get("tournament", ""),
101
- season=first.get("season"),
102
- )
103
- if enable_json_export:
104
- save_json(data=result_df, **kwargs, output_dir=output_dir)
105
- if enable_excel_export:
106
- save_excel(data=result_df, **kwargs, output_dir=output_dir)
107
-
108
- return result_df
1
+ import logging
2
+ from typing import TYPE_CHECKING, Optional
3
+ import pandas as pd
4
+ from datafc.utils._client import SofascoreClient
5
+ from datafc.utils._save_files import save_json, save_excel
6
+ from datafc.utils._config import API_URLS
7
+ from datafc.utils._validate import validate_source
8
+ from datafc.utils._helpers import _cast_int_cols
9
+ from datafc.sofascore._parsers import parse_team_match_history_records
10
+ from datafc.exceptions import APIError, DataNotAvailableError
11
+
12
+ if TYPE_CHECKING:
13
+ from datafc.utils._cache import DiskCache
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def team_match_history_data(
19
+ team_id: int,
20
+ data_source: str = "sofascore",
21
+ rate_limit: float = 2.0,
22
+ cache: Optional["DiskCache"] = None,
23
+ enable_json_export: bool = False,
24
+ enable_excel_export: bool = False,
25
+ output_dir: str = ".",
26
+ ) -> pd.DataFrame:
27
+ """
28
+ Fetches the complete match history for a single team across all competitions.
29
+
30
+ Paginates through all available history pages until no further pages exist.
31
+ The team_id can be obtained from standings_data(), squad_data(), or search_data().
32
+
33
+ Args:
34
+ team_id: The unique Sofascore identifier for the team.
35
+ data_source: The data source ('sofavpn' or 'sofascore'). Defaults to 'sofascore'.
36
+ rate_limit: Maximum requests per second. Defaults to 2.0.
37
+ cache: Optional DiskCache instance. Cached responses skip the API call.
38
+ enable_json_export: If True, saves output as JSON. Defaults to False.
39
+ enable_excel_export: If True, saves output as Excel. Defaults to False.
40
+ output_dir: Directory for exported files. Defaults to current directory.
41
+
42
+ Returns:
43
+ Past matches with country, tournament, season, week, home/away team names,
44
+ IDs, scores, start timestamp and status; sorted by start_timestamp ascending.
45
+
46
+ Raises:
47
+ InvalidParameterError: If an invalid data_source is given.
48
+ DataNotAvailableError: If no historical match data is found for the team.
49
+ APIError: On HTTP errors from the Sofascore API.
50
+ """
51
+ validate_source(data_source)
52
+
53
+ seen_game_ids: set = set()
54
+ records = []
55
+ page = 0
56
+
57
+ with SofascoreClient(rate_limit=rate_limit, cache=cache) as client:
58
+ while True:
59
+ url = f"{API_URLS[data_source]}/api/v1/team/{team_id}/events/last/{page}"
60
+ try:
61
+ data = client.get(url)
62
+ except APIError as exc:
63
+ logger.warning(
64
+ "Failed to fetch match history for team_id=%s page=%s: %s",
65
+ team_id, page, exc,
66
+ )
67
+ break
68
+
69
+ batch = parse_team_match_history_records(data, seen_game_ids)
70
+ records.extend(batch)
71
+
72
+ if not data.get("hasNextPage", False):
73
+ break
74
+ page += 1
75
+
76
+ result_df = pd.DataFrame(records)
77
+ if result_df.empty:
78
+ raise DataNotAvailableError(
79
+ f"No historical match data found for team_id={team_id}."
80
+ )
81
+
82
+ result_df = result_df.sort_values("start_timestamp").reset_index(drop=True)
83
+ result_df = _cast_int_cols(
84
+ result_df,
85
+ "week",
86
+ "home_team_id", "away_team_id",
87
+ "home_score_period1", "home_score_period2", "home_score_normaltime",
88
+ "home_score_display", "home_score_current",
89
+ "away_score_period1", "away_score_period2", "away_score_normaltime",
90
+ "away_score_display", "away_score_current",
91
+ "start_timestamp",
92
+ )
93
+
94
+ if enable_json_export or enable_excel_export:
95
+ first = result_df.iloc[0]
96
+ kwargs = dict(
97
+ fn_name="team_match_history_data",
98
+ data_source=data_source,
99
+ country=first.get("country", ""),
100
+ tournament=first.get("tournament", ""),
101
+ season=first.get("season"),
102
+ )
103
+ if enable_json_export:
104
+ save_json(data=result_df, **kwargs, output_dir=output_dir)
105
+ if enable_excel_export:
106
+ save_excel(data=result_df, **kwargs, output_dir=output_dir)
107
+
108
+ return result_df
@@ -40,6 +40,26 @@ TOURNAMENT_URL_PATTERNS = {
40
40
  "match_for_3rd_place": "{base_url}/api/v1/unique-tournament/{tournament_id}/season/{season_id}/events/round/{week_number}/slug/match-for-3rd-place",
41
41
  "final": "{base_url}/api/v1/unique-tournament/{tournament_id}/season/{season_id}/events/round/{week_number}/slug/final",
42
42
  },
43
+ "world_cup": {
44
+ "group_stage_week": "{base_url}/api/v1/unique-tournament/{tournament_id}/season/{season_id}/events/round/{week_number}",
45
+ "round_of_32": "{base_url}/api/v1/unique-tournament/{tournament_id}/season/{season_id}/events/round/{week_number}/slug/round-of-32",
46
+ "round_of_16": "{base_url}/api/v1/unique-tournament/{tournament_id}/season/{season_id}/events/round/{week_number}/slug/round-of-16",
47
+ "quarterfinals": "{base_url}/api/v1/unique-tournament/{tournament_id}/season/{season_id}/events/round/{week_number}/slug/quarterfinals",
48
+ "semifinals": "{base_url}/api/v1/unique-tournament/{tournament_id}/season/{season_id}/events/round/{week_number}/slug/semifinals",
49
+ "match_for_3rd_place": "{base_url}/api/v1/unique-tournament/{tournament_id}/season/{season_id}/events/round/{week_number}/slug/match-for-3rd-place",
50
+ "final": "{base_url}/api/v1/unique-tournament/{tournament_id}/season/{season_id}/events/round/{week_number}/slug/final",
51
+ },
52
+ }
53
+
54
+ # Slug used in the API URL for each world_cup knockout stage.
55
+ # Used to auto-resolve the season-specific round number via the /rounds endpoint.
56
+ WORLD_CUP_KNOCKOUT_SLUGS: dict[str, str] = {
57
+ "round_of_32": "round-of-32",
58
+ "round_of_16": "round-of-16",
59
+ "quarterfinals": "quarterfinals",
60
+ "semifinals": "semifinals",
61
+ "match_for_3rd_place": "match-for-3rd-place",
62
+ "final": "final",
43
63
  }
44
64
 
45
65
  # ---------------------------------------------------------------------------
@@ -75,7 +75,7 @@ def build_tournament_url(
75
75
  base_url: str,
76
76
  tournament_id: int,
77
77
  season_id: int,
78
- week_number: int,
78
+ week_number: Optional[int],
79
79
  tournament_type: Optional[str],
80
80
  tournament_stage: Optional[str],
81
81
  ) -> str:
@@ -84,13 +84,14 @@ def build_tournament_url(
84
84
  if tournament_type is not None:
85
85
  validate_tournament_type(tournament_type)
86
86
  validate_tournament_stage(tournament_type, tournament_stage)
87
- return patterns[tournament_type][tournament_stage].format(
88
- base_url=base_url,
89
- tournament_id=tournament_id,
90
- season_id=season_id,
91
- week_number=week_number,
87
+ template = patterns[tournament_type][tournament_stage]
88
+ else:
89
+ template = patterns["default"]
90
+ if "{week_number}" in template and week_number is None:
91
+ raise InvalidParameterError(
92
+ "week_number is required for this tournament_type/tournament_stage combination."
92
93
  )
93
- return patterns["default"].format(
94
+ return template.format(
94
95
  base_url=base_url,
95
96
  tournament_id=tournament_id,
96
97
  season_id=season_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datafc
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: Fetch, process, and export structured football data.
5
5
  Author-email: Uraz Akgül <urazdev@gmail.com>
6
6
  License: MIT
@@ -24,7 +24,7 @@ Requires-Dist: pytest>=8.0; extra == "dev"
24
24
  Requires-Dist: pytest-mock>=3.12; extra == "dev"
25
25
  Dynamic: license-file
26
26
 
27
- # datafc v2.1.0
27
+ # datafc v2.3.0
28
28
 
29
29
  ## Overview
30
30
 
@@ -530,15 +530,34 @@ ucl_df = match_data(
530
530
  tournament_type="uefa",
531
531
  tournament_stage="round_of_16",
532
532
  )
533
+
534
+ # World Cup knockout stages — week_number not needed:
535
+ wc_df = match_data(
536
+ tournament_id=16,
537
+ season_id=58210,
538
+ tournament_type="world_cup",
539
+ tournament_stage="round_of_16",
540
+ )
541
+
542
+ # World Cup group stage — week_number required:
543
+ wc_group_df = match_data(
544
+ tournament_id=16,
545
+ season_id=58210,
546
+ week_number=1,
547
+ tournament_type="world_cup",
548
+ tournament_stage="group_stage_week",
549
+ )
533
550
  ```
534
551
 
535
552
  Parameters:
536
553
 
537
554
  - `tournament_id` (int)
538
555
  - `season_id` (int)
539
- - `week_number` (int)
540
- - `tournament_type` (str, optional): `"uefa"` for UEFA competitions. `None` assumes a domestic league.
541
- - `tournament_stage` (str, optional): Required when `tournament_type="uefa"`. Options: `preliminary_semifinals`, `preliminary_final`, `qualification_round`, `qualification_playoff`, `group_stage_week`, `playoff_round`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
556
+ - `week_number` (int, optional): Required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Not needed for other `world_cup` stages.
557
+ - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup. `None` assumes a domestic league.
558
+ - `tournament_stage` (str, optional): Required when `tournament_type` is set.
559
+ - `"uefa"` options: `preliminary_semifinals`, `preliminary_final`, `qualification_round`, `qualification_playoff`, `group_stage_week`, `playoff_round`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
560
+ - `"world_cup"` options: `group_stage_week`, `round_of_32`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
542
561
 
543
562
  Columns: `country`, `tournament`, `season`, `week`, `game_id`, `home_team`, `home_team_id`, `away_team`, `away_team_id`, `injury_time_1`, `injury_time_2`, `start_timestamp`, `status`, `home_score_current`, `home_score_display`, `home_score_period1`, `home_score_period2`, `home_score_normaltime`, `away_score_current`, `away_score_display`, `away_score_period1`, `away_score_period2`, `away_score_normaltime`.
544
563
 
@@ -775,9 +794,9 @@ Parameters:
775
794
 
776
795
  - `tournament_id` (int)
777
796
  - `season_id` (int)
778
- - `week_number` (int)
779
- - `tournament_type` (str, optional): `"uefa"` for UEFA competitions.
780
- - `tournament_stage` (str, optional): Required when `tournament_type="uefa"`. Same options as `match_data`.
797
+ - `week_number` (int, optional): Required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Not needed for other `world_cup` stages.
798
+ - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup.
799
+ - `tournament_stage` (str, optional): Required when `tournament_type` is set. Same options as `match_data`.
781
800
 
782
801
  Same columns as `match_data`.
783
802
 
@@ -897,6 +916,20 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
897
916
 
898
917
  ## Changelog
899
918
 
919
+ ### v2.3.0
920
+
921
+ - **Fixed `match_data` for World Cup knockout stages across all seasons.** Round numbers are now resolved automatically from the API instead of being hardcoded, so older seasons work correctly.
922
+ - **Fixed `standings_data` for tournament-format competitions.** Calling this function for World Cup, Euro, or similar tournaments no longer raises an error. Only the available categories are returned.
923
+
924
+ ---
925
+
926
+ ### v2.2.0
927
+
928
+ - Added `tournament_type="world_cup"` support to `match_data` and `past_matches_data` for FIFA World Cup competitions. Knockout stage rounds are fixed internally; only `group_stage_week` requires `week_number`.
929
+ - `week_number` is now optional (`None` by default). It is required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Omitting it when required raises `InvalidParameterError`.
930
+
931
+ ---
932
+
900
933
  ### v2.1.0
901
934
 
902
935
  - Added `team_match_history_data`: fetches the complete match history for a single team across all competitions using `team_id` directly (no standings dependency).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datafc"
7
- version = "2.1.0"
7
+ version = "2.3.0"
8
8
  description = "Fetch, process, and export structured football data."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes