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,231 @@
1
+ """
2
+ Legacy helper functions that use a shared default :class:`Client`.
3
+ """
4
+
5
+ import sys
6
+ import warnings
7
+ from datetime import datetime
8
+ from typing import Any, Literal
9
+
10
+ if sys.version_info >= (3, 13):
11
+ from warnings import deprecated
12
+ else:
13
+
14
+ def deprecated(reason: str): # type: ignore
15
+ def decorator(func): # type: ignore
16
+ def wrapper(*args, **kwargs): # type: ignore
17
+ warnings.warn(
18
+ f"{func.__name__} is deprecated: {reason}", # type: ignore
19
+ DeprecationWarning,
20
+ stacklevel=2,
21
+ )
22
+ return func(*args, **kwargs) # type: ignore
23
+
24
+ wrapper.__name__ = func.__name__ # type: ignore
25
+ wrapper.__doc__ = func.__doc__ # type: ignore
26
+ wrapper.__dict__.update(func.__dict__) # type: ignore
27
+ return wrapper # type: ignore
28
+
29
+ return decorator # type: ignore
30
+
31
+
32
+ from ps3838api.models.bets import BetType, FillType, OddsFormat, PlaceStraightBetResponse, Side, Team
33
+ from ps3838api.models.client import BalanceData, BettingStatusResponse, LeagueV3, PeriodData
34
+ from ps3838api.models.fixtures import FixturesResponse
35
+ from ps3838api.models.lines import LineResponse
36
+ from ps3838api.models.odds import OddsResponse
37
+ from ps3838api.models.sports import SOCCER_SPORT_ID
38
+
39
+ from .client import PinnacleClient
40
+
41
+ _default_client: PinnacleClient | None = None
42
+
43
+
44
+ def _get_default_client() -> PinnacleClient:
45
+ global _default_client # noqa: PLW0603
46
+ if _default_client is None:
47
+ _default_client = PinnacleClient()
48
+ return _default_client
49
+
50
+
51
+ @deprecated("Use `ps3838api.client.PinnacleClient` base methods")
52
+ def get_client_balance() -> BalanceData:
53
+ return _get_default_client().get_client_balance()
54
+
55
+
56
+ @deprecated("Use `ps3838api.client.PinnacleClient` base methods")
57
+ def get_periods(sport_id: int | None = None) -> list[PeriodData]:
58
+ return _get_default_client().get_periods(sport_id=sport_id)
59
+
60
+
61
+ @deprecated("Use `ps3838api.models.sports` sports constant")
62
+ def get_sports() -> Any:
63
+ return _get_default_client().get_sports()
64
+
65
+
66
+ @deprecated("Use `ps3838api.client.PinnacleClient` base methods")
67
+ def get_leagues(sport_id: int | None = None) -> list[LeagueV3]:
68
+ return _get_default_client().get_leagues(sport_id=sport_id)
69
+
70
+
71
+ @deprecated("Use `ps3838api.client.PinnacleClient` base methods")
72
+ def get_fixtures(
73
+ sport_id: int | None = None,
74
+ league_ids: list[int] | None = None,
75
+ is_live: bool | None = None,
76
+ since: int | None = None,
77
+ event_ids: list[int] | None = None,
78
+ settled: bool = False,
79
+ ) -> FixturesResponse:
80
+ return _get_default_client().get_fixtures(
81
+ sport_id=sport_id,
82
+ league_ids=league_ids,
83
+ is_live=is_live,
84
+ since=since,
85
+ event_ids=event_ids,
86
+ settled=settled,
87
+ )
88
+
89
+
90
+ @deprecated("Use `ps3838api.client.PinnacleClient` base methods")
91
+ def get_odds(
92
+ sport_id: int | None = None,
93
+ is_special: bool = False,
94
+ league_ids: list[int] | None = None,
95
+ odds_format: OddsFormat = "DECIMAL",
96
+ since: int | None = None,
97
+ is_live: bool | None = None,
98
+ event_ids: list[int] | None = None,
99
+ ) -> OddsResponse:
100
+ return _get_default_client().get_odds(
101
+ sport_id=sport_id,
102
+ is_special=is_special,
103
+ league_ids=league_ids,
104
+ odds_format=odds_format,
105
+ since=since,
106
+ is_live=is_live,
107
+ event_ids=event_ids,
108
+ )
109
+
110
+
111
+ @deprecated("Use `ps3838api.client.PinnacleClient` base methods")
112
+ def get_special_fixtures(
113
+ sport_id: int | None = None,
114
+ league_ids: list[int] | None = None,
115
+ event_id: int | None = None,
116
+ ) -> Any:
117
+ return _get_default_client().get_special_fixtures(
118
+ sport_id=sport_id, league_ids=league_ids, event_id=event_id
119
+ )
120
+
121
+
122
+ @deprecated("Use `ps3838api.client.PinnacleClient` base methods")
123
+ def get_line(
124
+ league_id: int,
125
+ event_id: int,
126
+ period_number: int,
127
+ bet_type: Literal["SPREAD", "MONEYLINE", "TOTAL_POINTS", "TEAM_TOTAL_POINTS"],
128
+ handicap: float,
129
+ team: Literal["Team1", "Team2", "Draw"] | None = None,
130
+ side: Literal["OVER", "UNDER"] | None = None,
131
+ sport_id: int | None = None,
132
+ odds_format: str = "Decimal",
133
+ ) -> LineResponse:
134
+ return _get_default_client().get_line(
135
+ league_id=league_id,
136
+ event_id=event_id,
137
+ period_number=period_number,
138
+ bet_type=bet_type,
139
+ handicap=handicap,
140
+ team=team,
141
+ side=side,
142
+ sport_id=sport_id,
143
+ odds_format=odds_format,
144
+ )
145
+
146
+
147
+ @deprecated("Use `ps3838api.client.PinnacleClient` base methods")
148
+ def place_straigh_bet(
149
+ *,
150
+ stake: float,
151
+ event_id: int,
152
+ bet_type: BetType,
153
+ line_id: int | None,
154
+ period_number: int = 0,
155
+ sport_id: int = SOCCER_SPORT_ID,
156
+ alt_line_id: int | None = None,
157
+ unique_request_id: str | None = None,
158
+ odds_format: OddsFormat = "DECIMAL",
159
+ fill_type: FillType = "NORMAL",
160
+ accept_better_line: bool = True,
161
+ win_risk_stake: Literal["WIN", "RISK"] = "RISK",
162
+ pitcher1_must_start: bool = True,
163
+ pitcher2_must_start: bool = True,
164
+ team: Team | None = None,
165
+ side: Side | None = None,
166
+ handicap: float | None = None,
167
+ ) -> PlaceStraightBetResponse:
168
+ return _get_default_client().place_straight_bet(
169
+ stake=stake,
170
+ event_id=event_id,
171
+ bet_type=bet_type,
172
+ line_id=line_id,
173
+ period_number=period_number,
174
+ sport_id=sport_id,
175
+ alt_line_id=alt_line_id,
176
+ unique_request_id=unique_request_id,
177
+ odds_format=odds_format,
178
+ fill_type=fill_type,
179
+ accept_better_line=accept_better_line,
180
+ win_risk_stake=win_risk_stake,
181
+ pitcher1_must_start=pitcher1_must_start,
182
+ pitcher2_must_start=pitcher2_must_start,
183
+ team=team,
184
+ side=side,
185
+ handicap=handicap,
186
+ )
187
+
188
+
189
+ @deprecated("Use `ps3838api.client.PinnacleClient` base methods")
190
+ def get_betting_status() -> BettingStatusResponse:
191
+ return _get_default_client().get_betting_status()
192
+
193
+
194
+ def export_my_bets(
195
+ *,
196
+ from_datetime: datetime,
197
+ to_datetime: datetime,
198
+ d: int = -1,
199
+ status: Literal["UNSETTLED", "SETTLED"] = "SETTLED",
200
+ sd: bool = False,
201
+ bet_type: str = "WAGER",
202
+ product: str = "SB,PP,BG",
203
+ locale: str = "en_US",
204
+ timezone: str = "GMT-4",
205
+ ) -> bytes:
206
+ return _get_default_client().export_my_bets(
207
+ from_datetime=from_datetime,
208
+ to_datetime=to_datetime,
209
+ d=d,
210
+ status=status,
211
+ sd=sd,
212
+ bet_type=bet_type,
213
+ product=product,
214
+ locale=locale,
215
+ timezone=timezone,
216
+ )
217
+
218
+
219
+ __all__ = [
220
+ "get_client_balance",
221
+ "get_periods",
222
+ "get_sports",
223
+ "get_leagues",
224
+ "get_fixtures",
225
+ "get_odds",
226
+ "get_special_fixtures",
227
+ "get_line",
228
+ "place_straigh_bet",
229
+ "get_betting_status",
230
+ "export_my_bets",
231
+ ]
@@ -0,0 +1,107 @@
1
+ from typing import TYPE_CHECKING, Any, cast
2
+
3
+ from ps3838api.models.bets import OddsFormat
4
+ from ps3838api.models.odds import OddsResponseV4
5
+
6
+ if TYPE_CHECKING:
7
+ # to avoid ciruclar imports
8
+ from ps3838api.api.client import PinnacleClient
9
+
10
+
11
+ class V4PinnacleClient:
12
+ """Subclient for V4 API endpoints.
13
+
14
+ V4 endpoints provide enhanced responses, particularly for team totals
15
+ which return arrays of alternative lines instead of single objects.
16
+ """
17
+
18
+ def __init__(self, client: "PinnacleClient"):
19
+ self._client = client
20
+
21
+ def get_odds(
22
+ self,
23
+ sport_id: int | None = None,
24
+ league_ids: list[int] | None = None,
25
+ odds_format: OddsFormat = "DECIMAL",
26
+ since: int | None = None,
27
+ is_live: bool | None = None,
28
+ event_ids: list[int] | None = None,
29
+ to_currency_code: str | None = None,
30
+ ) -> OddsResponseV4:
31
+ """Get straight odds for non-settled events using V4 endpoint.
32
+
33
+ V4 returns enhanced team total data with arrays of alternative lines.
34
+
35
+ Args:
36
+ sport_id: Sport ID. Uses client's default_sport if not provided.
37
+ league_ids: List of league IDs to filter.
38
+ odds_format: Format for odds (DECIMAL, AMERICAN, etc.).
39
+ since: Used for incremental updates. Use 'last' from previous response.
40
+ is_live: True for live events only, False for prematch only.
41
+ event_ids: List of event IDs to filter.
42
+ to_currency_code: Convert limits to specified currency.
43
+
44
+ Returns:
45
+ OddsResponseV4 containing odds data with V4 enhanced structure.
46
+ """
47
+ endpoint = "/v4/odds"
48
+
49
+ resolved_sport_id = sport_id if sport_id is not None else self._client.default_sport
50
+
51
+ params: dict[str, Any] = {
52
+ "sportId": resolved_sport_id,
53
+ "oddsFormat": odds_format,
54
+ }
55
+ if league_ids:
56
+ params["leagueIds"] = ",".join(map(str, league_ids))
57
+ if since is not None:
58
+ params["since"] = since
59
+ if is_live is not None:
60
+ params["isLive"] = int(is_live)
61
+ if event_ids:
62
+ params["eventIds"] = ",".join(map(str, event_ids))
63
+ if to_currency_code is not None:
64
+ params["toCurrencyCode"] = to_currency_code
65
+
66
+ return cast(OddsResponseV4, self._client._get(endpoint, params)) # pyright: ignore[reportPrivateUsage]
67
+
68
+ def get_parlay_odds(
69
+ self,
70
+ sport_id: int | None = None,
71
+ league_ids: list[int] | None = None,
72
+ odds_format: OddsFormat = "DECIMAL",
73
+ since: int | None = None,
74
+ is_live: bool | None = None,
75
+ event_ids: list[int] | None = None,
76
+ ) -> OddsResponseV4:
77
+ """Get parlay odds for non-settled events using V4 endpoint.
78
+
79
+ Args:
80
+ sport_id: Sport ID. Uses client's default_sport if not provided.
81
+ league_ids: List of league IDs to filter.
82
+ odds_format: Format for odds (DECIMAL, AMERICAN, etc.).
83
+ since: Used for incremental updates. Use 'last' from previous response.
84
+ is_live: True for live events only, False for prematch only.
85
+ event_ids: List of event IDs to filter.
86
+
87
+ Returns:
88
+ OddsResponseV4 containing parlay odds data.
89
+ """
90
+ endpoint = "/v4/odds/parlay"
91
+
92
+ resolved_sport_id = sport_id if sport_id is not None else self._client.default_sport
93
+
94
+ params: dict[str, Any] = {
95
+ "sportId": resolved_sport_id,
96
+ "oddsFormat": odds_format,
97
+ }
98
+ if league_ids:
99
+ params["leagueIds"] = ",".join(map(str, league_ids))
100
+ if since is not None:
101
+ params["since"] = since
102
+ if is_live is not None:
103
+ params["isLive"] = int(is_live)
104
+ if event_ids:
105
+ params["eventIds"] = ",".join(map(str, event_ids))
106
+
107
+ return cast(OddsResponseV4, self._client._get(endpoint, params)) # pyright: ignore[reportPrivateUsage]
ps3838api/matching.py ADDED
@@ -0,0 +1,141 @@
1
+ import json
2
+ import warnings
3
+ from typing import Final, Literal
4
+
5
+ from rapidfuzz import fuzz
6
+
7
+ import ps3838api.api as ps
8
+ from ps3838api import ROOT_MODULE_DIR
9
+ from ps3838api.models.event import (
10
+ Failure,
11
+ MatchedLeague,
12
+ NoSuchEvent,
13
+ NoSuchLeague,
14
+ NoSuchLeagueFixtures,
15
+ NoSuchLeagueMatching,
16
+ WrongLeague,
17
+ )
18
+ from ps3838api.models.fixtures import FixturesLeagueV3, FixturesResponse
19
+ from ps3838api.models.tank import EventInfo
20
+ from ps3838api.utils.ops import normalize_to_set
21
+
22
+ warnings.warn(
23
+ f"{__name__} is experimental and its interface is not stable yet.",
24
+ FutureWarning,
25
+ )
26
+
27
+ with open(ROOT_MODULE_DIR / "out/matched_leagues.json") as file:
28
+ MATCHED_LEAGUES: Final[list[MatchedLeague]] = json.load(file)
29
+
30
+ with open(ROOT_MODULE_DIR / "out/ps3838_leagues.json") as file:
31
+ ALL_LEAGUES: Final[list[ps.LeagueV3]] = json.load(file)["leagues"]
32
+
33
+
34
+ def match_league(
35
+ *,
36
+ league_betsapi: str,
37
+ leagues_mapping: list[MatchedLeague] = MATCHED_LEAGUES,
38
+ ) -> MatchedLeague | NoSuchLeagueMatching | WrongLeague:
39
+ for league in leagues_mapping:
40
+ if league["betsapi_league"] == league_betsapi:
41
+ if league["ps3838_id"]:
42
+ return league
43
+ else:
44
+ return NoSuchLeagueMatching(league_betsapi)
45
+ return WrongLeague(league_betsapi)
46
+
47
+
48
+ def find_league_by_name(league: str, leagues: list[ps.LeagueV3] = ALL_LEAGUES) -> ps.LeagueV3 | NoSuchLeague:
49
+ normalized = normalize_to_set(league)
50
+ for leagueV3 in leagues:
51
+ if normalize_to_set(leagueV3["name"]) == normalized:
52
+ return leagueV3
53
+ return NoSuchLeagueMatching(league)
54
+
55
+
56
+ def find_event_in_league(
57
+ league_data: FixturesLeagueV3,
58
+ league: str,
59
+ home: str,
60
+ away: str,
61
+ live_status: Literal["PREMATCH", "LIVE"] | None = "PREMATCH",
62
+ ) -> EventInfo | NoSuchEvent:
63
+ """
64
+ If live_status is "LIVE", search only for events with a parentId.
65
+ This does not necessarily mean the event is live — it could also be a corners subevent.
66
+
67
+ If live_status is "PREMATCH", search only for events without a parentId.
68
+ Some prematch events (e.g. corners leagues) will be skipped.
69
+
70
+ Scans `league_data["events"]` for the best fuzzy match to `home` and `away`.
71
+ Returns the matching event with the highest sum of match scores, as long as
72
+ that sum >= 75 (which is 37.5% of the max possible 200).
73
+ Otherwise, returns NoSuchEvent.
74
+ """
75
+ best_event = None
76
+ best_sum_score = 0
77
+ for event in league_data["events"]:
78
+ match (live_status, "parentId" in event):
79
+ case None, _:
80
+ pass
81
+ case "PREMATCH", False:
82
+ pass
83
+ case "LIVE", True:
84
+ pass
85
+ case _:
86
+ continue
87
+
88
+ # Compare the user-provided home and away vs. the fixture's home and away.
89
+ # Using token_set_ratio (see below for comparison vs token_sort_ratio).
90
+ score_home = fuzz.token_set_ratio(home, event.get("home", ""))
91
+ score_away = fuzz.token_set_ratio(away, event.get("away", ""))
92
+ total_score = score_home + score_away
93
+ if total_score > best_sum_score:
94
+ best_sum_score = total_score
95
+ best_event = event
96
+ # If the best event's combined fuzzy match is < 37.5% of the total possible 200,
97
+ # treat it as no match:
98
+ if best_event is None or best_sum_score < 75:
99
+ return NoSuchEvent(league, home, away)
100
+ return {"eventId": best_event["id"], "leagueId": league_data["id"]}
101
+
102
+
103
+ def magic_find_event(
104
+ fixtures: FixturesResponse,
105
+ league: str,
106
+ home: str,
107
+ away: str,
108
+ live_status: Literal["PREMATCH", "LIVE"] | None = "PREMATCH",
109
+ ) -> EventInfo | Failure:
110
+ """
111
+ 1. Tries to find league by normalizng names;
112
+ 2. If don't, search for a league matching
113
+ 3. Then `find_event_in_league`
114
+
115
+ If live_status is "LIVE", search only for events with a parentId.
116
+ This does not necessarily mean the event is live — it could also be a corners subevent.
117
+
118
+ If live_status is "PREMATCH", search only for events without a parentId.
119
+ Some prematch events (e.g. corners leagues) will be skipped.
120
+
121
+ If live_status is None, search for any.
122
+
123
+ """
124
+
125
+ leagueV3 = find_league_by_name(league)
126
+ if isinstance(leagueV3, NoSuchLeague):
127
+ match match_league(league_betsapi=league):
128
+ case {"ps3838_id": int()} as value:
129
+ league_id: int = value["ps3838_id"] # type: ignore
130
+ case _:
131
+ return NoSuchLeagueMatching(league)
132
+ else:
133
+ league_id = leagueV3["id"]
134
+
135
+ for leagueV3 in fixtures["league"]:
136
+ if leagueV3["id"] == league_id:
137
+ break
138
+ else:
139
+ return NoSuchLeagueFixtures(league)
140
+
141
+ return find_event_in_league(leagueV3, league, home, away, live_status)
File without changes