ps3838api 0.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.
Potentially problematic release.
This version of ps3838api might be problematic. Click here for more details.
- ps3838api/__init__.py +5 -0
- ps3838api/api.py +528 -0
- ps3838api/logic.py +146 -0
- ps3838api/matching.py +106 -0
- ps3838api/models/__init__.py +0 -0
- ps3838api/models/bets.py +249 -0
- ps3838api/models/errors.py +42 -0
- ps3838api/models/event.py +57 -0
- ps3838api/models/fixtures.py +53 -0
- ps3838api/models/lines.py +27 -0
- ps3838api/models/odds.py +107 -0
- ps3838api/models/tank.py +5 -0
- ps3838api/tank.py +237 -0
- ps3838api/totals.py +52 -0
- ps3838api/utils/match_leagues.py +71 -0
- ps3838api-0.1.0.dist-info/METADATA +192 -0
- ps3838api-0.1.0.dist-info/RECORD +20 -0
- ps3838api-0.1.0.dist-info/WHEEL +5 -0
- ps3838api-0.1.0.dist-info/licenses/LICENSE +8 -0
- ps3838api-0.1.0.dist-info/top_level.txt +1 -0
ps3838api/matching.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from rapidfuzz import fuzz
|
|
2
|
+
from ps3838api import ROOT_DIR
|
|
3
|
+
from ps3838api.logic import normalize_to_set
|
|
4
|
+
from ps3838api.models.event import (
|
|
5
|
+
Failure,
|
|
6
|
+
MatchedLeague,
|
|
7
|
+
NoSuchEvent,
|
|
8
|
+
NoSuchLeague,
|
|
9
|
+
NoSuchLeagueFixtures,
|
|
10
|
+
NoSuchLeagueMatching,
|
|
11
|
+
WrongLeague,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
import ps3838api.api as ps
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from typing import Final
|
|
18
|
+
|
|
19
|
+
from ps3838api.models.fixtures import FixturesLeagueV3, FixturesResponse
|
|
20
|
+
from ps3838api.models.tank import EventInfo
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
with open(ROOT_DIR / "out/matched_leagues.json") as file:
|
|
24
|
+
MATCHED_LEAGUES: Final[list[MatchedLeague]] = json.load(file)
|
|
25
|
+
|
|
26
|
+
with open(ROOT_DIR / "out/ps3838_leagues.json") as file:
|
|
27
|
+
ALL_LEAGUES: Final[list[ps.LeagueV3]] = json.load(file)['leagues']
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def match_league(
|
|
31
|
+
*,
|
|
32
|
+
league_betsapi: str,
|
|
33
|
+
leagues_mapping: list[MatchedLeague] = MATCHED_LEAGUES,
|
|
34
|
+
) -> MatchedLeague | NoSuchLeagueMatching | WrongLeague:
|
|
35
|
+
for league in leagues_mapping:
|
|
36
|
+
if league["betsapi_league"] == league_betsapi:
|
|
37
|
+
if league["ps3838_id"]:
|
|
38
|
+
return league
|
|
39
|
+
else:
|
|
40
|
+
return NoSuchLeagueMatching(league_betsapi)
|
|
41
|
+
return WrongLeague(league_betsapi)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def find_league_by_name(
|
|
45
|
+
league: str, leagues: list[ps.LeagueV3] = ALL_LEAGUES
|
|
46
|
+
) -> ps.LeagueV3 | NoSuchLeague:
|
|
47
|
+
normalized = normalize_to_set(league)
|
|
48
|
+
for leagueV3 in leagues:
|
|
49
|
+
if normalize_to_set(leagueV3["name"]) == normalized:
|
|
50
|
+
return leagueV3
|
|
51
|
+
return NoSuchLeagueMatching(league)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_event_in_league(
|
|
55
|
+
league_data: FixturesLeagueV3, league: str, home: str, away: str
|
|
56
|
+
) -> EventInfo | NoSuchEvent:
|
|
57
|
+
"""
|
|
58
|
+
Scans `league_data["events"]` for the best fuzzy match to `home` and `away`.
|
|
59
|
+
Returns the matching event with the highest sum of match scores, as long as
|
|
60
|
+
that sum >= 75 (which is 37.5% of the max possible 200).
|
|
61
|
+
Otherwise, returns NoSuchEvent.
|
|
62
|
+
"""
|
|
63
|
+
best_event = None
|
|
64
|
+
best_sum_score = 0
|
|
65
|
+
for event in league_data["events"]:
|
|
66
|
+
# Compare the user-provided home and away vs. the fixture's home and away.
|
|
67
|
+
# Using token_set_ratio (see below for comparison vs token_sort_ratio).
|
|
68
|
+
score_home = fuzz.token_set_ratio(home, event.get("home", ""))
|
|
69
|
+
score_away = fuzz.token_set_ratio(away, event.get("away", ""))
|
|
70
|
+
total_score = score_home + score_away
|
|
71
|
+
if total_score > best_sum_score:
|
|
72
|
+
best_sum_score = total_score
|
|
73
|
+
best_event = event
|
|
74
|
+
# If the best event's combined fuzzy match is < 37.5% of the total possible 200,
|
|
75
|
+
# treat it as no match:
|
|
76
|
+
if best_event is None or best_sum_score < 75:
|
|
77
|
+
return NoSuchEvent(league, home, away)
|
|
78
|
+
return {"eventId": best_event["id"], "leagueId": league_data["id"]}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def magic_find_event(
|
|
82
|
+
fixtures: FixturesResponse, league: str, home: str, away: str
|
|
83
|
+
) -> EventInfo | Failure:
|
|
84
|
+
"""
|
|
85
|
+
1. Tries to find league by normalizng names;
|
|
86
|
+
2. If don't, search for a league matching
|
|
87
|
+
3. Then `find_event_in_league`
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
leagueV3 = find_league_by_name(league)
|
|
91
|
+
if isinstance(leagueV3, NoSuchLeague):
|
|
92
|
+
match match_league(league_betsapi=league):
|
|
93
|
+
case {"ps3838_id": int()} as value:
|
|
94
|
+
league_id: int = value["ps3838_id"] # type: ignore
|
|
95
|
+
case _:
|
|
96
|
+
return NoSuchLeagueMatching(league)
|
|
97
|
+
else:
|
|
98
|
+
league_id = leagueV3["id"]
|
|
99
|
+
|
|
100
|
+
for leagueV3 in fixtures["league"]:
|
|
101
|
+
if leagueV3["id"] == league_id:
|
|
102
|
+
break
|
|
103
|
+
else:
|
|
104
|
+
return NoSuchLeagueFixtures(league)
|
|
105
|
+
|
|
106
|
+
return find_event_in_league(leagueV3, league, home, away)
|
|
File without changes
|
ps3838api/models/bets.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
from typing import TypedDict, NotRequired, Literal
|
|
2
|
+
|
|
3
|
+
type OddsFormat = Literal["AMERICAN", "DECIMAL", "HONGKONG", "INDONESIAN", "MALAY"]
|
|
4
|
+
|
|
5
|
+
type FillType = Literal["NORMAL", "FILLANDKILL", "FILLMAXLIMIT"]
|
|
6
|
+
"""
|
|
7
|
+
### NORMAL
|
|
8
|
+
bet will be placed on specified stake.
|
|
9
|
+
|
|
10
|
+
### FILLANDKILL
|
|
11
|
+
|
|
12
|
+
If the stake is over the max limit, bet will be placed on max limit,
|
|
13
|
+
otherwise it will be placed on specified stake.
|
|
14
|
+
|
|
15
|
+
### FILLMAXLIMIT⚠️
|
|
16
|
+
|
|
17
|
+
bet will be places on max limit⚠️, stake amount will be ignored.
|
|
18
|
+
Please note that maximum limits can change at any moment, which may result in
|
|
19
|
+
risking more than anticipated. This option is replacement of isMaxStakeBet from
|
|
20
|
+
v1/bets/place'
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
type Team = Literal["TEAM1", "TEAM2", "DRAW"]
|
|
24
|
+
type Side = Literal["OVER", "UNDER"]
|
|
25
|
+
|
|
26
|
+
type BetStatus = Literal[
|
|
27
|
+
"ACCEPTED",
|
|
28
|
+
"CANCELLED",
|
|
29
|
+
"LOSE",
|
|
30
|
+
"PENDING_ACCEPTANCE",
|
|
31
|
+
"REFUNDED",
|
|
32
|
+
"NOT_ACCEPTED",
|
|
33
|
+
"WON",
|
|
34
|
+
"REJECTED",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
type BetStatus2 = Literal[
|
|
38
|
+
"ACCEPTED",
|
|
39
|
+
"CANCELLED",
|
|
40
|
+
"LOST",
|
|
41
|
+
"PENDING_ACCEPTANCE",
|
|
42
|
+
"REFUNDED",
|
|
43
|
+
"NOT_ACCEPTED",
|
|
44
|
+
"WON",
|
|
45
|
+
"REJECTED",
|
|
46
|
+
"HALF_WON_HALF_PUSHED",
|
|
47
|
+
"HALF_LOST_HALF_PUSHED",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
type BetType = Literal["MONEYLINE", "TEAM_TOTAL_POINTS", "SPREAD", "TOTAL_POINTS"]
|
|
51
|
+
|
|
52
|
+
type BetTypeFull = Literal[
|
|
53
|
+
"MONEYLINE",
|
|
54
|
+
"TEAM_TOTAL_POINTS",
|
|
55
|
+
"SPREAD",
|
|
56
|
+
"TOTAL_POINTS",
|
|
57
|
+
"SPECIAL",
|
|
58
|
+
"PARLAY",
|
|
59
|
+
"TEASER",
|
|
60
|
+
"MANUAL",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PlaceStraightBetRequest(TypedDict):
|
|
65
|
+
oddsFormat: OddsFormat
|
|
66
|
+
uniqueRequestId: str
|
|
67
|
+
acceptBetterLine: bool
|
|
68
|
+
stake: float
|
|
69
|
+
winRiskStake: Literal["WIN", "RISK"]
|
|
70
|
+
lineId: int
|
|
71
|
+
altLineId: NotRequired[int]
|
|
72
|
+
pitcher1MustStart: bool
|
|
73
|
+
pitcher2MustStart: bool
|
|
74
|
+
fillType: Literal["NORMAL", "FILLANDKILL", "FILLMAXLIMIT"]
|
|
75
|
+
sportId: int
|
|
76
|
+
eventId: int
|
|
77
|
+
periodNumber: int
|
|
78
|
+
betType: BetTypeFull
|
|
79
|
+
team: Literal["TEAM1", "TEAM2", "DRAW"]
|
|
80
|
+
side: NotRequired[Literal["OVER", "UNDER"]]
|
|
81
|
+
handicap: NotRequired[float]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CancellationDetails(TypedDict):
|
|
85
|
+
key: str
|
|
86
|
+
value: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CancellationReason(TypedDict):
|
|
90
|
+
code: str
|
|
91
|
+
details: list[CancellationDetails]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class StraightBet(TypedDict):
|
|
95
|
+
betId: int
|
|
96
|
+
wagerNumber: int
|
|
97
|
+
placedAt: str
|
|
98
|
+
betStatus: Literal[
|
|
99
|
+
"ACCEPTED",
|
|
100
|
+
"CANCELLED",
|
|
101
|
+
"LOSE",
|
|
102
|
+
"PENDING_ACCEPTANCE",
|
|
103
|
+
"REFUNDED",
|
|
104
|
+
"NOT_ACCEPTED",
|
|
105
|
+
"WON",
|
|
106
|
+
]
|
|
107
|
+
betType: BetTypeFull
|
|
108
|
+
win: float
|
|
109
|
+
risk: float
|
|
110
|
+
oddsFormat: OddsFormat
|
|
111
|
+
updateSequence: int
|
|
112
|
+
price: float
|
|
113
|
+
winLoss: NotRequired[float]
|
|
114
|
+
customerCommission: NotRequired[float]
|
|
115
|
+
cancellationReason: NotRequired[CancellationReason]
|
|
116
|
+
handicap: NotRequired[float]
|
|
117
|
+
side: NotRequired[Literal["OVER", "UNDER"]]
|
|
118
|
+
pitcher1: NotRequired[str]
|
|
119
|
+
pitcher2: NotRequired[str]
|
|
120
|
+
pitcher1MustStart: NotRequired[str]
|
|
121
|
+
pitcher2MustStart: NotRequired[str]
|
|
122
|
+
teamName: NotRequired[str]
|
|
123
|
+
team1: NotRequired[str]
|
|
124
|
+
team2: NotRequired[str]
|
|
125
|
+
periodNumber: NotRequired[int]
|
|
126
|
+
team1Score: NotRequired[float]
|
|
127
|
+
team2Score: NotRequired[float]
|
|
128
|
+
ftTeam1Score: NotRequired[float]
|
|
129
|
+
ftTeam2Score: NotRequired[float]
|
|
130
|
+
pTeam1Score: NotRequired[float]
|
|
131
|
+
pTeam2Score: NotRequired[float]
|
|
132
|
+
isLive: Literal["true", "false"]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class PlaceStraightBetResponse(TypedDict):
|
|
136
|
+
status: Literal["ACCEPTED", "PENDING_ACCEPTANCE", "PROCESSED_WITH_ERROR"]
|
|
137
|
+
uniqueRequestId: str
|
|
138
|
+
errorCode: NotRequired[
|
|
139
|
+
Literal[
|
|
140
|
+
"ALL_BETTING_CLOSED",
|
|
141
|
+
"ALL_LIVE_BETTING_CLOSED",
|
|
142
|
+
"ABOVE_EVENT_MAX",
|
|
143
|
+
"ABOVE_MAX_BET_AMOUNT",
|
|
144
|
+
"BELOW_MIN_BET_AMOUNT",
|
|
145
|
+
"BLOCKED_BETTING",
|
|
146
|
+
"BLOCKED_CLIENT",
|
|
147
|
+
"INSUFFICIENT_FUNDS",
|
|
148
|
+
"INVALID_COUNTRY",
|
|
149
|
+
"INVALID_EVENT",
|
|
150
|
+
"INVALID_ODDS_FORMAT",
|
|
151
|
+
"LINE_CHANGED",
|
|
152
|
+
"LISTED_PITCHERS_SELECTION_ERROR",
|
|
153
|
+
"OFFLINE_EVENT",
|
|
154
|
+
"PAST_CUTOFFTIME",
|
|
155
|
+
"RED_CARDS_CHANGED",
|
|
156
|
+
"SCORE_CHANGED",
|
|
157
|
+
"DUPLICATE_UNIQUE_REQUEST_ID",
|
|
158
|
+
"INCOMPLETE_CUSTOMER_BETTING_PROFILE",
|
|
159
|
+
"INVALID_CUSTOMER_PROFILE",
|
|
160
|
+
"LIMITS_CONFIGURATION_ISSUE",
|
|
161
|
+
"RESPONSIBLE_BETTING_LOSS_LIMIT_EXCEEDED",
|
|
162
|
+
"RESPONSIBLE_BETTING_RISK_LIMIT_EXCEEDED",
|
|
163
|
+
"RESUBMIT_REQUEST",
|
|
164
|
+
"SYSTEM_ERROR_3",
|
|
165
|
+
"LICENCE_RESTRICTION_LIVE_BETTING_BLOCKED",
|
|
166
|
+
"INVALID_HANDICAP",
|
|
167
|
+
"BETTING_SUSPENDED",
|
|
168
|
+
]
|
|
169
|
+
]
|
|
170
|
+
straightBet: NotRequired[StraightBet]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
from typing import TypedDict, NotRequired, Literal
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class CancellationDetailsV3(TypedDict):
|
|
177
|
+
key: str
|
|
178
|
+
value: str
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class CancellationReasonV3(TypedDict):
|
|
182
|
+
code: str
|
|
183
|
+
details: list[CancellationDetailsV3]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class StraightBetV3(TypedDict):
|
|
187
|
+
betId: int
|
|
188
|
+
wagerNumber: int
|
|
189
|
+
placedAt: str
|
|
190
|
+
betStatus: BetStatus
|
|
191
|
+
betStatus2: BetStatus2
|
|
192
|
+
betType: BetTypeFull
|
|
193
|
+
win: float
|
|
194
|
+
risk: float
|
|
195
|
+
oddsFormat: OddsFormat
|
|
196
|
+
updateSequence: int
|
|
197
|
+
price: float
|
|
198
|
+
isLive: bool
|
|
199
|
+
eventStartTime: str
|
|
200
|
+
|
|
201
|
+
# Optional fields (use NotRequired for fields that may be absent)
|
|
202
|
+
winLoss: NotRequired[float | None]
|
|
203
|
+
customerCommission: NotRequired[float | None]
|
|
204
|
+
cancellationReason: NotRequired[CancellationReasonV3]
|
|
205
|
+
sportId: NotRequired[int]
|
|
206
|
+
leagueId: NotRequired[int]
|
|
207
|
+
eventId: NotRequired[int]
|
|
208
|
+
handicap: NotRequired[float | None]
|
|
209
|
+
teamName: NotRequired[str]
|
|
210
|
+
side: NotRequired[Literal["OVER", "UNDER"] | None]
|
|
211
|
+
pitcher1: NotRequired[str | None]
|
|
212
|
+
pitcher2: NotRequired[str | None]
|
|
213
|
+
pitcher1MustStart: NotRequired[bool | None]
|
|
214
|
+
pitcher2MustStart: NotRequired[bool | None]
|
|
215
|
+
team1: NotRequired[str]
|
|
216
|
+
team2: NotRequired[str]
|
|
217
|
+
periodNumber: NotRequired[int]
|
|
218
|
+
team1Score: NotRequired[float | None]
|
|
219
|
+
team2Score: NotRequired[float | None]
|
|
220
|
+
ftTeam1Score: NotRequired[float | None]
|
|
221
|
+
ftTeam2Score: NotRequired[float | None]
|
|
222
|
+
pTeam1Score: NotRequired[float | None]
|
|
223
|
+
pTeam2Score: NotRequired[float | None]
|
|
224
|
+
resultingUnit: NotRequired[str]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# Not implemented placeholders:
|
|
228
|
+
class ParlayBetV2(TypedDict): ...
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class TeaserBet(TypedDict): ...
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class SpecialBetV3(TypedDict): ...
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ManualBet(TypedDict): ...
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class BetsResponse(TypedDict):
|
|
241
|
+
moreAvailable: bool
|
|
242
|
+
pageSize: int
|
|
243
|
+
fromRecord: int
|
|
244
|
+
toRecord: int
|
|
245
|
+
straightBets: list[StraightBetV3]
|
|
246
|
+
parlayBets: list[ParlayBetV2]
|
|
247
|
+
teaserBets: list[TeaserBet]
|
|
248
|
+
specialBets: list[SpecialBetV3]
|
|
249
|
+
manualBets: list[ManualBet]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BasePS3838Error(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResponseError(BasePS3838Error):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AccessBlockedError(ResponseError):
|
|
13
|
+
"""
|
|
14
|
+
Raised when the API returns an empty response, likely due to access restrictions.
|
|
15
|
+
|
|
16
|
+
This may not be a strict rate limit. In many cases, it indicates that the account
|
|
17
|
+
needs to meet certain behavioral criteria — such as placing $30–$40 in manual bets per day —
|
|
18
|
+
before automated requests are allowed again.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WrongEndpoint(ResponseError):
|
|
25
|
+
"""405 HTTP Error"""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PS3838APIError(ResponseError):
|
|
32
|
+
code: str | None
|
|
33
|
+
message: str | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LogicError(BasePS3838Error):
|
|
37
|
+
"""Raised when there is a violation of client-side logic or input invariant."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BaseballOnlyArgumentError(LogicError):
|
|
42
|
+
pass
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TypedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MatchedLeague(TypedDict):
|
|
6
|
+
betsapi_league: str
|
|
7
|
+
ps3838_league: str | None
|
|
8
|
+
ps3838_id: int | None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
#######################################
|
|
12
|
+
# Return Types For the Matching Tanks #
|
|
13
|
+
#######################################
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class NoSuchLeague:
|
|
18
|
+
league: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class NoSuchLeagueMatching(NoSuchLeague):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class NoSuchLeagueFixtures(NoSuchLeague):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class WrongLeague(NoSuchLeague):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class NoSuchEvent:
|
|
37
|
+
league: str
|
|
38
|
+
home: str
|
|
39
|
+
away: str
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class EventTooFarInFuture(NoSuchEvent):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
type Failure = NoSuchLeague | NoSuchEvent
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
#######################################
|
|
49
|
+
# Return Types For the Odds Tank #
|
|
50
|
+
#######################################
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class NoSuchOddsAvailable:
|
|
55
|
+
event_id: int
|
|
56
|
+
|
|
57
|
+
type NoResult = NoSuchLeague | NoSuchEvent | NoSuchOddsAvailable
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# models/fixtures.py
|
|
2
|
+
from typing import Required, TypedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FixtureV3(TypedDict, total=False):
|
|
6
|
+
"""
|
|
7
|
+
Represents a single fixture within the API response.
|
|
8
|
+
|
|
9
|
+
- liveStatus: 0=no live, 1=live event, 2=will be offered live
|
|
10
|
+
- status: Deprecated; check period status in /odds
|
|
11
|
+
- betAcceptanceType: 0=none, 1=danger zone, 2=live delay, 3=both
|
|
12
|
+
- parlayRestriction: 0=full parlay allowed, 1=not allowed, 2=partial
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
id: Required[int]
|
|
16
|
+
parentId: int
|
|
17
|
+
starts: str # date-time in UTC
|
|
18
|
+
home: str
|
|
19
|
+
away: str
|
|
20
|
+
rotNum: str # Will be removed in future; see docs
|
|
21
|
+
liveStatus: int
|
|
22
|
+
homePitcher: str # Baseball only
|
|
23
|
+
awayPitcher: str # Baseball only
|
|
24
|
+
status: str # "O", "H", or "I" (deprecated)
|
|
25
|
+
betAcceptanceType: int
|
|
26
|
+
parlayRestriction: int
|
|
27
|
+
altTeaser: bool
|
|
28
|
+
resultingUnit: str # e.g. "corners", "bookings"
|
|
29
|
+
version: int # fixture version changes with any update
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FixturesLeagueV3(TypedDict):
|
|
33
|
+
"""
|
|
34
|
+
Container for leagues in the Get Fixtures response.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
id: int
|
|
38
|
+
name: str
|
|
39
|
+
events: list[FixtureV3]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FixturesResponse(TypedDict):
|
|
43
|
+
"""
|
|
44
|
+
Full response for GET /v3/fixtures
|
|
45
|
+
|
|
46
|
+
- sportId: same as requested ID
|
|
47
|
+
- last: for delta updates (use as 'since' in next request)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
sportId: int
|
|
51
|
+
last: int
|
|
52
|
+
league: list[FixturesLeagueV3]
|
|
53
|
+
"""list of leagues"""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Literal, Required, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LineResponse(TypedDict, total=False):
|
|
5
|
+
status: Required[Literal["SUCCESS", "NOT_EXISTS"]]
|
|
6
|
+
|
|
7
|
+
# The following fields are present only when status == "SUCCESS"
|
|
8
|
+
price: float
|
|
9
|
+
lineId: int
|
|
10
|
+
altLineId: int
|
|
11
|
+
|
|
12
|
+
team1Score: int
|
|
13
|
+
team2Score: int
|
|
14
|
+
team1RedCards: int
|
|
15
|
+
team2RedCards: int
|
|
16
|
+
|
|
17
|
+
maxRiskStake: float
|
|
18
|
+
minRiskStake: float
|
|
19
|
+
maxWinStake: float
|
|
20
|
+
minWinStake: float
|
|
21
|
+
|
|
22
|
+
effectiveAsOf: str
|
|
23
|
+
|
|
24
|
+
periodTeam1Score: int
|
|
25
|
+
periodTeam2Score: int
|
|
26
|
+
periodTeam1RedCards: int
|
|
27
|
+
periodTeam2RedCards: int
|
ps3838api/models/odds.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TypedDict, Required, NotRequired
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# ─────────────────────────────────────────
|
|
6
|
+
# Top-level structure of the response
|
|
7
|
+
# ─────────────────────────────────────────
|
|
8
|
+
class OddsResponse(TypedDict):
|
|
9
|
+
sportId: int
|
|
10
|
+
last: int # Used for `since` in future incremental updates
|
|
11
|
+
leagues: list[OddsLeagueV3]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ─────────────────────────────────────────
|
|
15
|
+
# League level
|
|
16
|
+
# ─────────────────────────────────────────
|
|
17
|
+
class OddsLeagueV3(TypedDict):
|
|
18
|
+
id: int
|
|
19
|
+
events: list[OddsEventV3]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ─────────────────────────────────────────
|
|
23
|
+
# Event level
|
|
24
|
+
# ─────────────────────────────────────────
|
|
25
|
+
class OddsEventV3(TypedDict, total=False):
|
|
26
|
+
id: Required[int]
|
|
27
|
+
awayScore: float
|
|
28
|
+
homeScore: float
|
|
29
|
+
awayRedCards: int
|
|
30
|
+
homeRedCards: int
|
|
31
|
+
periods: list[OddsPeriodV3]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ─────────────────────────────────────────
|
|
35
|
+
# Period level (e.g., full match, 1st half, etc.)
|
|
36
|
+
# ─────────────────────────────────────────
|
|
37
|
+
class OddsPeriodV3(TypedDict, total=False):
|
|
38
|
+
lineId: int
|
|
39
|
+
number: int # 0 = full match, 1 = 1st half, etc.
|
|
40
|
+
cutoff: str # ISO datetime string (UTC)
|
|
41
|
+
status: int # 1 = online, 2 = offline
|
|
42
|
+
|
|
43
|
+
maxSpread: float
|
|
44
|
+
maxMoneyline: float
|
|
45
|
+
maxTotal: float
|
|
46
|
+
maxTeamTotal: float
|
|
47
|
+
|
|
48
|
+
moneylineUpdatedAt: str
|
|
49
|
+
spreadUpdatedAt: str
|
|
50
|
+
totalUpdatedAt: str
|
|
51
|
+
teamTotalUpdatedAt: str
|
|
52
|
+
|
|
53
|
+
spreads: list[OddsSpreadV3]
|
|
54
|
+
moneyline: OddsMoneylineV3
|
|
55
|
+
totals: list[OddsTotalV3]
|
|
56
|
+
teamTotal: OddsTeamTotalsV3
|
|
57
|
+
|
|
58
|
+
# Live stats at period level (Match and Extra Time only)
|
|
59
|
+
awayScore: float
|
|
60
|
+
homeScore: float
|
|
61
|
+
awayRedCards: int
|
|
62
|
+
homeRedCards: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ─────────────────────────────────────────
|
|
66
|
+
# Spread line data (handicap)
|
|
67
|
+
# ─────────────────────────────────────────
|
|
68
|
+
class OddsSpreadV3(TypedDict, total=False):
|
|
69
|
+
altLineId: int # Present only for alternative lines
|
|
70
|
+
hdp: float # Handicap
|
|
71
|
+
home: float # Decimal odds for home team
|
|
72
|
+
away: float # Decimal odds for away team
|
|
73
|
+
max: float # Overrides `maxSpread` if present
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ─────────────────────────────────────────
|
|
77
|
+
# Moneyline data (1X2 market)
|
|
78
|
+
# ─────────────────────────────────────────
|
|
79
|
+
class OddsMoneylineV3(TypedDict, total=False):
|
|
80
|
+
home: float
|
|
81
|
+
away: float
|
|
82
|
+
draw: float # Optional, only for sports/events with a draw
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ─────────────────────────────────────────
|
|
86
|
+
# Total Points line (Over/Under market)
|
|
87
|
+
# ─────────────────────────────────────────
|
|
88
|
+
class OddsTotalV3(TypedDict):
|
|
89
|
+
altLineId: NotRequired[int] # Optional alternative line
|
|
90
|
+
points: float # Total goals/points line
|
|
91
|
+
over: float # Decimal odds for over
|
|
92
|
+
under: float # Decimal odds for under
|
|
93
|
+
max: NotRequired[float] # Overrides `maxTotal` if present
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ─────────────────────────────────────────
|
|
97
|
+
# Team Total Points (each team separately)
|
|
98
|
+
# ─────────────────────────────────────────
|
|
99
|
+
class OddsTeamTotalsV3(TypedDict, total=False):
|
|
100
|
+
home: OddsTeamTotalV3
|
|
101
|
+
away: OddsTeamTotalV3
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class OddsTeamTotalV3(TypedDict, total=False):
|
|
105
|
+
points: float # Team-specific total line
|
|
106
|
+
over: float
|
|
107
|
+
under: float
|