datafc 2.7.0__tar.gz → 2.7.2__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.
- {datafc-2.7.0 → datafc-2.7.2}/PKG-INFO +29 -6
- {datafc-2.7.0 → datafc-2.7.2}/README.md +28 -5
- {datafc-2.7.0 → datafc-2.7.2}/datafc/__init__.py +1 -1
- {datafc-2.7.0 → datafc-2.7.2}/datafc/clubelo/__init__.py +18 -18
- {datafc-2.7.0 → datafc-2.7.2}/datafc/clubelo/_client.py +112 -112
- {datafc-2.7.0 → datafc-2.7.2}/datafc/clubelo/_parsers.py +40 -40
- {datafc-2.7.0 → datafc-2.7.2}/datafc/clubelo/fetch_club_history_data.py +34 -34
- {datafc-2.7.0 → datafc-2.7.2}/datafc/clubelo/fetch_daily_ranking_data.py +48 -48
- {datafc-2.7.0 → datafc-2.7.2}/datafc/clubelo/fetch_fixtures_data.py +27 -27
- {datafc-2.7.0 → datafc-2.7.2}/datafc/eloratings/__init__.py +26 -26
- {datafc-2.7.0 → datafc-2.7.2}/datafc/eloratings/_client.py +112 -112
- {datafc-2.7.0 → datafc-2.7.2}/datafc/eloratings/_parsers.py +212 -212
- {datafc-2.7.0 → datafc-2.7.2}/datafc/eloratings/fetch_country_codes_data.py +28 -28
- {datafc-2.7.0 → datafc-2.7.2}/datafc/eloratings/fetch_country_matches_data.py +35 -35
- {datafc-2.7.0 → datafc-2.7.2}/datafc/eloratings/fetch_teams_data.py +27 -27
- {datafc-2.7.0 → datafc-2.7.2}/datafc/eloratings/fetch_tournament_codes_data.py +28 -28
- {datafc-2.7.0 → datafc-2.7.2}/datafc/eloratings/fetch_tournament_editions_data.py +30 -30
- {datafc-2.7.0 → datafc-2.7.2}/datafc/eloratings/fetch_tournament_groups_data.py +33 -33
- {datafc-2.7.0 → datafc-2.7.2}/datafc/eloratings/fetch_world_ranking_data.py +26 -26
- {datafc-2.7.0 → datafc-2.7.2}/datafc/utils/_config.py +6 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc.egg-info/PKG-INFO +29 -6
- {datafc-2.7.0 → datafc-2.7.2}/pyproject.toml +1 -1
- {datafc-2.7.0 → datafc-2.7.2}/LICENSE +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/exceptions.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/__init__.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/_core.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/_parsers.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/aio.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_average_positions_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_coordinates_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_formations_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_goal_networks_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_incidents_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_league_player_stats_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_lineups_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_match_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_match_details_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_match_h2h_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_match_odds_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_match_stats_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_momentum_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_past_matches_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_player_attribute_overviews_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_player_career_stats_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_player_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_player_match_log_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_player_national_team_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_player_stats_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_player_transfers_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_pregame_form_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_referee_stats_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_search_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_season_rounds_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_seasons_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_shots_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_squad_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_standings_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_substitutions_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_team_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_team_match_history_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_team_stats_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_team_transfers_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/sofascore/fetch_upcoming_matches_data.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/utils/__init__.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/utils/_async_client.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/utils/_cache.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/utils/_client.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/utils/_helpers.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/utils/_save_files.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/utils/_tournament_info.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc/utils/_validate.py +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc.egg-info/SOURCES.txt +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc.egg-info/dependency_links.txt +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc.egg-info/requires.txt +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/datafc.egg-info/top_level.txt +0 -0
- {datafc-2.7.0 → datafc-2.7.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datafc
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.2
|
|
4
4
|
Summary: Fetch, process, and export structured football data.
|
|
5
5
|
Author-email: Uraz Akgül <urazdev@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -24,7 +24,7 @@ Requires-Dist: pytest>=8.0; extra == "dev"
|
|
|
24
24
|
Requires-Dist: pytest-mock>=3.12; extra == "dev"
|
|
25
25
|
Dynamic: license-file
|
|
26
26
|
|
|
27
|
-
# datafc v2.7.
|
|
27
|
+
# datafc v2.7.2
|
|
28
28
|
|
|
29
29
|
## Overview
|
|
30
30
|
|
|
@@ -580,17 +580,28 @@ wc_group_df = match_data(
|
|
|
580
580
|
tournament_type="world_cup",
|
|
581
581
|
tournament_stage="group_stage_week",
|
|
582
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
|
+
)
|
|
583
593
|
```
|
|
584
594
|
|
|
585
595
|
Parameters:
|
|
586
596
|
|
|
587
597
|
- `tournament_id` (int)
|
|
588
598
|
- `season_id` (int)
|
|
589
|
-
- `week_number` (int, optional): Required for league rounds, UEFA stages,
|
|
590
|
-
- `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.
|
|
591
601
|
- `tournament_stage` (str, optional): Required when `tournament_type` is set.
|
|
592
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`.
|
|
593
603
|
- `"world_cup"` options: `group_stage_week`, `round_of_32`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
|
|
604
|
+
- `"league_playoff"` options: `qualification_round`, `quarterfinals`, `semifinals`, `final`. For `qualification_round`, `week_number` doubles as the round number in the slug (`1` → `qualification-round-1`, `2` → `qualification-round-2`).
|
|
594
605
|
|
|
595
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`.
|
|
596
607
|
|
|
@@ -843,8 +854,8 @@ Parameters:
|
|
|
843
854
|
|
|
844
855
|
- `tournament_id` (int)
|
|
845
856
|
- `season_id` (int)
|
|
846
|
-
- `week_number` (int, optional): Required for league rounds, UEFA stages,
|
|
847
|
-
- `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.
|
|
848
859
|
- `tournament_stage` (str, optional): Required when `tournament_type` is set. Same options as `match_data`.
|
|
849
860
|
|
|
850
861
|
Same columns as `match_data`.
|
|
@@ -1067,6 +1078,18 @@ eloratings.set_eloratings_base_url("http://www.eloratings.net")
|
|
|
1067
1078
|
|
|
1068
1079
|
## What's new
|
|
1069
1080
|
|
|
1081
|
+
### v2.7.2
|
|
1082
|
+
|
|
1083
|
+
- **`league_playoff` gains `qualification_round`.** `match_data` and `past_matches_data` now cover the early elimination rounds of domestic promotion playoffs (e.g. TFF 1. Lig 1. and 2. eleme turu). `week_number` doubles as the round number in the slug (`1` → `qualification-round-1`, `2` → `qualification-round-2`), matching the existing UEFA `qualification_round` convention.
|
|
1084
|
+
|
|
1085
|
+
---
|
|
1086
|
+
|
|
1087
|
+
### v2.7.1
|
|
1088
|
+
|
|
1089
|
+
- **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.
|
|
1090
|
+
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1070
1093
|
### v2.7.0
|
|
1071
1094
|
|
|
1072
1095
|
- **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`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# datafc v2.7.
|
|
1
|
+
# datafc v2.7.2
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
@@ -554,17 +554,28 @@ wc_group_df = match_data(
|
|
|
554
554
|
tournament_type="world_cup",
|
|
555
555
|
tournament_stage="group_stage_week",
|
|
556
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
|
+
)
|
|
557
567
|
```
|
|
558
568
|
|
|
559
569
|
Parameters:
|
|
560
570
|
|
|
561
571
|
- `tournament_id` (int)
|
|
562
572
|
- `season_id` (int)
|
|
563
|
-
- `week_number` (int, optional): Required for league rounds, UEFA stages,
|
|
564
|
-
- `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.
|
|
565
575
|
- `tournament_stage` (str, optional): Required when `tournament_type` is set.
|
|
566
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`.
|
|
567
577
|
- `"world_cup"` options: `group_stage_week`, `round_of_32`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
|
|
578
|
+
- `"league_playoff"` options: `qualification_round`, `quarterfinals`, `semifinals`, `final`. For `qualification_round`, `week_number` doubles as the round number in the slug (`1` → `qualification-round-1`, `2` → `qualification-round-2`).
|
|
568
579
|
|
|
569
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`.
|
|
570
581
|
|
|
@@ -817,8 +828,8 @@ Parameters:
|
|
|
817
828
|
|
|
818
829
|
- `tournament_id` (int)
|
|
819
830
|
- `season_id` (int)
|
|
820
|
-
- `week_number` (int, optional): Required for league rounds, UEFA stages,
|
|
821
|
-
- `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.
|
|
822
833
|
- `tournament_stage` (str, optional): Required when `tournament_type` is set. Same options as `match_data`.
|
|
823
834
|
|
|
824
835
|
Same columns as `match_data`.
|
|
@@ -1041,6 +1052,18 @@ eloratings.set_eloratings_base_url("http://www.eloratings.net")
|
|
|
1041
1052
|
|
|
1042
1053
|
## What's new
|
|
1043
1054
|
|
|
1055
|
+
### v2.7.2
|
|
1056
|
+
|
|
1057
|
+
- **`league_playoff` gains `qualification_round`.** `match_data` and `past_matches_data` now cover the early elimination rounds of domestic promotion playoffs (e.g. TFF 1. Lig 1. and 2. eleme turu). `week_number` doubles as the round number in the slug (`1` → `qualification-round-1`, `2` → `qualification-round-2`), matching the existing UEFA `qualification_round` convention.
|
|
1058
|
+
|
|
1059
|
+
---
|
|
1060
|
+
|
|
1061
|
+
### v2.7.1
|
|
1062
|
+
|
|
1063
|
+
- **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.
|
|
1064
|
+
|
|
1065
|
+
---
|
|
1066
|
+
|
|
1044
1067
|
### v2.7.0
|
|
1045
1068
|
|
|
1046
1069
|
- **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`.
|
|
@@ -1,18 +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
|
-
]
|
|
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
|
+
]
|
|
@@ -1,112 +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()
|
|
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()
|
|
@@ -1,40 +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"))
|
|
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"))
|
|
@@ -1,34 +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)
|
|
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)
|