ps3838api 1.1.0__py3-none-any.whl
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.
- ps3838api/__init__.py +7 -0
- ps3838api/api/__init__.py +45 -0
- ps3838api/api/client.py +516 -0
- ps3838api/api/default_client.py +231 -0
- ps3838api/api/v4client.py +107 -0
- ps3838api/matching.py +141 -0
- ps3838api/models/__init__.py +0 -0
- ps3838api/models/bets.py +250 -0
- ps3838api/models/client.py +48 -0
- ps3838api/models/errors.py +43 -0
- ps3838api/models/event.py +61 -0
- ps3838api/models/fixtures.py +53 -0
- ps3838api/models/lines.py +27 -0
- ps3838api/models/odds.py +221 -0
- ps3838api/models/sports.py +256 -0
- ps3838api/models/tank.py +6 -0
- ps3838api/py.typed +0 -0
- ps3838api/tank.py +93 -0
- ps3838api/totals.py +62 -0
- ps3838api/utils/match_leagues.py +90 -0
- ps3838api/utils/ops.py +146 -0
- ps3838api-1.1.0.dist-info/METADATA +176 -0
- ps3838api-1.1.0.dist-info/RECORD +26 -0
- ps3838api-1.1.0.dist-info/WHEEL +5 -0
- ps3838api-1.1.0.dist-info/licenses/LICENSE +8 -0
- ps3838api-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sport identifiers exposed by the PS3838 API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Sport(IntEnum):
|
|
9
|
+
BADMINTON_SPORT_ID = 1
|
|
10
|
+
BANDY_SPORT_ID = 2
|
|
11
|
+
BASEBALL_SPORT_ID = 3
|
|
12
|
+
BASKETBALL_SPORT_ID = 4
|
|
13
|
+
BEACH_VOLLEYBALL_SPORT_ID = 5
|
|
14
|
+
BOXING_SPORT_ID = 6
|
|
15
|
+
CHESS_SPORT_ID = 7
|
|
16
|
+
CRICKET_SPORT_ID = 8
|
|
17
|
+
CURLING_SPORT_ID = 9
|
|
18
|
+
DARTS_SPORT_ID = 10
|
|
19
|
+
FIELD_HOCKEY_SPORT_ID = 13
|
|
20
|
+
FLOORBALL_SPORT_ID = 14
|
|
21
|
+
FOOTBALL_SPORT_ID = 15
|
|
22
|
+
FUTSAL_SPORT_ID = 16
|
|
23
|
+
GOLF_SPORT_ID = 17
|
|
24
|
+
HANDBALL_SPORT_ID = 18
|
|
25
|
+
HOCKEY_SPORT_ID = 19
|
|
26
|
+
HORSE_RACING_SPECIALS_SPORT_ID = 20
|
|
27
|
+
LACROSSE_SPORT_ID = 21
|
|
28
|
+
MMA_SPORT_ID = 22
|
|
29
|
+
OTHER_SPORTS_SPORT_ID = 23
|
|
30
|
+
POLITICS_SPORT_ID = 24
|
|
31
|
+
RUGBY_LEAGUE_SPORT_ID = 26
|
|
32
|
+
RUGBY_UNION_SPORT_ID = 27
|
|
33
|
+
SNOOKER_SPORT_ID = 28
|
|
34
|
+
SOCCER_SPORT_ID = 29
|
|
35
|
+
SOFTBALL_SPORT_ID = 30
|
|
36
|
+
SQUASH_SPORT_ID = 31
|
|
37
|
+
TABLE_TENNIS_SPORT_ID = 32
|
|
38
|
+
TENNIS_SPORT_ID = 33
|
|
39
|
+
VOLLEYBALL_SPORT_ID = 34
|
|
40
|
+
WATER_POLO_SPORT_ID = 36
|
|
41
|
+
PADEL_TENNIS_SPORT_ID = 37
|
|
42
|
+
AUSSIE_RULES_SPORT_ID = 39
|
|
43
|
+
ALPINE_SKIING_SPORT_ID = 40
|
|
44
|
+
BIATHLON_SPORT_ID = 41
|
|
45
|
+
SKI_JUMPING_SPORT_ID = 42
|
|
46
|
+
CROSS_COUNTRY_SPORT_ID = 43
|
|
47
|
+
FORMULA1_SPORT_ID = 44
|
|
48
|
+
CYCLING_SPORT_ID = 45
|
|
49
|
+
BOBSLEIGH_SPORT_ID = 46
|
|
50
|
+
FIGURE_SKATING_SPORT_ID = 47
|
|
51
|
+
FREESTYLE_SKIING_SPORT_ID = 48
|
|
52
|
+
LUGE_SPORT_ID = 49
|
|
53
|
+
NORDIC_COMBINED_SPORT_ID = 50
|
|
54
|
+
SHORT_TRACK_SPORT_ID = 51
|
|
55
|
+
SKELETON_SPORT_ID = 52
|
|
56
|
+
SNOWBOARDING_SPORT_ID = 53
|
|
57
|
+
SPEED_SKATING_SPORT_ID = 54
|
|
58
|
+
OLYMPICS_SPORT_ID = 55
|
|
59
|
+
ATHLETICS_SPORT_ID = 56
|
|
60
|
+
CROSSFIT_SPORT_ID = 57
|
|
61
|
+
ENTERTAINMENT_SPORT_ID = 58
|
|
62
|
+
ARCHERY_SPORT_ID = 59
|
|
63
|
+
DRONE_RACING_SPORT_ID = 60
|
|
64
|
+
POKER_SPORT_ID = 62
|
|
65
|
+
MOTORSPORT_SPORT_ID = 63
|
|
66
|
+
SIMULATED_GAMES_SPORT_ID = 64
|
|
67
|
+
SUMO_SPORT_ID = 65
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
BADMINTON_SPORT_ID = Sport.BADMINTON_SPORT_ID
|
|
71
|
+
BANDY_SPORT_ID = Sport.BANDY_SPORT_ID
|
|
72
|
+
BASEBALL_SPORT_ID = Sport.BASEBALL_SPORT_ID
|
|
73
|
+
BASKETBALL_SPORT_ID = Sport.BASKETBALL_SPORT_ID
|
|
74
|
+
BEACH_VOLLEYBALL_SPORT_ID = Sport.BEACH_VOLLEYBALL_SPORT_ID
|
|
75
|
+
BOXING_SPORT_ID = Sport.BOXING_SPORT_ID
|
|
76
|
+
CHESS_SPORT_ID = Sport.CHESS_SPORT_ID
|
|
77
|
+
CRICKET_SPORT_ID = Sport.CRICKET_SPORT_ID
|
|
78
|
+
CURLING_SPORT_ID = Sport.CURLING_SPORT_ID
|
|
79
|
+
DARTS_SPORT_ID = Sport.DARTS_SPORT_ID
|
|
80
|
+
FIELD_HOCKEY_SPORT_ID = Sport.FIELD_HOCKEY_SPORT_ID
|
|
81
|
+
FLOORBALL_SPORT_ID = Sport.FLOORBALL_SPORT_ID
|
|
82
|
+
FOOTBALL_SPORT_ID = Sport.FOOTBALL_SPORT_ID
|
|
83
|
+
FUTSAL_SPORT_ID = Sport.FUTSAL_SPORT_ID
|
|
84
|
+
GOLF_SPORT_ID = Sport.GOLF_SPORT_ID
|
|
85
|
+
HANDBALL_SPORT_ID = Sport.HANDBALL_SPORT_ID
|
|
86
|
+
HOCKEY_SPORT_ID = Sport.HOCKEY_SPORT_ID
|
|
87
|
+
HORSE_RACING_SPECIALS_SPORT_ID = Sport.HORSE_RACING_SPECIALS_SPORT_ID
|
|
88
|
+
LACROSSE_SPORT_ID = Sport.LACROSSE_SPORT_ID
|
|
89
|
+
MMA_SPORT_ID = Sport.MMA_SPORT_ID
|
|
90
|
+
OTHER_SPORTS_SPORT_ID = Sport.OTHER_SPORTS_SPORT_ID
|
|
91
|
+
POLITICS_SPORT_ID = Sport.POLITICS_SPORT_ID
|
|
92
|
+
RUGBY_LEAGUE_SPORT_ID = Sport.RUGBY_LEAGUE_SPORT_ID
|
|
93
|
+
RUGBY_UNION_SPORT_ID = Sport.RUGBY_UNION_SPORT_ID
|
|
94
|
+
SNOOKER_SPORT_ID = Sport.SNOOKER_SPORT_ID
|
|
95
|
+
SOCCER_SPORT_ID = Sport.SOCCER_SPORT_ID
|
|
96
|
+
SOFTBALL_SPORT_ID = Sport.SOFTBALL_SPORT_ID
|
|
97
|
+
SQUASH_SPORT_ID = Sport.SQUASH_SPORT_ID
|
|
98
|
+
TABLE_TENNIS_SPORT_ID = Sport.TABLE_TENNIS_SPORT_ID
|
|
99
|
+
TENNIS_SPORT_ID = Sport.TENNIS_SPORT_ID
|
|
100
|
+
VOLLEYBALL_SPORT_ID = Sport.VOLLEYBALL_SPORT_ID
|
|
101
|
+
WATER_POLO_SPORT_ID = Sport.WATER_POLO_SPORT_ID
|
|
102
|
+
PADEL_TENNIS_SPORT_ID = Sport.PADEL_TENNIS_SPORT_ID
|
|
103
|
+
AUSSIE_RULES_SPORT_ID = Sport.AUSSIE_RULES_SPORT_ID
|
|
104
|
+
ALPINE_SKIING_SPORT_ID = Sport.ALPINE_SKIING_SPORT_ID
|
|
105
|
+
BIATHLON_SPORT_ID = Sport.BIATHLON_SPORT_ID
|
|
106
|
+
SKI_JUMPING_SPORT_ID = Sport.SKI_JUMPING_SPORT_ID
|
|
107
|
+
CROSS_COUNTRY_SPORT_ID = Sport.CROSS_COUNTRY_SPORT_ID
|
|
108
|
+
FORMULA1_SPORT_ID = Sport.FORMULA1_SPORT_ID
|
|
109
|
+
CYCLING_SPORT_ID = Sport.CYCLING_SPORT_ID
|
|
110
|
+
BOBSLEIGH_SPORT_ID = Sport.BOBSLEIGH_SPORT_ID
|
|
111
|
+
FIGURE_SKATING_SPORT_ID = Sport.FIGURE_SKATING_SPORT_ID
|
|
112
|
+
FREESTYLE_SKIING_SPORT_ID = Sport.FREESTYLE_SKIING_SPORT_ID
|
|
113
|
+
LUGE_SPORT_ID = Sport.LUGE_SPORT_ID
|
|
114
|
+
NORDIC_COMBINED_SPORT_ID = Sport.NORDIC_COMBINED_SPORT_ID
|
|
115
|
+
SHORT_TRACK_SPORT_ID = Sport.SHORT_TRACK_SPORT_ID
|
|
116
|
+
SKELETON_SPORT_ID = Sport.SKELETON_SPORT_ID
|
|
117
|
+
SNOWBOARDING_SPORT_ID = Sport.SNOWBOARDING_SPORT_ID
|
|
118
|
+
SPEED_SKATING_SPORT_ID = Sport.SPEED_SKATING_SPORT_ID
|
|
119
|
+
OLYMPICS_SPORT_ID = Sport.OLYMPICS_SPORT_ID
|
|
120
|
+
ATHLETICS_SPORT_ID = Sport.ATHLETICS_SPORT_ID
|
|
121
|
+
CROSSFIT_SPORT_ID = Sport.CROSSFIT_SPORT_ID
|
|
122
|
+
ENTERTAINMENT_SPORT_ID = Sport.ENTERTAINMENT_SPORT_ID
|
|
123
|
+
ARCHERY_SPORT_ID = Sport.ARCHERY_SPORT_ID
|
|
124
|
+
DRONE_RACING_SPORT_ID = Sport.DRONE_RACING_SPORT_ID
|
|
125
|
+
POKER_SPORT_ID = Sport.POKER_SPORT_ID
|
|
126
|
+
MOTORSPORT_SPORT_ID = Sport.MOTORSPORT_SPORT_ID
|
|
127
|
+
SIMULATED_GAMES_SPORT_ID = Sport.SIMULATED_GAMES_SPORT_ID
|
|
128
|
+
SUMO_SPORT_ID = Sport.SUMO_SPORT_ID
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
SPORT_ID_TO_NAME: dict[Sport, str] = {
|
|
132
|
+
Sport.BADMINTON_SPORT_ID: "Badminton",
|
|
133
|
+
Sport.BANDY_SPORT_ID: "Bandy",
|
|
134
|
+
Sport.BASEBALL_SPORT_ID: "Baseball",
|
|
135
|
+
Sport.BASKETBALL_SPORT_ID: "Basketball",
|
|
136
|
+
Sport.BEACH_VOLLEYBALL_SPORT_ID: "Beach Volleyball",
|
|
137
|
+
Sport.BOXING_SPORT_ID: "Boxing",
|
|
138
|
+
Sport.CHESS_SPORT_ID: "Chess",
|
|
139
|
+
Sport.CRICKET_SPORT_ID: "Cricket",
|
|
140
|
+
Sport.CURLING_SPORT_ID: "Curling",
|
|
141
|
+
Sport.DARTS_SPORT_ID: "Darts",
|
|
142
|
+
Sport.FIELD_HOCKEY_SPORT_ID: "Field Hockey",
|
|
143
|
+
Sport.FLOORBALL_SPORT_ID: "Floorball",
|
|
144
|
+
Sport.FOOTBALL_SPORT_ID: "Football",
|
|
145
|
+
Sport.FUTSAL_SPORT_ID: "Futsal",
|
|
146
|
+
Sport.GOLF_SPORT_ID: "Golf",
|
|
147
|
+
Sport.HANDBALL_SPORT_ID: "Handball",
|
|
148
|
+
Sport.HOCKEY_SPORT_ID: "Hockey",
|
|
149
|
+
Sport.HORSE_RACING_SPECIALS_SPORT_ID: "Horse Racing Specials",
|
|
150
|
+
Sport.LACROSSE_SPORT_ID: "Lacrosse",
|
|
151
|
+
Sport.MMA_SPORT_ID: "Mixed Martial Arts",
|
|
152
|
+
Sport.OTHER_SPORTS_SPORT_ID: "Other Sports",
|
|
153
|
+
Sport.POLITICS_SPORT_ID: "Politics",
|
|
154
|
+
Sport.RUGBY_LEAGUE_SPORT_ID: "Rugby League",
|
|
155
|
+
Sport.RUGBY_UNION_SPORT_ID: "Rugby Union",
|
|
156
|
+
Sport.SNOOKER_SPORT_ID: "Snooker",
|
|
157
|
+
Sport.SOCCER_SPORT_ID: "Soccer",
|
|
158
|
+
Sport.SOFTBALL_SPORT_ID: "Softball",
|
|
159
|
+
Sport.SQUASH_SPORT_ID: "Squash",
|
|
160
|
+
Sport.TABLE_TENNIS_SPORT_ID: "Table Tennis",
|
|
161
|
+
Sport.TENNIS_SPORT_ID: "Tennis",
|
|
162
|
+
Sport.VOLLEYBALL_SPORT_ID: "Volleyball",
|
|
163
|
+
Sport.WATER_POLO_SPORT_ID: "Water Polo",
|
|
164
|
+
Sport.PADEL_TENNIS_SPORT_ID: "Padel Tennis",
|
|
165
|
+
Sport.AUSSIE_RULES_SPORT_ID: "Aussie Rules",
|
|
166
|
+
Sport.ALPINE_SKIING_SPORT_ID: "Alpine Skiing",
|
|
167
|
+
Sport.BIATHLON_SPORT_ID: "Biathlon",
|
|
168
|
+
Sport.SKI_JUMPING_SPORT_ID: "Ski Jumping",
|
|
169
|
+
Sport.CROSS_COUNTRY_SPORT_ID: "Cross Country",
|
|
170
|
+
Sport.FORMULA1_SPORT_ID: "Formula 1",
|
|
171
|
+
Sport.CYCLING_SPORT_ID: "Cycling",
|
|
172
|
+
Sport.BOBSLEIGH_SPORT_ID: "Bobsleigh",
|
|
173
|
+
Sport.FIGURE_SKATING_SPORT_ID: "Figure Skating",
|
|
174
|
+
Sport.FREESTYLE_SKIING_SPORT_ID: "Freestyle Skiing",
|
|
175
|
+
Sport.LUGE_SPORT_ID: "Luge",
|
|
176
|
+
Sport.NORDIC_COMBINED_SPORT_ID: "Nordic Combined",
|
|
177
|
+
Sport.SHORT_TRACK_SPORT_ID: "Short Track",
|
|
178
|
+
Sport.SKELETON_SPORT_ID: "Skeleton",
|
|
179
|
+
Sport.SNOWBOARDING_SPORT_ID: "Snow Boarding",
|
|
180
|
+
Sport.SPEED_SKATING_SPORT_ID: "Speed Skating",
|
|
181
|
+
Sport.OLYMPICS_SPORT_ID: "Olympics",
|
|
182
|
+
Sport.ATHLETICS_SPORT_ID: "Athletics",
|
|
183
|
+
Sport.CROSSFIT_SPORT_ID: "Crossfit",
|
|
184
|
+
Sport.ENTERTAINMENT_SPORT_ID: "Entertainment",
|
|
185
|
+
Sport.ARCHERY_SPORT_ID: "Archery",
|
|
186
|
+
Sport.DRONE_RACING_SPORT_ID: "Drone Racing",
|
|
187
|
+
Sport.POKER_SPORT_ID: "Poker",
|
|
188
|
+
Sport.MOTORSPORT_SPORT_ID: "Motorsport",
|
|
189
|
+
Sport.SIMULATED_GAMES_SPORT_ID: "Simulated Games",
|
|
190
|
+
Sport.SUMO_SPORT_ID: "Sumo",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
__all__ = [
|
|
195
|
+
"Sport",
|
|
196
|
+
"SPORT_ID_TO_NAME",
|
|
197
|
+
"BADMINTON_SPORT_ID",
|
|
198
|
+
"BANDY_SPORT_ID",
|
|
199
|
+
"BASEBALL_SPORT_ID",
|
|
200
|
+
"BASKETBALL_SPORT_ID",
|
|
201
|
+
"BEACH_VOLLEYBALL_SPORT_ID",
|
|
202
|
+
"BOXING_SPORT_ID",
|
|
203
|
+
"CHESS_SPORT_ID",
|
|
204
|
+
"CRICKET_SPORT_ID",
|
|
205
|
+
"CURLING_SPORT_ID",
|
|
206
|
+
"DARTS_SPORT_ID",
|
|
207
|
+
"FIELD_HOCKEY_SPORT_ID",
|
|
208
|
+
"FLOORBALL_SPORT_ID",
|
|
209
|
+
"FOOTBALL_SPORT_ID",
|
|
210
|
+
"FUTSAL_SPORT_ID",
|
|
211
|
+
"GOLF_SPORT_ID",
|
|
212
|
+
"HANDBALL_SPORT_ID",
|
|
213
|
+
"HOCKEY_SPORT_ID",
|
|
214
|
+
"HORSE_RACING_SPECIALS_SPORT_ID",
|
|
215
|
+
"LACROSSE_SPORT_ID",
|
|
216
|
+
"MMA_SPORT_ID",
|
|
217
|
+
"OTHER_SPORTS_SPORT_ID",
|
|
218
|
+
"POLITICS_SPORT_ID",
|
|
219
|
+
"RUGBY_LEAGUE_SPORT_ID",
|
|
220
|
+
"RUGBY_UNION_SPORT_ID",
|
|
221
|
+
"SNOOKER_SPORT_ID",
|
|
222
|
+
"SOCCER_SPORT_ID",
|
|
223
|
+
"SOFTBALL_SPORT_ID",
|
|
224
|
+
"SQUASH_SPORT_ID",
|
|
225
|
+
"TABLE_TENNIS_SPORT_ID",
|
|
226
|
+
"TENNIS_SPORT_ID",
|
|
227
|
+
"VOLLEYBALL_SPORT_ID",
|
|
228
|
+
"WATER_POLO_SPORT_ID",
|
|
229
|
+
"PADEL_TENNIS_SPORT_ID",
|
|
230
|
+
"AUSSIE_RULES_SPORT_ID",
|
|
231
|
+
"ALPINE_SKIING_SPORT_ID",
|
|
232
|
+
"BIATHLON_SPORT_ID",
|
|
233
|
+
"SKI_JUMPING_SPORT_ID",
|
|
234
|
+
"CROSS_COUNTRY_SPORT_ID",
|
|
235
|
+
"FORMULA1_SPORT_ID",
|
|
236
|
+
"CYCLING_SPORT_ID",
|
|
237
|
+
"BOBSLEIGH_SPORT_ID",
|
|
238
|
+
"FIGURE_SKATING_SPORT_ID",
|
|
239
|
+
"FREESTYLE_SKIING_SPORT_ID",
|
|
240
|
+
"LUGE_SPORT_ID",
|
|
241
|
+
"NORDIC_COMBINED_SPORT_ID",
|
|
242
|
+
"SHORT_TRACK_SPORT_ID",
|
|
243
|
+
"SKELETON_SPORT_ID",
|
|
244
|
+
"SNOWBOARDING_SPORT_ID",
|
|
245
|
+
"SPEED_SKATING_SPORT_ID",
|
|
246
|
+
"OLYMPICS_SPORT_ID",
|
|
247
|
+
"ATHLETICS_SPORT_ID",
|
|
248
|
+
"CROSSFIT_SPORT_ID",
|
|
249
|
+
"ENTERTAINMENT_SPORT_ID",
|
|
250
|
+
"ARCHERY_SPORT_ID",
|
|
251
|
+
"DRONE_RACING_SPORT_ID",
|
|
252
|
+
"POKER_SPORT_ID",
|
|
253
|
+
"MOTORSPORT_SPORT_ID",
|
|
254
|
+
"SIMULATED_GAMES_SPORT_ID",
|
|
255
|
+
"SUMO_SPORT_ID",
|
|
256
|
+
]
|
ps3838api/models/tank.py
ADDED
ps3838api/py.typed
ADDED
|
File without changes
|
ps3838api/tank.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Centralised fixtures/odds caching that respects PS3838 rate‑limits.
|
|
2
|
+
|
|
3
|
+
The core idea is identical for both resources:
|
|
4
|
+
• ≥ 60 s since previous call → **snapshot** (full refresh)
|
|
5
|
+
• 5–59 s → **delta** (incremental update, merged into cache)
|
|
6
|
+
• < 5 s → **use in‑memory cache**, no API hit
|
|
7
|
+
|
|
8
|
+
Odds were already following this contract. Fixtures now do too.
|
|
9
|
+
Additionally, fixtures are **no longer persisted** as one huge
|
|
10
|
+
``fixtures.json`` file. Every API response (snapshot *and* delta)
|
|
11
|
+
gets stored verbatim in *temp/responses/* for replay/debugging just
|
|
12
|
+
like odds. If a full history is ever required you can reconstruct it
|
|
13
|
+
from those files.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import warnings
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from time import time
|
|
21
|
+
|
|
22
|
+
from ps3838api.api.client import PinnacleClient
|
|
23
|
+
from ps3838api.matching import MATCHED_LEAGUES
|
|
24
|
+
from ps3838api.models.fixtures import FixturesResponse
|
|
25
|
+
from ps3838api.utils.ops import merge_fixtures
|
|
26
|
+
|
|
27
|
+
warnings.warn(
|
|
28
|
+
f"{__name__} is experimental, incomplete, and may change in future versions.",
|
|
29
|
+
UserWarning,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
SNAPSHOT_INTERVAL = 60
|
|
34
|
+
DELTA_INTERVAL = 5
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
TOP_LEAGUES = [league["ps3838_id"] for league in MATCHED_LEAGUES if league["ps3838_id"]]
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FixtureTank:
|
|
43
|
+
"""Lightweight cache for Pinnacle *fixtures*.
|
|
44
|
+
|
|
45
|
+
* No big persisted file – only individual API responses are archived.
|
|
46
|
+
* Shares the same timing policy as :class:`OddsTank`.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
client: PinnacleClient,
|
|
52
|
+
league_ids: list[int] | None = None,
|
|
53
|
+
response_dir: Path | str | None = None, # Path("temp/responses")
|
|
54
|
+
) -> None:
|
|
55
|
+
self.client = client
|
|
56
|
+
self.response_dir = Path(response_dir) if response_dir else None
|
|
57
|
+
# start with a fresh snapshot (fast + guarantees consistency)
|
|
58
|
+
self.data: FixturesResponse = client.get_fixtures(league_ids=league_ids)
|
|
59
|
+
self._last_call_time = time()
|
|
60
|
+
self._save_response(self.data, snapshot=True)
|
|
61
|
+
|
|
62
|
+
def _save_response(self, response_data: FixturesResponse, snapshot: bool) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Save fixture response to the temp/responses folder for future testing.
|
|
65
|
+
"""
|
|
66
|
+
if not self.response_dir:
|
|
67
|
+
return
|
|
68
|
+
kind = "snapshot" if snapshot else "delta"
|
|
69
|
+
self.response_dir.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
fn = self.response_dir / f"fixtures_{kind}_{int(time())}.json"
|
|
71
|
+
with open(fn, "w") as f:
|
|
72
|
+
json.dump(response_data, f, indent=4)
|
|
73
|
+
|
|
74
|
+
def update(self) -> None:
|
|
75
|
+
"""Refresh internal cache if timing thresholds are met."""
|
|
76
|
+
now = time()
|
|
77
|
+
elapsed = now - self._last_call_time
|
|
78
|
+
|
|
79
|
+
if elapsed < DELTA_INTERVAL:
|
|
80
|
+
return # 💡 Too soon – use cached data
|
|
81
|
+
|
|
82
|
+
if elapsed >= SNAPSHOT_INTERVAL:
|
|
83
|
+
# ── Full refresh ────────────────────────────────────────────
|
|
84
|
+
resp = self.client.get_fixtures()
|
|
85
|
+
self.data = resp
|
|
86
|
+
self._save_response(resp, snapshot=True)
|
|
87
|
+
else:
|
|
88
|
+
# ── Incremental update ──────────────────────────────────────
|
|
89
|
+
delta = self.client.get_fixtures(since=self.data["last"])
|
|
90
|
+
self.data = merge_fixtures(self.data, delta)
|
|
91
|
+
self._save_response(delta, snapshot=False)
|
|
92
|
+
|
|
93
|
+
self._last_call_time = now
|
ps3838api/totals.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from ps3838api.models.odds import OddsEventV3, OddsTotalV3
|
|
5
|
+
|
|
6
|
+
warnings.warn(
|
|
7
|
+
f"{__name__} is experimental and its interface is not stable yet.",
|
|
8
|
+
FutureWarning,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OddsTotal(OddsTotalV3):
|
|
13
|
+
"""Has line id"""
|
|
14
|
+
|
|
15
|
+
lineId: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def calculate_margin(total: OddsTotalV3) -> float:
|
|
19
|
+
return (1 / total["over"] + 1 / total["under"]) - 1
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_all_total_lines(
|
|
23
|
+
odds: OddsEventV3,
|
|
24
|
+
periods: list[int] = [
|
|
25
|
+
0,
|
|
26
|
+
],
|
|
27
|
+
) -> list[OddsTotal]:
|
|
28
|
+
result: list[OddsTotal] = []
|
|
29
|
+
for period in odds["periods"]: # type: ignore
|
|
30
|
+
if "number" not in period:
|
|
31
|
+
# skip if unknown period
|
|
32
|
+
continue
|
|
33
|
+
if period["number"] not in periods:
|
|
34
|
+
# skip if wrong periood
|
|
35
|
+
continue
|
|
36
|
+
if "totals" not in period:
|
|
37
|
+
# skip if no totals in this period
|
|
38
|
+
continue
|
|
39
|
+
if "lineId" not in period:
|
|
40
|
+
# skip if don't have lineId
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
lineId = period["lineId"]
|
|
44
|
+
maxTotal = period["maxTotal"] if "maxTotal" in period else None
|
|
45
|
+
|
|
46
|
+
for total in period["totals"]:
|
|
47
|
+
odds_total = cast(OddsTotal, total.copy())
|
|
48
|
+
odds_total["lineId"] = lineId
|
|
49
|
+
# each total should have lineId
|
|
50
|
+
|
|
51
|
+
if "altLineId" not in total:
|
|
52
|
+
if maxTotal is not None:
|
|
53
|
+
odds_total["max"] = maxTotal
|
|
54
|
+
result.append(odds_total)
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_best_total_line(odds: OddsEventV3, periods: list[int] = [0, 1]) -> OddsTotal | None:
|
|
59
|
+
try:
|
|
60
|
+
return min(get_all_total_lines(odds, periods=periods), key=calculate_margin)
|
|
61
|
+
except Exception:
|
|
62
|
+
return None
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from rapidfuzz import fuzz, process
|
|
7
|
+
|
|
8
|
+
from ps3838api import ROOT_MODULE_DIR
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Your threshold-based fuzzy function
|
|
12
|
+
def is_leagues_match(league1: str, league2: str, threshold: int = 80) -> bool:
|
|
13
|
+
"""
|
|
14
|
+
Returns True if leagues are a fuzzy match with a token sort ratio >= threshold.
|
|
15
|
+
fuzz.token_sort_ratio() returns 0-100, so 80 means 80% similar.
|
|
16
|
+
"""
|
|
17
|
+
return fuzz.token_sort_ratio(league1, league2) >= threshold
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_json(path: str | Path) -> list[Any] | dict[str, Any]:
|
|
21
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
22
|
+
return json.load(f)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
betsapi_path = ROOT_MODULE_DIR / Path("out/betsapi_leagues.json")
|
|
27
|
+
ps3838_path = ROOT_MODULE_DIR / Path("out/ps3838_leagues.json")
|
|
28
|
+
output_path = ROOT_MODULE_DIR / Path("out/matched_leagues.json")
|
|
29
|
+
|
|
30
|
+
# --------------------------------------------------------------------
|
|
31
|
+
# Load raw data
|
|
32
|
+
# --------------------------------------------------------------------
|
|
33
|
+
betsapi_leagues = load_json(betsapi_path) # e.g. ["Premier League", "La Liga", ...]
|
|
34
|
+
ps3838_data = load_json(ps3838_path)
|
|
35
|
+
# --------------------------------------------------------------------
|
|
36
|
+
# Build a RapidFuzz index of PS3838 league names to do quick "best-match" lookups
|
|
37
|
+
# --------------------------------------------------------------------
|
|
38
|
+
# 1) Just keep a list of league names:
|
|
39
|
+
ps_names = [league["name"] for league in ps3838_data]
|
|
40
|
+
|
|
41
|
+
# 2) Also map name -> (full record) for easy ID lookup:
|
|
42
|
+
ps_map = {league["name"]: league for league in ps3838_data}
|
|
43
|
+
|
|
44
|
+
# --------------------------------------------------------------------
|
|
45
|
+
# Compare each BetsAPI league to the best PS3838 league match
|
|
46
|
+
# --------------------------------------------------------------------
|
|
47
|
+
matched = []
|
|
48
|
+
for betsapi_league in betsapi_leagues:
|
|
49
|
+
# RapidFuzz: find the single best match
|
|
50
|
+
# extractOne returns a tuple: (best_match_string, score, index)
|
|
51
|
+
best_match = process.extractOne(betsapi_league, ps_names, scorer=fuzz.token_sort_ratio)
|
|
52
|
+
|
|
53
|
+
if best_match is not None:
|
|
54
|
+
ps_name, score, _ = best_match
|
|
55
|
+
if score >= 80:
|
|
56
|
+
# It's a good fuzzy match
|
|
57
|
+
matched_league_info = {
|
|
58
|
+
"betsapi_league": betsapi_league,
|
|
59
|
+
"ps3838_league": ps_map[ps_name]["name"],
|
|
60
|
+
"ps3838_id": ps_map[ps_name]["id"],
|
|
61
|
+
}
|
|
62
|
+
else:
|
|
63
|
+
# We got a best match but it's below threshold
|
|
64
|
+
matched_league_info = {
|
|
65
|
+
"betsapi_league": betsapi_league,
|
|
66
|
+
"ps3838_league": None,
|
|
67
|
+
"ps3838_id": None,
|
|
68
|
+
}
|
|
69
|
+
else:
|
|
70
|
+
# No match at all
|
|
71
|
+
matched_league_info = {
|
|
72
|
+
"betsapi_league": betsapi_league,
|
|
73
|
+
"ps3838_league": None,
|
|
74
|
+
"ps3838_id": None,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
matched.append(matched_league_info)
|
|
78
|
+
|
|
79
|
+
# --------------------------------------------------------------------
|
|
80
|
+
# Save output
|
|
81
|
+
# --------------------------------------------------------------------
|
|
82
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
84
|
+
json.dump(matched, f, indent=2, ensure_ascii=False)
|
|
85
|
+
|
|
86
|
+
print(f"✅ Matching complete. Output saved to: {output_path}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|
ps3838api/utils/ops.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
from ps3838api.models.event import NoSuchLeagueFixtures, NoSuchOddsAvailable
|
|
4
|
+
from ps3838api.models.fixtures import FixturesLeagueV3, FixturesResponse, FixtureV3
|
|
5
|
+
from ps3838api.models.odds import OddsEventV3, OddsLeagueV3, OddsResponse
|
|
6
|
+
from ps3838api.models.tank import EventInfo
|
|
7
|
+
|
|
8
|
+
warnings.warn(
|
|
9
|
+
f"{__name__} is experimental, incomplete, and may change in future versions.",
|
|
10
|
+
UserWarning,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def merge_odds_response(old: OddsResponse, new: OddsResponse) -> OddsResponse:
|
|
15
|
+
"""
|
|
16
|
+
Merge a snapshot OddsResponse (old) with a delta OddsResponse (new).
|
|
17
|
+
- Leagues are matched by league["id"].
|
|
18
|
+
- Events are matched by event["id"].
|
|
19
|
+
- Periods are matched by period["number"].
|
|
20
|
+
- Any period present in 'new' entirely replaces the same period number in 'old'.
|
|
21
|
+
- Periods not present in 'new' remain as they were in 'old'.
|
|
22
|
+
|
|
23
|
+
Returns a merged OddsResponse that includes updated odds and periods, retaining
|
|
24
|
+
old entries when no changes were reported in the delta.
|
|
25
|
+
|
|
26
|
+
Based on "How to get odds changes?" from https://ps3838api.github.io/FAQs.html
|
|
27
|
+
"""
|
|
28
|
+
# Index the old leagues by their IDs
|
|
29
|
+
league_index: dict[int, OddsLeagueV3] = {league["id"]: league for league in old.get("leagues", [])}
|
|
30
|
+
|
|
31
|
+
# Loop through the new leagues
|
|
32
|
+
for new_league in new.get("leagues", []):
|
|
33
|
+
lid = new_league["id"]
|
|
34
|
+
|
|
35
|
+
# If it's an entirely new league, just store it
|
|
36
|
+
if lid not in league_index:
|
|
37
|
+
league_index[lid] = new_league
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
# Otherwise merge it with the existing league
|
|
41
|
+
old_league = league_index[lid]
|
|
42
|
+
old_event_index = {event["id"]: event for event in old_league.get("events", [])}
|
|
43
|
+
|
|
44
|
+
# Loop through the new events
|
|
45
|
+
for new_event in new_league.get("events", []):
|
|
46
|
+
eid = new_event["id"]
|
|
47
|
+
|
|
48
|
+
# If it's an entirely new event, just store it
|
|
49
|
+
if eid not in old_event_index:
|
|
50
|
+
old_event_index[eid] = new_event
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# Otherwise, merge with the existing event
|
|
54
|
+
old_event = old_event_index[eid]
|
|
55
|
+
|
|
56
|
+
# Periods: build an index by 'number' from the old event
|
|
57
|
+
old_period_index = {p["number"]: p for p in old_event.get("periods", []) if "number" in p}
|
|
58
|
+
|
|
59
|
+
# Take all the new event's periods and override or insert them by 'number'
|
|
60
|
+
for new_period in new_event.get("periods", []):
|
|
61
|
+
if "number" not in new_period:
|
|
62
|
+
continue
|
|
63
|
+
old_period_index[new_period["number"]] = new_period
|
|
64
|
+
|
|
65
|
+
# Merge top-level fields: new event fields override old ones
|
|
66
|
+
merged_event = old_event.copy()
|
|
67
|
+
merged_event.update(new_event)
|
|
68
|
+
|
|
69
|
+
# Rebuild the merged_event's periods from the updated dictionary
|
|
70
|
+
merged_event["periods"] = list(old_period_index.values())
|
|
71
|
+
|
|
72
|
+
# Store back in the event index
|
|
73
|
+
old_event_index[eid] = merged_event
|
|
74
|
+
|
|
75
|
+
# Rebuild league's events list from the merged event index
|
|
76
|
+
old_league["events"] = list(old_event_index.values())
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"sportId": new.get("sportId", old["sportId"]),
|
|
80
|
+
# Always take the latest `last` timestamp from the new (delta) response
|
|
81
|
+
"last": new["last"],
|
|
82
|
+
# Rebuild leagues list
|
|
83
|
+
"leagues": list(league_index.values()),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def merge_fixtures(old: FixturesResponse, new: FixturesResponse) -> FixturesResponse:
|
|
88
|
+
league_index: dict[int, FixturesLeagueV3] = {league["id"]: league for league in old.get("league", [])}
|
|
89
|
+
|
|
90
|
+
for new_league in new.get("league", []):
|
|
91
|
+
lid = new_league["id"]
|
|
92
|
+
if lid in league_index:
|
|
93
|
+
old_events = {e["id"]: e for e in league_index[lid]["events"]}
|
|
94
|
+
for event in new_league["events"]:
|
|
95
|
+
old_events[event["id"]] = event # override or insert
|
|
96
|
+
league_index[lid]["events"] = list(old_events.values())
|
|
97
|
+
else:
|
|
98
|
+
league_index[lid] = new_league # new league entirely
|
|
99
|
+
return {
|
|
100
|
+
"sportId": new.get("sportId", old["sportId"]),
|
|
101
|
+
"last": new["last"],
|
|
102
|
+
"league": list(league_index.values()),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def find_league_in_fixtures(
|
|
107
|
+
fixtures: FixturesResponse, league: str, league_id: int
|
|
108
|
+
) -> FixturesLeagueV3 | NoSuchLeagueFixtures:
|
|
109
|
+
for leagueV3 in fixtures["league"]:
|
|
110
|
+
if leagueV3["id"] == league_id:
|
|
111
|
+
return leagueV3
|
|
112
|
+
else:
|
|
113
|
+
return NoSuchLeagueFixtures(league)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def find_fixtureV3_in_league(leagueV3: FixturesLeagueV3, event_id: int) -> FixtureV3:
|
|
117
|
+
for eventV3 in leagueV3["events"]:
|
|
118
|
+
if eventV3["id"] == event_id:
|
|
119
|
+
return eventV3
|
|
120
|
+
raise ValueError("No such event")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def filter_odds(
|
|
124
|
+
odds: OddsResponse, event_id: int, league_id: int | None = None
|
|
125
|
+
) -> OddsEventV3 | NoSuchOddsAvailable:
|
|
126
|
+
"""passing `league_id` makes search in json faster"""
|
|
127
|
+
for league in odds["leagues"]:
|
|
128
|
+
if league_id and league_id != league["id"]:
|
|
129
|
+
continue
|
|
130
|
+
for fixture in league["events"]:
|
|
131
|
+
if fixture["id"] == event_id:
|
|
132
|
+
return fixture
|
|
133
|
+
return NoSuchOddsAvailable(event_id)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def normalize_to_set(name: str) -> set[str]:
|
|
137
|
+
return set(name.replace(" II", " 2").replace(" I", "").lower().replace("-", " ").split())
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def find_event_by_id(fixtures: FixturesResponse, event: EventInfo) -> FixtureV3 | None:
|
|
141
|
+
for leagueV3 in fixtures["league"]:
|
|
142
|
+
if leagueV3["id"] == event["leagueId"]:
|
|
143
|
+
for fixtureV3 in leagueV3["events"]:
|
|
144
|
+
if fixtureV3["id"] == event["eventId"]:
|
|
145
|
+
return fixtureV3
|
|
146
|
+
return None
|