woltapi 0.0.1__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 (38) hide show
  1. {woltapi-0.0.1 → woltapi-0.2.0}/MANIFEST.in +1 -1
  2. woltapi-0.2.0/PKG-INFO +327 -0
  3. woltapi-0.2.0/README.md +309 -0
  4. woltapi-0.2.0/RELEASING.md +91 -0
  5. {woltapi-0.0.1 → woltapi-0.2.0}/examples/browse.py +86 -17
  6. woltapi-0.2.0/examples/order.py +419 -0
  7. {woltapi-0.0.1 → woltapi-0.2.0}/pyproject.toml +1 -1
  8. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi/__init__.py +4 -0
  9. woltapi-0.2.0/src/woltapi/auth.py +201 -0
  10. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi/client.py +21 -6
  11. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi/models.py +10 -3
  12. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi/selection.py +92 -8
  13. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi/transport.py +6 -1
  14. woltapi-0.2.0/src/woltapi.egg-info/PKG-INFO +327 -0
  15. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi.egg-info/SOURCES.txt +5 -1
  16. woltapi-0.2.0/tests/test_auth.py +553 -0
  17. woltapi-0.2.0/tests/test_browse.py +294 -0
  18. {woltapi-0.0.1 → woltapi-0.2.0}/tests/test_client.py +83 -12
  19. woltapi-0.2.0/tests/test_order_example.py +481 -0
  20. {woltapi-0.0.1 → woltapi-0.2.0}/tests/test_selection.py +55 -0
  21. woltapi-0.0.1/PKG-INFO +0 -215
  22. woltapi-0.0.1/README.md +0 -197
  23. woltapi-0.0.1/WOLT_API.md +0 -1040
  24. woltapi-0.0.1/src/woltapi.egg-info/PKG-INFO +0 -215
  25. woltapi-0.0.1/tests/test_browse.py +0 -123
  26. {woltapi-0.0.1 → woltapi-0.2.0}/LICENSE +0 -0
  27. {woltapi-0.0.1 → woltapi-0.2.0}/examples/check_session.py +0 -0
  28. {woltapi-0.0.1 → woltapi-0.2.0}/setup.cfg +0 -0
  29. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi/credentials.py +0 -0
  30. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi/errors.py +0 -0
  31. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi/purchase.py +0 -0
  32. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi/services.py +0 -0
  33. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi.egg-info/dependency_links.txt +0 -0
  34. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi.egg-info/requires.txt +0 -0
  35. {woltapi-0.0.1 → woltapi-0.2.0}/src/woltapi.egg-info/top_level.txt +0 -0
  36. {woltapi-0.0.1 → woltapi-0.2.0}/tests/__init__.py +0 -0
  37. {woltapi-0.0.1 → woltapi-0.2.0}/tests/test_check_session.py +0 -0
  38. {woltapi-0.0.1 → woltapi-0.2.0}/tests/test_purchase.py +0 -0
@@ -1,7 +1,7 @@
1
1
  global-exclude *.har
2
2
  global-exclude .env
3
3
  global-exclude .env.* *.sqlite3 *.sqlite3-*
4
- include LICENSE README.md WOLT_API.md
4
+ include LICENSE README.md RELEASING.md
5
5
  recursive-include examples *.py
6
6
  recursive-include tests *.py
7
7
  prune .tickets
woltapi-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,327 @@
1
+ Metadata-Version: 2.4
2
+ Name: woltapi
3
+ Version: 0.2.0
4
+ Summary: A small synchronous client for observed Wolt ordering flows.
5
+ License-Expression: AGPL-3.0-only
6
+ Project-URL: Repository, https://github.com/skorokithakis/woltapi
7
+ Project-URL: Issues, https://github.com/skorokithakis/woltapi/issues
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7; extra == "test"
17
+ Dynamic: license-file
18
+
19
+ # woltapi
20
+
21
+ A Python library for looking up restaurants, reading menus, and viewing your
22
+ Wolt orders.
23
+
24
+ This is an **unofficial** project. It also has code for placing orders, but that
25
+ part is unfinished and has not been tested with a real purchase.
26
+
27
+ ## Install
28
+
29
+ You need Python 3.10 or newer. Install the library from PyPI:
30
+
31
+ ```bash
32
+ pip install woltapi
33
+ ```
34
+
35
+ You can then use `from woltapi import WoltClient, SessionCredentials` in your
36
+ Python code. See [Use it in Python](#use-it-in-python) below.
37
+
38
+ ### Install from source
39
+
40
+ To run the example scripts or work on the library, clone this repository and
41
+ install an editable copy:
42
+
43
+ ```bash
44
+ git clone https://github.com/skorokithakis/woltapi.git
45
+ cd woltapi
46
+ python3 -m venv .venv
47
+ source .venv/bin/activate
48
+ python -m pip install -e .
49
+ ```
50
+
51
+ These commands create a separate Python environment and install the library in
52
+ it. On Windows, activate it with `.venv\Scripts\activate` instead of `source`.
53
+
54
+ ## Try it
55
+
56
+ Run these commands from the project folder after installing from source above.
57
+ The `examples/` scripts are not installed by `pip install woltapi`.
58
+
59
+ The browsing example shows your recent orders, searches for a restaurant, and
60
+ lists some of its menu items. **It will not order anything or change your basket.**
61
+
62
+ ```bash
63
+ python examples/browse.py \
64
+ --latitude 60.17 \
65
+ --longitude 24.94 \
66
+ --query "pizza" \
67
+ --token-file ~/.wolt-token
68
+ ```
69
+
70
+ Replace the two numbers with your location. The example numbers are in Helsinki.
71
+ Latitude and longitude are the two numbers that identify a place on a map.
72
+
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.
78
+
79
+ By default, it shows up to 5 recent orders and 20 menu items from the first
80
+ restaurant in the search results. You can change that:
81
+
82
+ ```bash
83
+ python examples/browse.py \
84
+ --latitude 60.17 \
85
+ --longitude 24.94 \
86
+ --query "burger" \
87
+ --token-file ~/.wolt-token \
88
+ --orders 3 \
89
+ --menu-limit 50 \
90
+ --venue-index 2
91
+ ```
92
+
93
+ `--venue-index 2` chooses the second search result. You can also search for a
94
+ restaurant by name. Use `--help` to see all the options.
95
+
96
+ Menu prices are converted from cents: for example, `1234` is shown as `12.34 EUR`.
97
+ These are item prices, not a final order total including delivery and other fees.
98
+ The library keeps the original integer amounts; only the display converts them.
99
+
100
+ ## Get a token
101
+
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.
104
+
105
+ 1. Sign in to Wolt in your browser.
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.
109
+
110
+ Do not share the token, put it in Git, or include it in screenshots.
111
+
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.
115
+
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).
134
+
135
+ ## Use it in Python
136
+
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:
139
+
140
+ ```python
141
+ from getpass import getpass
142
+
143
+ from woltapi import RefreshTokenCredentials, WoltClient
144
+
145
+ client = WoltClient(
146
+ RefreshTokenCredentials(
147
+ getpass("Wolt refresh token: ").strip(),
148
+ )
149
+ )
150
+
151
+ latitude = float(input("Latitude: "))
152
+ longitude = float(input("Longitude: "))
153
+
154
+ restaurants = client.search_venues(
155
+ "pizza",
156
+ latitude=latitude,
157
+ longitude=longitude,
158
+ )
159
+
160
+ for restaurant in restaurants:
161
+ print(restaurant.title or restaurant.slug)
162
+
163
+ if restaurants:
164
+ restaurant = restaurants[0]
165
+ menu = client.get_assortment(restaurant.slug)
166
+ print(f"Found {len(menu.get('items', []))} menu items.")
167
+ else:
168
+ print("No restaurants found. Try another search.")
169
+ ```
170
+
171
+ **The library does not find your token for you.** Your code passes it into
172
+ `RefreshTokenCredentials`. Reading a file, an environment variable, or a prompt
173
+ is the job of your script, not the library.
174
+
175
+ ### How token refresh works
176
+
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.
182
+
183
+ The first API call exchanges the refresh token at
184
+ `https://authentication.wolt.com/v1/wauth2/access_token`. Subsequent calls reuse
185
+ the access token until shortly before its server-reported expiry (30 minutes in
186
+ the verified response). The access token is supplied to the restaurant, consumer,
187
+ and payment hosts; the refresh token is sent only to the authentication host.
188
+ You can also call `credentials.refresh()` to exchange it explicitly.
189
+
190
+ Wolt may return a replacement refresh token. `credentials.refresh_token` always
191
+ holds the latest successfully validated value. For applications that run across
192
+ restarts, pass `on_refresh=save_refresh_token`, where your function accepts that
193
+ string and saves it to your secret store after each exchange. The library does
194
+ not read or write credential files. Without persistence, a rotated token may be
195
+ lost when your process exits. Do not share one refresh token across independent
196
+ processes that might refresh it concurrently.
197
+
198
+ The callback runs before the API request proceeds and must not call back into
199
+ `credentials.refresh()` or `credentials.headers_for()`. If it raises, the
200
+ exception propagates but the new tokens remain in memory. Later calls retry the
201
+ callback before any further authentication or API request; they remain blocked
202
+ until persistence succeeds. Make your callback safe to repeat with the same token.
203
+
204
+ Optional `restaurant_headers`, `consumer_headers`, and `payment_headers` scope
205
+ extra headers to their respective hosts. `Authorization` is managed by
206
+ `RefreshTokenCredentials`. Its `timeout` controls authentication requests
207
+ separately from `WoltClient`'s API timeout (both default to 10 seconds).
208
+
209
+ Refresh requests are not retried or redirected, and API requests are never
210
+ automatically replayed after a 401, including purchases. An expired or revoked
211
+ refresh token requires a new browser session credential. Authentication failures
212
+ use the existing exceptions, such as `HTTPStatusError` with
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`.
235
+
236
+ ### Useful methods
237
+
238
+ | Call | What you get |
239
+ | --- | --- |
240
+ | `client.search_venues("pizza", latitude, longitude)` | Restaurant results with names, IDs, and slugs. A slug is the name used in a restaurant's URL. |
241
+ | `client.get_assortment(slug)` | A dictionary containing menu items and their options. |
242
+ | `client.get_venue_static(slug)` | A dictionary of restaurant details. |
243
+ | `client.get_venue_dynamic(slug, latitude, longitude)` | Current opening and delivery information. |
244
+ | `client.get_orders_page()` | A dictionary containing the current page of order history. |
245
+ | `client.list_delivery_targets()` | References to your saved delivery addresses, with saved labels and address details. |
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. |
248
+
249
+ For example, after creating `client`:
250
+
251
+ ```python
252
+ history = client.get_orders_page()
253
+ orders = history.get("orders", [])
254
+ print(f"This page contains {len(orders)} orders.")
255
+
256
+ targets = client.list_delivery_targets()
257
+ print(f"You have {len(targets)} saved delivery addresses.")
258
+ ```
259
+
260
+ Responses can contain personal information. Avoid printing entire responses or
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
+ ## Can it order food?
265
+
266
+ Not as a simple, ready-to-use feature yet. There is no `order("pizza")` method.
267
+
268
+ To try **checkout without buying anything**, use the new checkout-only example
269
+ from an editable source installation:
270
+
271
+ ```bash
272
+ python examples/order.py --latitude 60.17 --longitude 24.94 --query pizza \
273
+ --token-file ~/.wolt-token
274
+ ```
275
+
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.
290
+
291
+ The library has methods to choose items, save a basket, ask Wolt for a price,
292
+ and submit a purchase. But some required inputs still need to come from your
293
+ own code, including browser/device information and detailed item data.
294
+
295
+ Order-history and saved-address reads have worked in live checks. **Payment and
296
+ purchase handling have not been verified with a real order.** The purchase code
297
+ only targets one restaurant, immediate home delivery, and one saved card.
298
+
299
+ `submit_prepared_order()` is the call that can place an order and charge you.
300
+ Do not call it unless you intend to buy the exact order you have reviewed. If it
301
+ times out or raises `OrderOutcomeUnknown`, **do not send it again**: Wolt may
302
+ already have received it. Check the order in Wolt instead.
303
+
304
+ Login, adding cards, payment verification screens, scheduled
305
+ orders, cancellation, and refunds are not supported.
306
+
307
+ ## Run the tests
308
+
309
+ With your Python environment active:
310
+
311
+ ```bash
312
+ python -m pip install -e ".[test]"
313
+ python -m pytest
314
+ ```
315
+
316
+ The tests use made-up responses. They do not contact Wolt, need your token, or
317
+ place orders.
318
+
319
+ ## Releases
320
+
321
+ Maintainers: see [RELEASING.md](RELEASING.md) for tests, release PRs, and automatic
322
+ PyPI publishing.
323
+
324
+ ## License
325
+
326
+ Licensed under the GNU Affero General Public License, version 3
327
+ (`AGPL-3.0-only`). See [LICENSE](LICENSE) for the full terms.
@@ -0,0 +1,309 @@
1
+ # woltapi
2
+
3
+ A Python library for looking up restaurants, reading menus, and viewing your
4
+ Wolt orders.
5
+
6
+ This is an **unofficial** project. It also has code for placing orders, but that
7
+ part is unfinished and has not been tested with a real purchase.
8
+
9
+ ## Install
10
+
11
+ You need Python 3.10 or newer. Install the library from PyPI:
12
+
13
+ ```bash
14
+ pip install woltapi
15
+ ```
16
+
17
+ You can then use `from woltapi import WoltClient, SessionCredentials` in your
18
+ Python code. See [Use it in Python](#use-it-in-python) below.
19
+
20
+ ### Install from source
21
+
22
+ To run the example scripts or work on the library, clone this repository and
23
+ install an editable copy:
24
+
25
+ ```bash
26
+ git clone https://github.com/skorokithakis/woltapi.git
27
+ cd woltapi
28
+ python3 -m venv .venv
29
+ source .venv/bin/activate
30
+ python -m pip install -e .
31
+ ```
32
+
33
+ These commands create a separate Python environment and install the library in
34
+ it. On Windows, activate it with `.venv\Scripts\activate` instead of `source`.
35
+
36
+ ## Try it
37
+
38
+ Run these commands from the project folder after installing from source above.
39
+ The `examples/` scripts are not installed by `pip install woltapi`.
40
+
41
+ The browsing example shows your recent orders, searches for a restaurant, and
42
+ lists some of its menu items. **It will not order anything or change your basket.**
43
+
44
+ ```bash
45
+ python examples/browse.py \
46
+ --latitude 60.17 \
47
+ --longitude 24.94 \
48
+ --query "pizza" \
49
+ --token-file ~/.wolt-token
50
+ ```
51
+
52
+ Replace the two numbers with your location. The example numbers are in Helsinki.
53
+ Latitude and longitude are the two numbers that identify a place on a map.
54
+
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.
60
+
61
+ By default, it shows up to 5 recent orders and 20 menu items from the first
62
+ restaurant in the search results. You can change that:
63
+
64
+ ```bash
65
+ python examples/browse.py \
66
+ --latitude 60.17 \
67
+ --longitude 24.94 \
68
+ --query "burger" \
69
+ --token-file ~/.wolt-token \
70
+ --orders 3 \
71
+ --menu-limit 50 \
72
+ --venue-index 2
73
+ ```
74
+
75
+ `--venue-index 2` chooses the second search result. You can also search for a
76
+ restaurant by name. Use `--help` to see all the options.
77
+
78
+ Menu prices are converted from cents: for example, `1234` is shown as `12.34 EUR`.
79
+ These are item prices, not a final order total including delivery and other fees.
80
+ The library keeps the original integer amounts; only the display converts them.
81
+
82
+ ## Get a token
83
+
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.
86
+
87
+ 1. Sign in to Wolt in your browser.
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.
91
+
92
+ Do not share the token, put it in Git, or include it in screenshots.
93
+
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.
97
+
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).
116
+
117
+ ## Use it in Python
118
+
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:
121
+
122
+ ```python
123
+ from getpass import getpass
124
+
125
+ from woltapi import RefreshTokenCredentials, WoltClient
126
+
127
+ client = WoltClient(
128
+ RefreshTokenCredentials(
129
+ getpass("Wolt refresh token: ").strip(),
130
+ )
131
+ )
132
+
133
+ latitude = float(input("Latitude: "))
134
+ longitude = float(input("Longitude: "))
135
+
136
+ restaurants = client.search_venues(
137
+ "pizza",
138
+ latitude=latitude,
139
+ longitude=longitude,
140
+ )
141
+
142
+ for restaurant in restaurants:
143
+ print(restaurant.title or restaurant.slug)
144
+
145
+ if restaurants:
146
+ restaurant = restaurants[0]
147
+ menu = client.get_assortment(restaurant.slug)
148
+ print(f"Found {len(menu.get('items', []))} menu items.")
149
+ else:
150
+ print("No restaurants found. Try another search.")
151
+ ```
152
+
153
+ **The library does not find your token for you.** Your code passes it into
154
+ `RefreshTokenCredentials`. Reading a file, an environment variable, or a prompt
155
+ is the job of your script, not the library.
156
+
157
+ ### How token refresh works
158
+
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.
164
+
165
+ The first API call exchanges the refresh token at
166
+ `https://authentication.wolt.com/v1/wauth2/access_token`. Subsequent calls reuse
167
+ the access token until shortly before its server-reported expiry (30 minutes in
168
+ the verified response). The access token is supplied to the restaurant, consumer,
169
+ and payment hosts; the refresh token is sent only to the authentication host.
170
+ You can also call `credentials.refresh()` to exchange it explicitly.
171
+
172
+ Wolt may return a replacement refresh token. `credentials.refresh_token` always
173
+ holds the latest successfully validated value. For applications that run across
174
+ restarts, pass `on_refresh=save_refresh_token`, where your function accepts that
175
+ string and saves it to your secret store after each exchange. The library does
176
+ not read or write credential files. Without persistence, a rotated token may be
177
+ lost when your process exits. Do not share one refresh token across independent
178
+ processes that might refresh it concurrently.
179
+
180
+ The callback runs before the API request proceeds and must not call back into
181
+ `credentials.refresh()` or `credentials.headers_for()`. If it raises, the
182
+ exception propagates but the new tokens remain in memory. Later calls retry the
183
+ callback before any further authentication or API request; they remain blocked
184
+ until persistence succeeds. Make your callback safe to repeat with the same token.
185
+
186
+ Optional `restaurant_headers`, `consumer_headers`, and `payment_headers` scope
187
+ extra headers to their respective hosts. `Authorization` is managed by
188
+ `RefreshTokenCredentials`. Its `timeout` controls authentication requests
189
+ separately from `WoltClient`'s API timeout (both default to 10 seconds).
190
+
191
+ Refresh requests are not retried or redirected, and API requests are never
192
+ automatically replayed after a 401, including purchases. An expired or revoked
193
+ refresh token requires a new browser session credential. Authentication failures
194
+ use the existing exceptions, such as `HTTPStatusError` with
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`.
217
+
218
+ ### Useful methods
219
+
220
+ | Call | What you get |
221
+ | --- | --- |
222
+ | `client.search_venues("pizza", latitude, longitude)` | Restaurant results with names, IDs, and slugs. A slug is the name used in a restaurant's URL. |
223
+ | `client.get_assortment(slug)` | A dictionary containing menu items and their options. |
224
+ | `client.get_venue_static(slug)` | A dictionary of restaurant details. |
225
+ | `client.get_venue_dynamic(slug, latitude, longitude)` | Current opening and delivery information. |
226
+ | `client.get_orders_page()` | A dictionary containing the current page of order history. |
227
+ | `client.list_delivery_targets()` | References to your saved delivery addresses, with saved labels and address details. |
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. |
230
+
231
+ For example, after creating `client`:
232
+
233
+ ```python
234
+ history = client.get_orders_page()
235
+ orders = history.get("orders", [])
236
+ print(f"This page contains {len(orders)} orders.")
237
+
238
+ targets = client.list_delivery_targets()
239
+ print(f"You have {len(targets)} saved delivery addresses.")
240
+ ```
241
+
242
+ Responses can contain personal information. Avoid printing entire responses or
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
+ ## Can it order food?
247
+
248
+ Not as a simple, ready-to-use feature yet. There is no `order("pizza")` method.
249
+
250
+ To try **checkout without buying anything**, use the new checkout-only example
251
+ from an editable source installation:
252
+
253
+ ```bash
254
+ python examples/order.py --latitude 60.17 --longitude 24.94 --query pizza \
255
+ --token-file ~/.wolt-token
256
+ ```
257
+
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.
272
+
273
+ The library has methods to choose items, save a basket, ask Wolt for a price,
274
+ and submit a purchase. But some required inputs still need to come from your
275
+ own code, including browser/device information and detailed item data.
276
+
277
+ Order-history and saved-address reads have worked in live checks. **Payment and
278
+ purchase handling have not been verified with a real order.** The purchase code
279
+ only targets one restaurant, immediate home delivery, and one saved card.
280
+
281
+ `submit_prepared_order()` is the call that can place an order and charge you.
282
+ Do not call it unless you intend to buy the exact order you have reviewed. If it
283
+ times out or raises `OrderOutcomeUnknown`, **do not send it again**: Wolt may
284
+ already have received it. Check the order in Wolt instead.
285
+
286
+ Login, adding cards, payment verification screens, scheduled
287
+ orders, cancellation, and refunds are not supported.
288
+
289
+ ## Run the tests
290
+
291
+ With your Python environment active:
292
+
293
+ ```bash
294
+ python -m pip install -e ".[test]"
295
+ python -m pytest
296
+ ```
297
+
298
+ The tests use made-up responses. They do not contact Wolt, need your token, or
299
+ place orders.
300
+
301
+ ## Releases
302
+
303
+ Maintainers: see [RELEASING.md](RELEASING.md) for tests, release PRs, and automatic
304
+ PyPI publishing.
305
+
306
+ ## License
307
+
308
+ Licensed under the GNU Affero General Public License, version 3
309
+ (`AGPL-3.0-only`). See [LICENSE](LICENSE) for the full terms.