datafc 2.5.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.5.0 → datafc-2.7.0}/PKG-INFO +150 -4
  2. {datafc-2.5.0 → datafc-2.7.0}/README.md +148 -2
  3. {datafc-2.5.0 → datafc-2.7.0}/datafc/__init__.py +8 -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.5.0 → datafc-2.7.0}/datafc/sofascore/__init__.py +2 -0
  21. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/_parsers.py +18 -0
  22. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/aio.py +23 -0
  23. datafc-2.7.0/datafc/sofascore/fetch_formations_data.py +44 -0
  24. {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_config.py +62 -0
  25. {datafc-2.5.0 → datafc-2.7.0}/datafc.egg-info/PKG-INFO +150 -4
  26. {datafc-2.5.0 → datafc-2.7.0}/datafc.egg-info/SOURCES.txt +17 -0
  27. {datafc-2.5.0 → datafc-2.7.0}/pyproject.toml +2 -2
  28. {datafc-2.5.0 → datafc-2.7.0}/LICENSE +0 -0
  29. {datafc-2.5.0 → datafc-2.7.0}/datafc/exceptions.py +0 -0
  30. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/_core.py +0 -0
  31. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_average_positions_data.py +0 -0
  32. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_coordinates_data.py +0 -0
  33. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_goal_networks_data.py +0 -0
  34. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_incidents_data.py +0 -0
  35. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_league_player_stats_data.py +0 -0
  36. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_lineups_data.py +0 -0
  37. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_data.py +0 -0
  38. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_details_data.py +0 -0
  39. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_h2h_data.py +0 -0
  40. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_odds_data.py +0 -0
  41. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_stats_data.py +0 -0
  42. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_momentum_data.py +0 -0
  43. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_past_matches_data.py +0 -0
  44. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_attribute_overviews_data.py +0 -0
  45. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_career_stats_data.py +0 -0
  46. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_data.py +0 -0
  47. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_match_log_data.py +0 -0
  48. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_national_team_data.py +0 -0
  49. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_stats_data.py +0 -0
  50. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_transfers_data.py +0 -0
  51. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_pregame_form_data.py +0 -0
  52. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_referee_stats_data.py +0 -0
  53. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_search_data.py +0 -0
  54. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_season_rounds_data.py +0 -0
  55. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_seasons_data.py +0 -0
  56. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_shots_data.py +0 -0
  57. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_squad_data.py +0 -0
  58. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_standings_data.py +0 -0
  59. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_substitutions_data.py +0 -0
  60. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_data.py +0 -0
  61. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_match_history_data.py +0 -0
  62. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_stats_data.py +0 -0
  63. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_transfers_data.py +0 -0
  64. {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_upcoming_matches_data.py +0 -0
  65. {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/__init__.py +0 -0
  66. {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_async_client.py +0 -0
  67. {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_cache.py +0 -0
  68. {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_client.py +0 -0
  69. {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_helpers.py +0 -0
  70. {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_save_files.py +0 -0
  71. {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_tournament_info.py +0 -0
  72. {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_validate.py +0 -0
  73. {datafc-2.5.0 → datafc-2.7.0}/datafc.egg-info/dependency_links.txt +0 -0
  74. {datafc-2.5.0 → datafc-2.7.0}/datafc.egg-info/requires.txt +0 -0
  75. {datafc-2.5.0 → datafc-2.7.0}/datafc.egg-info/top_level.txt +0 -0
  76. {datafc-2.5.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.5.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,16 +24,27 @@ 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.5.0
27
+ # datafc v2.7.0
28
28
 
29
29
  ## Overview
30
30
 
31
- `datafc` fetches, processes, and exports structured football data. It provides **33 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`.
35
35
  > - **Programmatically:** use `search_data("super lig", entity_type="tournament")` to find the tournament ID, then `seasons_data(tournament_id)` to list all available seasons.
36
36
 
37
+ ---
38
+
39
+ <p align="center">
40
+ If you find <strong>datafc</strong> useful, consider supporting the project ☕<br><br>
41
+ <a href="https://buymeacoffee.com/urazdev">
42
+ <img src="https://img.shields.io/badge/Buy%20Me%20A%20Coffee-urazdev-%23FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me A Coffee">
43
+ </a>
44
+ </p>
45
+
46
+ ---
47
+
37
48
  ## What Data Can You Access?
38
49
 
39
50
  ### Discovery
@@ -69,6 +80,7 @@ Dynamic: license-file
69
80
  |---|---|
70
81
  | `match_data` | Fixtures for a matchweek: score, status, home/away teams |
71
82
  | `match_details_data` | Referee info (name, cards, games) and venue details per match |
83
+ | `formations_data` | Home and away formation for each match |
72
84
  | `match_stats_data` | Aggregate team stats per match (possession, shots, fouls, …) |
73
85
  | `match_odds_data` | Pre-match 1/X/2 betting odds |
74
86
  | `match_h2h_data` | All-time H2H record between the two teams |
@@ -100,6 +112,26 @@ Dynamic: license-file
100
112
  |---|---|
101
113
  | `referee_stats_data` | Career stats for a referee: games, cards, and per-game averages |
102
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
+
103
135
  > **Coverage:** Any league and season available on Sofascore. For Turkey Super Lig, every season from 1980/81 to the present is accessible.
104
136
 
105
137
  ## Installation
@@ -580,6 +612,22 @@ Dependencies: `match_data`
580
612
 
581
613
  ---
582
614
 
615
+ #### `formations_data`
616
+
617
+ Fetch home and away formation for each match.
618
+
619
+ ```python
620
+ from datafc import formations_data
621
+
622
+ df = formations_data(match_df=match_df)
623
+ ```
624
+
625
+ Columns: `country`, `tournament`, `season`, `week`, `game_id`, `team` (`home` / `away`), `formation`.
626
+
627
+ Dependencies: `match_data`
628
+
629
+ ---
630
+
583
631
  #### `match_stats_data`
584
632
 
585
633
  Fetch aggregate team statistics (possession, shots, passes, etc.) for each match.
@@ -933,8 +981,106 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
933
981
 
934
982
  ---
935
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
+
936
1068
  ## What's new
937
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
+
1078
+ ### v2.6.0
1079
+
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`).
1081
+
1082
+ ---
1083
+
938
1084
  ### v2.5.0
939
1085
 
940
1086
  - **`player_data` now returns `position_detailed`.** New column containing Sofascore's detailed position list (e.g. `["AM", "MC", "RW"]`) alongside the existing single `position` field.
@@ -1,13 +1,24 @@
1
- # datafc v2.5.0
1
+ # datafc v2.7.0
2
2
 
3
3
  ## Overview
4
4
 
5
- `datafc` fetches, processes, and exports structured football data. It provides **33 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`.
9
9
  > - **Programmatically:** use `search_data("super lig", entity_type="tournament")` to find the tournament ID, then `seasons_data(tournament_id)` to list all available seasons.
10
10
 
11
+ ---
12
+
13
+ <p align="center">
14
+ If you find <strong>datafc</strong> useful, consider supporting the project ☕<br><br>
15
+ <a href="https://buymeacoffee.com/urazdev">
16
+ <img src="https://img.shields.io/badge/Buy%20Me%20A%20Coffee-urazdev-%23FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me A Coffee">
17
+ </a>
18
+ </p>
19
+
20
+ ---
21
+
11
22
  ## What Data Can You Access?
12
23
 
13
24
  ### Discovery
@@ -43,6 +54,7 @@
43
54
  |---|---|
44
55
  | `match_data` | Fixtures for a matchweek: score, status, home/away teams |
45
56
  | `match_details_data` | Referee info (name, cards, games) and venue details per match |
57
+ | `formations_data` | Home and away formation for each match |
46
58
  | `match_stats_data` | Aggregate team stats per match (possession, shots, fouls, …) |
47
59
  | `match_odds_data` | Pre-match 1/X/2 betting odds |
48
60
  | `match_h2h_data` | All-time H2H record between the two teams |
@@ -74,6 +86,26 @@
74
86
  |---|---|
75
87
  | `referee_stats_data` | Career stats for a referee: games, cards, and per-game averages |
76
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
+
77
109
  > **Coverage:** Any league and season available on Sofascore. For Turkey Super Lig, every season from 1980/81 to the present is accessible.
78
110
 
79
111
  ## Installation
@@ -554,6 +586,22 @@ Dependencies: `match_data`
554
586
 
555
587
  ---
556
588
 
589
+ #### `formations_data`
590
+
591
+ Fetch home and away formation for each match.
592
+
593
+ ```python
594
+ from datafc import formations_data
595
+
596
+ df = formations_data(match_df=match_df)
597
+ ```
598
+
599
+ Columns: `country`, `tournament`, `season`, `week`, `game_id`, `team` (`home` / `away`), `formation`.
600
+
601
+ Dependencies: `match_data`
602
+
603
+ ---
604
+
557
605
  #### `match_stats_data`
558
606
 
559
607
  Fetch aggregate team statistics (possession, shots, passes, etc.) for each match.
@@ -907,8 +955,106 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
907
955
 
908
956
  ---
909
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
+
910
1042
  ## What's new
911
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
+
1052
+ ### v2.6.0
1053
+
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`).
1055
+
1056
+ ---
1057
+
912
1058
  ### v2.5.0
913
1059
 
914
1060
  - **`player_data` now returns `position_detailed`.** New column containing Sofascore's detailed position list (e.g. `["AM", "MC", "RW"]`) alongside the existing single `position` field.
@@ -1,6 +1,8 @@
1
- __version__ = "2.5.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,
@@ -36,6 +38,7 @@ __all__ = [
36
38
  # Matchweek
37
39
  "match_data",
38
40
  "match_details_data",
41
+ "formations_data",
39
42
  "match_stats_data",
40
43
  "match_odds_data",
41
44
  "match_h2h_data",
@@ -60,6 +63,10 @@ __all__ = [
60
63
  "referee_stats_data",
61
64
  # Async API
62
65
  "aio",
66
+ # ClubElo subpackage
67
+ "clubelo",
68
+ # EloRatings subpackage
69
+ "eloratings",
63
70
  # Exceptions
64
71
  "DataFCError",
65
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)