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.
@@ -0,0 +1,250 @@
1
+ from typing import Literal, NotRequired, TypedDict
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 BetList = Literal["SETTLED", "RUNNING", "ALL"]
27
+
28
+ type SortDir = Literal["ASC", "DESC"]
29
+
30
+ type BetStatus = Literal[
31
+ "ACCEPTED",
32
+ "CANCELLED",
33
+ "LOSE",
34
+ "PENDING_ACCEPTANCE",
35
+ "REFUNDED",
36
+ "NOT_ACCEPTED",
37
+ "WON",
38
+ "REJECTED",
39
+ ]
40
+
41
+ type BetStatus2 = Literal[
42
+ "ACCEPTED",
43
+ "CANCELLED",
44
+ "LOST",
45
+ "PENDING_ACCEPTANCE",
46
+ "REFUNDED",
47
+ "NOT_ACCEPTED",
48
+ "WON",
49
+ "REJECTED",
50
+ "HALF_WON_HALF_PUSHED",
51
+ "HALF_LOST_HALF_PUSHED",
52
+ ]
53
+
54
+ type BetType = Literal["MONEYLINE", "TEAM_TOTAL_POINTS", "SPREAD", "TOTAL_POINTS"]
55
+
56
+ type BetTypeFull = Literal[
57
+ "MONEYLINE",
58
+ "TEAM_TOTAL_POINTS",
59
+ "SPREAD",
60
+ "TOTAL_POINTS",
61
+ "SPECIAL",
62
+ "PARLAY",
63
+ "TEASER",
64
+ "MANUAL",
65
+ ]
66
+
67
+
68
+ class PlaceStraightBetRequest(TypedDict):
69
+ oddsFormat: OddsFormat
70
+ uniqueRequestId: str
71
+ acceptBetterLine: bool
72
+ stake: float
73
+ winRiskStake: Literal["WIN", "RISK"]
74
+ lineId: int
75
+ altLineId: NotRequired[int]
76
+ pitcher1MustStart: bool
77
+ pitcher2MustStart: bool
78
+ fillType: Literal["NORMAL", "FILLANDKILL", "FILLMAXLIMIT"]
79
+ sportId: int
80
+ eventId: int
81
+ periodNumber: int
82
+ betType: BetTypeFull
83
+ team: Literal["TEAM1", "TEAM2", "DRAW"]
84
+ side: NotRequired[Literal["OVER", "UNDER"]]
85
+ handicap: NotRequired[float]
86
+
87
+
88
+ class CancellationDetails(TypedDict):
89
+ key: str
90
+ value: str
91
+
92
+
93
+ class CancellationReason(TypedDict):
94
+ code: str
95
+ details: list[CancellationDetails]
96
+
97
+
98
+ class StraightBet(TypedDict):
99
+ betId: int
100
+ wagerNumber: int
101
+ placedAt: str
102
+ betStatus: Literal[
103
+ "ACCEPTED",
104
+ "CANCELLED",
105
+ "LOSE",
106
+ "PENDING_ACCEPTANCE",
107
+ "REFUNDED",
108
+ "NOT_ACCEPTED",
109
+ "WON",
110
+ ]
111
+ betType: BetTypeFull
112
+ win: float
113
+ risk: float
114
+ oddsFormat: OddsFormat
115
+ updateSequence: int
116
+ price: float
117
+ winLoss: NotRequired[float]
118
+ customerCommission: NotRequired[float]
119
+ cancellationReason: NotRequired[CancellationReason]
120
+ handicap: NotRequired[float]
121
+ side: NotRequired[Literal["OVER", "UNDER"]]
122
+ pitcher1: NotRequired[str]
123
+ pitcher2: NotRequired[str]
124
+ pitcher1MustStart: NotRequired[str]
125
+ pitcher2MustStart: NotRequired[str]
126
+ teamName: NotRequired[str]
127
+ team1: NotRequired[str]
128
+ team2: NotRequired[str]
129
+ periodNumber: NotRequired[int]
130
+ team1Score: NotRequired[float]
131
+ team2Score: NotRequired[float]
132
+ ftTeam1Score: NotRequired[float]
133
+ ftTeam2Score: NotRequired[float]
134
+ pTeam1Score: NotRequired[float]
135
+ pTeam2Score: NotRequired[float]
136
+ isLive: Literal["true", "false"]
137
+
138
+
139
+ class PlaceStraightBetResponse(TypedDict):
140
+ status: Literal["ACCEPTED", "PENDING_ACCEPTANCE", "PROCESSED_WITH_ERROR"]
141
+ uniqueRequestId: str
142
+ errorCode: NotRequired[
143
+ Literal[
144
+ "ALL_BETTING_CLOSED",
145
+ "ALL_LIVE_BETTING_CLOSED",
146
+ "ABOVE_EVENT_MAX",
147
+ "ABOVE_MAX_BET_AMOUNT",
148
+ "BELOW_MIN_BET_AMOUNT",
149
+ "BLOCKED_BETTING",
150
+ "BLOCKED_CLIENT",
151
+ "INSUFFICIENT_FUNDS",
152
+ "INVALID_COUNTRY",
153
+ "INVALID_EVENT",
154
+ "INVALID_ODDS_FORMAT",
155
+ "LINE_CHANGED",
156
+ "LISTED_PITCHERS_SELECTION_ERROR",
157
+ "OFFLINE_EVENT",
158
+ "PAST_CUTOFFTIME",
159
+ "RED_CARDS_CHANGED",
160
+ "SCORE_CHANGED",
161
+ "DUPLICATE_UNIQUE_REQUEST_ID",
162
+ "INCOMPLETE_CUSTOMER_BETTING_PROFILE",
163
+ "INVALID_CUSTOMER_PROFILE",
164
+ "LIMITS_CONFIGURATION_ISSUE",
165
+ "RESPONSIBLE_BETTING_LOSS_LIMIT_EXCEEDED",
166
+ "RESPONSIBLE_BETTING_RISK_LIMIT_EXCEEDED",
167
+ "RESUBMIT_REQUEST",
168
+ "SYSTEM_ERROR_3",
169
+ "LICENCE_RESTRICTION_LIVE_BETTING_BLOCKED",
170
+ "INVALID_HANDICAP",
171
+ "BETTING_SUSPENDED",
172
+ ]
173
+ ]
174
+ straightBet: NotRequired[StraightBet]
175
+
176
+
177
+ class CancellationDetailsV3(TypedDict):
178
+ key: str
179
+ value: str
180
+
181
+
182
+ class CancellationReasonV3(TypedDict):
183
+ code: str
184
+ details: list[CancellationDetailsV3]
185
+
186
+
187
+ class StraightBetV3(TypedDict):
188
+ betId: int
189
+ wagerNumber: int
190
+ placedAt: str
191
+ betStatus: BetStatus
192
+ betStatus2: BetStatus2
193
+ betType: BetTypeFull
194
+ win: float
195
+ risk: float
196
+ oddsFormat: OddsFormat
197
+ updateSequence: int
198
+ price: float
199
+ isLive: bool
200
+ eventStartTime: str
201
+
202
+ # Optional fields (use NotRequired for fields that may be absent)
203
+ winLoss: NotRequired[float | None]
204
+ customerCommission: NotRequired[float | None]
205
+ cancellationReason: NotRequired[CancellationReasonV3]
206
+ sportId: NotRequired[int]
207
+ leagueId: NotRequired[int]
208
+ eventId: NotRequired[int]
209
+ handicap: NotRequired[float | None]
210
+ teamName: NotRequired[str]
211
+ side: NotRequired[Literal["OVER", "UNDER"] | None]
212
+ pitcher1: NotRequired[str | None]
213
+ pitcher2: NotRequired[str | None]
214
+ pitcher1MustStart: NotRequired[bool | None]
215
+ pitcher2MustStart: NotRequired[bool | None]
216
+ team1: NotRequired[str]
217
+ team2: NotRequired[str]
218
+ periodNumber: NotRequired[int]
219
+ team1Score: NotRequired[float | None]
220
+ team2Score: NotRequired[float | None]
221
+ ftTeam1Score: NotRequired[float | None]
222
+ ftTeam2Score: NotRequired[float | None]
223
+ pTeam1Score: NotRequired[float | None]
224
+ pTeam2Score: NotRequired[float | None]
225
+ resultingUnit: NotRequired[str]
226
+
227
+
228
+ # Not implemented placeholders:
229
+ class ParlayBetV2(TypedDict): ...
230
+
231
+
232
+ class TeaserBet(TypedDict): ...
233
+
234
+
235
+ class SpecialBetV3(TypedDict): ...
236
+
237
+
238
+ class ManualBet(TypedDict): ...
239
+
240
+
241
+ class BetsResponse(TypedDict):
242
+ moreAvailable: bool
243
+ pageSize: int
244
+ fromRecord: int
245
+ toRecord: int
246
+ straightBets: list[StraightBetV3]
247
+ parlayBets: list[ParlayBetV2]
248
+ teaserBets: list[TeaserBet]
249
+ specialBets: list[SpecialBetV3]
250
+ manualBets: list[ManualBet]
@@ -0,0 +1,48 @@
1
+ from typing import Literal, NotRequired, TypedDict
2
+
3
+
4
+ class BalanceData(TypedDict):
5
+ availableBalance: float
6
+ outstandingTransactions: float
7
+ givenCredit: float
8
+ currency: str
9
+
10
+
11
+ class PeriodData(TypedDict, total=False):
12
+ number: NotRequired[int]
13
+ description: NotRequired[str]
14
+ shortDescription: NotRequired[str]
15
+ spreadDescription: NotRequired[str]
16
+ moneylineDescription: NotRequired[str]
17
+ totalDescription: NotRequired[str]
18
+ team1TotalDescription: NotRequired[str]
19
+ team2TotalDescription: NotRequired[str]
20
+ spreadShortDescription: NotRequired[str]
21
+ moneylineShortDescription: NotRequired[str]
22
+ totalShortDescription: NotRequired[str]
23
+ team1TotalShortDescription: NotRequired[str]
24
+ team2TotalShortDescription: NotRequired[str]
25
+
26
+
27
+ class LeagueV3(TypedDict):
28
+ id: int
29
+ name: str
30
+ homeTeamType: NotRequired[str]
31
+ hasOfferings: NotRequired[bool]
32
+ container: NotRequired[str]
33
+ allowRoundRobins: NotRequired[bool]
34
+ leagueSpecialsCount: NotRequired[int]
35
+ eventSpecialsCount: NotRequired[int]
36
+ eventCount: NotRequired[int]
37
+
38
+
39
+ class BettingStatusResponse(TypedDict):
40
+ status: Literal["ALL_BETTING_ENABLED", "ALL_LIVE_BETTING_CLOSED", "ALL_BETTING_CLOSED"]
41
+
42
+
43
+ __all__ = [
44
+ "BalanceData",
45
+ "PeriodData",
46
+ "LeagueV3",
47
+ "BettingStatusResponse",
48
+ ]
@@ -0,0 +1,43 @@
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
+
39
+ pass
40
+
41
+
42
+ class BaseballOnlyArgumentError(LogicError):
43
+ pass
@@ -0,0 +1,61 @@
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
+
31
+ @dataclass
32
+ class WrongLeague(NoSuchLeague):
33
+ pass
34
+
35
+
36
+ @dataclass
37
+ class NoSuchEvent:
38
+ league: str
39
+ home: str
40
+ away: str
41
+
42
+
43
+ @dataclass
44
+ class EventTooFarInFuture(NoSuchEvent):
45
+ pass
46
+
47
+
48
+ type Failure = NoSuchLeague | NoSuchEvent
49
+
50
+
51
+ #######################################
52
+ # Return Types For the Odds Tank #
53
+ #######################################
54
+
55
+
56
+ @dataclass
57
+ class NoSuchOddsAvailable:
58
+ event_id: int
59
+
60
+
61
+ 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: Required[str]
19
+ away: Required[str]
20
+ rotNum: str # Will be removed in future; see docs
21
+ liveStatus: Required[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: Required[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,221 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import NotRequired, Required, TypedDict
4
+
5
+
6
+ # ─────────────────────────────────────────
7
+ # Top-level structure of the response (V3)
8
+ # ─────────────────────────────────────────
9
+ class OddsResponse(TypedDict):
10
+ sportId: int
11
+ last: int # Used for `since` in future incremental updates
12
+ leagues: list[OddsLeagueV3]
13
+
14
+
15
+ # ─────────────────────────────────────────
16
+ # League level
17
+ # ─────────────────────────────────────────
18
+ class OddsLeagueV3(TypedDict):
19
+ id: int
20
+ events: list[OddsEventV3]
21
+
22
+
23
+ # ─────────────────────────────────────────
24
+ # Event level
25
+ # ─────────────────────────────────────────
26
+ class OddsEventV3(TypedDict, total=False):
27
+ id: Required[int]
28
+ awayScore: float
29
+ homeScore: float
30
+ awayRedCards: int
31
+ homeRedCards: int
32
+ periods: list[OddsPeriodV3]
33
+
34
+
35
+ # ─────────────────────────────────────────
36
+ # Period level (e.g., full match, 1st half, etc.)
37
+ # ─────────────────────────────────────────
38
+ class OddsPeriodV3(TypedDict, total=False):
39
+ lineId: int
40
+ number: int # 0 = full match, 1 = 1st half, etc.
41
+ cutoff: str # ISO datetime string (UTC)
42
+ status: int # 1 = online, 2 = offline
43
+
44
+ maxSpread: float
45
+ maxMoneyline: float
46
+ maxTotal: float
47
+ maxTeamTotal: float
48
+
49
+ moneylineUpdatedAt: str
50
+ spreadUpdatedAt: str
51
+ totalUpdatedAt: str
52
+ teamTotalUpdatedAt: str
53
+
54
+ spreads: list[OddsSpreadV3]
55
+ moneyline: OddsMoneylineV3
56
+ totals: list[OddsTotalV3]
57
+ teamTotal: OddsTeamTotalsV3
58
+
59
+ # Live stats at period level (Match and Extra Time only)
60
+ awayScore: float
61
+ homeScore: float
62
+ awayRedCards: int
63
+ homeRedCards: int
64
+
65
+
66
+ # ─────────────────────────────────────────
67
+ # Spread line data (handicap)
68
+ # ─────────────────────────────────────────
69
+ class OddsSpreadV3(TypedDict, total=False):
70
+ altLineId: int # Present only for alternative lines
71
+ hdp: float # Handicap
72
+ home: float # Decimal odds for home team
73
+ away: float # Decimal odds for away team
74
+ max: float # Overrides `maxSpread` if present
75
+
76
+
77
+ # ─────────────────────────────────────────
78
+ # Moneyline data (1X2 market)
79
+ # ─────────────────────────────────────────
80
+ class OddsMoneylineV3(TypedDict, total=False):
81
+ home: float
82
+ away: float
83
+ draw: float # Optional, only for sports/events with a draw
84
+
85
+
86
+ # ─────────────────────────────────────────
87
+ # Total Points line (Over/Under market)
88
+ # ─────────────────────────────────────────
89
+ class OddsTotalV3(TypedDict):
90
+ altLineId: NotRequired[int] # Optional alternative line
91
+ points: float # Total goals/points line
92
+ over: float # Decimal odds for over
93
+ under: float # Decimal odds for under
94
+ max: NotRequired[float] # Overrides `maxTotal` if present
95
+
96
+
97
+ # ─────────────────────────────────────────
98
+ # Team Total Points (each team separately)
99
+ # ─────────────────────────────────────────
100
+ class OddsTeamTotalsV3(TypedDict, total=False):
101
+ home: OddsTeamTotalV3
102
+ away: OddsTeamTotalV3
103
+
104
+
105
+ class OddsTeamTotalV3(TypedDict, total=False):
106
+ points: float # Team-specific total line
107
+ over: float
108
+ under: float
109
+
110
+
111
+ # ═════════════════════════════════════════════════════════════════════════════
112
+ # V4 Odds Models
113
+ # ═════════════════════════════════════════════════════════════════════════════
114
+
115
+
116
+ # ─────────────────────────────────────────
117
+ # V4 Top-level response structure
118
+ # ─────────────────────────────────────────
119
+ class OddsResponseV4(TypedDict):
120
+ sportId: int
121
+ last: int # Used for `since` in future incremental updates
122
+ leagues: list[OddsLeagueV4]
123
+
124
+
125
+ # ─────────────────────────────────────────
126
+ # V4 League level
127
+ # ─────────────────────────────────────────
128
+ class OddsLeagueV4(TypedDict):
129
+ id: int
130
+ events: list[OddsEventV4]
131
+
132
+
133
+ # ─────────────────────────────────────────
134
+ # V4 Event level
135
+ # ─────────────────────────────────────────
136
+ class OddsEventV4(TypedDict, total=False):
137
+ id: Required[int]
138
+ awayScore: float
139
+ homeScore: float
140
+ awayRedCards: int
141
+ homeRedCards: int
142
+ periods: list[OddsPeriodV4]
143
+
144
+
145
+ # ─────────────────────────────────────────
146
+ # V4 Period level
147
+ # ─────────────────────────────────────────
148
+ class OddsPeriodV4(TypedDict, total=False):
149
+ lineId: int
150
+ number: int # 0 = full match, 1 = 1st half, etc.
151
+ cutoff: str # ISO datetime string (UTC)
152
+ status: int # 1 = online, 2 = offline
153
+
154
+ maxSpread: float
155
+ maxMoneyline: float
156
+ maxTotal: float
157
+ maxTeamTotal: float
158
+
159
+ moneylineUpdatedAt: str
160
+ spreadUpdatedAt: str
161
+ totalUpdatedAt: str
162
+ teamTotalUpdatedAt: str
163
+
164
+ spreads: list[OddsSpreadV4]
165
+ moneyline: OddsMoneylineV4
166
+ totals: list[OddsTotalV4]
167
+ teamTotal: OddsTeamTotalsV4
168
+
169
+ # Live stats at period level (Match and Extra Time only)
170
+ awayScore: float
171
+ homeScore: float
172
+ awayRedCards: int
173
+ homeRedCards: int
174
+
175
+
176
+ # ─────────────────────────────────────────
177
+ # V4 Spread line data (handicap)
178
+ # ─────────────────────────────────────────
179
+ class OddsSpreadV4(TypedDict, total=False):
180
+ altLineId: int # Present only for alternative lines
181
+ hdp: float # Handicap
182
+ home: float # Decimal odds for home team
183
+ away: float # Decimal odds for away team
184
+ max: float # Overrides `maxSpread` if present
185
+
186
+
187
+ # ─────────────────────────────────────────
188
+ # V4 Moneyline data (1X2 market)
189
+ # ─────────────────────────────────────────
190
+ class OddsMoneylineV4(TypedDict, total=False):
191
+ home: float
192
+ away: float
193
+ draw: float # Optional, only for sports/events with a draw
194
+
195
+
196
+ # ─────────────────────────────────────────
197
+ # V4 Total Points line (Over/Under market)
198
+ # ─────────────────────────────────────────
199
+ class OddsTotalV4(TypedDict, total=False):
200
+ altLineId: int # Optional alternative line
201
+ points: Required[float] # Total goals/points line
202
+ over: Required[float] # Decimal odds for over
203
+ under: Required[float] # Decimal odds for under
204
+ max: float # Overrides `maxTotal` if present
205
+
206
+
207
+ # ─────────────────────────────────────────
208
+ # V4 Team Total Points (each team separately)
209
+ # Key difference from V3: arrays instead of single objects
210
+ # ─────────────────────────────────────────
211
+ class OddsTeamTotalsV4(TypedDict, total=False):
212
+ home: list[OddsTeamTotalV4]
213
+ away: list[OddsTeamTotalV4]
214
+
215
+
216
+ class OddsTeamTotalV4(TypedDict, total=False):
217
+ points: Required[float] # Team-specific total line
218
+ over: Required[float]
219
+ under: Required[float]
220
+ altLineId: int # Present only for alternative lines
221
+ max: float # Maximum bet volume for alternative lines