Skip to content
17 changes: 9 additions & 8 deletions python_picnic_api/client.py
Original file line number Diff line number Diff line change
@@ -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/{}"
Expand All @@ -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):
Expand All @@ -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()

Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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)
Expand Down
57 changes: 45 additions & 12 deletions python_picnic_api/helper.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import json
import re
from typing import List, Dict, Any, Optional

# prefix components:
space = " "
Expand All @@ -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
Expand Down Expand Up @@ -37,42 +42,70 @@ 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:
return None
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)
return f"{IMAGE_BASE_URL}/recipes/{id}/{size}.png"


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}]
6 changes: 3 additions & 3 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}

Expand Down Expand Up @@ -34,7 +34,7 @@ def test_login_credentials(self):
PicnicAPI(username='[email protected]', password='test')
self.session_mock().post.assert_called_with(
self.expected_base_url + '/user/login',
json={'key': '[email protected]', 'secret': '098f6bcd4621d373cade4e832627b4f6', "client_id": 1}
json={'key': '[email protected]', 'secret': '098f6bcd4621d373cade4e832627b4f6', "client_id": 30100}
)

def test_login_auth_token(self):
Expand Down Expand Up @@ -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):
Expand Down