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/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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ from typing import TypedDict
2
+
3
+ class EventInfo(TypedDict):
4
+ leagueId: int
5
+ eventId: int