woltapi 0.1.0__tar.gz → 0.2.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.
- {woltapi-0.1.0/src/woltapi.egg-info → woltapi-0.2.0}/PKG-INFO +92 -69
- {woltapi-0.1.0 → woltapi-0.2.0}/README.md +91 -68
- {woltapi-0.1.0 → woltapi-0.2.0}/examples/browse.py +86 -17
- {woltapi-0.1.0 → woltapi-0.2.0}/examples/order.py +65 -41
- {woltapi-0.1.0 → woltapi-0.2.0}/pyproject.toml +1 -1
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/__init__.py +2 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/client.py +16 -2
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/models.py +10 -3
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/selection.py +57 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/transport.py +6 -1
- {woltapi-0.1.0 → woltapi-0.2.0/src/woltapi.egg-info}/PKG-INFO +92 -69
- woltapi-0.2.0/tests/test_browse.py +294 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_client.py +83 -12
- {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_order_example.py +209 -29
- {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_selection.py +55 -0
- woltapi-0.1.0/tests/test_browse.py +0 -123
- {woltapi-0.1.0 → woltapi-0.2.0}/LICENSE +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/MANIFEST.in +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/RELEASING.md +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/examples/check_session.py +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/setup.cfg +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/auth.py +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/credentials.py +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/errors.py +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/purchase.py +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/services.py +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi.egg-info/SOURCES.txt +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi.egg-info/dependency_links.txt +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi.egg-info/requires.txt +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi.egg-info/top_level.txt +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/tests/__init__.py +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_auth.py +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_check_session.py +0 -0
- {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_purchase.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: woltapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A small synchronous client for observed Wolt ordering flows.
|
|
5
5
|
License-Expression: AGPL-3.0-only
|
|
6
6
|
Project-URL: Repository, https://github.com/skorokithakis/woltapi
|
|
@@ -63,14 +63,18 @@ lists some of its menu items. **It will not order anything or change your basket
|
|
|
63
63
|
python examples/browse.py \
|
|
64
64
|
--latitude 60.17 \
|
|
65
65
|
--longitude 24.94 \
|
|
66
|
-
--query "pizza"
|
|
66
|
+
--query "pizza" \
|
|
67
|
+
--token-file ~/.wolt-token
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
Replace the two numbers with your location. The example numbers are in Helsinki.
|
|
70
71
|
Latitude and longitude are the two numbers that identify a place on a map.
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
`--token-file` is required. It names the file that holds your Wolt refresh
|
|
74
|
+
token. On the first run the file does not exist yet, so the script asks for the
|
|
75
|
+
token. Paste it and press Enter. Nothing will appear while you paste; that is
|
|
76
|
+
intentional. The script then saves the token to that file, and later runs read
|
|
77
|
+
it from there without asking. See [Get a token](#get-a-token) below.
|
|
74
78
|
|
|
75
79
|
By default, it shows up to 5 recent orders and 20 menu items from the first
|
|
76
80
|
restaurant in the search results. You can change that:
|
|
@@ -80,6 +84,7 @@ python examples/browse.py \
|
|
|
80
84
|
--latitude 60.17 \
|
|
81
85
|
--longitude 24.94 \
|
|
82
86
|
--query "burger" \
|
|
87
|
+
--token-file ~/.wolt-token \
|
|
83
88
|
--orders 3 \
|
|
84
89
|
--menu-limit 50 \
|
|
85
90
|
--venue-index 2
|
|
@@ -94,46 +99,52 @@ The library keeps the original integer amounts; only the display converts them.
|
|
|
94
99
|
|
|
95
100
|
## Get a token
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
account. Treat it like a password.
|
|
102
|
+
A **refresh token** is a long-lived key that lets the library sign requests
|
|
103
|
+
for your Wolt account. Treat it like a password.
|
|
99
104
|
|
|
100
105
|
1. Sign in to Wolt in your browser.
|
|
101
|
-
2. Open Developer Tools and select
|
|
102
|
-
3.
|
|
103
|
-
4.
|
|
104
|
-
5. Under **Request Headers**, find `authorization: Bearer ...`.
|
|
105
|
-
6. Copy the long token after `Bearer `.
|
|
106
|
+
2. Open Developer Tools and select **Application** (Firefox: **Storage**).
|
|
107
|
+
3. Under **Cookies**, find the cookie named `__wrtoken`.
|
|
108
|
+
4. Copy its value, without surrounding quotes.
|
|
106
109
|
|
|
107
110
|
Do not share the token, put it in Git, or include it in screenshots.
|
|
108
111
|
|
|
109
|
-
The
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
you to paste a token. No credential file is needed.
|
|
112
|
+
The example scripts read the token from the file named by `--token-file`. If
|
|
113
|
+
that file does not exist, or is empty, they ask with a hidden prompt and then
|
|
114
|
+
save what you paste. The file is created readable only by you.
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
Wolt may replace your refresh token over time. The scripts save each
|
|
117
|
+
replacement to the same file, so later runs keep working without another visit
|
|
118
|
+
to the browser. Point every run at the same file.
|
|
119
|
+
|
|
120
|
+
Because of this, the token file holds a live secret and becomes the only copy
|
|
121
|
+
once Wolt has replaced the original. Keep it out of Git and shared folders. Do
|
|
122
|
+
not point two programs that run at the same time at one token file: whichever
|
|
123
|
+
refreshes second will find its own token already replaced.
|
|
124
|
+
|
|
125
|
+
If a run receives **HTTP 401**, the saved token has expired or was revoked. Put
|
|
126
|
+
a current `__wrtoken` value in the token file, or delete the file and let the
|
|
127
|
+
script ask again. The library cannot sign you in.
|
|
128
|
+
|
|
129
|
+
The scripts exchange this refresh token for short-lived access tokens
|
|
130
|
+
automatically; you never handle access tokens yourself. If you want to supply
|
|
131
|
+
raw `Authorization` headers instead, use `SessionCredentials` from Python
|
|
132
|
+
directly, as described under
|
|
133
|
+
[Manually supplied headers](#manually-supplied-headers).
|
|
118
134
|
|
|
119
135
|
## Use it in Python
|
|
120
136
|
|
|
121
|
-
Here is a complete browsing example. It asks for your token and
|
|
122
|
-
searches for pizza and loads the first result's menu:
|
|
137
|
+
Here is a complete browsing example. It asks for your refresh token and
|
|
138
|
+
location, then searches for pizza and loads the first result's menu:
|
|
123
139
|
|
|
124
140
|
```python
|
|
125
141
|
from getpass import getpass
|
|
126
142
|
|
|
127
|
-
from woltapi import
|
|
128
|
-
|
|
129
|
-
token = getpass("Wolt access token: ").strip()
|
|
130
|
-
token = token.removeprefix("Bearer ")
|
|
131
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
143
|
+
from woltapi import RefreshTokenCredentials, WoltClient
|
|
132
144
|
|
|
133
145
|
client = WoltClient(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
consumer_headers=headers,
|
|
146
|
+
RefreshTokenCredentials(
|
|
147
|
+
getpass("Wolt refresh token: ").strip(),
|
|
137
148
|
)
|
|
138
149
|
)
|
|
139
150
|
|
|
@@ -157,35 +168,17 @@ else:
|
|
|
157
168
|
print("No restaurants found. Try another search.")
|
|
158
169
|
```
|
|
159
170
|
|
|
160
|
-
Wolt uses different servers for different jobs. `restaurant_headers` supplies
|
|
161
|
-
the login token for searches and saved delivery addresses. `consumer_headers`
|
|
162
|
-
supplies it for menus and order history. The library sends each set of headers
|
|
163
|
-
only to its matching server.
|
|
164
|
-
|
|
165
171
|
**The library does not find your token for you.** Your code passes it into
|
|
166
|
-
`
|
|
167
|
-
the job of your script, not the library.
|
|
168
|
-
|
|
169
|
-
### Automatic token refresh
|
|
172
|
+
`RefreshTokenCredentials`. Reading a file, an environment variable, or a prompt
|
|
173
|
+
is the job of your script, not the library.
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
client secret, or browser cookies need to accompany the request. In your logged-in
|
|
173
|
-
Wolt browser, find `__wrtoken` under **Developer Tools > Application > Cookies**.
|
|
174
|
-
Use its value, without surrounding quotes. This is not the access token from an
|
|
175
|
-
Authorization header or the refresh token used by the Converse support widget.
|
|
176
|
-
Treat it like a password and do not put it in Git or logs.
|
|
177
|
-
|
|
178
|
-
```python
|
|
179
|
-
from getpass import getpass
|
|
175
|
+
### How token refresh works
|
|
180
176
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
client = WoltClient(credentials)
|
|
187
|
-
history = client.get_orders_page()
|
|
188
|
-
```
|
|
177
|
+
Only the **consumer refresh token** is needed to start: no access token,
|
|
178
|
+
password, client secret, or browser cookies need to accompany the request. This
|
|
179
|
+
is the `__wrtoken` value from [Get a token](#get-a-token). It is not the access
|
|
180
|
+
token from an Authorization header or the refresh token used by the Converse
|
|
181
|
+
support widget.
|
|
189
182
|
|
|
190
183
|
The first API call exchanges the refresh token at
|
|
191
184
|
`https://authentication.wolt.com/v1/wauth2/access_token`. Subsequent calls reuse
|
|
@@ -208,8 +201,8 @@ exception propagates but the new tokens remain in memory. Later calls retry the
|
|
|
208
201
|
callback before any further authentication or API request; they remain blocked
|
|
209
202
|
until persistence succeeds. Make your callback safe to repeat with the same token.
|
|
210
203
|
|
|
211
|
-
Optional `restaurant_headers`, `consumer_headers`, and `payment_headers`
|
|
212
|
-
|
|
204
|
+
Optional `restaurant_headers`, `consumer_headers`, and `payment_headers` scope
|
|
205
|
+
extra headers to their respective hosts. `Authorization` is managed by
|
|
213
206
|
`RefreshTokenCredentials`. Its `timeout` controls authentication requests
|
|
214
207
|
separately from `WoltClient`'s API timeout (both default to 10 seconds).
|
|
215
208
|
|
|
@@ -217,8 +210,28 @@ Refresh requests are not retried or redirected, and API requests are never
|
|
|
217
210
|
automatically replayed after a 401, including purchases. An expired or revoked
|
|
218
211
|
refresh token requires a new browser session credential. Authentication failures
|
|
219
212
|
use the existing exceptions, such as `HTTPStatusError` with
|
|
220
|
-
`service == "authentication"`.
|
|
221
|
-
|
|
213
|
+
`service == "authentication"`.
|
|
214
|
+
|
|
215
|
+
### Manually supplied headers
|
|
216
|
+
|
|
217
|
+
If you already hold a short-lived access token and want to supply raw headers
|
|
218
|
+
yourself, use `SessionCredentials` instead:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from woltapi import SessionCredentials, WoltClient
|
|
222
|
+
|
|
223
|
+
headers = {"Authorization": "Bearer <access token>"}
|
|
224
|
+
client = WoltClient(
|
|
225
|
+
SessionCredentials(restaurant_headers=headers, consumer_headers=headers)
|
|
226
|
+
)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Wolt uses different servers for different jobs. `restaurant_headers` supplies
|
|
230
|
+
the login token for searches and saved delivery addresses. `consumer_headers`
|
|
231
|
+
supplies it for menus and order history. The library sends each set of headers
|
|
232
|
+
only to its matching server. With this class, nothing renews the token: after
|
|
233
|
+
about 30 minutes, requests fail with **HTTP 401**. The example scripts no
|
|
234
|
+
longer use this path; prefer `RefreshTokenCredentials`.
|
|
222
235
|
|
|
223
236
|
### Useful methods
|
|
224
237
|
|
|
@@ -229,8 +242,9 @@ automatic renewal is opt-in through this Python API.
|
|
|
229
242
|
| `client.get_venue_static(slug)` | A dictionary of restaurant details. |
|
|
230
243
|
| `client.get_venue_dynamic(slug, latitude, longitude)` | Current opening and delivery information. |
|
|
231
244
|
| `client.get_orders_page()` | A dictionary containing the current page of order history. |
|
|
232
|
-
| `client.list_delivery_targets()` | References to your saved delivery addresses,
|
|
245
|
+
| `client.list_delivery_targets()` | References to your saved delivery addresses, with saved labels and address details. |
|
|
233
246
|
| `client.get_order_status(purchase_id)` | An order's status and some price information. |
|
|
247
|
+
| `derive_checkout_fields(assortment, item)` | The checkout metadata fields for one menu item, derived from the assortment. Raises an error for items in zero or multiple categories. |
|
|
234
248
|
|
|
235
249
|
For example, after creating `client`:
|
|
236
250
|
|
|
@@ -244,7 +258,8 @@ print(f"You have {len(targets)} saved delivery addresses.")
|
|
|
244
258
|
```
|
|
245
259
|
|
|
246
260
|
Responses can contain personal information. Avoid printing entire responses or
|
|
247
|
-
sending them to shared logs. The
|
|
261
|
+
sending them to shared logs. The examples print saved addresses and card labels
|
|
262
|
+
to the local terminal only. The browsing example prints only selected fields.
|
|
248
263
|
|
|
249
264
|
## Can it order food?
|
|
250
265
|
|
|
@@ -254,16 +269,24 @@ To try **checkout without buying anything**, use the new checkout-only example
|
|
|
254
269
|
from an editable source installation:
|
|
255
270
|
|
|
256
271
|
```bash
|
|
257
|
-
python examples/order.py --latitude 60.17 --longitude 24.94 --query pizza
|
|
272
|
+
python examples/order.py --latitude 60.17 --longitude 24.94 --query pizza \
|
|
273
|
+
--token-file ~/.wolt-token
|
|
258
274
|
```
|
|
259
275
|
|
|
260
|
-
Use your own coordinates. It
|
|
261
|
-
guides you through selecting an item and a saved
|
|
262
|
-
before requesting a price. It never submits a
|
|
263
|
-
default and needs a separate confirmation if
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
276
|
+
Use your own coordinates. It reads and saves your refresh token as described in
|
|
277
|
+
[Get a token](#get-a-token), guides you through selecting an item and a saved
|
|
278
|
+
delivery target/card, then asks before requesting a price. It never submits a
|
|
279
|
+
purchase. Basket saving is off by default and needs a separate confirmation if
|
|
280
|
+
enabled.
|
|
281
|
+
|
|
282
|
+
The checkout example derives `category_id`, `category_ids`, and the three
|
|
283
|
+
checkout exclusion flags from the assortment, so those fields do not need a
|
|
284
|
+
browser context file. It only supports items in exactly one category and stops
|
|
285
|
+
rather than guessing for zero or multiple categories. If it stops, you can
|
|
286
|
+
supply the missing values yourself with `--context-file <path>`, a JSON object
|
|
287
|
+
whose `checkout_fields` entries were copied from your own browser's checkout
|
|
288
|
+
request for the same item. It may still stop if other required catalog data is
|
|
289
|
+
missing. Run `python examples/order.py --help` for available options.
|
|
267
290
|
|
|
268
291
|
The library has methods to choose items, save a basket, ask Wolt for a price,
|
|
269
292
|
and submit a purchase. But some required inputs still need to come from your
|
|
@@ -45,14 +45,18 @@ lists some of its menu items. **It will not order anything or change your basket
|
|
|
45
45
|
python examples/browse.py \
|
|
46
46
|
--latitude 60.17 \
|
|
47
47
|
--longitude 24.94 \
|
|
48
|
-
--query "pizza"
|
|
48
|
+
--query "pizza" \
|
|
49
|
+
--token-file ~/.wolt-token
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
Replace the two numbers with your location. The example numbers are in Helsinki.
|
|
52
53
|
Latitude and longitude are the two numbers that identify a place on a map.
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
`--token-file` is required. It names the file that holds your Wolt refresh
|
|
56
|
+
token. On the first run the file does not exist yet, so the script asks for the
|
|
57
|
+
token. Paste it and press Enter. Nothing will appear while you paste; that is
|
|
58
|
+
intentional. The script then saves the token to that file, and later runs read
|
|
59
|
+
it from there without asking. See [Get a token](#get-a-token) below.
|
|
56
60
|
|
|
57
61
|
By default, it shows up to 5 recent orders and 20 menu items from the first
|
|
58
62
|
restaurant in the search results. You can change that:
|
|
@@ -62,6 +66,7 @@ python examples/browse.py \
|
|
|
62
66
|
--latitude 60.17 \
|
|
63
67
|
--longitude 24.94 \
|
|
64
68
|
--query "burger" \
|
|
69
|
+
--token-file ~/.wolt-token \
|
|
65
70
|
--orders 3 \
|
|
66
71
|
--menu-limit 50 \
|
|
67
72
|
--venue-index 2
|
|
@@ -76,46 +81,52 @@ The library keeps the original integer amounts; only the display converts them.
|
|
|
76
81
|
|
|
77
82
|
## Get a token
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
account. Treat it like a password.
|
|
84
|
+
A **refresh token** is a long-lived key that lets the library sign requests
|
|
85
|
+
for your Wolt account. Treat it like a password.
|
|
81
86
|
|
|
82
87
|
1. Sign in to Wolt in your browser.
|
|
83
|
-
2. Open Developer Tools and select
|
|
84
|
-
3.
|
|
85
|
-
4.
|
|
86
|
-
5. Under **Request Headers**, find `authorization: Bearer ...`.
|
|
87
|
-
6. Copy the long token after `Bearer `.
|
|
88
|
+
2. Open Developer Tools and select **Application** (Firefox: **Storage**).
|
|
89
|
+
3. Under **Cookies**, find the cookie named `__wrtoken`.
|
|
90
|
+
4. Copy its value, without surrounding quotes.
|
|
88
91
|
|
|
89
92
|
Do not share the token, put it in Git, or include it in screenshots.
|
|
90
93
|
|
|
91
|
-
The
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
you to paste a token. No credential file is needed.
|
|
94
|
+
The example scripts read the token from the file named by `--token-file`. If
|
|
95
|
+
that file does not exist, or is empty, they ask with a hidden prompt and then
|
|
96
|
+
save what you paste. The file is created readable only by you.
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
Wolt may replace your refresh token over time. The scripts save each
|
|
99
|
+
replacement to the same file, so later runs keep working without another visit
|
|
100
|
+
to the browser. Point every run at the same file.
|
|
101
|
+
|
|
102
|
+
Because of this, the token file holds a live secret and becomes the only copy
|
|
103
|
+
once Wolt has replaced the original. Keep it out of Git and shared folders. Do
|
|
104
|
+
not point two programs that run at the same time at one token file: whichever
|
|
105
|
+
refreshes second will find its own token already replaced.
|
|
106
|
+
|
|
107
|
+
If a run receives **HTTP 401**, the saved token has expired or was revoked. Put
|
|
108
|
+
a current `__wrtoken` value in the token file, or delete the file and let the
|
|
109
|
+
script ask again. The library cannot sign you in.
|
|
110
|
+
|
|
111
|
+
The scripts exchange this refresh token for short-lived access tokens
|
|
112
|
+
automatically; you never handle access tokens yourself. If you want to supply
|
|
113
|
+
raw `Authorization` headers instead, use `SessionCredentials` from Python
|
|
114
|
+
directly, as described under
|
|
115
|
+
[Manually supplied headers](#manually-supplied-headers).
|
|
100
116
|
|
|
101
117
|
## Use it in Python
|
|
102
118
|
|
|
103
|
-
Here is a complete browsing example. It asks for your token and
|
|
104
|
-
searches for pizza and loads the first result's menu:
|
|
119
|
+
Here is a complete browsing example. It asks for your refresh token and
|
|
120
|
+
location, then searches for pizza and loads the first result's menu:
|
|
105
121
|
|
|
106
122
|
```python
|
|
107
123
|
from getpass import getpass
|
|
108
124
|
|
|
109
|
-
from woltapi import
|
|
110
|
-
|
|
111
|
-
token = getpass("Wolt access token: ").strip()
|
|
112
|
-
token = token.removeprefix("Bearer ")
|
|
113
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
125
|
+
from woltapi import RefreshTokenCredentials, WoltClient
|
|
114
126
|
|
|
115
127
|
client = WoltClient(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
consumer_headers=headers,
|
|
128
|
+
RefreshTokenCredentials(
|
|
129
|
+
getpass("Wolt refresh token: ").strip(),
|
|
119
130
|
)
|
|
120
131
|
)
|
|
121
132
|
|
|
@@ -139,35 +150,17 @@ else:
|
|
|
139
150
|
print("No restaurants found. Try another search.")
|
|
140
151
|
```
|
|
141
152
|
|
|
142
|
-
Wolt uses different servers for different jobs. `restaurant_headers` supplies
|
|
143
|
-
the login token for searches and saved delivery addresses. `consumer_headers`
|
|
144
|
-
supplies it for menus and order history. The library sends each set of headers
|
|
145
|
-
only to its matching server.
|
|
146
|
-
|
|
147
153
|
**The library does not find your token for you.** Your code passes it into
|
|
148
|
-
`
|
|
149
|
-
the job of your script, not the library.
|
|
150
|
-
|
|
151
|
-
### Automatic token refresh
|
|
154
|
+
`RefreshTokenCredentials`. Reading a file, an environment variable, or a prompt
|
|
155
|
+
is the job of your script, not the library.
|
|
152
156
|
|
|
153
|
-
|
|
154
|
-
client secret, or browser cookies need to accompany the request. In your logged-in
|
|
155
|
-
Wolt browser, find `__wrtoken` under **Developer Tools > Application > Cookies**.
|
|
156
|
-
Use its value, without surrounding quotes. This is not the access token from an
|
|
157
|
-
Authorization header or the refresh token used by the Converse support widget.
|
|
158
|
-
Treat it like a password and do not put it in Git or logs.
|
|
159
|
-
|
|
160
|
-
```python
|
|
161
|
-
from getpass import getpass
|
|
157
|
+
### How token refresh works
|
|
162
158
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
client = WoltClient(credentials)
|
|
169
|
-
history = client.get_orders_page()
|
|
170
|
-
```
|
|
159
|
+
Only the **consumer refresh token** is needed to start: no access token,
|
|
160
|
+
password, client secret, or browser cookies need to accompany the request. This
|
|
161
|
+
is the `__wrtoken` value from [Get a token](#get-a-token). It is not the access
|
|
162
|
+
token from an Authorization header or the refresh token used by the Converse
|
|
163
|
+
support widget.
|
|
171
164
|
|
|
172
165
|
The first API call exchanges the refresh token at
|
|
173
166
|
`https://authentication.wolt.com/v1/wauth2/access_token`. Subsequent calls reuse
|
|
@@ -190,8 +183,8 @@ exception propagates but the new tokens remain in memory. Later calls retry the
|
|
|
190
183
|
callback before any further authentication or API request; they remain blocked
|
|
191
184
|
until persistence succeeds. Make your callback safe to repeat with the same token.
|
|
192
185
|
|
|
193
|
-
Optional `restaurant_headers`, `consumer_headers`, and `payment_headers`
|
|
194
|
-
|
|
186
|
+
Optional `restaurant_headers`, `consumer_headers`, and `payment_headers` scope
|
|
187
|
+
extra headers to their respective hosts. `Authorization` is managed by
|
|
195
188
|
`RefreshTokenCredentials`. Its `timeout` controls authentication requests
|
|
196
189
|
separately from `WoltClient`'s API timeout (both default to 10 seconds).
|
|
197
190
|
|
|
@@ -199,8 +192,28 @@ Refresh requests are not retried or redirected, and API requests are never
|
|
|
199
192
|
automatically replayed after a 401, including purchases. An expired or revoked
|
|
200
193
|
refresh token requires a new browser session credential. Authentication failures
|
|
201
194
|
use the existing exceptions, such as `HTTPStatusError` with
|
|
202
|
-
`service == "authentication"`.
|
|
203
|
-
|
|
195
|
+
`service == "authentication"`.
|
|
196
|
+
|
|
197
|
+
### Manually supplied headers
|
|
198
|
+
|
|
199
|
+
If you already hold a short-lived access token and want to supply raw headers
|
|
200
|
+
yourself, use `SessionCredentials` instead:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from woltapi import SessionCredentials, WoltClient
|
|
204
|
+
|
|
205
|
+
headers = {"Authorization": "Bearer <access token>"}
|
|
206
|
+
client = WoltClient(
|
|
207
|
+
SessionCredentials(restaurant_headers=headers, consumer_headers=headers)
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Wolt uses different servers for different jobs. `restaurant_headers` supplies
|
|
212
|
+
the login token for searches and saved delivery addresses. `consumer_headers`
|
|
213
|
+
supplies it for menus and order history. The library sends each set of headers
|
|
214
|
+
only to its matching server. With this class, nothing renews the token: after
|
|
215
|
+
about 30 minutes, requests fail with **HTTP 401**. The example scripts no
|
|
216
|
+
longer use this path; prefer `RefreshTokenCredentials`.
|
|
204
217
|
|
|
205
218
|
### Useful methods
|
|
206
219
|
|
|
@@ -211,8 +224,9 @@ automatic renewal is opt-in through this Python API.
|
|
|
211
224
|
| `client.get_venue_static(slug)` | A dictionary of restaurant details. |
|
|
212
225
|
| `client.get_venue_dynamic(slug, latitude, longitude)` | Current opening and delivery information. |
|
|
213
226
|
| `client.get_orders_page()` | A dictionary containing the current page of order history. |
|
|
214
|
-
| `client.list_delivery_targets()` | References to your saved delivery addresses,
|
|
227
|
+
| `client.list_delivery_targets()` | References to your saved delivery addresses, with saved labels and address details. |
|
|
215
228
|
| `client.get_order_status(purchase_id)` | An order's status and some price information. |
|
|
229
|
+
| `derive_checkout_fields(assortment, item)` | The checkout metadata fields for one menu item, derived from the assortment. Raises an error for items in zero or multiple categories. |
|
|
216
230
|
|
|
217
231
|
For example, after creating `client`:
|
|
218
232
|
|
|
@@ -226,7 +240,8 @@ print(f"You have {len(targets)} saved delivery addresses.")
|
|
|
226
240
|
```
|
|
227
241
|
|
|
228
242
|
Responses can contain personal information. Avoid printing entire responses or
|
|
229
|
-
sending them to shared logs. The
|
|
243
|
+
sending them to shared logs. The examples print saved addresses and card labels
|
|
244
|
+
to the local terminal only. The browsing example prints only selected fields.
|
|
230
245
|
|
|
231
246
|
## Can it order food?
|
|
232
247
|
|
|
@@ -236,16 +251,24 @@ To try **checkout without buying anything**, use the new checkout-only example
|
|
|
236
251
|
from an editable source installation:
|
|
237
252
|
|
|
238
253
|
```bash
|
|
239
|
-
python examples/order.py --latitude 60.17 --longitude 24.94 --query pizza
|
|
254
|
+
python examples/order.py --latitude 60.17 --longitude 24.94 --query pizza \
|
|
255
|
+
--token-file ~/.wolt-token
|
|
240
256
|
```
|
|
241
257
|
|
|
242
|
-
Use your own coordinates. It
|
|
243
|
-
guides you through selecting an item and a saved
|
|
244
|
-
before requesting a price. It never submits a
|
|
245
|
-
default and needs a separate confirmation if
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
258
|
+
Use your own coordinates. It reads and saves your refresh token as described in
|
|
259
|
+
[Get a token](#get-a-token), guides you through selecting an item and a saved
|
|
260
|
+
delivery target/card, then asks before requesting a price. It never submits a
|
|
261
|
+
purchase. Basket saving is off by default and needs a separate confirmation if
|
|
262
|
+
enabled.
|
|
263
|
+
|
|
264
|
+
The checkout example derives `category_id`, `category_ids`, and the three
|
|
265
|
+
checkout exclusion flags from the assortment, so those fields do not need a
|
|
266
|
+
browser context file. It only supports items in exactly one category and stops
|
|
267
|
+
rather than guessing for zero or multiple categories. If it stops, you can
|
|
268
|
+
supply the missing values yourself with `--context-file <path>`, a JSON object
|
|
269
|
+
whose `checkout_fields` entries were copied from your own browser's checkout
|
|
270
|
+
request for the same item. It may still stop if other required catalog data is
|
|
271
|
+
missing. Run `python examples/order.py --help` for available options.
|
|
249
272
|
|
|
250
273
|
The library has methods to choose items, save a basket, ask Wolt for a price,
|
|
251
274
|
and submit a purchase. But some required inputs still need to come from your
|
|
@@ -8,8 +8,10 @@ import argparse
|
|
|
8
8
|
import getpass
|
|
9
9
|
import math
|
|
10
10
|
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
11
13
|
|
|
12
|
-
from woltapi import HTTPStatusError,
|
|
14
|
+
from woltapi import HTTPStatusError, RefreshTokenCredentials, WoltApiError, WoltClient
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
def text(value, language="en"):
|
|
@@ -97,10 +99,20 @@ def positive_int(value):
|
|
|
97
99
|
return number
|
|
98
100
|
|
|
99
101
|
|
|
102
|
+
def check_token_file(parser, token_file):
|
|
103
|
+
"""Fail early on an unusable token path, before any secret is typed."""
|
|
104
|
+
if token_file.exists():
|
|
105
|
+
if not token_file.is_file():
|
|
106
|
+
parser.error("--token-file must name a file.")
|
|
107
|
+
elif not token_file.parent.is_dir():
|
|
108
|
+
parser.error("The folder for --token-file does not exist.")
|
|
109
|
+
|
|
110
|
+
|
|
100
111
|
def main():
|
|
101
112
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
102
113
|
parser.add_argument("--latitude", type=float, required=True)
|
|
103
114
|
parser.add_argument("--longitude", type=float, required=True)
|
|
115
|
+
parser.add_argument("--token-file", type=Path, required=True)
|
|
104
116
|
parser.add_argument("--query", default="pizza")
|
|
105
117
|
parser.add_argument("--orders", type=positive_int, default=5)
|
|
106
118
|
parser.add_argument("--menu-limit", type=positive_int, default=20)
|
|
@@ -114,19 +126,26 @@ def main():
|
|
|
114
126
|
and -180 <= args.longitude <= 180
|
|
115
127
|
):
|
|
116
128
|
parser.error("Provide finite latitude [-90, 90] and longitude [-180, 180].")
|
|
129
|
+
check_token_file(parser, args.token_file)
|
|
117
130
|
try:
|
|
118
|
-
|
|
119
|
-
"Authorization": "Bearer " + access_token(),
|
|
120
|
-
"app-language": args.language,
|
|
121
|
-
}
|
|
122
|
-
client = WoltClient(
|
|
123
|
-
SessionCredentials(restaurant_headers=headers, consumer_headers=headers)
|
|
124
|
-
)
|
|
131
|
+
client = WoltClient(refresh_credentials(args.language, args.token_file))
|
|
125
132
|
browse(client, args)
|
|
133
|
+
except TokenFileError:
|
|
134
|
+
print(
|
|
135
|
+
"\nCould not save the refresh token to the token file."
|
|
136
|
+
" Wolt may have replaced it, so the stored value can be dead."
|
|
137
|
+
" Put a current __wrtoken value in the file."
|
|
138
|
+
)
|
|
139
|
+
return 1
|
|
126
140
|
except HTTPStatusError as exc:
|
|
127
|
-
print(
|
|
141
|
+
print(
|
|
142
|
+
f"\nRequest failed: {exc.service} HTTP {exc.status_code}. No retry was sent."
|
|
143
|
+
)
|
|
128
144
|
if exc.status_code == 401:
|
|
129
|
-
print(
|
|
145
|
+
print(
|
|
146
|
+
"The refresh token may be expired or revoked."
|
|
147
|
+
" Put a current __wrtoken value in the token file."
|
|
148
|
+
)
|
|
130
149
|
return 1
|
|
131
150
|
except (EOFError, OSError, UnicodeError, ValueError, WoltApiError) as exc:
|
|
132
151
|
print(f"\nBrowse failed: {type(exc).__name__}. Private error details omitted.")
|
|
@@ -134,16 +153,66 @@ def main():
|
|
|
134
153
|
return 0
|
|
135
154
|
|
|
136
155
|
|
|
137
|
-
|
|
138
|
-
token
|
|
156
|
+
class TokenFileError(OSError):
|
|
157
|
+
"""The refresh token could not be saved, so the stored value may be dead."""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def write_refresh_token(token_file, token):
|
|
161
|
+
"""Atomically persist a Wolt consumer refresh token with private permissions."""
|
|
162
|
+
temporary_file = tempfile.NamedTemporaryFile(
|
|
163
|
+
mode="w",
|
|
164
|
+
encoding="utf-8",
|
|
165
|
+
dir=token_file.parent,
|
|
166
|
+
prefix=f".{token_file.name}.",
|
|
167
|
+
delete=False,
|
|
168
|
+
)
|
|
169
|
+
temporary_path = Path(temporary_file.name)
|
|
170
|
+
replaced = False
|
|
171
|
+
try:
|
|
172
|
+
with temporary_file:
|
|
173
|
+
os.chmod(temporary_path, 0o600)
|
|
174
|
+
temporary_file.write(token)
|
|
175
|
+
os.replace(temporary_path, token_file)
|
|
176
|
+
replaced = True
|
|
177
|
+
except OSError:
|
|
178
|
+
raise TokenFileError from None
|
|
179
|
+
finally:
|
|
180
|
+
# Any failure, including KeyboardInterrupt, must not leave the token
|
|
181
|
+
# behind in a stray temporary file.
|
|
182
|
+
if not replaced:
|
|
183
|
+
try:
|
|
184
|
+
temporary_path.unlink()
|
|
185
|
+
except OSError:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def read_refresh_token(token_file):
|
|
190
|
+
"""Read the Wolt consumer refresh token (the __wrtoken cookie value)."""
|
|
191
|
+
try:
|
|
192
|
+
token = token_file.read_text(encoding="utf-8").strip()
|
|
193
|
+
except FileNotFoundError:
|
|
194
|
+
token = ""
|
|
139
195
|
if not token:
|
|
140
|
-
token = getpass.getpass("Wolt
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
raise ValueError("A nonempty bearer token is required.")
|
|
196
|
+
token = getpass.getpass("Wolt refresh token (hidden): ").strip()
|
|
197
|
+
if not token:
|
|
198
|
+
raise ValueError("A nonempty refresh token is required.")
|
|
199
|
+
write_refresh_token(token_file, token)
|
|
145
200
|
return token
|
|
146
201
|
|
|
147
202
|
|
|
203
|
+
def refresh_credentials(language, token_file):
|
|
204
|
+
"""Build auto-refreshing credentials from a token file or hidden prompt."""
|
|
205
|
+
token = read_refresh_token(token_file)
|
|
206
|
+
|
|
207
|
+
headers = {"app-language": language}
|
|
208
|
+
return RefreshTokenCredentials(
|
|
209
|
+
token,
|
|
210
|
+
on_refresh=lambda current_token: write_refresh_token(token_file, current_token),
|
|
211
|
+
restaurant_headers=headers,
|
|
212
|
+
consumer_headers=headers,
|
|
213
|
+
payment_headers=headers,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
148
217
|
if __name__ == "__main__":
|
|
149
218
|
raise SystemExit(main())
|