datafc 2.6.0__tar.gz → 2.7.1__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 (76) hide show
  1. {datafc-2.6.0 → datafc-2.7.1}/PKG-INFO +137 -8
  2. {datafc-2.6.0 → datafc-2.7.1}/README.md +135 -6
  3. {datafc-2.6.0 → datafc-2.7.1}/datafc/__init__.py +7 -1
  4. datafc-2.7.1/datafc/clubelo/__init__.py +18 -0
  5. datafc-2.7.1/datafc/clubelo/_client.py +112 -0
  6. datafc-2.7.1/datafc/clubelo/_parsers.py +40 -0
  7. datafc-2.7.1/datafc/clubelo/fetch_club_history_data.py +34 -0
  8. datafc-2.7.1/datafc/clubelo/fetch_daily_ranking_data.py +48 -0
  9. datafc-2.7.1/datafc/clubelo/fetch_fixtures_data.py +27 -0
  10. datafc-2.7.1/datafc/eloratings/__init__.py +26 -0
  11. datafc-2.7.1/datafc/eloratings/_client.py +112 -0
  12. datafc-2.7.1/datafc/eloratings/_parsers.py +212 -0
  13. datafc-2.7.1/datafc/eloratings/fetch_country_codes_data.py +28 -0
  14. datafc-2.7.1/datafc/eloratings/fetch_country_matches_data.py +35 -0
  15. datafc-2.7.1/datafc/eloratings/fetch_teams_data.py +27 -0
  16. datafc-2.7.1/datafc/eloratings/fetch_tournament_codes_data.py +28 -0
  17. datafc-2.7.1/datafc/eloratings/fetch_tournament_editions_data.py +30 -0
  18. datafc-2.7.1/datafc/eloratings/fetch_tournament_groups_data.py +33 -0
  19. datafc-2.7.1/datafc/eloratings/fetch_world_ranking_data.py +26 -0
  20. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_formations_data.py +44 -44
  21. {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_config.py +67 -0
  22. {datafc-2.6.0 → datafc-2.7.1}/datafc.egg-info/PKG-INFO +137 -8
  23. {datafc-2.6.0 → datafc-2.7.1}/datafc.egg-info/SOURCES.txt +16 -0
  24. {datafc-2.6.0 → datafc-2.7.1}/pyproject.toml +2 -2
  25. {datafc-2.6.0 → datafc-2.7.1}/LICENSE +0 -0
  26. {datafc-2.6.0 → datafc-2.7.1}/datafc/exceptions.py +0 -0
  27. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/__init__.py +0 -0
  28. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/_core.py +0 -0
  29. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/_parsers.py +0 -0
  30. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/aio.py +0 -0
  31. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_average_positions_data.py +0 -0
  32. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_coordinates_data.py +0 -0
  33. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_goal_networks_data.py +0 -0
  34. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_incidents_data.py +0 -0
  35. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_league_player_stats_data.py +0 -0
  36. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_lineups_data.py +0 -0
  37. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_match_data.py +0 -0
  38. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_match_details_data.py +0 -0
  39. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_match_h2h_data.py +0 -0
  40. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_match_odds_data.py +0 -0
  41. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_match_stats_data.py +0 -0
  42. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_momentum_data.py +0 -0
  43. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_past_matches_data.py +0 -0
  44. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_attribute_overviews_data.py +0 -0
  45. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_career_stats_data.py +0 -0
  46. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_data.py +0 -0
  47. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_match_log_data.py +0 -0
  48. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_national_team_data.py +0 -0
  49. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_stats_data.py +0 -0
  50. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_transfers_data.py +0 -0
  51. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_pregame_form_data.py +0 -0
  52. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_referee_stats_data.py +0 -0
  53. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_search_data.py +0 -0
  54. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_season_rounds_data.py +0 -0
  55. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_seasons_data.py +0 -0
  56. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_shots_data.py +0 -0
  57. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_squad_data.py +0 -0
  58. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_standings_data.py +0 -0
  59. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_substitutions_data.py +0 -0
  60. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_team_data.py +0 -0
  61. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_team_match_history_data.py +0 -0
  62. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_team_stats_data.py +0 -0
  63. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_team_transfers_data.py +0 -0
  64. {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_upcoming_matches_data.py +0 -0
  65. {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/__init__.py +0 -0
  66. {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_async_client.py +0 -0
  67. {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_cache.py +0 -0
  68. {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_client.py +0 -0
  69. {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_helpers.py +0 -0
  70. {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_save_files.py +0 -0
  71. {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_tournament_info.py +0 -0
  72. {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_validate.py +0 -0
  73. {datafc-2.6.0 → datafc-2.7.1}/datafc.egg-info/dependency_links.txt +0 -0
  74. {datafc-2.6.0 → datafc-2.7.1}/datafc.egg-info/requires.txt +0 -0
  75. {datafc-2.6.0 → datafc-2.7.1}/datafc.egg-info/top_level.txt +0 -0
  76. {datafc-2.6.0 → datafc-2.7.1}/setup.cfg +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datafc
3
- Version: 2.6.0
3
+ Version: 2.7.1
4
4
  Summary: Fetch, process, and export structured football data.
5
5
  Author-email: Uraz Akgül <urazdev@gmail.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/urazakgul/datafc
8
8
  Project-URL: Repository, https://github.com/urazakgul/datafc
9
9
  Project-URL: Issues, https://github.com/urazakgul/datafc/issues
10
- Keywords: football,soccer,data,analytics,sofascore
10
+ Keywords: football,soccer,data,analytics,sofascore,clubelo,eloratings,elo
11
11
  Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
@@ -24,11 +24,11 @@ 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.6.0
27
+ # datafc v2.7.1
28
28
 
29
29
  ## Overview
30
30
 
31
- `datafc` fetches, processes, and exports structured football data. It provides **35 functions** covering tournament metadata, standings, squad rosters, match fixtures, shots, lineups, player heatmaps, odds, and more — all returning clean `pandas` DataFrames ready for analysis. Sofascore is currently the only supported data source.
31
+ `datafc` fetches, processes, and exports structured football data. It provides **45 functions** covering tournament metadata, standings, squad rosters, match fixtures, shots, lineups, player heatmaps, odds, club & national-team Elo ratings, and more — all returning clean `pandas` DataFrames ready for analysis. Sofascore, ClubElo, and eloratings.net are the supported data sources.
32
32
 
33
33
  > **Finding IDs:** `tournament_id` and `season_id` can be discovered two ways:
34
34
  > - **From the URL:** navigating to a league page on Sofascore (e.g. `sofascore.com/.../trendyol-super-lig/52#id:63814`) shows `tournament_id=52` and `season_id=63814`.
@@ -112,6 +112,26 @@ Dynamic: license-file
112
112
  |---|---|
113
113
  | `referee_stats_data` | Career stats for a referee: games, cards, and per-game averages |
114
114
 
115
+ ### ClubElo
116
+
117
+ | Function | What it returns |
118
+ |---|---|
119
+ | `clubelo.daily_ranking_data` | Full ClubElo ranking for a given calendar day (since 1939) |
120
+ | `clubelo.club_history_data` | Complete Elo rating history for a single club |
121
+ | `clubelo.fixtures_data` | Per-goal-difference and exact-result probabilities for upcoming matches |
122
+
123
+ ### EloRatings (national teams)
124
+
125
+ | Function | What it returns |
126
+ |---|---|
127
+ | `eloratings.world_ranking_data` | Current global Elo ranking for all national teams |
128
+ | `eloratings.country_matches_data` | Complete international match history for a national team |
129
+ | `eloratings.country_codes_data` | Country code → English country name reference |
130
+ | `eloratings.tournament_codes_data` | Tournament code → English tournament name reference |
131
+ | `eloratings.tournament_editions_data` | Every tournament edition with start/end dates and page slug |
132
+ | `eloratings.tournament_groups_data` | Specific tournament code → broader group code (e.g. all WC qualifier variants → `WQT`) |
133
+ | `eloratings.teams_data` | Legacy/historical country code → ISO code mapping |
134
+
115
135
  > **Coverage:** Any league and season available on Sofascore. For Turkey Super Lig, every season from 1980/81 to the present is accessible.
116
136
 
117
137
  ## Installation
@@ -560,17 +580,28 @@ wc_group_df = match_data(
560
580
  tournament_type="world_cup",
561
581
  tournament_stage="group_stage_week",
562
582
  )
583
+
584
+ # Domestic league promotion playoff (e.g. TFF 1. Lig, EFL Championship):
585
+ # use season_rounds_data to discover the round_number for each stage.
586
+ playoff_df = match_data(
587
+ tournament_id=98,
588
+ season_id=<season_id>,
589
+ week_number=<round_number>,
590
+ tournament_type="league_playoff",
591
+ tournament_stage="final",
592
+ )
563
593
  ```
564
594
 
565
595
  Parameters:
566
596
 
567
597
  - `tournament_id` (int)
568
598
  - `season_id` (int)
569
- - `week_number` (int, optional): Required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Not needed for other `world_cup` stages.
570
- - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup. `None` assumes a domestic league.
599
+ - `week_number` (int, optional): Required for league rounds, UEFA stages, `world_cup` + `group_stage_week`, and all `league_playoff` stages. Not needed for other `world_cup` stages.
600
+ - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup, `"league_playoff"` for domestic promotion playoffs. `None` assumes a domestic league.
571
601
  - `tournament_stage` (str, optional): Required when `tournament_type` is set.
572
602
  - `"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`.
573
603
  - `"world_cup"` options: `group_stage_week`, `round_of_32`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
604
+ - `"league_playoff"` options: `quarterfinals`, `semifinals`, `final`.
574
605
 
575
606
  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`.
576
607
 
@@ -823,8 +854,8 @@ Parameters:
823
854
 
824
855
  - `tournament_id` (int)
825
856
  - `season_id` (int)
826
- - `week_number` (int, optional): Required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Not needed for other `world_cup` stages.
827
- - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup.
857
+ - `week_number` (int, optional): Required for league rounds, UEFA stages, `world_cup` + `group_stage_week`, and all `league_playoff` stages. Not needed for other `world_cup` stages.
858
+ - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup, `"league_playoff"` for domestic promotion playoffs.
828
859
  - `tournament_stage` (str, optional): Required when `tournament_type` is set. Same options as `match_data`.
829
860
 
830
861
  Same columns as `match_data`.
@@ -961,8 +992,106 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
961
992
 
962
993
  ---
963
994
 
995
+ ### ClubElo
996
+
997
+ ClubElo (<https://clubelo.com>) maintains daily Elo ratings for European clubs going back to 1939. Three functions wrap the public API; all return `pandas` DataFrames.
998
+
999
+ ```python
1000
+ from datafc import clubelo
1001
+
1002
+ # Full ranking on a given day
1003
+ df = clubelo.daily_ranking_data("2026-06-10")
1004
+
1005
+ # A single club's full Elo history (use the spelling shown in daily_ranking_data)
1006
+ hist = clubelo.club_history_data("Man City")
1007
+
1008
+ # Upcoming matches with per-goal-difference and exact-result probabilities
1009
+ fix = clubelo.fixtures_data()
1010
+ ```
1011
+
1012
+ Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (any `DiskCache` instance, shared with the Sofascore functions).
1013
+
1014
+ > **Notes:**
1015
+ > - Club names must match ClubElo's spelling exactly (e.g. `"Man City"`, `"Real Madrid"`, `"Paris SG"`). Run `daily_ranking_data` first and check the `club` column.
1016
+ > - Values before 1960 are considered provisional by ClubElo.
1017
+ > - Dates accept either `"YYYY-MM-DD"` strings or `datetime.date` objects.
1018
+
1019
+ If the default `https://api.clubelo.com` is unreachable from your network (some corporate firewalls block port 443 to this host while leaving port 80 open), override the base URL once at startup:
1020
+
1021
+ ```python
1022
+ from datafc import clubelo
1023
+ clubelo.set_clubelo_base_url("http://api.clubelo.com")
1024
+ ```
1025
+
1026
+ `clubelo.reset_clubelo_base_url()` restores the default.
1027
+
1028
+ ---
1029
+
1030
+ ### EloRatings (national teams)
1031
+
1032
+ [eloratings.net](https://www.eloratings.net) maintains daily Elo ratings for national teams going back to 1872. Seven functions wrap the site's underlying `.tsv` data files; all return `pandas` DataFrames.
1033
+
1034
+ ```python
1035
+ from datafc import eloratings
1036
+
1037
+ # Current global ranking of all national teams (30 columns)
1038
+ df = eloratings.world_ranking_data()
1039
+
1040
+ # Full match history for a single national team
1041
+ matches = eloratings.country_matches_data("Spain")
1042
+
1043
+ # Code reference tables (handy for joining onto the above)
1044
+ countries = eloratings.country_codes_data() # country_code → country_name
1045
+ tournaments = eloratings.tournament_codes_data() # tournament_code → tournament_name
1046
+ groups = eloratings.tournament_groups_data() # specific code → group code (long format)
1047
+ editions = eloratings.tournament_editions_data() # every tournament edition + dates + slug
1048
+
1049
+ # Legacy/historical country code → ISO code mapping
1050
+ teams = eloratings.teams_data()
1051
+
1052
+ # Example: enrich Spain's matches with full tournament names
1053
+ spain = (
1054
+ eloratings.country_matches_data("Spain")
1055
+ .merge(tournaments, left_on="tournament", right_on="tournament_code", how="left")
1056
+ )
1057
+
1058
+ # Example: every World Cup final tournament (filter editions on the WC group)
1059
+ wc_finals = editions[(editions["code"] == "WC") & editions["start_date"].notna()]
1060
+ ```
1061
+
1062
+ Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (same `DiskCache` instance as Sofascore/ClubElo).
1063
+
1064
+ > **Column names are verified against the site's JavaScript source.**
1065
+ > eloratings.net does not publish a documented API; the `.tsv` files have no header row. Column names in `world_ranking_data` are derived from index→field assignments in `scripts/ratings.js`; the remaining files are cross-checked against the live site's display. The ``WORLD_COLUMNS`` / ``COUNTRY_MATCH_COLUMNS`` lists in `datafc/eloratings/_parsers.py` document each position. If the site changes its layout, columns may be silently mis-labelled — please open an issue if you spot a discrepancy.
1066
+
1067
+ > **Country argument is a page slug, not an ISO code.**
1068
+ > `country_matches_data("Spain")` calls `https://www.eloratings.net/Spain.tsv`. Use the same capitalisation and spelling shown in the URL of the country page on eloratings.net (e.g. `"Czech_Republic"`, `"United_States"`).
1069
+
1070
+ Override the base URL the same way as ClubElo if needed:
1071
+
1072
+ ```python
1073
+ from datafc import eloratings
1074
+ eloratings.set_eloratings_base_url("http://www.eloratings.net")
1075
+ ```
1076
+
1077
+ ---
1078
+
964
1079
  ## What's new
965
1080
 
1081
+ ### v2.7.1
1082
+
1083
+ - **New `tournament_type="league_playoff"`** for `match_data` and `past_matches_data`. Covers domestic promotion playoffs (e.g. TFF 1. Lig, EFL Championship) with `quarterfinals`, `semifinals`, and `final` stages. Use `season_rounds_data` to discover the `week_number` for each stage.
1084
+
1085
+ ---
1086
+
1087
+ ### v2.7.0
1088
+
1089
+ - **Club Elo ratings via ClubElo.** Pull daily Elo rankings of European clubs (going back to 1939), a single club's full rating history, and probability-based predictions for upcoming matches — all as `pandas` DataFrames. Three functions under `datafc.clubelo`: `daily_ranking_data`, `club_history_data`, `fixtures_data`.
1090
+ - **National-team Elo ratings via eloratings.net.** Pull the current global ranking of every national team, a country's full international match history (back to 1872), and the site's reference tables for resolving the two-letter country codes and tournament codes into full names. Seven functions under `datafc.eloratings`, including `world_ranking_data`, `country_matches_data`, `country_codes_data`, and `tournament_codes_data`.
1091
+ - **Shared caching across all sources.** ClubElo and eloratings.net responses use the same `DiskCache` you already pass to the Sofascore functions, so a single cache directory keeps every fetch persistent across sessions.
1092
+
1093
+ ---
1094
+
966
1095
  ### v2.6.0
967
1096
 
968
1097
  - **New function: `formations_data`** — fetches home and away formation for each match. Returns two rows per match (`team`: `home` / `away`) with the formation string (e.g. `4-3-3`).
@@ -1,8 +1,8 @@
1
- # datafc v2.6.0
1
+ # datafc v2.7.1
2
2
 
3
3
  ## Overview
4
4
 
5
- `datafc` fetches, processes, and exports structured football data. It provides **35 functions** covering tournament metadata, standings, squad rosters, match fixtures, shots, lineups, player heatmaps, odds, and more — all returning clean `pandas` DataFrames ready for analysis. Sofascore is currently the only supported data source.
5
+ `datafc` fetches, processes, and exports structured football data. It provides **45 functions** covering tournament metadata, standings, squad rosters, match fixtures, shots, lineups, player heatmaps, odds, club & national-team Elo ratings, and more — all returning clean `pandas` DataFrames ready for analysis. Sofascore, ClubElo, and eloratings.net are the supported data sources.
6
6
 
7
7
  > **Finding IDs:** `tournament_id` and `season_id` can be discovered two ways:
8
8
  > - **From the URL:** navigating to a league page on Sofascore (e.g. `sofascore.com/.../trendyol-super-lig/52#id:63814`) shows `tournament_id=52` and `season_id=63814`.
@@ -86,6 +86,26 @@
86
86
  |---|---|
87
87
  | `referee_stats_data` | Career stats for a referee: games, cards, and per-game averages |
88
88
 
89
+ ### ClubElo
90
+
91
+ | Function | What it returns |
92
+ |---|---|
93
+ | `clubelo.daily_ranking_data` | Full ClubElo ranking for a given calendar day (since 1939) |
94
+ | `clubelo.club_history_data` | Complete Elo rating history for a single club |
95
+ | `clubelo.fixtures_data` | Per-goal-difference and exact-result probabilities for upcoming matches |
96
+
97
+ ### EloRatings (national teams)
98
+
99
+ | Function | What it returns |
100
+ |---|---|
101
+ | `eloratings.world_ranking_data` | Current global Elo ranking for all national teams |
102
+ | `eloratings.country_matches_data` | Complete international match history for a national team |
103
+ | `eloratings.country_codes_data` | Country code → English country name reference |
104
+ | `eloratings.tournament_codes_data` | Tournament code → English tournament name reference |
105
+ | `eloratings.tournament_editions_data` | Every tournament edition with start/end dates and page slug |
106
+ | `eloratings.tournament_groups_data` | Specific tournament code → broader group code (e.g. all WC qualifier variants → `WQT`) |
107
+ | `eloratings.teams_data` | Legacy/historical country code → ISO code mapping |
108
+
89
109
  > **Coverage:** Any league and season available on Sofascore. For Turkey Super Lig, every season from 1980/81 to the present is accessible.
90
110
 
91
111
  ## Installation
@@ -534,17 +554,28 @@ wc_group_df = match_data(
534
554
  tournament_type="world_cup",
535
555
  tournament_stage="group_stage_week",
536
556
  )
557
+
558
+ # Domestic league promotion playoff (e.g. TFF 1. Lig, EFL Championship):
559
+ # use season_rounds_data to discover the round_number for each stage.
560
+ playoff_df = match_data(
561
+ tournament_id=98,
562
+ season_id=<season_id>,
563
+ week_number=<round_number>,
564
+ tournament_type="league_playoff",
565
+ tournament_stage="final",
566
+ )
537
567
  ```
538
568
 
539
569
  Parameters:
540
570
 
541
571
  - `tournament_id` (int)
542
572
  - `season_id` (int)
543
- - `week_number` (int, optional): Required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Not needed for other `world_cup` stages.
544
- - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup. `None` assumes a domestic league.
573
+ - `week_number` (int, optional): Required for league rounds, UEFA stages, `world_cup` + `group_stage_week`, and all `league_playoff` stages. Not needed for other `world_cup` stages.
574
+ - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup, `"league_playoff"` for domestic promotion playoffs. `None` assumes a domestic league.
545
575
  - `tournament_stage` (str, optional): Required when `tournament_type` is set.
546
576
  - `"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`.
547
577
  - `"world_cup"` options: `group_stage_week`, `round_of_32`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
578
+ - `"league_playoff"` options: `quarterfinals`, `semifinals`, `final`.
548
579
 
549
580
  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`.
550
581
 
@@ -797,8 +828,8 @@ Parameters:
797
828
 
798
829
  - `tournament_id` (int)
799
830
  - `season_id` (int)
800
- - `week_number` (int, optional): Required for league rounds, UEFA stages, and `world_cup` + `group_stage_week`. Not needed for other `world_cup` stages.
801
- - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup.
831
+ - `week_number` (int, optional): Required for league rounds, UEFA stages, `world_cup` + `group_stage_week`, and all `league_playoff` stages. Not needed for other `world_cup` stages.
832
+ - `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup, `"league_playoff"` for domestic promotion playoffs.
802
833
  - `tournament_stage` (str, optional): Required when `tournament_type` is set. Same options as `match_data`.
803
834
 
804
835
  Same columns as `match_data`.
@@ -935,8 +966,106 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
935
966
 
936
967
  ---
937
968
 
969
+ ### ClubElo
970
+
971
+ ClubElo (<https://clubelo.com>) maintains daily Elo ratings for European clubs going back to 1939. Three functions wrap the public API; all return `pandas` DataFrames.
972
+
973
+ ```python
974
+ from datafc import clubelo
975
+
976
+ # Full ranking on a given day
977
+ df = clubelo.daily_ranking_data("2026-06-10")
978
+
979
+ # A single club's full Elo history (use the spelling shown in daily_ranking_data)
980
+ hist = clubelo.club_history_data("Man City")
981
+
982
+ # Upcoming matches with per-goal-difference and exact-result probabilities
983
+ fix = clubelo.fixtures_data()
984
+ ```
985
+
986
+ Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (any `DiskCache` instance, shared with the Sofascore functions).
987
+
988
+ > **Notes:**
989
+ > - Club names must match ClubElo's spelling exactly (e.g. `"Man City"`, `"Real Madrid"`, `"Paris SG"`). Run `daily_ranking_data` first and check the `club` column.
990
+ > - Values before 1960 are considered provisional by ClubElo.
991
+ > - Dates accept either `"YYYY-MM-DD"` strings or `datetime.date` objects.
992
+
993
+ If the default `https://api.clubelo.com` is unreachable from your network (some corporate firewalls block port 443 to this host while leaving port 80 open), override the base URL once at startup:
994
+
995
+ ```python
996
+ from datafc import clubelo
997
+ clubelo.set_clubelo_base_url("http://api.clubelo.com")
998
+ ```
999
+
1000
+ `clubelo.reset_clubelo_base_url()` restores the default.
1001
+
1002
+ ---
1003
+
1004
+ ### EloRatings (national teams)
1005
+
1006
+ [eloratings.net](https://www.eloratings.net) maintains daily Elo ratings for national teams going back to 1872. Seven functions wrap the site's underlying `.tsv` data files; all return `pandas` DataFrames.
1007
+
1008
+ ```python
1009
+ from datafc import eloratings
1010
+
1011
+ # Current global ranking of all national teams (30 columns)
1012
+ df = eloratings.world_ranking_data()
1013
+
1014
+ # Full match history for a single national team
1015
+ matches = eloratings.country_matches_data("Spain")
1016
+
1017
+ # Code reference tables (handy for joining onto the above)
1018
+ countries = eloratings.country_codes_data() # country_code → country_name
1019
+ tournaments = eloratings.tournament_codes_data() # tournament_code → tournament_name
1020
+ groups = eloratings.tournament_groups_data() # specific code → group code (long format)
1021
+ editions = eloratings.tournament_editions_data() # every tournament edition + dates + slug
1022
+
1023
+ # Legacy/historical country code → ISO code mapping
1024
+ teams = eloratings.teams_data()
1025
+
1026
+ # Example: enrich Spain's matches with full tournament names
1027
+ spain = (
1028
+ eloratings.country_matches_data("Spain")
1029
+ .merge(tournaments, left_on="tournament", right_on="tournament_code", how="left")
1030
+ )
1031
+
1032
+ # Example: every World Cup final tournament (filter editions on the WC group)
1033
+ wc_finals = editions[(editions["code"] == "WC") & editions["start_date"].notna()]
1034
+ ```
1035
+
1036
+ Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (same `DiskCache` instance as Sofascore/ClubElo).
1037
+
1038
+ > **Column names are verified against the site's JavaScript source.**
1039
+ > eloratings.net does not publish a documented API; the `.tsv` files have no header row. Column names in `world_ranking_data` are derived from index→field assignments in `scripts/ratings.js`; the remaining files are cross-checked against the live site's display. The ``WORLD_COLUMNS`` / ``COUNTRY_MATCH_COLUMNS`` lists in `datafc/eloratings/_parsers.py` document each position. If the site changes its layout, columns may be silently mis-labelled — please open an issue if you spot a discrepancy.
1040
+
1041
+ > **Country argument is a page slug, not an ISO code.**
1042
+ > `country_matches_data("Spain")` calls `https://www.eloratings.net/Spain.tsv`. Use the same capitalisation and spelling shown in the URL of the country page on eloratings.net (e.g. `"Czech_Republic"`, `"United_States"`).
1043
+
1044
+ Override the base URL the same way as ClubElo if needed:
1045
+
1046
+ ```python
1047
+ from datafc import eloratings
1048
+ eloratings.set_eloratings_base_url("http://www.eloratings.net")
1049
+ ```
1050
+
1051
+ ---
1052
+
938
1053
  ## What's new
939
1054
 
1055
+ ### v2.7.1
1056
+
1057
+ - **New `tournament_type="league_playoff"`** for `match_data` and `past_matches_data`. Covers domestic promotion playoffs (e.g. TFF 1. Lig, EFL Championship) with `quarterfinals`, `semifinals`, and `final` stages. Use `season_rounds_data` to discover the `week_number` for each stage.
1058
+
1059
+ ---
1060
+
1061
+ ### v2.7.0
1062
+
1063
+ - **Club Elo ratings via ClubElo.** Pull daily Elo rankings of European clubs (going back to 1939), a single club's full rating history, and probability-based predictions for upcoming matches — all as `pandas` DataFrames. Three functions under `datafc.clubelo`: `daily_ranking_data`, `club_history_data`, `fixtures_data`.
1064
+ - **National-team Elo ratings via eloratings.net.** Pull the current global ranking of every national team, a country's full international match history (back to 1872), and the site's reference tables for resolving the two-letter country codes and tournament codes into full names. Seven functions under `datafc.eloratings`, including `world_ranking_data`, `country_matches_data`, `country_codes_data`, and `tournament_codes_data`.
1065
+ - **Shared caching across all sources.** ClubElo and eloratings.net responses use the same `DiskCache` you already pass to the Sofascore functions, so a single cache directory keeps every fetch persistent across sessions.
1066
+
1067
+ ---
1068
+
940
1069
  ### v2.6.0
941
1070
 
942
1071
  - **New function: `formations_data`** — fetches home and away formation for each match. Returns two rows per match (`team`: `home` / `away`) with the formation string (e.g. `4-3-3`).
@@ -1,6 +1,8 @@
1
- __version__ = "2.6.0"
1
+ __version__ = "2.7.1"
2
2
 
3
3
  from .sofascore import *
4
+ from . import clubelo
5
+ from . import eloratings
4
6
  from .exceptions import (
5
7
  DataFCError,
6
8
  InvalidParameterError,
@@ -61,6 +63,10 @@ __all__ = [
61
63
  "referee_stats_data",
62
64
  # Async API
63
65
  "aio",
66
+ # ClubElo subpackage
67
+ "clubelo",
68
+ # EloRatings subpackage
69
+ "eloratings",
64
70
  # Exceptions
65
71
  "DataFCError",
66
72
  "InvalidParameterError",
@@ -0,0 +1,18 @@
1
+ from datafc.utils._config import (
2
+ get_clubelo_base_url,
3
+ reset_clubelo_base_url,
4
+ set_clubelo_base_url,
5
+ )
6
+
7
+ from .fetch_club_history_data import club_history_data
8
+ from .fetch_daily_ranking_data import daily_ranking_data
9
+ from .fetch_fixtures_data import fixtures_data
10
+
11
+ __all__ = [
12
+ "daily_ranking_data",
13
+ "club_history_data",
14
+ "fixtures_data",
15
+ "get_clubelo_base_url",
16
+ "set_clubelo_base_url",
17
+ "reset_clubelo_base_url",
18
+ ]
@@ -0,0 +1,112 @@
1
+ import logging
2
+ import threading
3
+ import time
4
+ from typing import Optional
5
+
6
+ from curl_cffi import requests as cf_requests
7
+
8
+ from datafc.exceptions import APIError, RateLimitError, ServerError
9
+ from datafc.utils._cache import get_default_cache
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ClubEloClient:
15
+ """
16
+ HTTP client for the ClubElo public API. Responses are CSV text.
17
+
18
+ Cached payloads are stored as ``{"csv": "<text>"}`` so they share the same
19
+ DiskCache backend used by SofascoreClient.
20
+
21
+ Args:
22
+ rate_limit: Maximum requests per second. Defaults to 2.0.
23
+ timeout: Request timeout in seconds. Defaults to 30.
24
+ retries: Number of retry attempts on transient errors. Defaults to 3.
25
+ cache: Optional DiskCache. Falls back to the module-level default cache.
26
+ """
27
+
28
+ _class_lock: threading.Lock = threading.Lock()
29
+ _class_last_request_time: float = 0.0
30
+
31
+ def __init__(
32
+ self,
33
+ rate_limit: float = 2.0,
34
+ timeout: int = 30,
35
+ retries: int = 3,
36
+ cache=None,
37
+ ) -> None:
38
+ self._min_interval = 1.0 / rate_limit if rate_limit > 0 else 0.0
39
+ self._timeout = timeout
40
+ self._retries = retries
41
+ self._cache = cache if cache is not None else get_default_cache()
42
+ self._session = cf_requests.Session(impersonate="chrome124")
43
+
44
+ def _rate_limit_wait(self) -> None:
45
+ with ClubEloClient._class_lock:
46
+ elapsed = time.monotonic() - ClubEloClient._class_last_request_time
47
+ if elapsed < self._min_interval:
48
+ time.sleep(self._min_interval - elapsed)
49
+ ClubEloClient._class_last_request_time = time.monotonic()
50
+
51
+ def get(self, url: str) -> str:
52
+ """Perform a GET, return the response body as CSV text."""
53
+ if self._cache is not None:
54
+ cached = self._cache.get(url)
55
+ if isinstance(cached, dict) and "csv" in cached:
56
+ logger.debug("Cache hit: %s", url)
57
+ return cached["csv"]
58
+
59
+ last_exc: Optional[Exception] = None
60
+
61
+ for attempt in range(1, self._retries + 1):
62
+ self._rate_limit_wait()
63
+ try:
64
+ response = self._session.get(url, timeout=self._timeout)
65
+
66
+ if response.status_code == 200:
67
+ text = response.text
68
+ if self._cache is not None:
69
+ self._cache.set(url, {"csv": text})
70
+ return text
71
+
72
+ if response.status_code == 429:
73
+ wait = 2 ** attempt
74
+ logger.warning(
75
+ "Rate limited (429). Waiting %ds before retry %d/%d. URL: %s",
76
+ wait, attempt, self._retries, url,
77
+ )
78
+ time.sleep(wait)
79
+ last_exc = RateLimitError(429, url)
80
+ continue
81
+
82
+ if response.status_code in (500, 502, 503, 504):
83
+ wait = 2 ** attempt
84
+ logger.warning(
85
+ "Server error %d. Waiting %ds before retry %d/%d. URL: %s",
86
+ response.status_code, wait, attempt, self._retries, url,
87
+ )
88
+ time.sleep(wait)
89
+ last_exc = ServerError(response.status_code, url)
90
+ continue
91
+
92
+ raise APIError(response.status_code, url)
93
+
94
+ except (APIError, RateLimitError, ServerError):
95
+ raise
96
+ except Exception as exc:
97
+ logger.warning("Request failed (attempt %d/%d): %s", attempt, self._retries, exc)
98
+ last_exc = exc
99
+ time.sleep(2 ** attempt)
100
+
101
+ if isinstance(last_exc, (RateLimitError, ServerError, APIError)):
102
+ raise last_exc
103
+ raise APIError(0, url, f"All {self._retries} attempts failed") from last_exc
104
+
105
+ def close(self) -> None:
106
+ self._session.close()
107
+
108
+ def __enter__(self) -> "ClubEloClient":
109
+ return self
110
+
111
+ def __exit__(self, *_) -> None:
112
+ self.close()
@@ -0,0 +1,40 @@
1
+ from io import StringIO
2
+
3
+ import pandas as pd
4
+
5
+ from datafc.exceptions import DataNotAvailableError
6
+
7
+
8
+ def _read_csv(text: str, *, context: str) -> pd.DataFrame:
9
+ if not text or not text.strip():
10
+ raise DataNotAvailableError(f"Empty response from ClubElo for {context}.")
11
+ df = pd.read_csv(StringIO(text))
12
+ if df.empty:
13
+ raise DataNotAvailableError(f"No rows returned from ClubElo for {context}.")
14
+ return df
15
+
16
+
17
+ def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
18
+ df.columns = [str(c).strip().lower() for c in df.columns]
19
+ return df
20
+
21
+
22
+ def parse_daily_ranking(text: str, date: str) -> pd.DataFrame:
23
+ """Columns: rank, club, country, level, elo, from, to."""
24
+ df = _normalize_columns(_read_csv(text, context=f"date={date}"))
25
+ df.insert(0, "query_date", date)
26
+ return df
27
+
28
+
29
+ def parse_club_history(text: str, club: str) -> pd.DataFrame:
30
+ """Columns: rank, club, country, level, elo, from, to (rank may be 'None')."""
31
+ df = _normalize_columns(_read_csv(text, context=f"club={club}"))
32
+ if "rank" in df.columns:
33
+ df["rank"] = pd.to_numeric(df["rank"], errors="coerce")
34
+ df.insert(0, "query_club", club)
35
+ return df
36
+
37
+
38
+ def parse_fixtures(text: str) -> pd.DataFrame:
39
+ """Upcoming fixtures with per-goal-difference and exact-result probabilities."""
40
+ return _normalize_columns(_read_csv(text, context="fixtures"))
@@ -0,0 +1,34 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+
3
+ import pandas as pd
4
+
5
+ from datafc.clubelo._client import ClubEloClient
6
+ from datafc.clubelo._parsers import parse_club_history
7
+ from datafc.exceptions import InvalidParameterError
8
+ from datafc.utils._config import get_clubelo_base_url
9
+
10
+ if TYPE_CHECKING:
11
+ from datafc.utils._cache import DiskCache
12
+
13
+
14
+ def club_history_data(
15
+ club: str,
16
+ rate_limit: float = 2.0,
17
+ cache: Optional["DiskCache"] = None,
18
+ ) -> pd.DataFrame:
19
+ """Fetches the full Elo rating history for one club.
20
+
21
+ Args:
22
+ club: Club name exactly as ClubElo spells it (use a daily ranking to
23
+ confirm the spelling, e.g. ``"Barcelona"``, ``"ManCity"``).
24
+
25
+ Returns:
26
+ DataFrame with columns: ``query_club, rank, club, country, level, elo, from, to``.
27
+ """
28
+ if not isinstance(club, str) or not club.strip():
29
+ raise InvalidParameterError("club must be a non-empty string.")
30
+ club_clean = club.strip()
31
+ url = f"{get_clubelo_base_url()}/{club_clean}"
32
+ with ClubEloClient(rate_limit=rate_limit, cache=cache) as client:
33
+ text = client.get(url)
34
+ return parse_club_history(text, club_clean)
@@ -0,0 +1,48 @@
1
+ from datetime import date as _date_cls
2
+ from typing import TYPE_CHECKING, Optional, Union
3
+
4
+ import pandas as pd
5
+
6
+ from datafc.clubelo._client import ClubEloClient
7
+ from datafc.clubelo._parsers import parse_daily_ranking
8
+ from datafc.exceptions import InvalidParameterError
9
+ from datafc.utils._config import get_clubelo_base_url
10
+
11
+ if TYPE_CHECKING:
12
+ from datafc.utils._cache import DiskCache
13
+
14
+
15
+ def _coerce_date(value: Union[str, _date_cls]) -> str:
16
+ if isinstance(value, _date_cls):
17
+ return value.strftime("%Y-%m-%d")
18
+ if isinstance(value, str):
19
+ try:
20
+ return _date_cls.fromisoformat(value).strftime("%Y-%m-%d")
21
+ except ValueError as exc:
22
+ raise InvalidParameterError(
23
+ f"date must be 'YYYY-MM-DD' or a datetime.date; got {value!r}."
24
+ ) from exc
25
+ raise InvalidParameterError(
26
+ f"date must be a string or datetime.date; got {type(value).__name__}."
27
+ )
28
+
29
+
30
+ def daily_ranking_data(
31
+ date: Union[str, _date_cls],
32
+ rate_limit: float = 2.0,
33
+ cache: Optional["DiskCache"] = None,
34
+ ) -> pd.DataFrame:
35
+ """Fetches the full ClubElo ranking for the given calendar day.
36
+
37
+ Args:
38
+ date: 'YYYY-MM-DD' string or ``datetime.date``. Values before 1960 are
39
+ considered provisional by ClubElo.
40
+
41
+ Returns:
42
+ DataFrame with columns: ``query_date, rank, club, country, level, elo, from, to``.
43
+ """
44
+ date_str = _coerce_date(date)
45
+ url = f"{get_clubelo_base_url()}/{date_str}"
46
+ with ClubEloClient(rate_limit=rate_limit, cache=cache) as client:
47
+ text = client.get(url)
48
+ return parse_daily_ranking(text, date_str)