datafc 2.6.0__tar.gz → 2.7.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 (76) hide show
  1. {datafc-2.6.0 → datafc-2.7.0}/PKG-INFO +116 -4
  2. {datafc-2.6.0 → datafc-2.7.0}/README.md +114 -2
  3. {datafc-2.6.0 → datafc-2.7.0}/datafc/__init__.py +7 -1
  4. datafc-2.7.0/datafc/clubelo/__init__.py +18 -0
  5. datafc-2.7.0/datafc/clubelo/_client.py +112 -0
  6. datafc-2.7.0/datafc/clubelo/_parsers.py +40 -0
  7. datafc-2.7.0/datafc/clubelo/fetch_club_history_data.py +34 -0
  8. datafc-2.7.0/datafc/clubelo/fetch_daily_ranking_data.py +48 -0
  9. datafc-2.7.0/datafc/clubelo/fetch_fixtures_data.py +27 -0
  10. datafc-2.7.0/datafc/eloratings/__init__.py +26 -0
  11. datafc-2.7.0/datafc/eloratings/_client.py +112 -0
  12. datafc-2.7.0/datafc/eloratings/_parsers.py +212 -0
  13. datafc-2.7.0/datafc/eloratings/fetch_country_codes_data.py +28 -0
  14. datafc-2.7.0/datafc/eloratings/fetch_country_matches_data.py +35 -0
  15. datafc-2.7.0/datafc/eloratings/fetch_teams_data.py +27 -0
  16. datafc-2.7.0/datafc/eloratings/fetch_tournament_codes_data.py +28 -0
  17. datafc-2.7.0/datafc/eloratings/fetch_tournament_editions_data.py +30 -0
  18. datafc-2.7.0/datafc/eloratings/fetch_tournament_groups_data.py +33 -0
  19. datafc-2.7.0/datafc/eloratings/fetch_world_ranking_data.py +26 -0
  20. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_formations_data.py +44 -44
  21. {datafc-2.6.0 → datafc-2.7.0}/datafc/utils/_config.py +62 -0
  22. {datafc-2.6.0 → datafc-2.7.0}/datafc.egg-info/PKG-INFO +116 -4
  23. {datafc-2.6.0 → datafc-2.7.0}/datafc.egg-info/SOURCES.txt +16 -0
  24. {datafc-2.6.0 → datafc-2.7.0}/pyproject.toml +2 -2
  25. {datafc-2.6.0 → datafc-2.7.0}/LICENSE +0 -0
  26. {datafc-2.6.0 → datafc-2.7.0}/datafc/exceptions.py +0 -0
  27. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/__init__.py +0 -0
  28. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/_core.py +0 -0
  29. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/_parsers.py +0 -0
  30. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/aio.py +0 -0
  31. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_average_positions_data.py +0 -0
  32. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_coordinates_data.py +0 -0
  33. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_goal_networks_data.py +0 -0
  34. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_incidents_data.py +0 -0
  35. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_league_player_stats_data.py +0 -0
  36. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_lineups_data.py +0 -0
  37. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_data.py +0 -0
  38. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_details_data.py +0 -0
  39. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_h2h_data.py +0 -0
  40. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_odds_data.py +0 -0
  41. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_stats_data.py +0 -0
  42. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_momentum_data.py +0 -0
  43. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_past_matches_data.py +0 -0
  44. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_attribute_overviews_data.py +0 -0
  45. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_career_stats_data.py +0 -0
  46. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_data.py +0 -0
  47. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_match_log_data.py +0 -0
  48. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_national_team_data.py +0 -0
  49. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_stats_data.py +0 -0
  50. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_transfers_data.py +0 -0
  51. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_pregame_form_data.py +0 -0
  52. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_referee_stats_data.py +0 -0
  53. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_search_data.py +0 -0
  54. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_season_rounds_data.py +0 -0
  55. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_seasons_data.py +0 -0
  56. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_shots_data.py +0 -0
  57. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_squad_data.py +0 -0
  58. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_standings_data.py +0 -0
  59. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_substitutions_data.py +0 -0
  60. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_data.py +0 -0
  61. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_match_history_data.py +0 -0
  62. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_stats_data.py +0 -0
  63. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_transfers_data.py +0 -0
  64. {datafc-2.6.0 → datafc-2.7.0}/datafc/sofascore/fetch_upcoming_matches_data.py +0 -0
  65. {datafc-2.6.0 → datafc-2.7.0}/datafc/utils/__init__.py +0 -0
  66. {datafc-2.6.0 → datafc-2.7.0}/datafc/utils/_async_client.py +0 -0
  67. {datafc-2.6.0 → datafc-2.7.0}/datafc/utils/_cache.py +0 -0
  68. {datafc-2.6.0 → datafc-2.7.0}/datafc/utils/_client.py +0 -0
  69. {datafc-2.6.0 → datafc-2.7.0}/datafc/utils/_helpers.py +0 -0
  70. {datafc-2.6.0 → datafc-2.7.0}/datafc/utils/_save_files.py +0 -0
  71. {datafc-2.6.0 → datafc-2.7.0}/datafc/utils/_tournament_info.py +0 -0
  72. {datafc-2.6.0 → datafc-2.7.0}/datafc/utils/_validate.py +0 -0
  73. {datafc-2.6.0 → datafc-2.7.0}/datafc.egg-info/dependency_links.txt +0 -0
  74. {datafc-2.6.0 → datafc-2.7.0}/datafc.egg-info/requires.txt +0 -0
  75. {datafc-2.6.0 → datafc-2.7.0}/datafc.egg-info/top_level.txt +0 -0
  76. {datafc-2.6.0 → datafc-2.7.0}/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.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
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.0
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
@@ -961,8 +981,100 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
961
981
 
962
982
  ---
963
983
 
984
+ ### ClubElo
985
+
986
+ 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.
987
+
988
+ ```python
989
+ from datafc import clubelo
990
+
991
+ # Full ranking on a given day
992
+ df = clubelo.daily_ranking_data("2026-06-10")
993
+
994
+ # A single club's full Elo history (use the spelling shown in daily_ranking_data)
995
+ hist = clubelo.club_history_data("Man City")
996
+
997
+ # Upcoming matches with per-goal-difference and exact-result probabilities
998
+ fix = clubelo.fixtures_data()
999
+ ```
1000
+
1001
+ Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (any `DiskCache` instance, shared with the Sofascore functions).
1002
+
1003
+ > **Notes:**
1004
+ > - 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.
1005
+ > - Values before 1960 are considered provisional by ClubElo.
1006
+ > - Dates accept either `"YYYY-MM-DD"` strings or `datetime.date` objects.
1007
+
1008
+ 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:
1009
+
1010
+ ```python
1011
+ from datafc import clubelo
1012
+ clubelo.set_clubelo_base_url("http://api.clubelo.com")
1013
+ ```
1014
+
1015
+ `clubelo.reset_clubelo_base_url()` restores the default.
1016
+
1017
+ ---
1018
+
1019
+ ### EloRatings (national teams)
1020
+
1021
+ [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.
1022
+
1023
+ ```python
1024
+ from datafc import eloratings
1025
+
1026
+ # Current global ranking of all national teams (30 columns)
1027
+ df = eloratings.world_ranking_data()
1028
+
1029
+ # Full match history for a single national team
1030
+ matches = eloratings.country_matches_data("Spain")
1031
+
1032
+ # Code reference tables (handy for joining onto the above)
1033
+ countries = eloratings.country_codes_data() # country_code → country_name
1034
+ tournaments = eloratings.tournament_codes_data() # tournament_code → tournament_name
1035
+ groups = eloratings.tournament_groups_data() # specific code → group code (long format)
1036
+ editions = eloratings.tournament_editions_data() # every tournament edition + dates + slug
1037
+
1038
+ # Legacy/historical country code → ISO code mapping
1039
+ teams = eloratings.teams_data()
1040
+
1041
+ # Example: enrich Spain's matches with full tournament names
1042
+ spain = (
1043
+ eloratings.country_matches_data("Spain")
1044
+ .merge(tournaments, left_on="tournament", right_on="tournament_code", how="left")
1045
+ )
1046
+
1047
+ # Example: every World Cup final tournament (filter editions on the WC group)
1048
+ wc_finals = editions[(editions["code"] == "WC") & editions["start_date"].notna()]
1049
+ ```
1050
+
1051
+ Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (same `DiskCache` instance as Sofascore/ClubElo).
1052
+
1053
+ > **Column names are verified against the site's JavaScript source.**
1054
+ > 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.
1055
+
1056
+ > **Country argument is a page slug, not an ISO code.**
1057
+ > `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"`).
1058
+
1059
+ Override the base URL the same way as ClubElo if needed:
1060
+
1061
+ ```python
1062
+ from datafc import eloratings
1063
+ eloratings.set_eloratings_base_url("http://www.eloratings.net")
1064
+ ```
1065
+
1066
+ ---
1067
+
964
1068
  ## What's new
965
1069
 
1070
+ ### v2.7.0
1071
+
1072
+ - **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`.
1073
+ - **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`.
1074
+ - **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.
1075
+
1076
+ ---
1077
+
966
1078
  ### v2.6.0
967
1079
 
968
1080
  - **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.0
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
@@ -935,8 +955,100 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
935
955
 
936
956
  ---
937
957
 
958
+ ### ClubElo
959
+
960
+ 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.
961
+
962
+ ```python
963
+ from datafc import clubelo
964
+
965
+ # Full ranking on a given day
966
+ df = clubelo.daily_ranking_data("2026-06-10")
967
+
968
+ # A single club's full Elo history (use the spelling shown in daily_ranking_data)
969
+ hist = clubelo.club_history_data("Man City")
970
+
971
+ # Upcoming matches with per-goal-difference and exact-result probabilities
972
+ fix = clubelo.fixtures_data()
973
+ ```
974
+
975
+ Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (any `DiskCache` instance, shared with the Sofascore functions).
976
+
977
+ > **Notes:**
978
+ > - 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.
979
+ > - Values before 1960 are considered provisional by ClubElo.
980
+ > - Dates accept either `"YYYY-MM-DD"` strings or `datetime.date` objects.
981
+
982
+ 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:
983
+
984
+ ```python
985
+ from datafc import clubelo
986
+ clubelo.set_clubelo_base_url("http://api.clubelo.com")
987
+ ```
988
+
989
+ `clubelo.reset_clubelo_base_url()` restores the default.
990
+
991
+ ---
992
+
993
+ ### EloRatings (national teams)
994
+
995
+ [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.
996
+
997
+ ```python
998
+ from datafc import eloratings
999
+
1000
+ # Current global ranking of all national teams (30 columns)
1001
+ df = eloratings.world_ranking_data()
1002
+
1003
+ # Full match history for a single national team
1004
+ matches = eloratings.country_matches_data("Spain")
1005
+
1006
+ # Code reference tables (handy for joining onto the above)
1007
+ countries = eloratings.country_codes_data() # country_code → country_name
1008
+ tournaments = eloratings.tournament_codes_data() # tournament_code → tournament_name
1009
+ groups = eloratings.tournament_groups_data() # specific code → group code (long format)
1010
+ editions = eloratings.tournament_editions_data() # every tournament edition + dates + slug
1011
+
1012
+ # Legacy/historical country code → ISO code mapping
1013
+ teams = eloratings.teams_data()
1014
+
1015
+ # Example: enrich Spain's matches with full tournament names
1016
+ spain = (
1017
+ eloratings.country_matches_data("Spain")
1018
+ .merge(tournaments, left_on="tournament", right_on="tournament_code", how="left")
1019
+ )
1020
+
1021
+ # Example: every World Cup final tournament (filter editions on the WC group)
1022
+ wc_finals = editions[(editions["code"] == "WC") & editions["start_date"].notna()]
1023
+ ```
1024
+
1025
+ Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (same `DiskCache` instance as Sofascore/ClubElo).
1026
+
1027
+ > **Column names are verified against the site's JavaScript source.**
1028
+ > 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.
1029
+
1030
+ > **Country argument is a page slug, not an ISO code.**
1031
+ > `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"`).
1032
+
1033
+ Override the base URL the same way as ClubElo if needed:
1034
+
1035
+ ```python
1036
+ from datafc import eloratings
1037
+ eloratings.set_eloratings_base_url("http://www.eloratings.net")
1038
+ ```
1039
+
1040
+ ---
1041
+
938
1042
  ## What's new
939
1043
 
1044
+ ### v2.7.0
1045
+
1046
+ - **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`.
1047
+ - **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`.
1048
+ - **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.
1049
+
1050
+ ---
1051
+
940
1052
  ### v2.6.0
941
1053
 
942
1054
  - **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.0"
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)
@@ -0,0 +1,27 @@
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_fixtures
7
+ from datafc.utils._config import get_clubelo_base_url
8
+
9
+ if TYPE_CHECKING:
10
+ from datafc.utils._cache import DiskCache
11
+
12
+
13
+ def fixtures_data(
14
+ rate_limit: float = 2.0,
15
+ cache: Optional["DiskCache"] = None,
16
+ ) -> pd.DataFrame:
17
+ """Fetches calculated probabilities for all upcoming ClubElo fixtures.
18
+
19
+ Each row gives goal-difference probabilities from -5 to +5 (with the tails
20
+ aggregated) and exact-result probabilities for matches with 6 or fewer
21
+ total goals. For 1X2 odds: sum negative goal differences for an away win
22
+ and positive ones for a home win; goal difference == 0 is the draw.
23
+ """
24
+ url = f"{get_clubelo_base_url()}/Fixtures"
25
+ with ClubEloClient(rate_limit=rate_limit, cache=cache) as client:
26
+ text = client.get(url)
27
+ return parse_fixtures(text)
@@ -0,0 +1,26 @@
1
+ from datafc.utils._config import (
2
+ get_eloratings_base_url,
3
+ reset_eloratings_base_url,
4
+ set_eloratings_base_url,
5
+ )
6
+
7
+ from .fetch_country_codes_data import country_codes_data
8
+ from .fetch_country_matches_data import country_matches_data
9
+ from .fetch_teams_data import teams_data
10
+ from .fetch_tournament_codes_data import tournament_codes_data
11
+ from .fetch_tournament_editions_data import tournament_editions_data
12
+ from .fetch_tournament_groups_data import tournament_groups_data
13
+ from .fetch_world_ranking_data import world_ranking_data
14
+
15
+ __all__ = [
16
+ "world_ranking_data",
17
+ "country_matches_data",
18
+ "country_codes_data",
19
+ "tournament_codes_data",
20
+ "tournament_editions_data",
21
+ "tournament_groups_data",
22
+ "teams_data",
23
+ "get_eloratings_base_url",
24
+ "set_eloratings_base_url",
25
+ "reset_eloratings_base_url",
26
+ ]