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.
Files changed (34) hide show
  1. {woltapi-0.1.0/src/woltapi.egg-info → woltapi-0.2.0}/PKG-INFO +92 -69
  2. {woltapi-0.1.0 → woltapi-0.2.0}/README.md +91 -68
  3. {woltapi-0.1.0 → woltapi-0.2.0}/examples/browse.py +86 -17
  4. {woltapi-0.1.0 → woltapi-0.2.0}/examples/order.py +65 -41
  5. {woltapi-0.1.0 → woltapi-0.2.0}/pyproject.toml +1 -1
  6. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/__init__.py +2 -0
  7. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/client.py +16 -2
  8. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/models.py +10 -3
  9. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/selection.py +57 -0
  10. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/transport.py +6 -1
  11. {woltapi-0.1.0 → woltapi-0.2.0/src/woltapi.egg-info}/PKG-INFO +92 -69
  12. woltapi-0.2.0/tests/test_browse.py +294 -0
  13. {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_client.py +83 -12
  14. {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_order_example.py +209 -29
  15. {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_selection.py +55 -0
  16. woltapi-0.1.0/tests/test_browse.py +0 -123
  17. {woltapi-0.1.0 → woltapi-0.2.0}/LICENSE +0 -0
  18. {woltapi-0.1.0 → woltapi-0.2.0}/MANIFEST.in +0 -0
  19. {woltapi-0.1.0 → woltapi-0.2.0}/RELEASING.md +0 -0
  20. {woltapi-0.1.0 → woltapi-0.2.0}/examples/check_session.py +0 -0
  21. {woltapi-0.1.0 → woltapi-0.2.0}/setup.cfg +0 -0
  22. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/auth.py +0 -0
  23. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/credentials.py +0 -0
  24. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/errors.py +0 -0
  25. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/purchase.py +0 -0
  26. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi/services.py +0 -0
  27. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi.egg-info/SOURCES.txt +0 -0
  28. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi.egg-info/dependency_links.txt +0 -0
  29. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi.egg-info/requires.txt +0 -0
  30. {woltapi-0.1.0 → woltapi-0.2.0}/src/woltapi.egg-info/top_level.txt +0 -0
  31. {woltapi-0.1.0 → woltapi-0.2.0}/tests/__init__.py +0 -0
  32. {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_auth.py +0 -0
  33. {woltapi-0.1.0 → woltapi-0.2.0}/tests/test_check_session.py +0 -0
  34. {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.1.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
- The script asks for your Wolt access token. Paste it and press Enter. Nothing
73
- will appear while you paste; that is intentional.
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
- An **access token** is a temporary key that lets the library use your Wolt
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 the **Network** tab.
102
- 3. Open your order history in Wolt.
103
- 4. Select a successful request to `consumer-api.wolt.com`.
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 browsing script can also read the token from an environment variable named
110
- `WOLT_ACCESS_TOKEN`. An environment variable is a setting passed to a program
111
- when it starts. If that variable is set, the script uses it instead of asking
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
- Access tokens expire. With manually supplied headers, get a current access token
115
- if you receive **HTTP 401**, and update `WOLT_ACCESS_TOKEN` if you use it. For
116
- automatic renewal in Python, use a consumer refresh token as described below.
117
- The library cannot sign you in.
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 location, then
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 SessionCredentials, WoltClient
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
- SessionCredentials(
135
- restaurant_headers=headers,
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
- `SessionCredentials`. Reading an environment variable or asking for a token is
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
- Only a **consumer refresh token** is needed to start: no access token, password,
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
- from woltapi import RefreshTokenCredentials, WoltClient
182
-
183
- credentials = RefreshTokenCredentials(
184
- getpass("Wolt consumer refresh token: ").strip(),
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` still
212
- scope extra headers to their respective hosts. `Authorization` is managed by
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"`. The example scripts still accept access tokens;
221
- automatic renewal is opt-in through this Python API.
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, without printing the 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 browsing example prints only selected fields.
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 prompts for your token (or uses `WOLT_ACCESS_TOKEN`),
261
- guides you through selecting an item and a saved delivery target/card, then asks
262
- before requesting a price. It never submits a purchase. Basket saving is off by
263
- default and needs a separate confirmation if enabled.
264
-
265
- This is still a test tool: it may stop if the catalog lacks required checkout
266
- fields. Run `python examples/order.py --help` for available options.
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
- The script asks for your Wolt access token. Paste it and press Enter. Nothing
55
- will appear while you paste; that is intentional.
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
- An **access token** is a temporary key that lets the library use your Wolt
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 the **Network** tab.
84
- 3. Open your order history in Wolt.
85
- 4. Select a successful request to `consumer-api.wolt.com`.
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 browsing script can also read the token from an environment variable named
92
- `WOLT_ACCESS_TOKEN`. An environment variable is a setting passed to a program
93
- when it starts. If that variable is set, the script uses it instead of asking
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
- Access tokens expire. With manually supplied headers, get a current access token
97
- if you receive **HTTP 401**, and update `WOLT_ACCESS_TOKEN` if you use it. For
98
- automatic renewal in Python, use a consumer refresh token as described below.
99
- The library cannot sign you in.
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 location, then
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 SessionCredentials, WoltClient
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
- SessionCredentials(
117
- restaurant_headers=headers,
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
- `SessionCredentials`. Reading an environment variable or asking for a token is
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
- Only a **consumer refresh token** is needed to start: no access token, password,
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
- from woltapi import RefreshTokenCredentials, WoltClient
164
-
165
- credentials = RefreshTokenCredentials(
166
- getpass("Wolt consumer refresh token: ").strip(),
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` still
194
- scope extra headers to their respective hosts. `Authorization` is managed by
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"`. The example scripts still accept access tokens;
203
- automatic renewal is opt-in through this Python API.
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, without printing the 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 browsing example prints only selected fields.
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 prompts for your token (or uses `WOLT_ACCESS_TOKEN`),
243
- guides you through selecting an item and a saved delivery target/card, then asks
244
- before requesting a price. It never submits a purchase. Basket saving is off by
245
- default and needs a separate confirmation if enabled.
246
-
247
- This is still a test tool: it may stop if the catalog lacks required checkout
248
- fields. Run `python examples/order.py --help` for available options.
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, SessionCredentials, WoltApiError, WoltClient
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
- headers = {
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(f"\nRequest failed: HTTP {exc.status_code}. No retry was sent.")
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("Supply a current access token from a successful browser request.")
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
- def access_token():
138
- token = os.environ.get("WOLT_ACCESS_TOKEN", "").strip()
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 access token (hidden): ").strip()
141
- if token.lower().startswith("bearer "):
142
- token = token[7:].strip()
143
- if not token or not token.isascii() or any(c.isspace() for c in token):
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())