python-picnic-api2 1.3.4__py3-none-any.whl → 2.0.0__py3-none-any.whl
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.
- python_picnic_api2/__init__.py +46 -3
- python_picnic_api2/client.py +74 -76
- python_picnic_api2/exceptions.py +32 -0
- python_picnic_api2/helper.py +0 -123
- python_picnic_api2/models/__init__.py +45 -0
- python_picnic_api2/models/article.py +262 -0
- python_picnic_api2/models/base.py +42 -0
- python_picnic_api2/models/cart.py +28 -0
- python_picnic_api2/models/category.py +30 -0
- python_picnic_api2/models/common.py +125 -0
- python_picnic_api2/models/delivery.py +50 -0
- python_picnic_api2/models/pml.py +173 -0
- python_picnic_api2/models/search.py +65 -0
- python_picnic_api2/models/user.py +58 -0
- python_picnic_api2-2.0.0.dist-info/METADATA +205 -0
- python_picnic_api2-2.0.0.dist-info/RECORD +19 -0
- {python_picnic_api2-1.3.4.dist-info → python_picnic_api2-2.0.0.dist-info}/WHEEL +1 -1
- python_picnic_api2-1.3.4.dist-info/METADATA +0 -95
- python_picnic_api2-1.3.4.dist-info/RECORD +0 -8
- {python_picnic_api2-1.3.4.dist-info → python_picnic_api2-2.0.0.dist-info}/licenses/LICENSE.md +0 -0
python_picnic_api2/__init__.py
CHANGED
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
from .client import PicnicAPI
|
|
2
|
-
from .
|
|
2
|
+
from .exceptions import PicnicParseError
|
|
3
|
+
from .models import (
|
|
4
|
+
Address,
|
|
5
|
+
Article,
|
|
6
|
+
Cart,
|
|
7
|
+
Category,
|
|
8
|
+
Delivery,
|
|
9
|
+
DeliverySlots,
|
|
10
|
+
DeliverySummary,
|
|
11
|
+
HouseholdDetails,
|
|
12
|
+
Order,
|
|
13
|
+
OrderArticle,
|
|
14
|
+
OrderLine,
|
|
15
|
+
PicnicModel,
|
|
16
|
+
SearchResult,
|
|
17
|
+
SearchResultItem,
|
|
18
|
+
Slot,
|
|
19
|
+
Subscription,
|
|
20
|
+
User,
|
|
21
|
+
)
|
|
22
|
+
from .session import Picnic2FAError, Picnic2FARequired, PicnicAuthError
|
|
3
23
|
|
|
4
|
-
__all__ = [
|
|
24
|
+
__all__ = [
|
|
25
|
+
"PicnicAPI",
|
|
26
|
+
"PicnicAuthError",
|
|
27
|
+
"Picnic2FAError",
|
|
28
|
+
"Picnic2FARequired",
|
|
29
|
+
"PicnicParseError",
|
|
30
|
+
"PicnicModel",
|
|
31
|
+
"Article",
|
|
32
|
+
"Category",
|
|
33
|
+
"SearchResult",
|
|
34
|
+
"SearchResultItem",
|
|
35
|
+
"User",
|
|
36
|
+
"Address",
|
|
37
|
+
"Subscription",
|
|
38
|
+
"HouseholdDetails",
|
|
39
|
+
"Cart",
|
|
40
|
+
"DeliverySlots",
|
|
41
|
+
"Delivery",
|
|
42
|
+
"DeliverySummary",
|
|
43
|
+
"Slot",
|
|
44
|
+
"Order",
|
|
45
|
+
"OrderLine",
|
|
46
|
+
"OrderArticle",
|
|
47
|
+
]
|
|
5
48
|
__title__ = "python-picnic-api"
|
|
6
|
-
__version__ = "
|
|
49
|
+
__version__ = "2.0.0"
|
|
7
50
|
__author__ = "Mike Brink"
|
python_picnic_api2/client.py
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import re
|
|
2
1
|
from hashlib import md5
|
|
3
2
|
from urllib.parse import quote
|
|
4
3
|
|
|
5
4
|
import typing_extensions
|
|
6
5
|
|
|
7
|
-
from .
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
from .exceptions import PicnicParseError
|
|
7
|
+
from .helper import _url_generator
|
|
8
|
+
from .models import (
|
|
9
|
+
Article,
|
|
10
|
+
Cart,
|
|
11
|
+
Category,
|
|
12
|
+
Delivery,
|
|
13
|
+
DeliverySlots,
|
|
14
|
+
DeliverySummary,
|
|
15
|
+
SearchResult,
|
|
16
|
+
User,
|
|
17
|
+
pml,
|
|
12
18
|
)
|
|
13
19
|
from .session import (
|
|
14
20
|
Picnic2FAError,
|
|
@@ -18,7 +24,6 @@ from .session import (
|
|
|
18
24
|
)
|
|
19
25
|
|
|
20
26
|
DEFAULT_URL = "https://storefront-prod.{}.picnicinternational.com/api/{}"
|
|
21
|
-
GLOBAL_GATEWAY_URL = "https://gateway-prod.global.picnicinternational.com"
|
|
22
27
|
DEFAULT_COUNTRY_CODE = "NL"
|
|
23
28
|
DEFAULT_API_VERSION = "15"
|
|
24
29
|
_HEADERS = {
|
|
@@ -46,13 +51,6 @@ class PicnicAPI:
|
|
|
46
51
|
if not self.session.authenticated and username and password:
|
|
47
52
|
self.login(username, password)
|
|
48
53
|
|
|
49
|
-
self.high_level_categories = None
|
|
50
|
-
|
|
51
|
-
def initialize_high_level_categories(self):
|
|
52
|
-
"""Initialize high-level categories once to avoid multiple requests."""
|
|
53
|
-
if not self.high_level_categories:
|
|
54
|
-
self.high_level_categories = self.get_categories(depth=1)
|
|
55
|
-
|
|
56
54
|
def _get(self, path: str, add_picnic_headers=False):
|
|
57
55
|
url = self._base_url + path
|
|
58
56
|
|
|
@@ -86,7 +84,7 @@ class PicnicAPI:
|
|
|
86
84
|
if not isinstance(response, dict):
|
|
87
85
|
return False
|
|
88
86
|
|
|
89
|
-
error_code = response.
|
|
87
|
+
error_code = response.get("error", {}).get("code")
|
|
90
88
|
return error_code == "AUTH_ERROR" or error_code == "AUTH_INVALID_CRED"
|
|
91
89
|
|
|
92
90
|
@staticmethod
|
|
@@ -169,85 +167,82 @@ class PicnicAPI:
|
|
|
169
167
|
def logged_in(self):
|
|
170
168
|
return self.session.authenticated
|
|
171
169
|
|
|
172
|
-
def get_user(self):
|
|
173
|
-
return self._get("/user")
|
|
170
|
+
def get_user(self) -> User:
|
|
171
|
+
return User.from_api(self._get("/user"))
|
|
174
172
|
|
|
175
|
-
def search(self, term: str):
|
|
173
|
+
def search(self, term: str) -> SearchResult:
|
|
176
174
|
path = f"/pages/search-page-results?search_term={quote(term)}"
|
|
177
175
|
raw_results = self._get(path, add_picnic_headers=True)
|
|
178
|
-
return
|
|
176
|
+
return SearchResult.from_page(raw_results)
|
|
179
177
|
|
|
180
|
-
def get_cart(self):
|
|
181
|
-
return self._get("/cart")
|
|
178
|
+
def get_cart(self) -> Cart:
|
|
179
|
+
return Cart.from_api(self._get("/cart"))
|
|
182
180
|
|
|
183
|
-
def get_article(self, article_id: str, add_category=False):
|
|
181
|
+
def get_article(self, article_id: str, add_category=False) -> Article | None:
|
|
184
182
|
path = f"/pages/product-details-page-root?id={article_id}" + \
|
|
185
183
|
"&show_category_action=true"
|
|
186
184
|
data = self._get(path, add_picnic_headers=True)
|
|
187
|
-
article_details = []
|
|
188
185
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if len(root_container) == 0:
|
|
186
|
+
article = Article.from_page(data, article_id)
|
|
187
|
+
if article is None:
|
|
192
188
|
return None
|
|
193
189
|
|
|
194
|
-
article_details = root_container[0]["pml"]["component"]["children"]
|
|
195
|
-
|
|
196
|
-
if len(article_details) == 0:
|
|
197
|
-
return None
|
|
198
|
-
|
|
199
|
-
article = {}
|
|
200
190
|
if add_category:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
cat_node[0]["pml"]["component"]["onPress"]["target"]).groups()
|
|
210
|
-
article["category"] = self.get_category_by_ids(
|
|
211
|
-
int(cat_ids[1]), int(cat_ids[2]))
|
|
212
|
-
|
|
213
|
-
color_regex = re.compile(r"#\(#\d{6}\)")
|
|
214
|
-
producer = re.sub(color_regex, "", str(
|
|
215
|
-
article_details[1].get("markdown", "")))
|
|
216
|
-
article_name = re.sub(color_regex, "", str(
|
|
217
|
-
article_details[0]["markdown"]))
|
|
218
|
-
|
|
219
|
-
article["name"] = f"{producer} {article_name}"
|
|
220
|
-
article["id"] = article_id
|
|
191
|
+
cat_ids = Article.category_ids_from_page(data)
|
|
192
|
+
if cat_ids is None:
|
|
193
|
+
raise PicnicParseError(
|
|
194
|
+
f"Could not extract category from article with id {article_id}",
|
|
195
|
+
endpoint="product-details-page-root",
|
|
196
|
+
)
|
|
197
|
+
_, l2_id, l3_id = cat_ids
|
|
198
|
+
article.category = self.get_category_by_ids(l2_id, l3_id)
|
|
221
199
|
|
|
222
200
|
return article
|
|
223
201
|
|
|
224
202
|
def get_article_category(self, article_id: str):
|
|
203
|
+
"""Return the raw category payload for an article.
|
|
204
|
+
|
|
205
|
+
Not modelled: this endpoint appears to have been removed by Picnic (it
|
|
206
|
+
returns an error object, like ``get_categories``). Kept for backwards
|
|
207
|
+
compatibility; returns the raw dict. Use ``get_article(id,
|
|
208
|
+
add_category=True)`` to resolve an article's category instead.
|
|
209
|
+
"""
|
|
225
210
|
path = "/articles/" + article_id + "/category"
|
|
226
211
|
return self._get(path)
|
|
227
212
|
|
|
228
|
-
def add_product(self, product_id: str, count: int = 1):
|
|
213
|
+
def add_product(self, product_id: str, count: int = 1) -> Cart:
|
|
229
214
|
data = {"product_id": product_id, "count": count}
|
|
230
|
-
return self._post("/cart/add_product", data)
|
|
215
|
+
return Cart.from_api(self._post("/cart/add_product", data))
|
|
231
216
|
|
|
232
|
-
def remove_product(self, product_id: str, count: int = 1):
|
|
217
|
+
def remove_product(self, product_id: str, count: int = 1) -> Cart:
|
|
233
218
|
data = {"product_id": product_id, "count": count}
|
|
234
|
-
return self._post("/cart/remove_product", data)
|
|
219
|
+
return Cart.from_api(self._post("/cart/remove_product", data))
|
|
235
220
|
|
|
236
|
-
def clear_cart(self):
|
|
237
|
-
return self._post("/cart/clear")
|
|
221
|
+
def clear_cart(self) -> Cart:
|
|
222
|
+
return Cart.from_api(self._post("/cart/clear"))
|
|
238
223
|
|
|
239
|
-
def get_delivery_slots(self):
|
|
240
|
-
return self._get("/cart/delivery_slots")
|
|
224
|
+
def get_delivery_slots(self) -> DeliverySlots:
|
|
225
|
+
return DeliverySlots.from_api(self._get("/cart/delivery_slots"))
|
|
241
226
|
|
|
242
|
-
def get_delivery(self, delivery_id: str):
|
|
227
|
+
def get_delivery(self, delivery_id: str) -> Delivery:
|
|
243
228
|
path = "/deliveries/" + delivery_id
|
|
244
|
-
return self._get(path)
|
|
229
|
+
return Delivery.from_api(self._get(path))
|
|
245
230
|
|
|
246
231
|
def get_delivery_scenario(self, delivery_id: str):
|
|
232
|
+
"""Return the raw driving-scenario payload for a delivery.
|
|
233
|
+
|
|
234
|
+
Not modelled: it is only populated while a delivery is en route, so
|
|
235
|
+
there is no stable sample to build a model against. Returns the raw dict.
|
|
236
|
+
"""
|
|
247
237
|
path = "/deliveries/" + delivery_id + "/scenario"
|
|
248
238
|
return self._get(path, add_picnic_headers=True)
|
|
249
239
|
|
|
250
240
|
def get_delivery_position(self, delivery_id: str):
|
|
241
|
+
"""Return the raw driver-position payload for a delivery.
|
|
242
|
+
|
|
243
|
+
Not modelled: only populated while a delivery is en route (otherwise
|
|
244
|
+
empty). Returns the raw dict.
|
|
245
|
+
"""
|
|
251
246
|
path = "/deliveries/" + delivery_id + "/position"
|
|
252
247
|
return self._get(path, add_picnic_headers=True)
|
|
253
248
|
|
|
@@ -258,33 +253,36 @@ class PicnicAPI:
|
|
|
258
253
|
You can ignore this warning if you do not pass the 'summary' argument to
|
|
259
254
|
this function."""
|
|
260
255
|
)
|
|
261
|
-
def get_deliveries(
|
|
256
|
+
def get_deliveries(
|
|
257
|
+
self, summary: bool = True, data: list = None
|
|
258
|
+
) -> list[DeliverySummary]:
|
|
262
259
|
data = [] if data is None else data
|
|
263
260
|
if not summary:
|
|
264
261
|
raise NotImplementedError()
|
|
265
|
-
|
|
262
|
+
raw = self._post("/deliveries/summary", data=data)
|
|
263
|
+
return [DeliverySummary.from_api(item) for item in raw]
|
|
266
264
|
|
|
267
|
-
def get_current_deliveries(self):
|
|
265
|
+
def get_current_deliveries(self) -> list[DeliverySummary]:
|
|
268
266
|
return self.get_deliveries(data=["CURRENT"])
|
|
269
267
|
|
|
270
268
|
def get_categories(self, depth: int = 0):
|
|
271
269
|
raise NotImplementedError("This endpoint has been removed by picnic\
|
|
272
270
|
and is no longer functional.")
|
|
273
271
|
|
|
274
|
-
def get_category_by_ids(self, l2_id: int, l3_id: int):
|
|
272
|
+
def get_category_by_ids(self, l2_id: int, l3_id: int) -> Category:
|
|
275
273
|
path = "/pages/L2-category-page-root" + \
|
|
276
274
|
f"?category_id={l2_id}&l3_category_id={l3_id}"
|
|
277
275
|
data = self._get(path, add_picnic_headers=True)
|
|
278
|
-
|
|
279
|
-
data,
|
|
280
|
-
if
|
|
281
|
-
raise
|
|
282
|
-
|
|
283
|
-
"
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
276
|
+
node = pml.find(
|
|
277
|
+
data, id=f"vertical-article-tiles-sub-header-{l3_id}")
|
|
278
|
+
if node is None:
|
|
279
|
+
raise PicnicParseError(
|
|
280
|
+
"Could not find category with specified IDs",
|
|
281
|
+
endpoint="L2-category-page-root",
|
|
282
|
+
)
|
|
283
|
+
return Category(
|
|
284
|
+
l2_id=l2_id, l3_id=l3_id,
|
|
285
|
+
name=pml.accessibility_label(node), raw=data)
|
|
288
286
|
|
|
289
287
|
def get_article_by_gtin(self, etan: str, maxRedirects: int = 5):
|
|
290
288
|
# Finds the article ID for a gtin/ean (barcode).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Exceptions raised by the Picnic API client.
|
|
2
|
+
|
|
3
|
+
Authentication-related exceptions live in :mod:`python_picnic_api2.session` for
|
|
4
|
+
historical reasons and are re-exported here so that all public exceptions can be
|
|
5
|
+
imported from a single place.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .session import Picnic2FAError, Picnic2FARequired, PicnicAuthError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PicnicParseError(Exception):
|
|
12
|
+
"""Raised when a Picnic PML ("layout") response cannot be parsed into a model.
|
|
13
|
+
|
|
14
|
+
The Picnic ``/pages/*`` endpoints return a UI layout tree of widgets that
|
|
15
|
+
varies by country and A/B test. When an expected node or field is missing
|
|
16
|
+
this is raised instead of a bare ``KeyError`` so callers get a clear,
|
|
17
|
+
actionable message about what was being looked for.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, message: str, *, endpoint: str | None = None):
|
|
21
|
+
if endpoint:
|
|
22
|
+
message = f"{message} (endpoint: {endpoint})"
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.endpoint = endpoint
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"PicnicParseError",
|
|
29
|
+
"PicnicAuthError",
|
|
30
|
+
"Picnic2FARequired",
|
|
31
|
+
"Picnic2FAError",
|
|
32
|
+
]
|
python_picnic_api2/helper.py
CHANGED
|
@@ -1,73 +1,15 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import logging
|
|
3
|
-
import re
|
|
4
|
-
|
|
5
|
-
# prefix components:
|
|
6
|
-
space = " "
|
|
7
|
-
branch = "│ "
|
|
8
|
-
# pointers:
|
|
9
|
-
tee = "├── "
|
|
10
|
-
last = "└── "
|
|
11
2
|
|
|
12
3
|
LOGGER = logging.getLogger(__name__)
|
|
13
4
|
|
|
14
5
|
IMAGE_SIZES = ["small", "medium", "regular", "large", "extra-large"]
|
|
15
6
|
IMAGE_BASE_URL = "https://storefront-prod.nl.picnicinternational.com/static/images"
|
|
16
7
|
|
|
17
|
-
SOLE_ARTICLE_ID_PATTERN = re.compile(r'"sole_article_id":"(\w+)"')
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _tree_generator(response: list, prefix: str = ""):
|
|
21
|
-
"""A recursive tree generator,
|
|
22
|
-
will yield a visual tree structure line by line
|
|
23
|
-
with each line prefixed by the same characters
|
|
24
|
-
"""
|
|
25
|
-
# response each get pointers that are ├── with a final └── :
|
|
26
|
-
pointers = [tee] * (len(response) - 1) + [last]
|
|
27
|
-
for pointer, item in zip(pointers, response, strict=False):
|
|
28
|
-
if "name" in item: # print the item
|
|
29
|
-
pre = ""
|
|
30
|
-
if "unit_quantity" in item:
|
|
31
|
-
pre = f"{item['unit_quantity']} "
|
|
32
|
-
after = ""
|
|
33
|
-
if "display_price" in item:
|
|
34
|
-
after = f" €{int(item['display_price']) / 100.0:.2f}"
|
|
35
|
-
|
|
36
|
-
yield prefix + pointer + pre + item["name"] + after
|
|
37
|
-
if "items" in item: # extend the prefix and recurse:
|
|
38
|
-
extension = branch if pointer == tee else space
|
|
39
|
-
# i.e. space because last, └── , above so no more |
|
|
40
|
-
yield from _tree_generator(item["items"], prefix=prefix + extension)
|
|
41
|
-
|
|
42
8
|
|
|
43
9
|
def _url_generator(url: str, country_code: str, api_version: str):
|
|
44
10
|
return url.format(country_code.lower(), api_version)
|
|
45
11
|
|
|
46
12
|
|
|
47
|
-
def _get_category_id_from_link(category_link: str) -> str | None:
|
|
48
|
-
pattern = r"categories/(\d+)"
|
|
49
|
-
first_number = re.search(pattern, category_link)
|
|
50
|
-
if first_number:
|
|
51
|
-
result = str(first_number.group(1))
|
|
52
|
-
return result
|
|
53
|
-
else:
|
|
54
|
-
return None
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _get_category_name(category_link: str, categories: list) -> str | None:
|
|
58
|
-
category_id = _get_category_id_from_link(category_link)
|
|
59
|
-
if category_id:
|
|
60
|
-
category = next(
|
|
61
|
-
(item for item in categories if item["id"] == category_id), None
|
|
62
|
-
)
|
|
63
|
-
if category:
|
|
64
|
-
return category["name"]
|
|
65
|
-
else:
|
|
66
|
-
return None
|
|
67
|
-
else:
|
|
68
|
-
return None
|
|
69
|
-
|
|
70
|
-
|
|
71
13
|
def get_recipe_image(id: str, size="regular"):
|
|
72
14
|
sizes = IMAGE_SIZES + ["1250x1250"]
|
|
73
15
|
assert size in sizes, "size must be one of: " + ", ".join(sizes)
|
|
@@ -83,68 +25,3 @@ def get_image(id: str, size="regular", suffix="webp"):
|
|
|
83
25
|
|
|
84
26
|
assert size in sizes, "size must be one of: " + ", ".join(sizes)
|
|
85
27
|
return f"{IMAGE_BASE_URL}/{id}/{size}.{suffix}"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def find_nodes_by_content(node, filter, max_nodes: int = 10):
|
|
89
|
-
nodes = []
|
|
90
|
-
|
|
91
|
-
if len(nodes) >= 10:
|
|
92
|
-
return nodes
|
|
93
|
-
|
|
94
|
-
def is_dict_included(node_dict, filter_dict):
|
|
95
|
-
for k, v in filter_dict.items():
|
|
96
|
-
if k not in node_dict:
|
|
97
|
-
return False
|
|
98
|
-
if isinstance(v, dict) and isinstance(node_dict[k], dict):
|
|
99
|
-
if not is_dict_included(node_dict[k], v):
|
|
100
|
-
return False
|
|
101
|
-
elif node_dict[k] != v and v is not None:
|
|
102
|
-
return False
|
|
103
|
-
return True
|
|
104
|
-
|
|
105
|
-
if is_dict_included(node, filter):
|
|
106
|
-
nodes.append(node)
|
|
107
|
-
|
|
108
|
-
if isinstance(node, dict):
|
|
109
|
-
for _, v in node.items():
|
|
110
|
-
if isinstance(v, dict):
|
|
111
|
-
nodes.extend(find_nodes_by_content(v, filter, max_nodes))
|
|
112
|
-
continue
|
|
113
|
-
if isinstance(v, list):
|
|
114
|
-
for item in v:
|
|
115
|
-
if isinstance(v, dict | list):
|
|
116
|
-
nodes.extend(find_nodes_by_content(
|
|
117
|
-
item, filter, max_nodes))
|
|
118
|
-
continue
|
|
119
|
-
|
|
120
|
-
return nodes
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _extract_search_results(raw_results, max_items: int = 10):
|
|
124
|
-
"""Extract search results from the nested dictionary structure returned by
|
|
125
|
-
Picnic search. Number of max items can be defined to reduce excessive nested
|
|
126
|
-
search"""
|
|
127
|
-
|
|
128
|
-
LOGGER.debug(f"Extracting search results from {raw_results}")
|
|
129
|
-
|
|
130
|
-
body = raw_results.get("body", {})
|
|
131
|
-
nodes = find_nodes_by_content(body.get("child", {}), {
|
|
132
|
-
"type": "SELLING_UNIT_TILE", "sellingUnit": {}})
|
|
133
|
-
|
|
134
|
-
search_results = []
|
|
135
|
-
for node in nodes:
|
|
136
|
-
selling_unit = node["sellingUnit"]
|
|
137
|
-
sole_article_ids = SOLE_ARTICLE_ID_PATTERN.findall(
|
|
138
|
-
json.dumps(node))
|
|
139
|
-
sole_article_id = sole_article_ids[0] if sole_article_ids else None
|
|
140
|
-
result_entry = {
|
|
141
|
-
**selling_unit,
|
|
142
|
-
"sole_article_id": sole_article_id,
|
|
143
|
-
}
|
|
144
|
-
LOGGER.debug(f"Found article {result_entry}")
|
|
145
|
-
search_results.append(result_entry)
|
|
146
|
-
|
|
147
|
-
LOGGER.debug(
|
|
148
|
-
f"Found {len(search_results)}/{max_items} products after extraction")
|
|
149
|
-
|
|
150
|
-
return [{"items": search_results}]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Typed data models for Picnic API responses."""
|
|
2
|
+
|
|
3
|
+
from .article import Article
|
|
4
|
+
from .base import PicnicModel
|
|
5
|
+
from .cart import Cart
|
|
6
|
+
from .category import Category
|
|
7
|
+
from .common import (
|
|
8
|
+
Decorator,
|
|
9
|
+
Eta,
|
|
10
|
+
Order,
|
|
11
|
+
OrderArticle,
|
|
12
|
+
OrderLine,
|
|
13
|
+
SelectedSlot,
|
|
14
|
+
Slot,
|
|
15
|
+
TransactionInfo,
|
|
16
|
+
)
|
|
17
|
+
from .delivery import Delivery, DeliverySlots, DeliverySummary
|
|
18
|
+
from .search import SearchResult, SearchResultItem
|
|
19
|
+
from .user import Address, HouseholdDetails, Subscription, User
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"PicnicModel",
|
|
23
|
+
"Article",
|
|
24
|
+
"Category",
|
|
25
|
+
"SearchResult",
|
|
26
|
+
"SearchResultItem",
|
|
27
|
+
# Domain-JSON models
|
|
28
|
+
"User",
|
|
29
|
+
"Address",
|
|
30
|
+
"Subscription",
|
|
31
|
+
"HouseholdDetails",
|
|
32
|
+
"Cart",
|
|
33
|
+
"DeliverySlots",
|
|
34
|
+
"Delivery",
|
|
35
|
+
"DeliverySummary",
|
|
36
|
+
# Shared building blocks
|
|
37
|
+
"Slot",
|
|
38
|
+
"SelectedSlot",
|
|
39
|
+
"Eta",
|
|
40
|
+
"TransactionInfo",
|
|
41
|
+
"Order",
|
|
42
|
+
"OrderLine",
|
|
43
|
+
"OrderArticle",
|
|
44
|
+
"Decorator",
|
|
45
|
+
]
|