55tech 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.
- 55tech-0.1.0.dist-info/METADATA +84 -0
- 55tech-0.1.0.dist-info/RECORD +26 -0
- 55tech-0.1.0.dist-info/WHEEL +4 -0
- 55tech-0.1.0.dist-info/licenses/LICENSE +21 -0
- fiftyfivetech/__init__.py +36 -0
- fiftyfivetech/bookmakers/__init__.py +8 -0
- fiftyfivetech/bookmakers/endpoints.py +30 -0
- fiftyfivetech/bookmakers/resources.py +36 -0
- fiftyfivetech/client.py +38 -0
- fiftyfivetech/connection.py +169 -0
- fiftyfivetech/endpoints.py +15 -0
- fiftyfivetech/exceptions.py +108 -0
- fiftyfivetech/fixtures/__init__.py +37 -0
- fiftyfivetech/fixtures/endpoints.py +158 -0
- fiftyfivetech/fixtures/resources.py +216 -0
- fiftyfivetech/markets/__init__.py +9 -0
- fiftyfivetech/markets/endpoints.py +37 -0
- fiftyfivetech/markets/resources.py +43 -0
- fiftyfivetech/py.typed +0 -0
- fiftyfivetech/resources.py +44 -0
- fiftyfivetech/streaming/__init__.py +55 -0
- fiftyfivetech/streaming/endpoints.py +61 -0
- fiftyfivetech/streaming/listener.py +256 -0
- fiftyfivetech/streaming/resources.py +224 -0
- fiftyfivetech/streaming/stream.py +353 -0
- fiftyfivetech/utils.py +254 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from fiftyfivetech.endpoints import Endpoints
|
|
5
|
+
from fiftyfivetech.exceptions import FiftyFiveTechInvalidRequestError
|
|
6
|
+
from fiftyfivetech.fixtures.resources import (
|
|
7
|
+
FixtureMappingResponse,
|
|
8
|
+
FixtureOddsResponse,
|
|
9
|
+
FixturesResponse,
|
|
10
|
+
)
|
|
11
|
+
from fiftyfivetech.utils import to_epoch_seconds
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FixturesEndpoints(Endpoints):
|
|
15
|
+
"""
|
|
16
|
+
Fixtures, fixture mappings and per-fixture odds.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def get_fixtures(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
fixture_ids: Sequence[str] | None = None,
|
|
23
|
+
sport_id: int | None = None,
|
|
24
|
+
tournament_id: int | None = None,
|
|
25
|
+
start_time_from: datetime | int | None = None,
|
|
26
|
+
start_time_to: datetime | int | None = None,
|
|
27
|
+
bookmakers: Sequence[str] | None = None,
|
|
28
|
+
) -> FixturesResponse:
|
|
29
|
+
"""
|
|
30
|
+
Fetch fixtures: ``GET /fixtures``. Give ``fixture_ids``, or filters.
|
|
31
|
+
|
|
32
|
+
:param fixture_ids: These fixtures (the fast path).
|
|
33
|
+
:param sport_id: Fixtures of one sport, e.g. ``10`` for soccer.
|
|
34
|
+
:param tournament_id: Fixtures of one tournament.
|
|
35
|
+
:param start_time_from: Earliest scheduled start; datetime or epoch seconds.
|
|
36
|
+
:param start_time_to: Latest scheduled start; datetime or epoch seconds.
|
|
37
|
+
:param bookmakers: Include each fixture's ``bookmakers`` state for these slugs,
|
|
38
|
+
and keep only fixtures they are mapped to.
|
|
39
|
+
:returns: The matching fixtures.
|
|
40
|
+
"""
|
|
41
|
+
return self.conn.call(FixturesResponse, "GET", "/fixtures", wrap="fixtures", params={
|
|
42
|
+
"fixtureIds": fixture_ids,
|
|
43
|
+
"sportId": sport_id,
|
|
44
|
+
"tournamentId": tournament_id,
|
|
45
|
+
"startTimeFrom": to_epoch_seconds(start_time_from),
|
|
46
|
+
"startTimeTo": to_epoch_seconds(start_time_to),
|
|
47
|
+
"bookmakers": bookmakers,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
def get_fixtures_live(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
sport_id: int | None = None,
|
|
54
|
+
tournament_id: int | None = None,
|
|
55
|
+
bookmakers: Sequence[str] | None = None,
|
|
56
|
+
) -> FixturesResponse:
|
|
57
|
+
"""
|
|
58
|
+
Fetch fixtures in play: ``GET /fixtures/live``.
|
|
59
|
+
|
|
60
|
+
:param sport_id: Fixtures of one sport.
|
|
61
|
+
:param tournament_id: Fixtures of one tournament.
|
|
62
|
+
:param bookmakers: Bookmaker slugs to include state for, and filter by.
|
|
63
|
+
:returns: The live fixtures.
|
|
64
|
+
"""
|
|
65
|
+
return self.conn.call(
|
|
66
|
+
FixturesResponse, "GET", "/fixtures/live", wrap="fixtures", params={
|
|
67
|
+
"sportId": sport_id,
|
|
68
|
+
"tournamentId": tournament_id,
|
|
69
|
+
"bookmakers": bookmakers,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
def get_fixtures_today(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
sport_id: int | None = None,
|
|
76
|
+
tournament_id: int | None = None,
|
|
77
|
+
bookmakers: Sequence[str] | None = None,
|
|
78
|
+
) -> FixturesResponse:
|
|
79
|
+
"""
|
|
80
|
+
Fetch today's fixtures: ``GET /fixtures/today``.
|
|
81
|
+
|
|
82
|
+
:param sport_id: Fixtures of one sport.
|
|
83
|
+
:param tournament_id: Fixtures of one tournament.
|
|
84
|
+
:param bookmakers: Bookmaker slugs to include state for, and filter by.
|
|
85
|
+
:returns: Today's fixtures.
|
|
86
|
+
"""
|
|
87
|
+
return self.conn.call(
|
|
88
|
+
FixturesResponse, "GET", "/fixtures/today", wrap="fixtures", params={
|
|
89
|
+
"sportId": sport_id,
|
|
90
|
+
"tournamentId": tournament_id,
|
|
91
|
+
"bookmakers": bookmakers,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
def get_fixture_mapping(
|
|
95
|
+
self,
|
|
96
|
+
*,
|
|
97
|
+
bookmaker: str,
|
|
98
|
+
fixture_ids: Sequence[str] | None = None,
|
|
99
|
+
bookmaker_fixture_ids: Sequence[str] | None = None,
|
|
100
|
+
) -> FixtureMappingResponse:
|
|
101
|
+
"""
|
|
102
|
+
Pair 55Tech fixtures with a bookmaker's: ``GET /fixtures/mapping``. Give
|
|
103
|
+
exactly one of ``fixture_ids`` or ``bookmaker_fixture_ids``.
|
|
104
|
+
|
|
105
|
+
Send long lists with ``utils.batch``.
|
|
106
|
+
|
|
107
|
+
:param bookmaker: Bookmaker slug, e.g. ``"pinnacle"``. The spec makes it
|
|
108
|
+
optional, but the API rejects a request without it.
|
|
109
|
+
:param fixture_ids: 55Tech fixture ids to find the bookmaker's ids for.
|
|
110
|
+
:param bookmaker_fixture_ids: The bookmaker's ids to find 55Tech fixtures for.
|
|
111
|
+
:returns: One row per id; the other side of an unpaired id is ``None``.
|
|
112
|
+
:raises FiftyFiveTechInvalidRequestError: Unless exactly one non-empty id list is given.
|
|
113
|
+
"""
|
|
114
|
+
given = [ids for ids in (fixture_ids, bookmaker_fixture_ids) if ids]
|
|
115
|
+
if len(given) != 1:
|
|
116
|
+
raise FiftyFiveTechInvalidRequestError(
|
|
117
|
+
"get_fixture_mapping needs one of fixture_ids or bookmaker_fixture_ids")
|
|
118
|
+
return self.conn.call(
|
|
119
|
+
FixtureMappingResponse, "GET", "/fixtures/mapping", wrap="mappings", params={
|
|
120
|
+
"bookmaker": bookmaker,
|
|
121
|
+
"fixtureIds": fixture_ids,
|
|
122
|
+
"bookmakerFixtureIds": (
|
|
123
|
+
[str(i) for i in bookmaker_fixture_ids] if bookmaker_fixture_ids else None),
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
def get_fixture_odds(
|
|
127
|
+
self,
|
|
128
|
+
fixture_id: str,
|
|
129
|
+
*,
|
|
130
|
+
bookmakers: Sequence[str] | None = None,
|
|
131
|
+
since: int | None = None,
|
|
132
|
+
market_active: bool | None = None,
|
|
133
|
+
main_line: bool | None = None,
|
|
134
|
+
) -> FixtureOddsResponse:
|
|
135
|
+
"""
|
|
136
|
+
Fetch one fixture's current odds: ``GET /fixtures/odds``.
|
|
137
|
+
|
|
138
|
+
One fixture per call: a comma-joined id is treated as a single unknown one.
|
|
139
|
+
Every line is returned, alternates included, unless ``main_line`` says otherwise.
|
|
140
|
+
Merge into streamed state per ``oddsId``, keeping the later ``changed_at``.
|
|
141
|
+
|
|
142
|
+
:param fixture_id: 55Tech fixture id.
|
|
143
|
+
:param bookmakers: Bookmaker slugs to include; all the key allows if omitted.
|
|
144
|
+
:param since: Only records with ``changedAt`` at or after this, epoch milliseconds.
|
|
145
|
+
:param market_active: Only records whose ``marketActive`` is this.
|
|
146
|
+
:param main_line: Only records whose ``mainLine`` is this.
|
|
147
|
+
:returns: The fixture with each bookmaker's records.
|
|
148
|
+
:raises FiftyFiveTechInvalidRequestError: If ``fixture_id`` is empty.
|
|
149
|
+
"""
|
|
150
|
+
if not fixture_id:
|
|
151
|
+
raise FiftyFiveTechInvalidRequestError("get_fixture_odds needs a fixture_id")
|
|
152
|
+
return self.conn.call(FixtureOddsResponse, "GET", "/fixtures/odds", params={
|
|
153
|
+
"fixtureId": fixture_id,
|
|
154
|
+
"bookmakers": bookmakers,
|
|
155
|
+
"since": since,
|
|
156
|
+
"marketActive": market_active,
|
|
157
|
+
"mainLine": main_line,
|
|
158
|
+
})
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fiftyfivetech.resources import BaseModel, Response
|
|
6
|
+
from fiftyfivetech.utils import from_timestamp
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(kw_only=True, slots=True)
|
|
10
|
+
class FixtureStatus(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
Where a fixture is in its lifecycle.
|
|
13
|
+
"""
|
|
14
|
+
live: bool
|
|
15
|
+
status_id: int | None = None
|
|
16
|
+
status_name: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(kw_only=True, slots=True)
|
|
20
|
+
class Sport(BaseModel):
|
|
21
|
+
"""
|
|
22
|
+
A sport, e.g. ``10`` Soccer.
|
|
23
|
+
"""
|
|
24
|
+
sport_id: int
|
|
25
|
+
sport_name: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(kw_only=True, slots=True)
|
|
29
|
+
class Tournament(BaseModel):
|
|
30
|
+
"""
|
|
31
|
+
A competition, e.g. a league; ``category_name`` is usually its country.
|
|
32
|
+
"""
|
|
33
|
+
tournament_id: int
|
|
34
|
+
tournament_name: str
|
|
35
|
+
category_name: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(kw_only=True, slots=True)
|
|
39
|
+
class Season(BaseModel):
|
|
40
|
+
"""
|
|
41
|
+
The season a fixture belongs to.
|
|
42
|
+
"""
|
|
43
|
+
season_id: int | None = None
|
|
44
|
+
season_name: str | None = None
|
|
45
|
+
season_round: int | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(kw_only=True, slots=True)
|
|
49
|
+
class Venue(BaseModel):
|
|
50
|
+
"""
|
|
51
|
+
Where a fixture is played.
|
|
52
|
+
"""
|
|
53
|
+
venue_id: int | None = None
|
|
54
|
+
venue_name: str | None = None
|
|
55
|
+
venue_location: str | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(kw_only=True, slots=True)
|
|
59
|
+
class Participants(BaseModel):
|
|
60
|
+
"""
|
|
61
|
+
A fixture's two sides, in 55Tech's order: outcome ``"1"`` is ``participant1``.
|
|
62
|
+
"""
|
|
63
|
+
participant1_id: int
|
|
64
|
+
participant1_name: str
|
|
65
|
+
participant1_short_name: str | None = None
|
|
66
|
+
participant1_abbr: str | None = None
|
|
67
|
+
participant1_rot_nr: int | None = None
|
|
68
|
+
participant2_id: int
|
|
69
|
+
participant2_name: str
|
|
70
|
+
participant2_short_name: str | None = None
|
|
71
|
+
participant2_abbr: str | None = None
|
|
72
|
+
participant2_rot_nr: int | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(kw_only=True, slots=True)
|
|
76
|
+
class Score(BaseModel):
|
|
77
|
+
"""
|
|
78
|
+
The score of one period, e.g. ``"p1"``, ``"fulltime"``, ``"result"``.
|
|
79
|
+
"""
|
|
80
|
+
period: str
|
|
81
|
+
participant1_score: int | None = None
|
|
82
|
+
participant2_score: int | None = None
|
|
83
|
+
updated_at: datetime | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(kw_only=True, slots=True)
|
|
87
|
+
class Clock(BaseModel):
|
|
88
|
+
"""
|
|
89
|
+
A live fixture's match clock; every field is ``None`` before kick-off.
|
|
90
|
+
"""
|
|
91
|
+
current_period: str | None = None
|
|
92
|
+
current_time: str | None = None
|
|
93
|
+
remaining_time: str | None = None
|
|
94
|
+
remaining_time_in_period: str | None = None
|
|
95
|
+
stopped: bool | None = None
|
|
96
|
+
updated_at: datetime | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(kw_only=True, slots=True)
|
|
100
|
+
class BookmakerFixtureMeta(BaseModel):
|
|
101
|
+
"""
|
|
102
|
+
One bookmaker's state on one fixture, from ``GET /fixtures?bookmakers=...`` or the
|
|
103
|
+
``bookmakers`` stream channel. Present only while the bookmaker has valid odds on it.
|
|
104
|
+
"""
|
|
105
|
+
bookmaker: str
|
|
106
|
+
bookmaker_fixture_id: str | None = None
|
|
107
|
+
fixture_path: str | None = None
|
|
108
|
+
has_odds: bool
|
|
109
|
+
stale_odds: bool
|
|
110
|
+
suspended: bool
|
|
111
|
+
participants_rotated: bool
|
|
112
|
+
updated_at: datetime
|
|
113
|
+
meta: dict[str, Any] | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(kw_only=True, slots=True)
|
|
117
|
+
class Fixture(BaseModel):
|
|
118
|
+
"""
|
|
119
|
+
A fixture (an event): who plays, when, and its live state.
|
|
120
|
+
"""
|
|
121
|
+
fixture_id: str
|
|
122
|
+
reissued_fixture_id: str | None = None
|
|
123
|
+
status: FixtureStatus
|
|
124
|
+
sport: Sport
|
|
125
|
+
tournament: Tournament
|
|
126
|
+
season: Season | None = None
|
|
127
|
+
venue: Venue | None = None
|
|
128
|
+
start_time: int
|
|
129
|
+
true_start_time: datetime | None = None
|
|
130
|
+
true_end_time: datetime | None = None
|
|
131
|
+
participants: Participants
|
|
132
|
+
scores: dict[str, Score] = field(default_factory=dict)
|
|
133
|
+
clock: Clock | None = None
|
|
134
|
+
expected_periods: int | None = None
|
|
135
|
+
period_length: int | None = None
|
|
136
|
+
external_providers: dict[str, Any] = field(default_factory=dict)
|
|
137
|
+
bookmakers: dict[str, BookmakerFixtureMeta] = field(default_factory=dict)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def starts_at(self) -> datetime:
|
|
141
|
+
"""
|
|
142
|
+
``start_time`` as an aware UTC datetime.
|
|
143
|
+
"""
|
|
144
|
+
return from_timestamp(self.start_time)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(kw_only=True, slots=True)
|
|
148
|
+
class FixturesResponse(Response):
|
|
149
|
+
"""
|
|
150
|
+
Fixtures (``GET /fixtures``; the array is wrapped as ``fixtures``).
|
|
151
|
+
"""
|
|
152
|
+
fixtures: list[Fixture] = field(default_factory=list)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(kw_only=True, slots=True)
|
|
156
|
+
class FixtureMapping(BaseModel):
|
|
157
|
+
"""
|
|
158
|
+
One bookmaker fixture id resolved to a 55Tech fixture.
|
|
159
|
+
One row per id asked for. The unknown side of an unpaired id is ``None`` (the
|
|
160
|
+
spec says otherwise, the API does this); pairings fill in as coverage catches up.
|
|
161
|
+
"""
|
|
162
|
+
bookmaker: str
|
|
163
|
+
fixture_id: str | None = None
|
|
164
|
+
bookmaker_fixture_id: str | None = None
|
|
165
|
+
participants_rotated: bool | None = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(kw_only=True, slots=True)
|
|
169
|
+
class FixtureMappingResponse(Response):
|
|
170
|
+
"""
|
|
171
|
+
Fixture pairings (``GET /fixtures/mapping``; the array is wrapped as ``mappings``),
|
|
172
|
+
one per id asked for, paired or not.
|
|
173
|
+
"""
|
|
174
|
+
mappings: list[FixtureMapping] = field(default_factory=list)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(kw_only=True, slots=True)
|
|
178
|
+
class OddsRecord(BaseModel):
|
|
179
|
+
"""
|
|
180
|
+
One bookmaker's price on one outcome, as REST and the stream both send it.
|
|
181
|
+
|
|
182
|
+
Keyed by its ``oddsId``, ``"{fixtureId}:{bookmaker}:{outcomeId}:{playerId}"``;
|
|
183
|
+
treat the key as opaque and read the fields. Each record is the latest state,
|
|
184
|
+
not a delta. Kind and line come from the market dictionary, via ``outcome_id``;
|
|
185
|
+
the bookmaker's own ids sit beside them as sent.
|
|
186
|
+
"""
|
|
187
|
+
bookmaker: str
|
|
188
|
+
outcome_id: int
|
|
189
|
+
player_id: int
|
|
190
|
+
price: float
|
|
191
|
+
active: bool
|
|
192
|
+
changed_at: int
|
|
193
|
+
market_id: int | None = None
|
|
194
|
+
market_active: bool | None = None
|
|
195
|
+
main_line: bool | None = None
|
|
196
|
+
limit: float | None = None
|
|
197
|
+
bookmaker_changed_at: int | None = None
|
|
198
|
+
bookmaker_market_id: str | None = None
|
|
199
|
+
bookmaker_outcome_id: str | None = None
|
|
200
|
+
price_fractional: str | None = None
|
|
201
|
+
price_american: int | None = None
|
|
202
|
+
meta: dict[str, Any] | None = None
|
|
203
|
+
betslip: str | None = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(kw_only=True, slots=True)
|
|
207
|
+
class FixtureOddsResponse(Fixture):
|
|
208
|
+
"""
|
|
209
|
+
One fixture with its current odds (``GET /fixtures/odds``): every ``Fixture``
|
|
210
|
+
field, plus ``odds`` and the response timings.
|
|
211
|
+
"""
|
|
212
|
+
odds: dict[str, dict[str, OddsRecord]] = field(default_factory=dict)
|
|
213
|
+
raw: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
214
|
+
sent_at: datetime = field(init=False, repr=False, compare=False)
|
|
215
|
+
received_at: datetime = field(init=False, repr=False, compare=False)
|
|
216
|
+
latency_ms: float = field(init=False, repr=False, compare=False)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from fiftyfivetech.endpoints import Endpoints
|
|
4
|
+
from fiftyfivetech.exceptions import FiftyFiveTechInvalidRequestError
|
|
5
|
+
from fiftyfivetech.markets.resources import MarketsResponse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MarketsEndpoints(Endpoints):
|
|
9
|
+
"""
|
|
10
|
+
The market dictionary.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def get_markets(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
sport_id: int | None = None,
|
|
17
|
+
market_ids: Sequence[int] | None = None,
|
|
18
|
+
outcome_ids: Sequence[int] | None = None,
|
|
19
|
+
) -> MarketsResponse:
|
|
20
|
+
"""
|
|
21
|
+
Fetch market definitions: ``GET /markets``. Give exactly one filter.
|
|
22
|
+
|
|
23
|
+
:param sport_id: Every market of one sport, e.g. ``10`` for soccer.
|
|
24
|
+
:param market_ids: These markets.
|
|
25
|
+
:param outcome_ids: The markets these outcomes belong to.
|
|
26
|
+
:returns: The matching definitions.
|
|
27
|
+
:raises FiftyFiveTechInvalidRequestError: Unless exactly one filter is given.
|
|
28
|
+
"""
|
|
29
|
+
given = [v for v in (sport_id, market_ids, outcome_ids) if v is not None]
|
|
30
|
+
if len(given) != 1:
|
|
31
|
+
raise FiftyFiveTechInvalidRequestError(
|
|
32
|
+
"get_markets needs exactly one of sport_id, market_ids or outcome_ids")
|
|
33
|
+
return self.conn.call(MarketsResponse, "GET", "/markets", wrap="markets", params={
|
|
34
|
+
"sportId": sport_id,
|
|
35
|
+
"marketIds": market_ids,
|
|
36
|
+
"outcomeIds": outcome_ids,
|
|
37
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from fiftyfivetech.resources import BaseModel, Response
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(kw_only=True, slots=True)
|
|
7
|
+
class MarketOutcome(BaseModel):
|
|
8
|
+
"""
|
|
9
|
+
One outcome (side) of a market definition.
|
|
10
|
+
"""
|
|
11
|
+
outcome_id: int
|
|
12
|
+
outcome_name: str | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(kw_only=True, slots=True)
|
|
16
|
+
class Market(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
What a 55Tech market id is: odds records name markets and outcomes only by id.
|
|
19
|
+
|
|
20
|
+
A market is the coordinates ``market_type -> period -> handicap``; every line is its
|
|
21
|
+
own ``market_id``, so "Over 2.5" and "Over 3.5" are two markets. Corners and
|
|
22
|
+
bookings have their own types (``spreads-corners``, ``totals-bookings``, ...).
|
|
23
|
+
``market_type`` and ``period`` are open vocabularies that grow, and have been
|
|
24
|
+
renamed (see the 2026-10-05 changelog): key on ``market_id`` where you can.
|
|
25
|
+
"""
|
|
26
|
+
market_id: int
|
|
27
|
+
sport_id: int | None = None
|
|
28
|
+
market_type: str | None = None
|
|
29
|
+
period: str | None = None
|
|
30
|
+
handicap: float | None = None
|
|
31
|
+
market_length: int | None = None
|
|
32
|
+
player_prop: bool | None = None
|
|
33
|
+
market_name: str | None = None
|
|
34
|
+
market_name_short: str | None = None
|
|
35
|
+
outcomes: list[MarketOutcome] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(kw_only=True, slots=True)
|
|
39
|
+
class MarketsResponse(Response):
|
|
40
|
+
"""
|
|
41
|
+
Market definitions (``GET /markets``; the array is wrapped as ``markets``).
|
|
42
|
+
"""
|
|
43
|
+
markets: list[Market] = field(default_factory=list)
|
fiftyfivetech/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fiftyfivetech.utils import decode, encode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(kw_only=True, slots=True)
|
|
9
|
+
class BaseModel:
|
|
10
|
+
"""
|
|
11
|
+
Base class for every model.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_json(cls: Any, data: dict[str, Any]) -> Any:
|
|
16
|
+
"""
|
|
17
|
+
Build the model from a decoded JSON object.
|
|
18
|
+
|
|
19
|
+
:param data: Object as the API returns it; undeclared keys are ignored.
|
|
20
|
+
:returns: The model.
|
|
21
|
+
"""
|
|
22
|
+
return decode(cls, data)
|
|
23
|
+
|
|
24
|
+
def to_json(self) -> dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Convert the model to a JSON-ready dict.
|
|
27
|
+
|
|
28
|
+
:returns: The non-``None`` fields, with camelCase keys.
|
|
29
|
+
"""
|
|
30
|
+
return encode(self)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(kw_only=True, slots=True)
|
|
34
|
+
class Response(BaseModel):
|
|
35
|
+
"""
|
|
36
|
+
Base class for a top-level API response.
|
|
37
|
+
|
|
38
|
+
Endpoints whose body is a JSON array are wrapped in an object, e.g.
|
|
39
|
+
``{"markets": [...]}``, so every response has named fields and a ``raw``.
|
|
40
|
+
"""
|
|
41
|
+
raw: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
42
|
+
sent_at: datetime = field(init=False, repr=False, compare=False)
|
|
43
|
+
received_at: datetime = field(init=False, repr=False, compare=False)
|
|
44
|
+
latency_ms: float = field(init=False, repr=False, compare=False)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from fiftyfivetech.streaming.endpoints import StreamingEndpoints
|
|
2
|
+
from fiftyfivetech.streaming.listener import BaseListener, StreamListener, parse
|
|
3
|
+
from fiftyfivetech.streaming.resources import (
|
|
4
|
+
BOOKMAKERS_CHANNEL,
|
|
5
|
+
CHANNEL_TYPES,
|
|
6
|
+
CONTROL_TYPES,
|
|
7
|
+
ODDS_CHANNEL,
|
|
8
|
+
Access,
|
|
9
|
+
BookmakersPayload,
|
|
10
|
+
BookmakersUpdate,
|
|
11
|
+
ChannelUpdate,
|
|
12
|
+
Control,
|
|
13
|
+
DictFrame,
|
|
14
|
+
ErrorMessage,
|
|
15
|
+
FixtureBook,
|
|
16
|
+
LoginOk,
|
|
17
|
+
OddsPayload,
|
|
18
|
+
OddsUpdate,
|
|
19
|
+
ReceiveType,
|
|
20
|
+
Reconnect,
|
|
21
|
+
ResumeComplete,
|
|
22
|
+
ResumeInfo,
|
|
23
|
+
SnapshotRequired,
|
|
24
|
+
Update,
|
|
25
|
+
)
|
|
26
|
+
from fiftyfivetech.streaming.stream import FiftyFiveTechStream
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"BOOKMAKERS_CHANNEL",
|
|
30
|
+
"CHANNEL_TYPES",
|
|
31
|
+
"CONTROL_TYPES",
|
|
32
|
+
"ODDS_CHANNEL",
|
|
33
|
+
"Access",
|
|
34
|
+
"BaseListener",
|
|
35
|
+
"BookmakersPayload",
|
|
36
|
+
"BookmakersUpdate",
|
|
37
|
+
"ChannelUpdate",
|
|
38
|
+
"Control",
|
|
39
|
+
"DictFrame",
|
|
40
|
+
"ErrorMessage",
|
|
41
|
+
"FixtureBook",
|
|
42
|
+
"LoginOk",
|
|
43
|
+
"FiftyFiveTechStream",
|
|
44
|
+
"OddsPayload",
|
|
45
|
+
"OddsUpdate",
|
|
46
|
+
"ReceiveType",
|
|
47
|
+
"Reconnect",
|
|
48
|
+
"ResumeComplete",
|
|
49
|
+
"ResumeInfo",
|
|
50
|
+
"SnapshotRequired",
|
|
51
|
+
"StreamListener",
|
|
52
|
+
"StreamingEndpoints",
|
|
53
|
+
"Update",
|
|
54
|
+
"parse",
|
|
55
|
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from fiftyfivetech.endpoints import Endpoints
|
|
4
|
+
from fiftyfivetech.streaming.listener import BaseListener, StreamListener
|
|
5
|
+
from fiftyfivetech.streaming.resources import ODDS_CHANNEL, ReceiveType
|
|
6
|
+
from fiftyfivetech.streaming.stream import FiftyFiveTechStream
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StreamingEndpoints(Endpoints):
|
|
10
|
+
"""
|
|
11
|
+
The WebSocket gateway.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def create_stream(
|
|
15
|
+
self,
|
|
16
|
+
listener: BaseListener | None = None,
|
|
17
|
+
*,
|
|
18
|
+
url: str | None = None,
|
|
19
|
+
channels: Sequence[str] = (ODDS_CHANNEL,),
|
|
20
|
+
bookmakers: Sequence[str] | None = None,
|
|
21
|
+
sport_ids: Sequence[int] | None = None,
|
|
22
|
+
tournament_ids: Sequence[int] | None = None,
|
|
23
|
+
fixture_ids: Sequence[str] | None = None,
|
|
24
|
+
receive_type: ReceiveType = "zstd",
|
|
25
|
+
lang: str | None = None,
|
|
26
|
+
client_name: str | None = None,
|
|
27
|
+
timeout: float = 30.0,
|
|
28
|
+
keepalive: float | None = 60.0,
|
|
29
|
+
) -> FiftyFiveTechStream:
|
|
30
|
+
"""
|
|
31
|
+
Create a stream on this connection; nothing is sent until it connects.
|
|
32
|
+
|
|
33
|
+
:param listener: Receives every frame; a new ``StreamListener`` if omitted.
|
|
34
|
+
:param url: Gateway URL; defaults to the connection's ``stream_url``.
|
|
35
|
+
:param channels: Channels to receive.
|
|
36
|
+
:param bookmakers: Bookmaker slugs to receive; all the key allows if omitted.
|
|
37
|
+
:param sport_ids: Sports to receive; all if omitted.
|
|
38
|
+
:param tournament_ids: Tournaments to receive; all if omitted.
|
|
39
|
+
:param fixture_ids: Fixtures to receive; all if omitted.
|
|
40
|
+
:param receive_type: ``"zstd"``, ``"zstd-dict"`` or ``"json"``.
|
|
41
|
+
:param lang: Language for translated names, e.g. ``"en"``.
|
|
42
|
+
:param client_name: Name to identify this client to the gateway.
|
|
43
|
+
:param timeout: Seconds to wait to connect, or for the login reply.
|
|
44
|
+
:param keepalive: Seconds without a frame before sending a ping; ``None`` never pings.
|
|
45
|
+
:returns: The stream.
|
|
46
|
+
"""
|
|
47
|
+
return FiftyFiveTechStream(
|
|
48
|
+
self.conn,
|
|
49
|
+
listener if listener is not None else StreamListener(),
|
|
50
|
+
url=url,
|
|
51
|
+
channels=channels,
|
|
52
|
+
bookmakers=bookmakers,
|
|
53
|
+
sport_ids=sport_ids,
|
|
54
|
+
tournament_ids=tournament_ids,
|
|
55
|
+
fixture_ids=fixture_ids,
|
|
56
|
+
receive_type=receive_type,
|
|
57
|
+
lang=lang,
|
|
58
|
+
client_name=client_name,
|
|
59
|
+
timeout=timeout,
|
|
60
|
+
keepalive=keepalive,
|
|
61
|
+
)
|