lottevo 0.1.0__tar.gz
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.
- lottevo-0.1.0/LICENSE +21 -0
- lottevo-0.1.0/PKG-INFO +86 -0
- lottevo-0.1.0/README.md +67 -0
- lottevo-0.1.0/lottevo/__init__.py +5 -0
- lottevo-0.1.0/lottevo/_resources.py +383 -0
- lottevo-0.1.0/lottevo/client.py +166 -0
- lottevo-0.1.0/lottevo/py.typed +0 -0
- lottevo-0.1.0/lottevo.egg-info/PKG-INFO +86 -0
- lottevo-0.1.0/lottevo.egg-info/SOURCES.txt +11 -0
- lottevo-0.1.0/lottevo.egg-info/dependency_links.txt +1 -0
- lottevo-0.1.0/lottevo.egg-info/top_level.txt +1 -0
- lottevo-0.1.0/pyproject.toml +32 -0
- lottevo-0.1.0/setup.cfg +4 -0
lottevo-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Astefa Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lottevo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lottevo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A thin Python client for the Lottevo lottery data API.
|
|
5
|
+
Author-email: Lottevo <contact@lottevo.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://lottevo.com/docs/sdks
|
|
8
|
+
Project-URL: Source, https://github.com/astefalabs/lottevo-python
|
|
9
|
+
Project-URL: Issues, https://github.com/astefalabs/lottevo-python/issues
|
|
10
|
+
Keywords: lottevo,lottery,results,api,sdk
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Lottevo Python
|
|
21
|
+
|
|
22
|
+
A thin client for the [Lottevo](https://lottevo.com) lottery data API: results, jackpots, schedules,
|
|
23
|
+
prize breakdowns and statistics for lotteries worldwide. Standard library only; Python 3.8+.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install lottevo
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## First call
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from lottevo import Client
|
|
33
|
+
|
|
34
|
+
lottevo = Client("lv_…") # or set LOTTEVO_API_KEY and pass nothing
|
|
35
|
+
|
|
36
|
+
account = lottevo.account.get() # the smoke test: plan, scopes, quota
|
|
37
|
+
draws = lottevo.lotteries.draws("EU-002", {"per_page": 5}) # EuroJackpot's latest results
|
|
38
|
+
|
|
39
|
+
for draw in draws["data"]:
|
|
40
|
+
print(draw["drawn_at"], *draw["numbers"])
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every operation of the API is a method on a resource — `lottevo.lotteries`, `.draws`, `.webhooks`,
|
|
44
|
+
`.countries` … — and returns the decoded JSON. Path parameters come first, then the query (or, for
|
|
45
|
+
`create`/`update`, the JSON body):
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
lottevo.draws.list({"filter": {"lottery": ["EU-002", "RO-001"], "from": "2026-01-01"}, "include": "prizes"})
|
|
49
|
+
lottevo.lotteries.check("EU-002", {"numbers": "4,7,12,31,44", "bonus": "8,11"})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Pagination
|
|
53
|
+
|
|
54
|
+
Listings are cursor-paginated. `paginate()` walks every page from the one you give it:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
for lottery in lottevo.paginate(lottevo.lotteries.list()):
|
|
58
|
+
print(lottery["code"])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Errors
|
|
62
|
+
|
|
63
|
+
A refused call raises `LottevoError`. Branch on `code`, not on the message:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from lottevo import LottevoError
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
lottevo.lotteries.draws("US-001")
|
|
70
|
+
except LottevoError as error:
|
|
71
|
+
if error.code == "lottery_not_entitled":
|
|
72
|
+
... # not in your plan
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`error.status`, `error.request_id` and `error.retry_after` are there too; `lottevo.last_headers` holds
|
|
76
|
+
the last answer's headers, including `x-quota-remaining`.
|
|
77
|
+
|
|
78
|
+
## Options
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
Client(api_key, locale="ro", timeout=30)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`locale` is sent as `Accept-Language`: game names, schedules and error messages come back in it.
|
|
85
|
+
|
|
86
|
+
Full reference: <https://lottevo.com/docs/reference>.
|
lottevo-0.1.0/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Lottevo Python
|
|
2
|
+
|
|
3
|
+
A thin client for the [Lottevo](https://lottevo.com) lottery data API: results, jackpots, schedules,
|
|
4
|
+
prize breakdowns and statistics for lotteries worldwide. Standard library only; Python 3.8+.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install lottevo
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## First call
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from lottevo import Client
|
|
14
|
+
|
|
15
|
+
lottevo = Client("lv_…") # or set LOTTEVO_API_KEY and pass nothing
|
|
16
|
+
|
|
17
|
+
account = lottevo.account.get() # the smoke test: plan, scopes, quota
|
|
18
|
+
draws = lottevo.lotteries.draws("EU-002", {"per_page": 5}) # EuroJackpot's latest results
|
|
19
|
+
|
|
20
|
+
for draw in draws["data"]:
|
|
21
|
+
print(draw["drawn_at"], *draw["numbers"])
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Every operation of the API is a method on a resource — `lottevo.lotteries`, `.draws`, `.webhooks`,
|
|
25
|
+
`.countries` … — and returns the decoded JSON. Path parameters come first, then the query (or, for
|
|
26
|
+
`create`/`update`, the JSON body):
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
lottevo.draws.list({"filter": {"lottery": ["EU-002", "RO-001"], "from": "2026-01-01"}, "include": "prizes"})
|
|
30
|
+
lottevo.lotteries.check("EU-002", {"numbers": "4,7,12,31,44", "bonus": "8,11"})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Pagination
|
|
34
|
+
|
|
35
|
+
Listings are cursor-paginated. `paginate()` walks every page from the one you give it:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
for lottery in lottevo.paginate(lottevo.lotteries.list()):
|
|
39
|
+
print(lottery["code"])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Errors
|
|
43
|
+
|
|
44
|
+
A refused call raises `LottevoError`. Branch on `code`, not on the message:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from lottevo import LottevoError
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
lottevo.lotteries.draws("US-001")
|
|
51
|
+
except LottevoError as error:
|
|
52
|
+
if error.code == "lottery_not_entitled":
|
|
53
|
+
... # not in your plan
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`error.status`, `error.request_id` and `error.retry_after` are there too; `lottevo.last_headers` holds
|
|
57
|
+
the last answer's headers, including `x-quota-remaining`.
|
|
58
|
+
|
|
59
|
+
## Options
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
Client(api_key, locale="ro", timeout=30)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`locale` is sent as `Accept-Language`: game names, schedules and error messages come back in it.
|
|
66
|
+
|
|
67
|
+
Full reference: <https://lottevo.com/docs/reference>.
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# Generated from the Lottevo OpenAPI document. Do not edit.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .client import Client
|
|
10
|
+
|
|
11
|
+
PathValue = Union[str, int]
|
|
12
|
+
Params = Optional[Dict[str, Any]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _segment(value: PathValue) -> str:
|
|
16
|
+
return quote(str(value), safe="")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Resource:
|
|
20
|
+
def __init__(self, client: Client) -> None:
|
|
21
|
+
self._client = client
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Account(_Resource):
|
|
25
|
+
def get(self, params: Params = None) -> Any:
|
|
26
|
+
r"""What the calling key is: its account's plan, its scopes and what the account
|
|
27
|
+
has spent today. The integration smoke test — a client can verify a key
|
|
28
|
+
works without needing a scope, and read its own ceiling from the answer
|
|
29
|
+
|
|
30
|
+
GET /account
|
|
31
|
+
"""
|
|
32
|
+
return self._client.request("GET", "/account", params=params)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Lotteries(_Resource):
|
|
36
|
+
def compliance(self, lottery: PathValue, params: Params = None) -> Any:
|
|
37
|
+
r"""The legal frame of every jurisdiction a lottery is sold in
|
|
38
|
+
|
|
39
|
+
GET /lotteries/{lottery}/compliance
|
|
40
|
+
|
|
41
|
+
A national game lists its country only: which states or provinces
|
|
42
|
+
sell it is not known for every game, and the country entry says when
|
|
43
|
+
the rules differ between them.
|
|
44
|
+
"""
|
|
45
|
+
return self._client.request("GET", f"/lotteries/{_segment(lottery)}/compliance", params=params)
|
|
46
|
+
|
|
47
|
+
def list(self, params: Params = None) -> Any:
|
|
48
|
+
r"""The catalogue. Inactive games are not listed
|
|
49
|
+
|
|
50
|
+
GET /lotteries
|
|
51
|
+
"""
|
|
52
|
+
return self._client.request("GET", "/lotteries", params=params)
|
|
53
|
+
|
|
54
|
+
def get(self, lottery: PathValue, params: Params = None) -> Any:
|
|
55
|
+
r"""One game, addressed by its code, with the long description the list leaves
|
|
56
|
+
out — plus its last results and its payout table, which are cheap for one
|
|
57
|
+
game and are what a game's own page shows
|
|
58
|
+
|
|
59
|
+
GET /lotteries/{lottery}
|
|
60
|
+
"""
|
|
61
|
+
return self._client.request("GET", f"/lotteries/{_segment(lottery)}", params=params)
|
|
62
|
+
|
|
63
|
+
def data_quality(self, lottery: PathValue, params: Params = None) -> Any:
|
|
64
|
+
r"""How complete and how fast a game's results have been
|
|
65
|
+
|
|
66
|
+
GET /lotteries/{lottery}/data-quality
|
|
67
|
+
|
|
68
|
+
Over the last 30 days: how many draws the game's schedule called for,
|
|
69
|
+
how many were published, how many the operator did not hold, and how
|
|
70
|
+
many are missing — plus how long after each draw's scheduled time its
|
|
71
|
+
result was available, as the median, the 90th percentile and the
|
|
72
|
+
slowest. Recomputed once a day. `completeness` is null for a game drawn
|
|
73
|
+
on a calendar no fixed schedule describes; `data` is null for a game
|
|
74
|
+
that has not been measured yet.
|
|
75
|
+
"""
|
|
76
|
+
return self._client.request("GET", f"/lotteries/{_segment(lottery)}/data-quality", params=params)
|
|
77
|
+
|
|
78
|
+
def draws(self, lottery: PathValue, params: Params = None) -> Any:
|
|
79
|
+
r"""One game's result history, newest first, as far back as your plan includes
|
|
80
|
+
(see `/v1/account`). Pages stay equally fast however deep you walk back
|
|
81
|
+
|
|
82
|
+
GET /lotteries/{lottery}/draws
|
|
83
|
+
"""
|
|
84
|
+
return self._client.request("GET", f"/lotteries/{_segment(lottery)}/draws", params=params)
|
|
85
|
+
|
|
86
|
+
def freshness(self, lottery: PathValue, params: Params = None) -> Any:
|
|
87
|
+
r"""How current a game's results are right now
|
|
88
|
+
|
|
89
|
+
GET /lotteries/{lottery}/freshness
|
|
90
|
+
|
|
91
|
+
Whether the latest scheduled draw has its result (`current`), is still
|
|
92
|
+
within the time results normally take (`awaiting`) or is late and
|
|
93
|
+
flagged as missing (`overdue`); when the game's results last changed
|
|
94
|
+
here, which is not the same as when it was last drawn; how long after
|
|
95
|
+
its scheduled time the latest result arrived, against the usual delay;
|
|
96
|
+
and how many results were corrected after publication recently.
|
|
97
|
+
Computed on every request, unlike the daily data-quality figures.
|
|
98
|
+
"""
|
|
99
|
+
return self._client.request("GET", f"/lotteries/{_segment(lottery)}/freshness", params=params)
|
|
100
|
+
|
|
101
|
+
def jackpots(self, lottery: PathValue, params: Params = None) -> Any:
|
|
102
|
+
r"""How a game's announced jackpot has moved
|
|
103
|
+
|
|
104
|
+
GET /lotteries/{lottery}/jackpots
|
|
105
|
+
|
|
106
|
+
Every distinct figure the operator announced for an upcoming draw, newest
|
|
107
|
+
first, with the span over which it stood unchanged. A figure revised
|
|
108
|
+
during the week becomes a new entry, so the list shows how the estimate
|
|
109
|
+
grew toward each draw. The history starts when we began recording it.
|
|
110
|
+
"""
|
|
111
|
+
return self._client.request("GET", f"/lotteries/{_segment(lottery)}/jackpots", params=params)
|
|
112
|
+
|
|
113
|
+
def payouts(self, lottery: PathValue, params: Params = None) -> Any:
|
|
114
|
+
r"""The payout table of a fixed-odds game
|
|
115
|
+
|
|
116
|
+
GET /lotteries/{lottery}/payouts
|
|
117
|
+
|
|
118
|
+
Some games do not split a pool between winners: the prize is a function
|
|
119
|
+
of the ticket — what was bet on, how many numbers were played, how many
|
|
120
|
+
matched — so it is the same for every draw and is published here rather
|
|
121
|
+
than with each result. A game that splits a pool returns an empty list;
|
|
122
|
+
its money is on the draws.
|
|
123
|
+
"""
|
|
124
|
+
return self._client.request("GET", f"/lotteries/{_segment(lottery)}/payouts", params=params)
|
|
125
|
+
|
|
126
|
+
def schedule(self, lottery: PathValue, params: Params = None) -> Any:
|
|
127
|
+
r"""When a game draws: each schedule written out in the language of the request,
|
|
128
|
+
plus the next few occurrences already resolved to UTC instants so a client
|
|
129
|
+
does not have to handle DST to build a calendar
|
|
130
|
+
|
|
131
|
+
GET /lotteries/{lottery}/schedule
|
|
132
|
+
|
|
133
|
+
`regular` is false for a game that draws on irregular days, such as
|
|
134
|
+
one tied to horse-racing fixtures: it has no timetable, so the
|
|
135
|
+
schedules, the next draw and the upcoming occurrences are empty.
|
|
136
|
+
Query `occurrences`
|
|
137
|
+
"""
|
|
138
|
+
return self._client.request("GET", f"/lotteries/{_segment(lottery)}/schedule", params=params)
|
|
139
|
+
|
|
140
|
+
def statistics(self, lottery: PathValue, params: Params = None) -> Any:
|
|
141
|
+
r"""The three rankings for a game: most drawn, least drawn, and longest without
|
|
142
|
+
appearing
|
|
143
|
+
|
|
144
|
+
GET /lotteries/{lottery}/statistics
|
|
145
|
+
|
|
146
|
+
Historical analysis of past results, not a prediction of future ones.
|
|
147
|
+
Query `combo_size`
|
|
148
|
+
Query `limit`
|
|
149
|
+
"""
|
|
150
|
+
return self._client.request("GET", f"/lotteries/{_segment(lottery)}/statistics", params=params)
|
|
151
|
+
|
|
152
|
+
def check(self, lottery: PathValue, params: Params = None) -> Any:
|
|
153
|
+
r"""Check a ticket against a game's history
|
|
154
|
+
|
|
155
|
+
GET /lotteries/{lottery}/check
|
|
156
|
+
|
|
157
|
+
Pass the ticket as `numbers=3,11,17,24,38,45` — plus `bonus=7` for a game
|
|
158
|
+
whose bonus is played separately — and get every past draw it matched,
|
|
159
|
+
newest first. For a number game a draw is listed when it contained at
|
|
160
|
+
least `min_matched` of the numbers; for a digit game (Pick 3, Joker, …)
|
|
161
|
+
when it was an exact or any-order hit, or matched at least `min_matched`
|
|
162
|
+
digits by position or in a row from either end. `meta.summary` counts
|
|
163
|
+
every outcome over the whole window, and for a game whose operator
|
|
164
|
+
publishes prizes per draw `meta.winnings` adds up what the ticket would
|
|
165
|
+
have won, by currency, at the amounts paid per winner then — gross, as
|
|
166
|
+
published; `complete` is false when some draws could not be judged.
|
|
167
|
+
For a fixed-odds game (Lotto's wheels, Pick 3, keno) name the bet with
|
|
168
|
+
`bet_type` — one of the game's `ticket_check.bet_types` — and
|
|
169
|
+
optionally `stake`, and every draw is priced at the game's payout table
|
|
170
|
+
instead: `meta.bet` says the stake, currency and whether the amounts
|
|
171
|
+
are before withholdings. The window is `range` (`3months`,
|
|
172
|
+
`6months`, `1year`, `2years`, `all`) or `from`/`to`, and never reaches
|
|
173
|
+
back before the game took its current format nor further than your plan
|
|
174
|
+
includes (`meta.window`).
|
|
175
|
+
Query `numbers`
|
|
176
|
+
Query `bonus`
|
|
177
|
+
Query `bet_type`
|
|
178
|
+
Query `stake`
|
|
179
|
+
Query `min_matched`
|
|
180
|
+
"""
|
|
181
|
+
return self._client.request("GET", f"/lotteries/{_segment(lottery)}/check", params=params)
|
|
182
|
+
|
|
183
|
+
def check_batch(self, lottery: PathValue, body: Params = None) -> Any:
|
|
184
|
+
r"""Check up to twenty tickets against a game's history at once
|
|
185
|
+
|
|
186
|
+
POST /lotteries/{lottery}/check
|
|
187
|
+
|
|
188
|
+
Send `tickets` — each `{"numbers": [...], "bonus": [...]}`, plus
|
|
189
|
+
`bet_type` and `stake` for a fixed-odds game — with the same window as
|
|
190
|
+
a single check (`range` or `from`/`to`), and get each ticket's summary:
|
|
191
|
+
how many draws it matched, and how. The draws themselves are
|
|
192
|
+
listed by the single-ticket check. Counts as one request. A game with a
|
|
193
|
+
very long history may need a shorter window for many tickets at once
|
|
194
|
+
(`ticket_check_too_large`).
|
|
195
|
+
"""
|
|
196
|
+
return self._client.request("POST", f"/lotteries/{_segment(lottery)}/check", body=body)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Compliance(_Resource):
|
|
200
|
+
def list(self, params: Params = None) -> Any:
|
|
201
|
+
r"""The legal frame of every researched jurisdiction, optionally within one
|
|
202
|
+
country
|
|
203
|
+
|
|
204
|
+
GET /compliance
|
|
205
|
+
|
|
206
|
+
Countries and their states or provinces come in one list, each
|
|
207
|
+
country followed by its states. A US state carries the federal tax
|
|
208
|
+
level alongside its own, so it can be read on its own.
|
|
209
|
+
"""
|
|
210
|
+
return self._client.request("GET", "/compliance", params=params)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Draws(_Resource):
|
|
214
|
+
def list(self, params: Params = None) -> Any:
|
|
215
|
+
r"""The results feed, newest first, across the whole catalogue or narrowed to a
|
|
216
|
+
game, a country or a date range. The cursor is what a client re-syncs
|
|
217
|
+
against; an offset page would be shifted by every draw that lands while it
|
|
218
|
+
reads. Results older than the history your plan includes are not listed —
|
|
219
|
+
`/v1/account` gives the exact start
|
|
220
|
+
|
|
221
|
+
GET /draws
|
|
222
|
+
"""
|
|
223
|
+
return self._client.request("GET", "/draws", params=params)
|
|
224
|
+
|
|
225
|
+
def latest(self, params: Params = None) -> Any:
|
|
226
|
+
r"""The latest result of every game, paginated over games — the board a
|
|
227
|
+
dashboard renders. A game with no result yet is listed with `latest_draw:
|
|
228
|
+
null`
|
|
229
|
+
|
|
230
|
+
GET /draws/latest
|
|
231
|
+
"""
|
|
232
|
+
return self._client.request("GET", "/draws/latest", params=params)
|
|
233
|
+
|
|
234
|
+
def get(self, draw: PathValue, params: Params = None) -> Any:
|
|
235
|
+
r"""One result by id. Ids are stable, and this is the URL a webhook payload and
|
|
236
|
+
an export row both point back at. A result older than the history your plan
|
|
237
|
+
includes is refused with `history_not_included`
|
|
238
|
+
|
|
239
|
+
GET /draws/{draw}
|
|
240
|
+
"""
|
|
241
|
+
return self._client.request("GET", f"/draws/{_segment(draw)}", params=params)
|
|
242
|
+
|
|
243
|
+
def changes(self, params: Params = None) -> Any:
|
|
244
|
+
r"""Everything that happened to the results, in order
|
|
245
|
+
|
|
246
|
+
GET /draws/changes
|
|
247
|
+
|
|
248
|
+
The feed to keep a copy of the results in step without re-reading
|
|
249
|
+
them. Each entry says what happened — `published`, `updated` (money
|
|
250
|
+
filled in later: the jackpot, the prize categories), `revised` (the
|
|
251
|
+
numbers or the draw time corrected) or `deleted` — which fields it
|
|
252
|
+
touched, and carries the draw as it stands now; a deleted draw carries
|
|
253
|
+
only its id. Start from `filter[since]`, then follow `next_cursor`
|
|
254
|
+
from one request to the next. Entries older than the retention window
|
|
255
|
+
are no longer listed, except corrections; a client further behind
|
|
256
|
+
than that re-reads the results feed.
|
|
257
|
+
Query `per_page`
|
|
258
|
+
"""
|
|
259
|
+
return self._client.request("GET", "/draws/changes", params=params)
|
|
260
|
+
|
|
261
|
+
def revisions(self, draw: PathValue, params: Params = None) -> Any:
|
|
262
|
+
r"""The corrections of one result, oldest first
|
|
263
|
+
|
|
264
|
+
GET /draws/{draw}/revisions
|
|
265
|
+
|
|
266
|
+
Each entry holds the fields that were corrected, with their values
|
|
267
|
+
before and after. A result that was never corrected has none.
|
|
268
|
+
"""
|
|
269
|
+
return self._client.request("GET", f"/draws/{_segment(draw)}/revisions", params=params)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class Continents(_Resource):
|
|
273
|
+
def list(self, params: Params = None) -> Any:
|
|
274
|
+
r"""The continents that have any lottery. Unpaginated on purpose: there are only
|
|
275
|
+
a handful of them, and a client building a dropdown should not have to
|
|
276
|
+
follow a cursor for it
|
|
277
|
+
|
|
278
|
+
GET /continents
|
|
279
|
+
"""
|
|
280
|
+
return self._client.request("GET", "/continents", params=params)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class Countries(_Resource):
|
|
284
|
+
def list(self, params: Params = None) -> Any:
|
|
285
|
+
r"""The countries that have any lottery, optionally within one continent. Lists
|
|
286
|
+
both the countries that own games and those where games are sold, so Cyprus
|
|
287
|
+
appears for the Greek games
|
|
288
|
+
|
|
289
|
+
GET /countries
|
|
290
|
+
Query `filter[continent]`
|
|
291
|
+
"""
|
|
292
|
+
return self._client.request("GET", "/countries", params=params)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class States(_Resource):
|
|
296
|
+
def list(self, params: Params = None) -> Any:
|
|
297
|
+
r"""The states that have any lottery, optionally within one country
|
|
298
|
+
|
|
299
|
+
GET /states
|
|
300
|
+
Query `filter[country]`
|
|
301
|
+
"""
|
|
302
|
+
return self._client.request("GET", "/states", params=params)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class Webhooks(_Resource):
|
|
306
|
+
def deliveries(self, webhook_endpoint: PathValue, params: Params = None) -> Any:
|
|
307
|
+
r"""What this subscription has been sent, newest first. Filterable by
|
|
308
|
+
`filter[status]=dead` — which is the dead-letter list, and the only thing
|
|
309
|
+
most clients ever look at
|
|
310
|
+
|
|
311
|
+
GET /webhooks/{webhookEndpoint}/deliveries
|
|
312
|
+
"""
|
|
313
|
+
return self._client.request("GET", f"/webhooks/{_segment(webhook_endpoint)}/deliveries", params=params)
|
|
314
|
+
|
|
315
|
+
def replay(self, webhook_endpoint: PathValue, uuid: PathValue, body: Params = None) -> Any:
|
|
316
|
+
r"""Offer a delivery again — the recovery path for a receiver that was down
|
|
317
|
+
longer than the backoff lasts
|
|
318
|
+
|
|
319
|
+
POST /webhooks/{webhookEndpoint}/deliveries/{uuid}/replay
|
|
320
|
+
|
|
321
|
+
The uuid is kept, so a receiver that DID process the original still
|
|
322
|
+
recognises it as the same event and can drop it. Only a delivery that
|
|
323
|
+
is no longer being retried — dead, or already delivered — can be
|
|
324
|
+
offered again.
|
|
325
|
+
"""
|
|
326
|
+
return self._client.request("POST", f"/webhooks/{_segment(webhook_endpoint)}/deliveries/{_segment(uuid)}/replay", body=body)
|
|
327
|
+
|
|
328
|
+
def list(self, params: Params = None) -> Any:
|
|
329
|
+
r"""The subscriptions this key holds
|
|
330
|
+
|
|
331
|
+
GET /webhooks
|
|
332
|
+
"""
|
|
333
|
+
return self._client.request("GET", "/webhooks", params=params)
|
|
334
|
+
|
|
335
|
+
def create(self, body: Params = None) -> Any:
|
|
336
|
+
r"""Subscribe. The signing secret comes back on this response only —
|
|
337
|
+
afterwards it is encrypted at rest and can be rotated but not read
|
|
338
|
+
|
|
339
|
+
POST /webhooks
|
|
340
|
+
"""
|
|
341
|
+
return self._client.request("POST", "/webhooks", body=body)
|
|
342
|
+
|
|
343
|
+
def get(self, webhook_endpoint: PathValue, params: Params = None) -> Any:
|
|
344
|
+
r"""Show one subscription
|
|
345
|
+
|
|
346
|
+
GET /webhooks/{webhookEndpoint}
|
|
347
|
+
"""
|
|
348
|
+
return self._client.request("GET", f"/webhooks/{_segment(webhook_endpoint)}", params=params)
|
|
349
|
+
|
|
350
|
+
def update(self, webhook_endpoint: PathValue, body: Params = None) -> Any:
|
|
351
|
+
r"""Change what a subscription listens to, or switch it back on after it was
|
|
352
|
+
disabled for being unreachable
|
|
353
|
+
|
|
354
|
+
PATCH /webhooks/{webhookEndpoint}
|
|
355
|
+
"""
|
|
356
|
+
return self._client.request("PATCH", f"/webhooks/{_segment(webhook_endpoint)}", body=body)
|
|
357
|
+
|
|
358
|
+
def delete(self, webhook_endpoint: PathValue, params: Params = None) -> Any:
|
|
359
|
+
r"""Unsubscribe for good, taking the delivery history with it
|
|
360
|
+
|
|
361
|
+
DELETE /webhooks/{webhookEndpoint}
|
|
362
|
+
"""
|
|
363
|
+
return self._client.request("DELETE", f"/webhooks/{_segment(webhook_endpoint)}", params=params)
|
|
364
|
+
|
|
365
|
+
def rotate_secret(self, webhook_endpoint: PathValue, body: Params = None) -> Any:
|
|
366
|
+
r"""Replace the signing secret, returning the new one once. The old one stops
|
|
367
|
+
verifying immediately — rotate when it has leaked, not on a schedule
|
|
368
|
+
|
|
369
|
+
POST /webhooks/{webhookEndpoint}/rotate-secret
|
|
370
|
+
"""
|
|
371
|
+
return self._client.request("POST", f"/webhooks/{_segment(webhook_endpoint)}/rotate-secret", body=body)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class Resources:
|
|
375
|
+
def _boot_resources(self) -> None:
|
|
376
|
+
self.account = Account(self)
|
|
377
|
+
self.lotteries = Lotteries(self)
|
|
378
|
+
self.compliance = Compliance(self)
|
|
379
|
+
self.draws = Draws(self)
|
|
380
|
+
self.continents = Continents(self)
|
|
381
|
+
self.countries = Countries(self)
|
|
382
|
+
self.states = States(self)
|
|
383
|
+
self.webhooks = Webhooks(self)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""The Lottevo API client: transport, errors and pagination."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
|
10
|
+
from urllib.parse import urlencode
|
|
11
|
+
|
|
12
|
+
from ._resources import Resources
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://api.lottevo.com/v1"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LottevoError(Exception):
|
|
20
|
+
"""A refused or failed call.
|
|
21
|
+
|
|
22
|
+
Branch on ``code`` (``lottery_not_entitled``, ``quota_exceeded``, ...) —
|
|
23
|
+
the message is translated and will change. ``status`` is 0 when the API
|
|
24
|
+
was never reached.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
message: str,
|
|
30
|
+
status: int = 0,
|
|
31
|
+
code: Optional[str] = None,
|
|
32
|
+
request_id: Optional[str] = None,
|
|
33
|
+
retry_after: Optional[int] = None,
|
|
34
|
+
body: Any = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.message = message
|
|
38
|
+
self.status = status
|
|
39
|
+
self.code = code
|
|
40
|
+
self.request_id = request_id
|
|
41
|
+
self.retry_after = retry_after
|
|
42
|
+
self.body = body
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Client(Resources):
|
|
46
|
+
"""The Lottevo API client.
|
|
47
|
+
|
|
48
|
+
Every operation hangs off a resource — ``client.lotteries.draws("EU-002")``
|
|
49
|
+
— and returns the decoded JSON. The key defaults to the LOTTEVO_API_KEY
|
|
50
|
+
environment variable; ``locale`` is sent as Accept-Language.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
api_key: Optional[str] = None,
|
|
56
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
57
|
+
locale: Optional[str] = None,
|
|
58
|
+
timeout: float = 30,
|
|
59
|
+
) -> None:
|
|
60
|
+
api_key = api_key or os.environ.get("LOTTEVO_API_KEY")
|
|
61
|
+
if not api_key:
|
|
62
|
+
raise ValueError("Pass an api_key or set the LOTTEVO_API_KEY environment variable.")
|
|
63
|
+
|
|
64
|
+
self._api_key = api_key
|
|
65
|
+
self._base_url = base_url.rstrip("/")
|
|
66
|
+
self._locale = locale
|
|
67
|
+
self._timeout = timeout
|
|
68
|
+
self.last_headers: Dict[str, str] = {}
|
|
69
|
+
self._boot_resources()
|
|
70
|
+
|
|
71
|
+
def request(
|
|
72
|
+
self,
|
|
73
|
+
method: str,
|
|
74
|
+
path: str,
|
|
75
|
+
params: Optional[Dict[str, Any]] = None,
|
|
76
|
+
body: Optional[Dict[str, Any]] = None,
|
|
77
|
+
) -> Any:
|
|
78
|
+
"""Send one call and decode its JSON.
|
|
79
|
+
|
|
80
|
+
A list value in ``params`` is sent comma-separated, the way the API
|
|
81
|
+
reads ``filter[lottery]=EU-002,RO-001``; ``{"filter": {"lottery": "EU-002"}}``
|
|
82
|
+
and ``{"filter[lottery]": "EU-002"}`` are the same.
|
|
83
|
+
"""
|
|
84
|
+
query = urlencode(_flatten(params or {}))
|
|
85
|
+
headers = {
|
|
86
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
87
|
+
"Accept": "application/json",
|
|
88
|
+
"User-Agent": f"lottevo-python/{__version__}",
|
|
89
|
+
}
|
|
90
|
+
if self._locale:
|
|
91
|
+
headers["Accept-Language"] = self._locale
|
|
92
|
+
data = None
|
|
93
|
+
if body:
|
|
94
|
+
headers["Content-Type"] = "application/json"
|
|
95
|
+
data = json.dumps(body).encode()
|
|
96
|
+
|
|
97
|
+
request = urllib.request.Request(
|
|
98
|
+
f"{self._base_url}{path}{'?' + query if query else ''}", data=data, headers=headers, method=method
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
with urllib.request.urlopen(request, timeout=self._timeout) as response:
|
|
103
|
+
status, raw = response.status, response.read()
|
|
104
|
+
self.last_headers = {k.lower(): v for k, v in response.headers.items()}
|
|
105
|
+
except urllib.error.HTTPError as error:
|
|
106
|
+
status, raw = error.code, error.read()
|
|
107
|
+
self.last_headers = {k.lower(): v for k, v in error.headers.items()}
|
|
108
|
+
except (urllib.error.URLError, OSError) as error:
|
|
109
|
+
raise LottevoError(f"Could not reach the Lottevo API: {error}", code="network_error") from error
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
payload = json.loads(raw) if raw else {}
|
|
113
|
+
except ValueError:
|
|
114
|
+
payload = None
|
|
115
|
+
|
|
116
|
+
if status >= 400 or not isinstance(payload, dict):
|
|
117
|
+
payload = payload if isinstance(payload, dict) else {}
|
|
118
|
+
retry_after = payload.get("retry_after", self.last_headers.get("retry-after"))
|
|
119
|
+
raise LottevoError(
|
|
120
|
+
payload.get("message", f"The Lottevo API answered {status}."),
|
|
121
|
+
status=status,
|
|
122
|
+
code=payload.get("code"),
|
|
123
|
+
request_id=self.last_headers.get("x-request-id"),
|
|
124
|
+
retry_after=int(retry_after) if retry_after is not None else None,
|
|
125
|
+
body=payload,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return payload
|
|
129
|
+
|
|
130
|
+
def paginate(self, page: Dict[str, Any]) -> Iterator[Any]:
|
|
131
|
+
"""Every item of a listing, following ``links.next`` from the page given.
|
|
132
|
+
|
|
133
|
+
``for draw in client.paginate(client.draws.list()): ...``
|
|
134
|
+
"""
|
|
135
|
+
while True:
|
|
136
|
+
yield from page.get("data") or []
|
|
137
|
+
|
|
138
|
+
next_url = (page.get("links") or {}).get("next")
|
|
139
|
+
if not next_url:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# The key is only ever sent to the host the client was built for.
|
|
143
|
+
if not next_url.startswith(self._base_url + "/"):
|
|
144
|
+
raise LottevoError(
|
|
145
|
+
f"Refusing to follow a next page outside {self._base_url}: {next_url}",
|
|
146
|
+
code="unexpected_next_page",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
page = self.request("GET", next_url[len(self._base_url):])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _flatten(values: Dict[str, Any], prefix: Optional[str] = None) -> List[Tuple[str, str]]:
|
|
153
|
+
pairs: List[Tuple[str, str]] = []
|
|
154
|
+
for key, value in values.items():
|
|
155
|
+
name = f"{prefix}[{key}]" if prefix else key
|
|
156
|
+
if value is None:
|
|
157
|
+
continue
|
|
158
|
+
if isinstance(value, dict):
|
|
159
|
+
pairs.extend(_flatten(value, name))
|
|
160
|
+
elif isinstance(value, (list, tuple)):
|
|
161
|
+
pairs.append((name, ",".join(str(item) for item in value)))
|
|
162
|
+
elif isinstance(value, bool):
|
|
163
|
+
pairs.append((name, "true" if value else "false"))
|
|
164
|
+
else:
|
|
165
|
+
pairs.append((name, str(value)))
|
|
166
|
+
return pairs
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lottevo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A thin Python client for the Lottevo lottery data API.
|
|
5
|
+
Author-email: Lottevo <contact@lottevo.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://lottevo.com/docs/sdks
|
|
8
|
+
Project-URL: Source, https://github.com/astefalabs/lottevo-python
|
|
9
|
+
Project-URL: Issues, https://github.com/astefalabs/lottevo-python/issues
|
|
10
|
+
Keywords: lottevo,lottery,results,api,sdk
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Lottevo Python
|
|
21
|
+
|
|
22
|
+
A thin client for the [Lottevo](https://lottevo.com) lottery data API: results, jackpots, schedules,
|
|
23
|
+
prize breakdowns and statistics for lotteries worldwide. Standard library only; Python 3.8+.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install lottevo
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## First call
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from lottevo import Client
|
|
33
|
+
|
|
34
|
+
lottevo = Client("lv_…") # or set LOTTEVO_API_KEY and pass nothing
|
|
35
|
+
|
|
36
|
+
account = lottevo.account.get() # the smoke test: plan, scopes, quota
|
|
37
|
+
draws = lottevo.lotteries.draws("EU-002", {"per_page": 5}) # EuroJackpot's latest results
|
|
38
|
+
|
|
39
|
+
for draw in draws["data"]:
|
|
40
|
+
print(draw["drawn_at"], *draw["numbers"])
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every operation of the API is a method on a resource — `lottevo.lotteries`, `.draws`, `.webhooks`,
|
|
44
|
+
`.countries` … — and returns the decoded JSON. Path parameters come first, then the query (or, for
|
|
45
|
+
`create`/`update`, the JSON body):
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
lottevo.draws.list({"filter": {"lottery": ["EU-002", "RO-001"], "from": "2026-01-01"}, "include": "prizes"})
|
|
49
|
+
lottevo.lotteries.check("EU-002", {"numbers": "4,7,12,31,44", "bonus": "8,11"})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Pagination
|
|
53
|
+
|
|
54
|
+
Listings are cursor-paginated. `paginate()` walks every page from the one you give it:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
for lottery in lottevo.paginate(lottevo.lotteries.list()):
|
|
58
|
+
print(lottery["code"])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Errors
|
|
62
|
+
|
|
63
|
+
A refused call raises `LottevoError`. Branch on `code`, not on the message:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from lottevo import LottevoError
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
lottevo.lotteries.draws("US-001")
|
|
70
|
+
except LottevoError as error:
|
|
71
|
+
if error.code == "lottery_not_entitled":
|
|
72
|
+
... # not in your plan
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`error.status`, `error.request_id` and `error.retry_after` are there too; `lottevo.last_headers` holds
|
|
76
|
+
the last answer's headers, including `x-quota-remaining`.
|
|
77
|
+
|
|
78
|
+
## Options
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
Client(api_key, locale="ro", timeout=30)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`locale` is sent as `Accept-Language`: game names, schedules and error messages come back in it.
|
|
85
|
+
|
|
86
|
+
Full reference: <https://lottevo.com/docs/reference>.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lottevo
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lottevo"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A thin Python client for the Lottevo lottery data API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Lottevo", email = "contact@lottevo.com" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
keywords = ["lottevo", "lottery", "results", "api", "sdk"]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://lottevo.com/docs/sdks"
|
|
25
|
+
Source = "https://github.com/astefalabs/lottevo-python"
|
|
26
|
+
Issues = "https://github.com/astefalabs/lottevo-python/issues"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = ["lottevo"]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
lottevo = ["py.typed"]
|
lottevo-0.1.0/setup.cfg
ADDED