woltapi 0.4.1__tar.gz → 0.5.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 (37) hide show
  1. {woltapi-0.4.1/src/woltapi.egg-info → woltapi-0.5.0}/PKG-INFO +55 -5
  2. {woltapi-0.4.1 → woltapi-0.5.0}/README.md +54 -4
  3. {woltapi-0.4.1 → woltapi-0.5.0}/pyproject.toml +1 -1
  4. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/client.py +24 -0
  5. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/transport.py +5 -0
  6. {woltapi-0.4.1 → woltapi-0.5.0/src/woltapi.egg-info}/PKG-INFO +55 -5
  7. {woltapi-0.4.1 → woltapi-0.5.0}/tests/test_client.py +47 -0
  8. {woltapi-0.4.1 → woltapi-0.5.0}/LICENSE +0 -0
  9. {woltapi-0.4.1 → woltapi-0.5.0}/MANIFEST.in +0 -0
  10. {woltapi-0.4.1 → woltapi-0.5.0}/RELEASING.md +0 -0
  11. {woltapi-0.4.1 → woltapi-0.5.0}/examples/browse.py +0 -0
  12. {woltapi-0.4.1 → woltapi-0.5.0}/examples/check_session.py +0 -0
  13. {woltapi-0.4.1 → woltapi-0.5.0}/examples/order.py +0 -0
  14. {woltapi-0.4.1 → woltapi-0.5.0}/examples/verify_saved_basket_rebuild.py +0 -0
  15. {woltapi-0.4.1 → woltapi-0.5.0}/setup.cfg +0 -0
  16. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/__init__.py +0 -0
  17. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/auth.py +0 -0
  18. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/basket.py +0 -0
  19. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/credentials.py +0 -0
  20. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/errors.py +0 -0
  21. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/models.py +0 -0
  22. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/purchase.py +0 -0
  23. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/selection.py +0 -0
  24. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi/services.py +0 -0
  25. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi.egg-info/SOURCES.txt +0 -0
  26. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi.egg-info/dependency_links.txt +0 -0
  27. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi.egg-info/requires.txt +0 -0
  28. {woltapi-0.4.1 → woltapi-0.5.0}/src/woltapi.egg-info/top_level.txt +0 -0
  29. {woltapi-0.4.1 → woltapi-0.5.0}/tests/__init__.py +0 -0
  30. {woltapi-0.4.1 → woltapi-0.5.0}/tests/test_auth.py +0 -0
  31. {woltapi-0.4.1 → woltapi-0.5.0}/tests/test_basket.py +0 -0
  32. {woltapi-0.4.1 → woltapi-0.5.0}/tests/test_browse.py +0 -0
  33. {woltapi-0.4.1 → woltapi-0.5.0}/tests/test_check_session.py +0 -0
  34. {woltapi-0.4.1 → woltapi-0.5.0}/tests/test_order_example.py +0 -0
  35. {woltapi-0.4.1 → woltapi-0.5.0}/tests/test_purchase.py +0 -0
  36. {woltapi-0.4.1 → woltapi-0.5.0}/tests/test_selection.py +0 -0
  37. {woltapi-0.4.1 → woltapi-0.5.0}/tests/test_verify_saved_basket_rebuild_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: woltapi
3
- Version: 0.4.1
3
+ Version: 0.5.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
@@ -298,14 +298,16 @@ Item, option, and value IDs come from the assortment dictionary. For
298
298
  current menu.
299
299
 
300
300
  Wolt also stores one basket per restaurant on its servers. These are the
301
- baskets you see in the Wolt app. The library can read and replace them:
301
+ baskets you see in the Wolt app. The library can read, replace, and delete
302
+ them:
302
303
 
303
304
  | Call | What it does |
304
305
  | --- | --- |
305
306
  | `client.save_basket_items(assortment, venue=..., items=...)` | Replaces the whole server basket for that restaurant. Needs only the menu, a venue context, and items. This does not place an order. |
306
307
  | `client.save_basket(selection)` | Also replaces the whole server basket, but takes a full `OrderSelection`. |
307
308
  | `client.get_basket_count()` | The number of baskets stored on the server. |
308
- | `client.get_baskets_page(latitude, longitude)` | The full baskets page as a dictionary. It can contain personal data; do not log it raw. |
309
+ | `client.get_baskets_page(latitude, longitude)` | Lists the saved baskets, as one page of the raw dictionary. It can contain personal data; do not log it raw. |
310
+ | `client.delete_baskets(basket_ids)` | Deletes the listed server baskets. |
309
311
  | `client.get_venue_checkout_context(slug)` | The `VenueCheckoutContext` for a restaurant, read from its static page. |
310
312
 
311
313
  There are two ways to save. `save_basket_items()` needs no saved address and no
@@ -315,8 +317,56 @@ after `list_delivery_targets()` and `get_payment_methods()` on the same client,
315
317
  so it needs a saved delivery address and an enabled card. That cost is worth
316
318
  paying only when you go on to request a checkout quote with the same selection.
317
319
 
318
- Deleting a server basket is not supported. There is also no call that adds or
319
- removes a single item: every save replaces the whole basket.
320
+ There is no call that adds or removes a single item: every save replaces the
321
+ whole basket.
322
+
323
+ #### Delete a saved basket
324
+
325
+ `delete_baskets()` takes a list of basket IDs and deletes all of them in one
326
+ call. You get a basket ID from `get_baskets_page()`, or from the `SavedBasket`
327
+ that `save_basket()` and `save_basket_items()` return.
328
+
329
+ An empty list is rejected, because Wolt's behaviour in that case has never been
330
+ observed. So check the list before you send it, as both examples below do.
331
+
332
+ To delete every saved basket:
333
+
334
+ ```python
335
+ page = client.get_baskets_page(60.17, 24.94)
336
+ ids = [entry["id"] for entry in page["baskets"]]
337
+ if ids:
338
+ client.delete_baskets(ids)
339
+ ```
340
+
341
+ To delete the basket for one restaurant:
342
+
343
+ ```python
344
+ page = client.get_baskets_page(60.17, 24.94)
345
+ ids = [
346
+ entry["id"] for entry in page["baskets"]
347
+ if entry["venue"]["slug"] == "<restaurant slug>"
348
+ ]
349
+ if ids:
350
+ client.delete_baskets(ids)
351
+ ```
352
+
353
+ Every saved basket has items in it, because a basket only exists once you put
354
+ something in one. There is no empty saved basket to skip.
355
+
356
+ Wolt sends nothing back, so the call cannot tell you whether the baskets were
357
+ really there or whether anything was deleted. If you need to be sure, read
358
+ `get_basket_count()` or `get_baskets_page()` again afterwards.
359
+
360
+ Two limits of `get_baskets_page()` matter when you delete in bulk:
361
+
362
+ - It reads one page. The response carries a `start_after_time` cursor, which
363
+ suggests Wolt can split a long list, and the library does not follow that
364
+ cursor. Compare `len(page["baskets"])` with `get_basket_count()` to see
365
+ whether you have the whole list.
366
+ - It requires coordinates, and it is not known whether they only drive the
367
+ delivery estimates shown in the app or actually restrict which baskets come
368
+ back. If a basket you expect is missing, try coordinates near that
369
+ restaurant.
320
370
 
321
371
  #### Change a saved basket
322
372
 
@@ -280,14 +280,16 @@ Item, option, and value IDs come from the assortment dictionary. For
280
280
  current menu.
281
281
 
282
282
  Wolt also stores one basket per restaurant on its servers. These are the
283
- baskets you see in the Wolt app. The library can read and replace them:
283
+ baskets you see in the Wolt app. The library can read, replace, and delete
284
+ them:
284
285
 
285
286
  | Call | What it does |
286
287
  | --- | --- |
287
288
  | `client.save_basket_items(assortment, venue=..., items=...)` | Replaces the whole server basket for that restaurant. Needs only the menu, a venue context, and items. This does not place an order. |
288
289
  | `client.save_basket(selection)` | Also replaces the whole server basket, but takes a full `OrderSelection`. |
289
290
  | `client.get_basket_count()` | The number of baskets stored on the server. |
290
- | `client.get_baskets_page(latitude, longitude)` | The full baskets page as a dictionary. It can contain personal data; do not log it raw. |
291
+ | `client.get_baskets_page(latitude, longitude)` | Lists the saved baskets, as one page of the raw dictionary. It can contain personal data; do not log it raw. |
292
+ | `client.delete_baskets(basket_ids)` | Deletes the listed server baskets. |
291
293
  | `client.get_venue_checkout_context(slug)` | The `VenueCheckoutContext` for a restaurant, read from its static page. |
292
294
 
293
295
  There are two ways to save. `save_basket_items()` needs no saved address and no
@@ -297,8 +299,56 @@ after `list_delivery_targets()` and `get_payment_methods()` on the same client,
297
299
  so it needs a saved delivery address and an enabled card. That cost is worth
298
300
  paying only when you go on to request a checkout quote with the same selection.
299
301
 
300
- Deleting a server basket is not supported. There is also no call that adds or
301
- removes a single item: every save replaces the whole basket.
302
+ There is no call that adds or removes a single item: every save replaces the
303
+ whole basket.
304
+
305
+ #### Delete a saved basket
306
+
307
+ `delete_baskets()` takes a list of basket IDs and deletes all of them in one
308
+ call. You get a basket ID from `get_baskets_page()`, or from the `SavedBasket`
309
+ that `save_basket()` and `save_basket_items()` return.
310
+
311
+ An empty list is rejected, because Wolt's behaviour in that case has never been
312
+ observed. So check the list before you send it, as both examples below do.
313
+
314
+ To delete every saved basket:
315
+
316
+ ```python
317
+ page = client.get_baskets_page(60.17, 24.94)
318
+ ids = [entry["id"] for entry in page["baskets"]]
319
+ if ids:
320
+ client.delete_baskets(ids)
321
+ ```
322
+
323
+ To delete the basket for one restaurant:
324
+
325
+ ```python
326
+ page = client.get_baskets_page(60.17, 24.94)
327
+ ids = [
328
+ entry["id"] for entry in page["baskets"]
329
+ if entry["venue"]["slug"] == "<restaurant slug>"
330
+ ]
331
+ if ids:
332
+ client.delete_baskets(ids)
333
+ ```
334
+
335
+ Every saved basket has items in it, because a basket only exists once you put
336
+ something in one. There is no empty saved basket to skip.
337
+
338
+ Wolt sends nothing back, so the call cannot tell you whether the baskets were
339
+ really there or whether anything was deleted. If you need to be sure, read
340
+ `get_basket_count()` or `get_baskets_page()` again afterwards.
341
+
342
+ Two limits of `get_baskets_page()` matter when you delete in bulk:
343
+
344
+ - It reads one page. The response carries a `start_after_time` cursor, which
345
+ suggests Wolt can split a long list, and the library does not follow that
346
+ cursor. Compare `len(page["baskets"])` with `get_basket_count()` to see
347
+ whether you have the whole list.
348
+ - It requires coordinates, and it is not known whether they only drive the
349
+ delivery estimates shown in the app or actually restrict which baskets come
350
+ back. If a basket you expect is missing, try coordinates near that
351
+ restaurant.
302
352
 
303
353
  #### Change a saved basket
304
354
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "woltapi"
7
- version = "0.4.1"
7
+ version = "0.5.0"
8
8
  description = "A small synchronous client for observed Wolt ordering flows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -149,6 +149,22 @@ class WoltClient:
149
149
  query={"lat": latitude, "lon": longitude},
150
150
  )
151
151
 
152
+ def delete_baskets(self, basket_ids: Sequence[str]) -> None:
153
+ """Delete server baskets without confirmation from the response.
154
+
155
+ The response carries no result, so a caller who wants to confirm must
156
+ re-read get_basket_count() or get_baskets_page().
157
+ """
158
+
159
+ basket_ids = _basket_ids(basket_ids)
160
+ self._transport.request(
161
+ ServiceHost.CONSUMER,
162
+ "POST",
163
+ "/order-xp/v1/baskets/bulk/delete",
164
+ json_body={"ids": basket_ids},
165
+ expect_response_body=False,
166
+ )
167
+
152
168
  def get_venue_static(self, venue_slug: str) -> dict[str, Any]:
153
169
  """Read the static venue page for a slug."""
154
170
 
@@ -546,6 +562,14 @@ def _required_text(value: object, name: str) -> str:
546
562
  return value
547
563
 
548
564
 
565
+ def _basket_ids(value: object) -> list[str]:
566
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
567
+ raise TypeError("basket_ids must be a sequence.")
568
+ if not value:
569
+ raise ValueError("basket_ids must not be empty.")
570
+ return [_required_text(basket_id, "basket ID") for basket_id in value]
571
+
572
+
549
573
  def _path_segment(value: object, name: str) -> str:
550
574
  return quote(_required_text(value, name), safe="")
551
575
 
@@ -74,6 +74,7 @@ class WoltTransport:
74
74
  *,
75
75
  query: Mapping[str, Any] | None = None,
76
76
  json_body: Mapping[str, Any] | None = None,
77
+ expect_response_body: bool = True,
77
78
  ) -> dict[str, Any]:
78
79
  """Make one JSON request to a fixed host without retries or redirects."""
79
80
 
@@ -87,6 +88,8 @@ class WoltTransport:
87
88
  raise TypeError("query must be a mapping when provided.")
88
89
  if json_body is not None and not isinstance(json_body, Mapping):
89
90
  raise TypeError("json_body must be a mapping when provided.")
91
+ if not isinstance(expect_response_body, bool):
92
+ raise TypeError("expect_response_body must be a bool.")
90
93
 
91
94
  data = _encode_json(json_body, service) if json_body is not None else None
92
95
  headers = _request_headers(
@@ -116,6 +119,8 @@ class WoltTransport:
116
119
 
117
120
  if not 200 <= status_code < 300:
118
121
  raise HTTPStatusError(service.value, status_code)
122
+ if not expect_response_body:
123
+ return {}
119
124
  return _decode_json_object(response_body, service)
120
125
 
121
126
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: woltapi
3
- Version: 0.4.1
3
+ Version: 0.5.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
@@ -298,14 +298,16 @@ Item, option, and value IDs come from the assortment dictionary. For
298
298
  current menu.
299
299
 
300
300
  Wolt also stores one basket per restaurant on its servers. These are the
301
- baskets you see in the Wolt app. The library can read and replace them:
301
+ baskets you see in the Wolt app. The library can read, replace, and delete
302
+ them:
302
303
 
303
304
  | Call | What it does |
304
305
  | --- | --- |
305
306
  | `client.save_basket_items(assortment, venue=..., items=...)` | Replaces the whole server basket for that restaurant. Needs only the menu, a venue context, and items. This does not place an order. |
306
307
  | `client.save_basket(selection)` | Also replaces the whole server basket, but takes a full `OrderSelection`. |
307
308
  | `client.get_basket_count()` | The number of baskets stored on the server. |
308
- | `client.get_baskets_page(latitude, longitude)` | The full baskets page as a dictionary. It can contain personal data; do not log it raw. |
309
+ | `client.get_baskets_page(latitude, longitude)` | Lists the saved baskets, as one page of the raw dictionary. It can contain personal data; do not log it raw. |
310
+ | `client.delete_baskets(basket_ids)` | Deletes the listed server baskets. |
309
311
  | `client.get_venue_checkout_context(slug)` | The `VenueCheckoutContext` for a restaurant, read from its static page. |
310
312
 
311
313
  There are two ways to save. `save_basket_items()` needs no saved address and no
@@ -315,8 +317,56 @@ after `list_delivery_targets()` and `get_payment_methods()` on the same client,
315
317
  so it needs a saved delivery address and an enabled card. That cost is worth
316
318
  paying only when you go on to request a checkout quote with the same selection.
317
319
 
318
- Deleting a server basket is not supported. There is also no call that adds or
319
- removes a single item: every save replaces the whole basket.
320
+ There is no call that adds or removes a single item: every save replaces the
321
+ whole basket.
322
+
323
+ #### Delete a saved basket
324
+
325
+ `delete_baskets()` takes a list of basket IDs and deletes all of them in one
326
+ call. You get a basket ID from `get_baskets_page()`, or from the `SavedBasket`
327
+ that `save_basket()` and `save_basket_items()` return.
328
+
329
+ An empty list is rejected, because Wolt's behaviour in that case has never been
330
+ observed. So check the list before you send it, as both examples below do.
331
+
332
+ To delete every saved basket:
333
+
334
+ ```python
335
+ page = client.get_baskets_page(60.17, 24.94)
336
+ ids = [entry["id"] for entry in page["baskets"]]
337
+ if ids:
338
+ client.delete_baskets(ids)
339
+ ```
340
+
341
+ To delete the basket for one restaurant:
342
+
343
+ ```python
344
+ page = client.get_baskets_page(60.17, 24.94)
345
+ ids = [
346
+ entry["id"] for entry in page["baskets"]
347
+ if entry["venue"]["slug"] == "<restaurant slug>"
348
+ ]
349
+ if ids:
350
+ client.delete_baskets(ids)
351
+ ```
352
+
353
+ Every saved basket has items in it, because a basket only exists once you put
354
+ something in one. There is no empty saved basket to skip.
355
+
356
+ Wolt sends nothing back, so the call cannot tell you whether the baskets were
357
+ really there or whether anything was deleted. If you need to be sure, read
358
+ `get_basket_count()` or `get_baskets_page()` again afterwards.
359
+
360
+ Two limits of `get_baskets_page()` matter when you delete in bulk:
361
+
362
+ - It reads one page. The response carries a `start_after_time` cursor, which
363
+ suggests Wolt can split a long list, and the library does not follow that
364
+ cursor. Compare `len(page["baskets"])` with `get_basket_count()` to see
365
+ whether you have the whole list.
366
+ - It requires coordinates, and it is not known whether they only drive the
367
+ delivery estimates shown in the app or actually restrict which baskets come
368
+ back. If a basket you expect is missing, try coordinates near that
369
+ restaurant.
320
370
 
321
371
  #### Change a saved basket
322
372
 
@@ -17,6 +17,7 @@ from woltapi import (
17
17
  WoltClient,
18
18
  WoltTransport,
19
19
  )
20
+ from woltapi.services import ServiceHost
20
21
 
21
22
 
22
23
  class FakeResponse:
@@ -261,6 +262,52 @@ class WoltClientTests(unittest.TestCase):
261
262
  self.assertEqual(headers["x-consumer-session"], "consumer-secret")
262
263
  self.assertNotIn("x-restaurant-session", headers)
263
264
 
265
+ def test_delete_baskets_posts_ids_and_accepts_null_response(self) -> None:
266
+ client, opener = make_client(FakeResponse(None))
267
+
268
+ result = client.delete_baskets(["basket-1", "basket-2"])
269
+
270
+ self.assertIsNone(result)
271
+ self.assertEqual(len(opener.requests), 1)
272
+ request = opener.requests[0]
273
+ self.assertEqual(request.method, "POST")
274
+ self.assertEqual(urlsplit(request.full_url).netloc, "consumer-api.wolt.com")
275
+ self.assertEqual(
276
+ urlsplit(request.full_url).path,
277
+ "/order-xp/v1/baskets/bulk/delete",
278
+ )
279
+ headers = _headers(request)
280
+ self.assertEqual(headers["x-consumer-session"], "consumer-secret")
281
+ self.assertEqual(
282
+ json.loads(request.data.decode("utf-8")), {"ids": ["basket-1", "basket-2"]}
283
+ )
284
+
285
+ def test_transport_rejects_null_body_when_response_is_expected(self) -> None:
286
+ client, _ = make_client(FakeResponse(None))
287
+
288
+ with self.assertRaises(ResponseShapeError):
289
+ client._transport.request(
290
+ ServiceHost.CONSUMER, "POST", "/order-xp/v1/baskets/bulk/delete"
291
+ )
292
+
293
+ def test_delete_baskets_validates_ids_before_request(self) -> None:
294
+ for basket_ids, error_type in (
295
+ ([], ValueError),
296
+ ("basket-1", TypeError),
297
+ (None, TypeError),
298
+ (1, TypeError),
299
+ (b"basket-1", TypeError),
300
+ ([1], ValueError),
301
+ ([" "], ValueError),
302
+ ):
303
+ with self.subTest(basket_ids=basket_ids):
304
+ client, opener = make_client()
305
+
306
+ with self.assertRaises(error_type):
307
+ client.delete_baskets(basket_ids) # type: ignore[arg-type]
308
+
309
+ self.assertEqual(opener.requests, [])
310
+
264
311
  def test_get_basket_count_rejects_non_integer_counts(self) -> None:
265
312
  for payload in ({}, {"count": True}, {"count": "1"}):
266
313
  with self.subTest(payload=payload):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes