woltapi 0.1.0__tar.gz → 0.3.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 (36) hide show
  1. {woltapi-0.1.0/src/woltapi.egg-info → woltapi-0.3.0}/PKG-INFO +139 -70
  2. {woltapi-0.1.0 → woltapi-0.3.0}/README.md +138 -69
  3. {woltapi-0.1.0 → woltapi-0.3.0}/examples/browse.py +86 -17
  4. {woltapi-0.1.0 → woltapi-0.3.0}/examples/order.py +86 -102
  5. {woltapi-0.1.0 → woltapi-0.3.0}/pyproject.toml +1 -1
  6. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi/__init__.py +4 -0
  7. woltapi-0.3.0/src/woltapi/basket.py +297 -0
  8. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi/client.py +67 -4
  9. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi/models.py +10 -3
  10. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi/selection.py +80 -14
  11. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi/transport.py +6 -1
  12. {woltapi-0.1.0 → woltapi-0.3.0/src/woltapi.egg-info}/PKG-INFO +139 -70
  13. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi.egg-info/SOURCES.txt +2 -0
  14. woltapi-0.3.0/tests/test_basket.py +229 -0
  15. woltapi-0.3.0/tests/test_browse.py +294 -0
  16. {woltapi-0.1.0 → woltapi-0.3.0}/tests/test_client.py +148 -12
  17. {woltapi-0.1.0 → woltapi-0.3.0}/tests/test_order_example.py +211 -47
  18. {woltapi-0.1.0 → woltapi-0.3.0}/tests/test_selection.py +148 -0
  19. woltapi-0.1.0/tests/test_browse.py +0 -123
  20. {woltapi-0.1.0 → woltapi-0.3.0}/LICENSE +0 -0
  21. {woltapi-0.1.0 → woltapi-0.3.0}/MANIFEST.in +0 -0
  22. {woltapi-0.1.0 → woltapi-0.3.0}/RELEASING.md +0 -0
  23. {woltapi-0.1.0 → woltapi-0.3.0}/examples/check_session.py +0 -0
  24. {woltapi-0.1.0 → woltapi-0.3.0}/setup.cfg +0 -0
  25. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi/auth.py +0 -0
  26. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi/credentials.py +0 -0
  27. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi/errors.py +0 -0
  28. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi/purchase.py +0 -0
  29. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi/services.py +0 -0
  30. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi.egg-info/dependency_links.txt +0 -0
  31. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi.egg-info/requires.txt +0 -0
  32. {woltapi-0.1.0 → woltapi-0.3.0}/src/woltapi.egg-info/top_level.txt +0 -0
  33. {woltapi-0.1.0 → woltapi-0.3.0}/tests/__init__.py +0 -0
  34. {woltapi-0.1.0 → woltapi-0.3.0}/tests/test_auth.py +0 -0
  35. {woltapi-0.1.0 → woltapi-0.3.0}/tests/test_check_session.py +0 -0
  36. {woltapi-0.1.0 → woltapi-0.3.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.3.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.
175
+ ### How token refresh works
177
176
 
178
- ```python
179
- from getpass import getpass
180
-
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,55 @@ 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.
263
+
264
+ ### Baskets
265
+
266
+ A basket is your list of chosen items at one restaurant. The `Basket` class
267
+ builds that list locally, with no network requests. It reads names, option
268
+ prices, and totals from the restaurant's menu, so you do not type any prices:
269
+
270
+ ```python
271
+ from woltapi import Basket, OptionSelection, OptionValueSelection
272
+
273
+ menu = client.get_assortment(restaurant.slug)
274
+ basket = Basket(menu, "en")
275
+
276
+ basket.add_item("<item id>", count=2)
277
+ basket.add_item(
278
+ "<other item id>",
279
+ options=[
280
+ OptionSelection(
281
+ "<item option configuration id>", [OptionValueSelection("<value id>", 1)]
282
+ ),
283
+ ],
284
+ )
285
+ basket.set_count("<item id>", 1)
286
+ basket.set_options("<item id>", [])
287
+ basket.remove_item("<other item id>")
288
+
289
+ items = basket.item_selections()
290
+ ```
291
+
292
+ Item, option, and value IDs come from the assortment dictionary. For
293
+ `OptionSelection`, use the configuration ID from the catalog item's own
294
+ `options` list, not the root option ID from the assortment's top-level
295
+ `options`. Pass the result of `item_selections()` to
296
+ `client.create_selection(...)`, which validates the whole selection against the
297
+ current menu.
298
+
299
+ Wolt also stores one basket per restaurant on its servers. These are the
300
+ baskets you see in the Wolt app. The library can read and replace them:
301
+
302
+ | Call | What it does |
303
+ | --- | --- |
304
+ | `client.save_basket(selection)` | Replaces the server basket for that restaurant with your selection. This does not place an order. |
305
+ | `client.get_basket_count()` | The number of baskets stored on the server. |
306
+ | `client.get_venue_basket(venue_id)` | The server basket for one restaurant, or `None` if there is none. |
307
+ | `client.get_baskets_page(latitude, longitude)` | The full baskets page as a dictionary. It can contain personal data; do not log it raw. |
308
+
309
+ Deleting a server basket is not supported.
248
310
 
249
311
  ## Can it order food?
250
312
 
@@ -254,20 +316,27 @@ To try **checkout without buying anything**, use the new checkout-only example
254
316
  from an editable source installation:
255
317
 
256
318
  ```bash
257
- python examples/order.py --latitude 60.17 --longitude 24.94 --query pizza
319
+ python examples/order.py --latitude 60.17 --longitude 24.94 --query pizza \
320
+ --token-file ~/.wolt-token
258
321
  ```
259
322
 
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
323
+ Use your own coordinates. It reads and saves your refresh token as described in
324
+ [Get a token](#get-a-token), guides you through selecting one or more items with
325
+ their options and a saved delivery target/card, then asks before requesting a
326
+ price. Line totals come from the menu through the local basket builder; you do
327
+ not type any amounts. It never submits a purchase. Basket saving is off by
263
328
  default and needs a separate confirmation if enabled.
264
329
 
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.
330
+ The basket builder derives `category_id`, `category_ids`, and the three checkout
331
+ exclusion flags from the assortment. It only supports items in exactly one
332
+ category and stops rather than guessing for zero or multiple categories. It may
333
+ also stop if required catalog data is missing. Run
334
+ `python examples/order.py --help` for available options.
267
335
 
268
- The library has methods to choose items, save a basket, ask Wolt for a price,
269
- and submit a purchase. But some required inputs still need to come from your
270
- own code, including browser/device information and detailed item data.
336
+ The library has methods to build and manage a basket, save it, ask Wolt for a
337
+ price, and submit a purchase. But some purchase inputs still need to come from
338
+ your own code, including browser/device information and a few purchase-only
339
+ item fields.
271
340
 
272
341
  Order-history and saved-address reads have worked in live checks. **Payment and
273
342
  purchase handling have not been verified with a real order.** The purchase code
@@ -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.
157
+ ### How token refresh works
159
158
 
160
- ```python
161
- from getpass import getpass
162
-
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,55 @@ 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.
245
+
246
+ ### Baskets
247
+
248
+ A basket is your list of chosen items at one restaurant. The `Basket` class
249
+ builds that list locally, with no network requests. It reads names, option
250
+ prices, and totals from the restaurant's menu, so you do not type any prices:
251
+
252
+ ```python
253
+ from woltapi import Basket, OptionSelection, OptionValueSelection
254
+
255
+ menu = client.get_assortment(restaurant.slug)
256
+ basket = Basket(menu, "en")
257
+
258
+ basket.add_item("<item id>", count=2)
259
+ basket.add_item(
260
+ "<other item id>",
261
+ options=[
262
+ OptionSelection(
263
+ "<item option configuration id>", [OptionValueSelection("<value id>", 1)]
264
+ ),
265
+ ],
266
+ )
267
+ basket.set_count("<item id>", 1)
268
+ basket.set_options("<item id>", [])
269
+ basket.remove_item("<other item id>")
270
+
271
+ items = basket.item_selections()
272
+ ```
273
+
274
+ Item, option, and value IDs come from the assortment dictionary. For
275
+ `OptionSelection`, use the configuration ID from the catalog item's own
276
+ `options` list, not the root option ID from the assortment's top-level
277
+ `options`. Pass the result of `item_selections()` to
278
+ `client.create_selection(...)`, which validates the whole selection against the
279
+ current menu.
280
+
281
+ Wolt also stores one basket per restaurant on its servers. These are the
282
+ baskets you see in the Wolt app. The library can read and replace them:
283
+
284
+ | Call | What it does |
285
+ | --- | --- |
286
+ | `client.save_basket(selection)` | Replaces the server basket for that restaurant with your selection. This does not place an order. |
287
+ | `client.get_basket_count()` | The number of baskets stored on the server. |
288
+ | `client.get_venue_basket(venue_id)` | The server basket for one restaurant, or `None` if there is none. |
289
+ | `client.get_baskets_page(latitude, longitude)` | The full baskets page as a dictionary. It can contain personal data; do not log it raw. |
290
+
291
+ Deleting a server basket is not supported.
230
292
 
231
293
  ## Can it order food?
232
294
 
@@ -236,20 +298,27 @@ To try **checkout without buying anything**, use the new checkout-only example
236
298
  from an editable source installation:
237
299
 
238
300
  ```bash
239
- python examples/order.py --latitude 60.17 --longitude 24.94 --query pizza
301
+ python examples/order.py --latitude 60.17 --longitude 24.94 --query pizza \
302
+ --token-file ~/.wolt-token
240
303
  ```
241
304
 
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
305
+ Use your own coordinates. It reads and saves your refresh token as described in
306
+ [Get a token](#get-a-token), guides you through selecting one or more items with
307
+ their options and a saved delivery target/card, then asks before requesting a
308
+ price. Line totals come from the menu through the local basket builder; you do
309
+ not type any amounts. It never submits a purchase. Basket saving is off by
245
310
  default and needs a separate confirmation if enabled.
246
311
 
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.
312
+ The basket builder derives `category_id`, `category_ids`, and the three checkout
313
+ exclusion flags from the assortment. It only supports items in exactly one
314
+ category and stops rather than guessing for zero or multiple categories. It may
315
+ also stop if required catalog data is missing. Run
316
+ `python examples/order.py --help` for available options.
249
317
 
250
- The library has methods to choose items, save a basket, ask Wolt for a price,
251
- and submit a purchase. But some required inputs still need to come from your
252
- own code, including browser/device information and detailed item data.
318
+ The library has methods to build and manage a basket, save it, ask Wolt for a
319
+ price, and submit a purchase. But some purchase inputs still need to come from
320
+ your own code, including browser/device information and a few purchase-only
321
+ item fields.
253
322
 
254
323
  Order-history and saved-address reads have worked in live checks. **Payment and
255
324
  purchase handling have not been verified with a real order.** The purchase code