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 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>.
@@ -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,5 @@
1
+ """A thin Python client for the Lottevo lottery data API."""
2
+
3
+ from .client import DEFAULT_BASE_URL, Client, LottevoError, __version__
4
+
5
+ __all__ = ["Client", "LottevoError", "DEFAULT_BASE_URL", "__version__"]
@@ -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,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ lottevo/__init__.py
5
+ lottevo/_resources.py
6
+ lottevo/client.py
7
+ lottevo/py.typed
8
+ lottevo.egg-info/PKG-INFO
9
+ lottevo.egg-info/SOURCES.txt
10
+ lottevo.egg-info/dependency_links.txt
11
+ lottevo.egg-info/top_level.txt
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+