managebetcodes 1.0.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.
- managebetcodes-1.0.0/LICENSE +21 -0
- managebetcodes-1.0.0/PKG-INFO +270 -0
- managebetcodes-1.0.0/README.md +250 -0
- managebetcodes-1.0.0/managebetcodes/__init__.py +22 -0
- managebetcodes-1.0.0/managebetcodes/cli.py +121 -0
- managebetcodes-1.0.0/managebetcodes/client.py +701 -0
- managebetcodes-1.0.0/managebetcodes/errors.py +88 -0
- managebetcodes-1.0.0/managebetcodes/webhooks.py +23 -0
- managebetcodes-1.0.0/managebetcodes.egg-info/PKG-INFO +270 -0
- managebetcodes-1.0.0/managebetcodes.egg-info/SOURCES.txt +15 -0
- managebetcodes-1.0.0/managebetcodes.egg-info/dependency_links.txt +1 -0
- managebetcodes-1.0.0/managebetcodes.egg-info/entry_points.txt +2 -0
- managebetcodes-1.0.0/managebetcodes.egg-info/requires.txt +3 -0
- managebetcodes-1.0.0/managebetcodes.egg-info/top_level.txt +1 -0
- managebetcodes-1.0.0/pyproject.toml +37 -0
- managebetcodes-1.0.0/setup.cfg +4 -0
- managebetcodes-1.0.0/tests/test_client.py +367 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manage Bet Codes
|
|
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.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: managebetcodes
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the Manage Bet Codes public API v1
|
|
5
|
+
Author: Manage Bet Codes
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Documentation, https://managebetcodes.app/developers/sdk/python
|
|
8
|
+
Project-URL: Source, https://github.com/kayceepeece/Viewbetbot
|
|
9
|
+
Keywords: betting,bet-codes,sportybet,1xbet
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Manage Bet Codes Python SDK
|
|
22
|
+
|
|
23
|
+
The official Python SDK for the Manage Bet Codes public API v1.
|
|
24
|
+
|
|
25
|
+
- Python 3.9 or later.
|
|
26
|
+
- No runtime dependencies. The SDK uses the Python standard library.
|
|
27
|
+
- Synchronous client.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install managebetcodes
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Command line
|
|
36
|
+
|
|
37
|
+
The package installs an `mbc` command with the same client.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export MBC_API_KEY=mbc_live_your_key
|
|
41
|
+
mbc convert 7B9A2F --from sportybet --to 1xng
|
|
42
|
+
mbc view 7B9A2F --bookmaker sportybet
|
|
43
|
+
mbc usage
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from managebetcodes import Client
|
|
50
|
+
|
|
51
|
+
client = Client("mbc_live_your_key")
|
|
52
|
+
|
|
53
|
+
result = client.convert("7B9A2F", "sportybet", "1xng")
|
|
54
|
+
print(result["target"]["code"])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The client uses this base URL by default:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
https://api.managebetcodes.app/v1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Change the base URL for a test server:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
client = Client("mbc_live_your_key", base_url="http://127.0.0.1:8000/v1")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Authentication
|
|
70
|
+
|
|
71
|
+
Pass the API key to the constructor.
|
|
72
|
+
The SDK sends the key in the `X-API-Key` header.
|
|
73
|
+
Keys start with `mbc_live_`.
|
|
74
|
+
Keep the key on the server.
|
|
75
|
+
Do not put the key in browser code.
|
|
76
|
+
|
|
77
|
+
## Client options
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
Client(
|
|
81
|
+
api_key,
|
|
82
|
+
base_url="https://api.managebetcodes.app/v1",
|
|
83
|
+
timeout=30,
|
|
84
|
+
max_retries=3,
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `timeout` is the socket timeout in seconds.
|
|
89
|
+
- `max_retries` is the number of extra attempts.
|
|
90
|
+
|
|
91
|
+
## Methods
|
|
92
|
+
|
|
93
|
+
| Method | Call | Price |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `convert(code, source, target)` | `POST /convert` | 2 credits per target |
|
|
96
|
+
| `convert_to_all(code, source, to=...)` | `POST /convert/all` | 2 credits per target |
|
|
97
|
+
| `convert_bulk(codes, to=...)` | `POST /convert/bulk` | 2 credits per pair |
|
|
98
|
+
| `view(code, bookmaker)` | `GET /view` | 1 credit |
|
|
99
|
+
| `view_image(code, bookmaker)` | `GET /view/image` | 2 credits |
|
|
100
|
+
| `split(code, bookmaker, count=2, ...)` | `POST /split` | 2 credits plus 1 per extra part |
|
|
101
|
+
| `merge(pairs, target=None)` | `POST /merge` | 2 credits plus 1 per extra code |
|
|
102
|
+
| `book(bookmaker, plan_id=..., picks=...)` | `POST /book` | 2 credits per code |
|
|
103
|
+
| `events_search(bookmaker, queries)` | `POST /events/search` | 0 credits |
|
|
104
|
+
| `events(q=...)` | `GET /events` | 0 credits |
|
|
105
|
+
| `event_markets(event_id)` | `GET /events/{id}/markets` | 0 credits |
|
|
106
|
+
| `resolve(bookmaker, picks)` | `POST /resolve` | 0 credits |
|
|
107
|
+
| `markets()` | `GET /markets` | 0 credits |
|
|
108
|
+
| `books(limit=..., offset=...)` | `GET /books` | 0 credits |
|
|
109
|
+
| `capabilities()` | `GET /capabilities` | 0 credits |
|
|
110
|
+
| `usage()` | `GET /usage` | 0 credits |
|
|
111
|
+
| `history(limit=..., offset=...)` | `GET /history` | 0 credits |
|
|
112
|
+
| `jobs_create(payload)` | `POST /jobs` | Job price |
|
|
113
|
+
| `job(job_id)` | `GET /jobs/{id}` | 0 credits |
|
|
114
|
+
| `jobs()` | `GET /jobs` | 0 credits |
|
|
115
|
+
| `cancel_job(job_id)` | `DELETE /jobs/{id}` | 0 credits |
|
|
116
|
+
| `track_start(code, bookmaker)` | `POST /track` | 1 credit |
|
|
117
|
+
| `track(track_id)` | `GET /track/{id}` | 0 credits |
|
|
118
|
+
| `tracks()` | `GET /tracks` | 0 credits |
|
|
119
|
+
| `stop_track(track_id)` | `DELETE /track/{id}` | 0 credits |
|
|
120
|
+
|
|
121
|
+
The methods return the JSON response as a `dict`.
|
|
122
|
+
`view_image` returns `bytes`.
|
|
123
|
+
|
|
124
|
+
## Convert
|
|
125
|
+
|
|
126
|
+
Convert one code to one target:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
client.convert("7B9A2F", "sportybet", "1xng")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Convert one code to a batch:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
client.convert("7B9A2F", "sportybet", ["1xng", "msport", "bet9ja"])
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A batch accepts 2 to 6 targets.
|
|
139
|
+
Use a job for more targets.
|
|
140
|
+
|
|
141
|
+
## Book
|
|
142
|
+
|
|
143
|
+
Book from a resolve plan:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
client.book("sportybet", plan_id="plan_123")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Book from direct picks:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
client.book(
|
|
153
|
+
"sportybet",
|
|
154
|
+
picks=[{"event_id": "sr:match:123", "market_id": "18",
|
|
155
|
+
"outcome_id": "12", "odds": "1.90"}],
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Jobs
|
|
160
|
+
|
|
161
|
+
`jobs_create` accepts a job body:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
job = client.jobs_create({
|
|
165
|
+
"kind": "convert",
|
|
166
|
+
"code": "7B9A2F",
|
|
167
|
+
"from": "sportybet",
|
|
168
|
+
"to": ["1xng", "msport", "bet9ja", "melbet"],
|
|
169
|
+
})
|
|
170
|
+
client.job(job["id"])
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Image
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
png = client.view_image("7B9A2F", "sportybet")
|
|
177
|
+
with open("slip.png", "wb") as file:
|
|
178
|
+
file.write(png)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The method returns the image bytes.
|
|
182
|
+
It raises a typed error when the API returns an error envelope.
|
|
183
|
+
|
|
184
|
+
## Idempotency
|
|
185
|
+
|
|
186
|
+
The SDK adds an `Idempotency-Key` header to every POST call.
|
|
187
|
+
Pass your own key when you need a stable value:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
client.convert("7B9A2F", "sportybet", "1xng", idempotency_key="order-123")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The SDK sends the same key on a retry.
|
|
194
|
+
|
|
195
|
+
## Retries
|
|
196
|
+
|
|
197
|
+
The SDK retries on these status codes:
|
|
198
|
+
|
|
199
|
+
- `429`
|
|
200
|
+
- `502`
|
|
201
|
+
- `503`
|
|
202
|
+
- `504`
|
|
203
|
+
|
|
204
|
+
The SDK reads the `Retry-After` header.
|
|
205
|
+
If the header is absent, the SDK uses exponential backoff.
|
|
206
|
+
|
|
207
|
+
## Errors
|
|
208
|
+
|
|
209
|
+
All errors inherit from `APIError`:
|
|
210
|
+
|
|
211
|
+
| Error | Status | Meaning |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| `AuthenticationError` | 401, 403 | The key is missing, bad, or not allowed |
|
|
214
|
+
| `QuotaError` | 429 | The plan credits or search cap are finished |
|
|
215
|
+
| `RateLimitError` | 429 | The request rate is too high |
|
|
216
|
+
| `ValidationError` | 400, 422 | A parameter or the body is invalid |
|
|
217
|
+
| `APIError` | Other | Any other API error |
|
|
218
|
+
|
|
219
|
+
Each error has these attributes:
|
|
220
|
+
|
|
221
|
+
- `status`
|
|
222
|
+
- `code`
|
|
223
|
+
- `message`
|
|
224
|
+
- `request_id`
|
|
225
|
+
- `docs`
|
|
226
|
+
- `details`
|
|
227
|
+
|
|
228
|
+
`RateLimitError` also has `retry_after`.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from managebetcodes import RateLimitError, ValidationError
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
client.convert("BAD", "sportybet", "1xng")
|
|
235
|
+
except ValidationError as error:
|
|
236
|
+
print(error.code, error.details)
|
|
237
|
+
except RateLimitError as error:
|
|
238
|
+
print("Retry after", error.retry_after)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Webhooks
|
|
242
|
+
|
|
243
|
+
Verify the raw request body before you trust a webhook:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from managebetcodes import verify_webhook
|
|
247
|
+
|
|
248
|
+
if not verify_webhook(secret, raw_body, request.headers["X-MBC-Signature"]):
|
|
249
|
+
return "invalid signature", 400
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`raw_body` must be `bytes`.
|
|
253
|
+
Do not parse the body before the check.
|
|
254
|
+
The function returns `True` for a valid signature.
|
|
255
|
+
It returns `False` for a bad signature.
|
|
256
|
+
|
|
257
|
+
## Pagination
|
|
258
|
+
|
|
259
|
+
Pass `limit` and `offset` to `history`, `jobs`, `books`, and `events`.
|
|
260
|
+
The response returns `next_offset` when another page exists.
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
page = client.history(limit=20, offset=0)
|
|
264
|
+
for item in page["items"]:
|
|
265
|
+
print(item["id"])
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Manage Bet Codes Python SDK
|
|
2
|
+
|
|
3
|
+
The official Python SDK for the Manage Bet Codes public API v1.
|
|
4
|
+
|
|
5
|
+
- Python 3.9 or later.
|
|
6
|
+
- No runtime dependencies. The SDK uses the Python standard library.
|
|
7
|
+
- Synchronous client.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install managebetcodes
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Command line
|
|
16
|
+
|
|
17
|
+
The package installs an `mbc` command with the same client.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export MBC_API_KEY=mbc_live_your_key
|
|
21
|
+
mbc convert 7B9A2F --from sportybet --to 1xng
|
|
22
|
+
mbc view 7B9A2F --bookmaker sportybet
|
|
23
|
+
mbc usage
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from managebetcodes import Client
|
|
30
|
+
|
|
31
|
+
client = Client("mbc_live_your_key")
|
|
32
|
+
|
|
33
|
+
result = client.convert("7B9A2F", "sportybet", "1xng")
|
|
34
|
+
print(result["target"]["code"])
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The client uses this base URL by default:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
https://api.managebetcodes.app/v1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Change the base URL for a test server:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
client = Client("mbc_live_your_key", base_url="http://127.0.0.1:8000/v1")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Authentication
|
|
50
|
+
|
|
51
|
+
Pass the API key to the constructor.
|
|
52
|
+
The SDK sends the key in the `X-API-Key` header.
|
|
53
|
+
Keys start with `mbc_live_`.
|
|
54
|
+
Keep the key on the server.
|
|
55
|
+
Do not put the key in browser code.
|
|
56
|
+
|
|
57
|
+
## Client options
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
Client(
|
|
61
|
+
api_key,
|
|
62
|
+
base_url="https://api.managebetcodes.app/v1",
|
|
63
|
+
timeout=30,
|
|
64
|
+
max_retries=3,
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `timeout` is the socket timeout in seconds.
|
|
69
|
+
- `max_retries` is the number of extra attempts.
|
|
70
|
+
|
|
71
|
+
## Methods
|
|
72
|
+
|
|
73
|
+
| Method | Call | Price |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `convert(code, source, target)` | `POST /convert` | 2 credits per target |
|
|
76
|
+
| `convert_to_all(code, source, to=...)` | `POST /convert/all` | 2 credits per target |
|
|
77
|
+
| `convert_bulk(codes, to=...)` | `POST /convert/bulk` | 2 credits per pair |
|
|
78
|
+
| `view(code, bookmaker)` | `GET /view` | 1 credit |
|
|
79
|
+
| `view_image(code, bookmaker)` | `GET /view/image` | 2 credits |
|
|
80
|
+
| `split(code, bookmaker, count=2, ...)` | `POST /split` | 2 credits plus 1 per extra part |
|
|
81
|
+
| `merge(pairs, target=None)` | `POST /merge` | 2 credits plus 1 per extra code |
|
|
82
|
+
| `book(bookmaker, plan_id=..., picks=...)` | `POST /book` | 2 credits per code |
|
|
83
|
+
| `events_search(bookmaker, queries)` | `POST /events/search` | 0 credits |
|
|
84
|
+
| `events(q=...)` | `GET /events` | 0 credits |
|
|
85
|
+
| `event_markets(event_id)` | `GET /events/{id}/markets` | 0 credits |
|
|
86
|
+
| `resolve(bookmaker, picks)` | `POST /resolve` | 0 credits |
|
|
87
|
+
| `markets()` | `GET /markets` | 0 credits |
|
|
88
|
+
| `books(limit=..., offset=...)` | `GET /books` | 0 credits |
|
|
89
|
+
| `capabilities()` | `GET /capabilities` | 0 credits |
|
|
90
|
+
| `usage()` | `GET /usage` | 0 credits |
|
|
91
|
+
| `history(limit=..., offset=...)` | `GET /history` | 0 credits |
|
|
92
|
+
| `jobs_create(payload)` | `POST /jobs` | Job price |
|
|
93
|
+
| `job(job_id)` | `GET /jobs/{id}` | 0 credits |
|
|
94
|
+
| `jobs()` | `GET /jobs` | 0 credits |
|
|
95
|
+
| `cancel_job(job_id)` | `DELETE /jobs/{id}` | 0 credits |
|
|
96
|
+
| `track_start(code, bookmaker)` | `POST /track` | 1 credit |
|
|
97
|
+
| `track(track_id)` | `GET /track/{id}` | 0 credits |
|
|
98
|
+
| `tracks()` | `GET /tracks` | 0 credits |
|
|
99
|
+
| `stop_track(track_id)` | `DELETE /track/{id}` | 0 credits |
|
|
100
|
+
|
|
101
|
+
The methods return the JSON response as a `dict`.
|
|
102
|
+
`view_image` returns `bytes`.
|
|
103
|
+
|
|
104
|
+
## Convert
|
|
105
|
+
|
|
106
|
+
Convert one code to one target:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
client.convert("7B9A2F", "sportybet", "1xng")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Convert one code to a batch:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
client.convert("7B9A2F", "sportybet", ["1xng", "msport", "bet9ja"])
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
A batch accepts 2 to 6 targets.
|
|
119
|
+
Use a job for more targets.
|
|
120
|
+
|
|
121
|
+
## Book
|
|
122
|
+
|
|
123
|
+
Book from a resolve plan:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
client.book("sportybet", plan_id="plan_123")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Book from direct picks:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
client.book(
|
|
133
|
+
"sportybet",
|
|
134
|
+
picks=[{"event_id": "sr:match:123", "market_id": "18",
|
|
135
|
+
"outcome_id": "12", "odds": "1.90"}],
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Jobs
|
|
140
|
+
|
|
141
|
+
`jobs_create` accepts a job body:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
job = client.jobs_create({
|
|
145
|
+
"kind": "convert",
|
|
146
|
+
"code": "7B9A2F",
|
|
147
|
+
"from": "sportybet",
|
|
148
|
+
"to": ["1xng", "msport", "bet9ja", "melbet"],
|
|
149
|
+
})
|
|
150
|
+
client.job(job["id"])
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Image
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
png = client.view_image("7B9A2F", "sportybet")
|
|
157
|
+
with open("slip.png", "wb") as file:
|
|
158
|
+
file.write(png)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The method returns the image bytes.
|
|
162
|
+
It raises a typed error when the API returns an error envelope.
|
|
163
|
+
|
|
164
|
+
## Idempotency
|
|
165
|
+
|
|
166
|
+
The SDK adds an `Idempotency-Key` header to every POST call.
|
|
167
|
+
Pass your own key when you need a stable value:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
client.convert("7B9A2F", "sportybet", "1xng", idempotency_key="order-123")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The SDK sends the same key on a retry.
|
|
174
|
+
|
|
175
|
+
## Retries
|
|
176
|
+
|
|
177
|
+
The SDK retries on these status codes:
|
|
178
|
+
|
|
179
|
+
- `429`
|
|
180
|
+
- `502`
|
|
181
|
+
- `503`
|
|
182
|
+
- `504`
|
|
183
|
+
|
|
184
|
+
The SDK reads the `Retry-After` header.
|
|
185
|
+
If the header is absent, the SDK uses exponential backoff.
|
|
186
|
+
|
|
187
|
+
## Errors
|
|
188
|
+
|
|
189
|
+
All errors inherit from `APIError`:
|
|
190
|
+
|
|
191
|
+
| Error | Status | Meaning |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `AuthenticationError` | 401, 403 | The key is missing, bad, or not allowed |
|
|
194
|
+
| `QuotaError` | 429 | The plan credits or search cap are finished |
|
|
195
|
+
| `RateLimitError` | 429 | The request rate is too high |
|
|
196
|
+
| `ValidationError` | 400, 422 | A parameter or the body is invalid |
|
|
197
|
+
| `APIError` | Other | Any other API error |
|
|
198
|
+
|
|
199
|
+
Each error has these attributes:
|
|
200
|
+
|
|
201
|
+
- `status`
|
|
202
|
+
- `code`
|
|
203
|
+
- `message`
|
|
204
|
+
- `request_id`
|
|
205
|
+
- `docs`
|
|
206
|
+
- `details`
|
|
207
|
+
|
|
208
|
+
`RateLimitError` also has `retry_after`.
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from managebetcodes import RateLimitError, ValidationError
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
client.convert("BAD", "sportybet", "1xng")
|
|
215
|
+
except ValidationError as error:
|
|
216
|
+
print(error.code, error.details)
|
|
217
|
+
except RateLimitError as error:
|
|
218
|
+
print("Retry after", error.retry_after)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Webhooks
|
|
222
|
+
|
|
223
|
+
Verify the raw request body before you trust a webhook:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from managebetcodes import verify_webhook
|
|
227
|
+
|
|
228
|
+
if not verify_webhook(secret, raw_body, request.headers["X-MBC-Signature"]):
|
|
229
|
+
return "invalid signature", 400
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`raw_body` must be `bytes`.
|
|
233
|
+
Do not parse the body before the check.
|
|
234
|
+
The function returns `True` for a valid signature.
|
|
235
|
+
It returns `False` for a bad signature.
|
|
236
|
+
|
|
237
|
+
## Pagination
|
|
238
|
+
|
|
239
|
+
Pass `limit` and `offset` to `history`, `jobs`, `books`, and `events`.
|
|
240
|
+
The response returns `next_offset` when another page exists.
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
page = client.history(limit=20, offset=0)
|
|
244
|
+
for item in page["items"]:
|
|
245
|
+
print(item["id"])
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Official SDK for the Manage Bet Codes public API v1."""
|
|
2
|
+
|
|
3
|
+
from .client import Client, __version__
|
|
4
|
+
from .errors import (
|
|
5
|
+
APIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
QuotaError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
)
|
|
11
|
+
from .webhooks import verify_webhook
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"APIError",
|
|
15
|
+
"AuthenticationError",
|
|
16
|
+
"Client",
|
|
17
|
+
"QuotaError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"ValidationError",
|
|
20
|
+
"verify_webhook",
|
|
21
|
+
"__version__",
|
|
22
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""The `mbc` command: convert and view codes from a terminal."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
10
|
+
|
|
11
|
+
from .client import BASE_URL, Client, __version__
|
|
12
|
+
from .errors import APIError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
16
|
+
"""Build the `mbc` argument parser."""
|
|
17
|
+
|
|
18
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
19
|
+
common.add_argument(
|
|
20
|
+
"--api-key",
|
|
21
|
+
default=os.environ.get("MBC_API_KEY"),
|
|
22
|
+
help="API key (or set MBC_API_KEY). Not needed for capabilities.",
|
|
23
|
+
)
|
|
24
|
+
common.add_argument(
|
|
25
|
+
"--base-url",
|
|
26
|
+
default=os.environ.get("MBC_BASE_URL", BASE_URL),
|
|
27
|
+
help="API base URL (or set MBC_BASE_URL).",
|
|
28
|
+
)
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="mbc",
|
|
31
|
+
description="Manage Bet Codes public API v1",
|
|
32
|
+
parents=[common],
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--version",
|
|
36
|
+
action="version",
|
|
37
|
+
version=f"%(prog)s {__version__}",
|
|
38
|
+
)
|
|
39
|
+
sub = parser.add_subparsers(dest="command", required=True, metavar="command")
|
|
40
|
+
|
|
41
|
+
convert = sub.add_parser(
|
|
42
|
+
"convert", parents=[common], help="Convert a code to one or more targets."
|
|
43
|
+
)
|
|
44
|
+
convert.add_argument("code", help="Source booking code.")
|
|
45
|
+
convert.add_argument("--from", dest="source", required=True, help="Source bookmaker.")
|
|
46
|
+
convert.add_argument(
|
|
47
|
+
"--to",
|
|
48
|
+
dest="targets",
|
|
49
|
+
required=True,
|
|
50
|
+
help="Comma-separated target list, e.g. 1xng or 1xng,msport.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
view = sub.add_parser("view", parents=[common], help="View a slip as text.")
|
|
54
|
+
view.add_argument("code", help="Booking code.")
|
|
55
|
+
view.add_argument("--bookmaker", required=True, help="Bookmaker of the code.")
|
|
56
|
+
|
|
57
|
+
sub.add_parser("capabilities", parents=[common], help="Show the API manifest.")
|
|
58
|
+
sub.add_parser("usage", parents=[common], help="Show plan, credits, and limits.")
|
|
59
|
+
return parser
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _targets(raw: str) -> List[str]:
|
|
63
|
+
return [part.strip() for part in raw.split(",") if part.strip()]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _public_capabilities(base_url: str) -> int:
|
|
67
|
+
"""Read the public manifest without an API key."""
|
|
68
|
+
|
|
69
|
+
url = base_url.rstrip("/") + "/capabilities"
|
|
70
|
+
request = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
71
|
+
try:
|
|
72
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
73
|
+
print(json.dumps(json.loads(response.read().decode("utf-8")), indent=2))
|
|
74
|
+
return 0
|
|
75
|
+
except urllib.error.HTTPError as exc:
|
|
76
|
+
print(f"mbc: HTTP {exc.code} [http_{exc.code}]: the manifest read failed.",
|
|
77
|
+
file=sys.stderr)
|
|
78
|
+
return 1
|
|
79
|
+
except (urllib.error.URLError, TimeoutError) as exc:
|
|
80
|
+
print(f"mbc: the request to the API failed: {exc}", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _print(payload: Dict[str, Any]) -> int:
|
|
85
|
+
print(json.dumps(payload, indent=2))
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
90
|
+
"""Run the `mbc` command. Returns the process exit code."""
|
|
91
|
+
|
|
92
|
+
args = build_parser().parse_args(argv)
|
|
93
|
+
if args.command == "capabilities":
|
|
94
|
+
return _public_capabilities(args.base_url)
|
|
95
|
+
if not args.api_key:
|
|
96
|
+
print("mbc: set --api-key or MBC_API_KEY.", file=sys.stderr)
|
|
97
|
+
return 2
|
|
98
|
+
client = Client(args.api_key, base_url=args.base_url)
|
|
99
|
+
try:
|
|
100
|
+
if args.command == "convert":
|
|
101
|
+
targets = _targets(args.targets)
|
|
102
|
+
if not targets:
|
|
103
|
+
print("mbc: --to needs at least one target.", file=sys.stderr)
|
|
104
|
+
return 2
|
|
105
|
+
target = targets[0] if len(targets) == 1 else targets
|
|
106
|
+
return _print(client.convert(args.code, args.source, target))
|
|
107
|
+
if args.command == "view":
|
|
108
|
+
return _print(client.view(args.code, args.bookmaker))
|
|
109
|
+
if args.command == "capabilities":
|
|
110
|
+
return _print(client.capabilities())
|
|
111
|
+
if args.command == "usage":
|
|
112
|
+
return _print(client.usage())
|
|
113
|
+
except APIError as error:
|
|
114
|
+
print(f"mbc: {error}", file=sys.stderr)
|
|
115
|
+
return 1
|
|
116
|
+
print(f"mbc: unknown command {args.command}.", file=sys.stderr)
|
|
117
|
+
return 2
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
raise SystemExit(main())
|