diff --git a/python_picnic_api/client.py b/python_picnic_api/client.py index 83ab1ec..0415588 100644 --- a/python_picnic_api/client.py +++ b/python_picnic_api/client.py @@ -1,6 +1,6 @@ from hashlib import md5 -from .helper import _tree_generator, _url_generator, _get_category_name +from .helper import _tree_generator, _url_generator, _get_category_name, _extract_search_results from .session import PicnicAPISession, PicnicAuthError DEFAULT_URL = "https://storefront-prod.{}.picnicinternational.com/api/{}" @@ -23,7 +23,7 @@ def __init__( # Login if not authenticated if not self.session.authenticated and username and password: self.login(username, password) - + self.high_level_categories = None def initialize_high_level_categories(self): @@ -36,8 +36,8 @@ def _get(self, path: str, add_picnic_headers=False): # Make the request, add special picnic headers if needed headers = { - "x-picnic-agent": "30100;1.15.183-14941;", - "x-picnic-did": "00DE6414C744E7CB" + "x-picnic-agent": "30100;1.15.232-15154;", + "x-picnic-did": "3C417201548B2E3B" } if add_picnic_headers else None response = self.session.get(url, headers=headers).json() @@ -77,8 +77,9 @@ def get_user(self): return self._get("/user") def search(self, term: str): - path = "/search?search_term=" + term - return self._get(path) + path = f"/pages/search-page-results?search_term={term}" + raw_results = self._get(path, add_picnic_headers=True) + return _extract_search_results(raw_results) def get_lists(self, list_id: str = None): if list_id: @@ -101,7 +102,7 @@ def get_sublist(self, list_id: str, sublist_id: str) -> list: def get_cart(self): return self._get("/cart") - + def get_article(self, article_id: str, add_category_name=False): path = "/articles/" + article_id article = self._get(path) @@ -111,7 +112,7 @@ def get_article(self, article_id: str, add_category_name=False): category_name=_get_category_name(article['category_link'], self.high_level_categories) ) return article - + def get_article_category(self, article_id: str): path = "/articles/" + article_id + "/category" return self._get(path) diff --git a/python_picnic_api/helper.py b/python_picnic_api/helper.py index b28fb1e..bb800b5 100644 --- a/python_picnic_api/helper.py +++ b/python_picnic_api/helper.py @@ -1,4 +1,6 @@ +import json import re +from typing import List, Dict, Any, Optional # prefix components: space = " " @@ -10,6 +12,9 @@ IMAGE_SIZES = ["small", "medium", "regular", "large", "extra-large"] IMAGE_BASE_URL = "https://storefront-prod.nl.picnicinternational.com/static/images" +SOLE_ARTICLE_ID_PATTERN = re.compile(r'"sole_article_id":"(\w+)"') + + def _tree_generator(response: list, prefix: str = ""): """A recursive tree generator, will yield a visual tree structure line by line @@ -37,20 +42,22 @@ def _url_generator(url: str, country_code: str, api_version: str): return url.format(country_code.lower(), api_version) -def _get_category_id_from_link(category_link: str) -> str: - pattern = r'categories/(\d+)' +def _get_category_id_from_link(category_link: str) -> Optional[str]: + pattern = r"categories/(\d+)" first_number = re.search(pattern, category_link) if first_number: result = str(first_number.group(1)) return result else: return None - - -def _get_category_name(category_link: str, categories: list) -> str: + + +def _get_category_name(category_link: str, categories: list) -> Optional[str]: category_id = _get_category_id_from_link(category_link) if category_id: - category = next((item for item in categories if item["id"] == category_id), None) + category = next( + (item for item in categories if item["id"] == category_id), None + ) if category: return category["name"] else: @@ -58,6 +65,7 @@ def _get_category_name(category_link: str, categories: list) -> str: else: return None + def get_recipe_image(id: str, size="regular"): sizes = IMAGE_SIZES + ["1250x1250"] assert size in sizes, "size must be one of: " + ", ".join(sizes) @@ -65,14 +73,39 @@ def get_recipe_image(id: str, size="regular"): def get_image(id: str, size="regular", suffix="webp"): - assert "tile" in size if suffix == "webp" else True, ( - "webp format only supports tile sizes" - ) + assert ( + "tile" in size if suffix == "webp" else True + ), "webp format only supports tile sizes" assert suffix in ["webp", "png"], "suffix must be webp or png" sizes = IMAGE_SIZES + [f"tile-{size}" for size in IMAGE_SIZES] - assert size in sizes, ( - "size must be one of: " + ", ".join(sizes) - ) + assert size in sizes, "size must be one of: " + ", ".join(sizes) return f"{IMAGE_BASE_URL}/{id}/{size}.{suffix}" +def _extract_search_results(raw_results, max_items: int = 10): + """Extract search results from the nested dictionary structure returned by Picnic search. + Number of max items can be defined to reduce excessive nested search""" + search_results = [] + + def find_articles(node): + if len(search_results) >= max_items: + return + + content = node.get("content", {}) + if content.get("type") == "SELLING_UNIT_TILE" and "sellingUnit" in content: + selling_unit = content["sellingUnit"] + sole_article_ids = SOLE_ARTICLE_ID_PATTERN.findall(json.dumps(node)) + sole_article_id = sole_article_ids[0] if sole_article_ids else None + result_entry = { + **selling_unit, + "sole_article_id": sole_article_id, + } + search_results.append(result_entry) + + for child in node.get("children", []): + find_articles(child) + + body = raw_results.get("body", {}) + find_articles(body.get("child", {})) + + return [{"items": search_results}] diff --git a/tests/test_client.py b/tests/test_client.py index 359082c..975a699 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,7 +6,7 @@ from python_picnic_api.session import PicnicAuthError PICNIC_HEADERS = { - "x-picnic-agent": "30100;1.15.77-10293", + "x-picnic-agent": "30100;1.15.232-15154", "x-picnic-did": "3C417201548B2E3B", } @@ -34,7 +34,7 @@ def test_login_credentials(self): PicnicAPI(username='test@test.nl', password='test') self.session_mock().post.assert_called_with( self.expected_base_url + '/user/login', - json={'key': 'test@test.nl', 'secret': '098f6bcd4621d373cade4e832627b4f6', "client_id": 1} + json={'key': 'test@test.nl', 'secret': '098f6bcd4621d373cade4e832627b4f6', "client_id": 30100} ) def test_login_auth_token(self): @@ -83,7 +83,7 @@ def test_get_user(self): def test_search(self): self.client.search("test-product") self.session_mock().get.assert_called_with( - self.expected_base_url + "/search?search_term=test-product", headers=None + self.expected_base_url + "/pages/search-page-results?search_term=test-product", headers=PICNIC_HEADERS ) def test_get_lists(self):