python-picnic-api2 1.3.0__tar.gz → 1.3.2__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 (28) hide show
  1. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/ISSUE_TEMPLATE/config.yml +2 -2
  2. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/workflows/ci.yaml +2 -2
  3. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/workflows/it.yaml +2 -2
  4. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/workflows/release.yml +4 -4
  5. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/PKG-INFO +1 -1
  6. python_picnic_api2-1.3.2/README.md +104 -0
  7. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/integration_tests/test_client.py +6 -3
  8. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/pyproject.toml +1 -1
  9. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/src/python_picnic_api2/client.py +44 -12
  10. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/src/python_picnic_api2/helper.py +52 -25
  11. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/tests/test_client.py +129 -3
  12. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/uv.lock +1 -1
  13. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.devcontainer/devcontainer.json +0 -0
  14. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.env.example +0 -0
  15. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/dependabot.yml +0 -0
  17. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/release.yml +0 -0
  18. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.gitignore +0 -0
  19. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/LICENSE.md +0 -0
  20. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/README.rst +0 -0
  21. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/codecov.yml +0 -0
  22. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/integration_tests/__init__.py +0 -0
  23. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/integration_tests/test_helper.py +0 -0
  24. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/integration_tests/test_session.py +0 -0
  25. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/src/python_picnic_api2/__init__.py +0 -0
  26. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/src/python_picnic_api2/session.py +0 -0
  27. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/tests/__init__.py +0 -0
  28. {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/tests/test_session.py +0 -0
@@ -7,8 +7,8 @@ contact_links:
7
7
  url: https://www.home-assistant.io/help
8
8
  about: We use GitHub for tracking bugs, check the Home Assistant website for resources on getting help.
9
9
  - name: Feature Request
10
- url: https://community.home-assistant.io/c/feature-requests
11
- about: Please use the Home Assistant Community Forum for making feature requests.
10
+ url: https://github.com/orgs/home-assistant/discussions/new?category=integration-enhancements&integration_name=picnic
11
+ about: Please use the Home Assistant Discussions for making feature requests.
12
12
  - name: I'm unsure where to go
13
13
  url: https://www.home-assistant.io/join-chat
14
14
  about: If you are unsure where to go, then joining our chat is recommended; Just ask!
@@ -17,10 +17,10 @@ jobs:
17
17
  runs-on: ubuntu-latest
18
18
 
19
19
  steps:
20
- - uses: actions/checkout@v4
20
+ - uses: actions/checkout@v5
21
21
 
22
22
  - name: Set up Python ${{ matrix.python-version }}
23
- uses: actions/setup-python@v5
23
+ uses: actions/setup-python@v6
24
24
  with:
25
25
  python-version: ${{ matrix.python-version }}
26
26
 
@@ -11,10 +11,10 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
 
13
13
  steps:
14
- - uses: actions/checkout@v4
14
+ - uses: actions/checkout@v5
15
15
 
16
16
  - name: Set up Python 3.12
17
- uses: actions/setup-python@v5
17
+ uses: actions/setup-python@v6
18
18
  with:
19
19
  python-version: 3.12
20
20
 
@@ -4,10 +4,10 @@ jobs:
4
4
  build:
5
5
  runs-on: ubuntu-latest
6
6
  steps:
7
- - uses: actions/checkout@v4
7
+ - uses: actions/checkout@v5
8
8
 
9
9
  - name: Set up Python 3.11
10
- uses: actions/setup-python@v5
10
+ uses: actions/setup-python@v6
11
11
  with:
12
12
  python-version: 3.11
13
13
 
@@ -40,10 +40,10 @@ jobs:
40
40
 
41
41
  steps:
42
42
  - name: Retrieve release distributions
43
- uses: actions/download-artifact@v4
43
+ uses: actions/download-artifact@v5
44
44
  with:
45
45
  name: release-dists
46
46
  path: dist/
47
47
 
48
48
  - name: Publish release distributions to PyPI
49
- uses: pypa/gh-action-pypi-publish@v1.12.4
49
+ uses: pypa/gh-action-pypi-publish@v1.13.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-picnic-api2
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Project-URL: homepage, https://github.com/codesalatdev/python-picnic-api
5
5
  Project-URL: repository, https://github.com/codesalatdev/python-picnic-api
6
6
  Author-email: Mike Brink <mjh.brink@icloud.com>, CodeSalat <pypi@codesalat.dev>
@@ -0,0 +1,104 @@
1
+ # Python-Picnic-API
2
+
3
+ **This library is undergoing rapid changes as is the Picnic API itself. It is mainly intended for use within Home Assistant, but there are integration tests running regularly checking for failures in features not used by the Home Assistant integration.**
4
+
5
+ **If you want to know why interacting with Picnic is getting harder than ever, check out their blogpost about architectural changes: https://blog.picnic.nl/adding-write-functionality-to-pages-with-self-service-apis-d09aa7dbc9c0**
6
+
7
+ Fork of the Unofficial Python wrapper for the [Picnic](https://picnic.app) API. While not all API methods have been implemented yet, you'll find most of what you need to build a working application are available.
8
+
9
+ This library is not affiliated with Picnic and retrieves data from the endpoints of the mobile application. **Use at your own risk.**
10
+
11
+ ## Credits
12
+
13
+ A big thanks to @MikeBrink for building the first versions of this library.
14
+
15
+ @maartenpaul and @thijmen-j continously provided fixes that were then merged into this fork.
16
+
17
+ ## Getting started
18
+
19
+ The easiest way to install is directly from pip:
20
+
21
+ ```bash
22
+ $ pip install python-picnic-api2
23
+ ```
24
+
25
+ Then create a new instance of `PicnicAPI` and login using your credentials:
26
+
27
+ ```python
28
+ from python_picnic_api2 import PicnicAPI
29
+
30
+ picnic = PicnicAPI(username='username', password='password', country_code="NL")
31
+ ```
32
+
33
+ The country_code parameter defaults to `NL`, but you have to change it if you live in a different country than the Netherlands (ISO 3166-1 Alpha-2). This obviously only works for countries that picnic services.
34
+
35
+ ## Searching for an article
36
+
37
+ ```python
38
+ picnic.search('coffee')
39
+ ```
40
+
41
+ ```python
42
+ [{'items': [{'id': 's1019822', 'name': 'Lavazza Caffè Crema e Aroma Bohnen', 'decorators': [], 'display_price': 1799, 'image_id': 'aecbf7d3b018025ec78daf5a1099b6842a860a2e3faeceec777c13d708ce442c', 'max_count': 99, 'unit_quantity': '1kg', 'sole_article_id': None}, ... ]}]
43
+ ```
44
+
45
+ ## Get article by ID
46
+
47
+ ```python
48
+ picnic.get_article("s1019822")
49
+ ```
50
+ ```python
51
+ {'name': 'Lavazza Caffè Crema e Aroma Bohnen', 'id': 's1019822'}
52
+ ```
53
+
54
+ ## Get article by GTIN (EAN)
55
+ ```python
56
+ picnic.get_article_by_gtin("8000070025400")
57
+ ```
58
+ ```python
59
+ {'name': 'Lavazza Caffè Crema e Aroma Bohnen', 'id': 's1019822'}
60
+ ```
61
+
62
+ ## Check cart
63
+
64
+ ```python
65
+ picnic.get_cart()
66
+ ```
67
+
68
+ ```python
69
+ {'type': 'ORDER', 'id': 'shopping_cart', 'items': [{'type': 'ORDER_LINE', 'id': '1470', 'items': [{'type': 'ORDER_ARTICLE', 'id': 's1019822', 'name': 'Lavazza Caffè Crema e Aroma Bohnen',...
70
+ ```
71
+
72
+ ## Manipulating your cart
73
+ All of these methods will return the shopping cart.
74
+
75
+ ```python
76
+ # Add product with ID "s1019822" 2x
77
+ picnic.add_product("s1019822", 2)
78
+
79
+ # Remove product with ID "s1019822" 1x
80
+ picnic.remove_product("s1019822")
81
+
82
+ # Clear your cart
83
+ picnic.clear_cart()
84
+ ```
85
+
86
+ ## See upcoming deliveries
87
+
88
+ ```python
89
+ picnic.get_current_deliveries()
90
+ ```
91
+
92
+ ```python
93
+ [{'delivery_id': 'XXYYZZ', 'creation_time': '2025-04-28T08:08:41.666+02:00', 'slot': {'slot_id': 'XXYYZZ', 'hub_id': '...
94
+ ```
95
+
96
+ ## See available delivery slots
97
+
98
+ ```python
99
+ picnic.get_delivery_slots()
100
+ ```
101
+
102
+ ```python
103
+ {'delivery_slots': [{'slot_id': 'XXYYZZ', 'hub_id': 'YYY', 'fc_id': 'FCX', 'window_start': '2025-04-29T17:15:00.000+02:00', 'window_end': '2025-04-29T19:15:00.000+02:00'...
104
+ ```
@@ -52,8 +52,10 @@ def test_get_article():
52
52
 
53
53
 
54
54
  def test_get_article_with_category_name():
55
- with pytest.raises(NotImplementedError):
56
- picnic.get_article("s1018620", add_category_name=True)
55
+ response = picnic.get_article("s1018620", add_category=True)
56
+ assert isinstance(response, dict)
57
+ assert "category" in response
58
+ assert response["category"]["name"] == "H-Milch"
57
59
 
58
60
 
59
61
  def test_get_article_by_gtin():
@@ -81,7 +83,8 @@ def test_add_product():
81
83
 
82
84
  assert isinstance(response, dict)
83
85
  assert "items" in response
84
- assert any(item["id"] == "s1018620" for item in response["items"][0]["items"])
86
+ assert any(
87
+ item["id"] == "s1018620" for item in response["items"][0]["items"])
85
88
  assert _get_amount(response, "s1018620") == 2
86
89
 
87
90
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-picnic-api2"
3
- version = "1.3.0"
3
+ version = "1.3.2"
4
4
  description = ""
5
5
  readme = "README.rst"
6
6
  license = {text = "Apache-2.0"}
@@ -8,6 +8,7 @@ from .helper import (
8
8
  _extract_search_results,
9
9
  _tree_generator,
10
10
  _url_generator,
11
+ find_nodes_by_content,
11
12
  )
12
13
  from .session import PicnicAPISession, PicnicAuthError
13
14
 
@@ -16,8 +17,8 @@ GLOBAL_GATEWAY_URL = "https://gateway-prod.global.picnicinternational.com"
16
17
  DEFAULT_COUNTRY_CODE = "NL"
17
18
  DEFAULT_API_VERSION = "15"
18
19
  _HEADERS = {
19
- "x-picnic-agent": "30100;1.15.272-15295;",
20
- "x-picnic-did": "3C417201548B2E3B",
20
+ "x-picnic-agent": "30100;1.206.1-#15408",
21
+ "x-picnic-did": "598F770380CA54B6",
21
22
  }
22
23
 
23
24
 
@@ -100,24 +101,44 @@ class PicnicAPI:
100
101
  def get_cart(self):
101
102
  return self._get("/cart")
102
103
 
103
- def get_article(self, article_id: str, add_category_name=False):
104
- if add_category_name:
105
- raise NotImplementedError()
106
- path = f"/pages/product-details-page-root?id={article_id}"
104
+ def get_article(self, article_id: str, add_category=False):
105
+ path = f"/pages/product-details-page-root?id={article_id}" + \
106
+ "&show_category_action=true"
107
107
  data = self._get(path, add_picnic_headers=True)
108
108
  article_details = []
109
- for block in data["body"]["child"]["child"]["children"]:
110
- if block["id"] == "product-details-page-root-main-container":
111
- article_details = block["pml"]["component"]["children"]
109
+
110
+ root_container = find_nodes_by_content(
111
+ data, {"id": "product-details-page-root-main-container"}, max_nodes=1)
112
+ if len(root_container) == 0:
113
+ return None
114
+
115
+ article_details = root_container[0]["pml"]["component"]["children"]
112
116
 
113
117
  if len(article_details) == 0:
114
118
  return None
115
119
 
120
+ article = {}
121
+ if add_category:
122
+ cat_node = find_nodes_by_content(
123
+ data, {"id": "category-button"}, max_nodes=1)
124
+ if len(cat_node) == 0:
125
+ raise KeyError(
126
+ f"Could not extract category from article with id {article_id}")
127
+ category_regex = re.compile(
128
+ "app\\.picnic:\\/\\/categories\\/(\\d+)\\/l2\\/(\\d+)\\/l3\\/(\\d+)")
129
+ cat_ids = category_regex.match(
130
+ cat_node[0]["pml"]["component"]["onPress"]["target"]).groups()
131
+ article["category"] = self.get_category_by_ids(
132
+ int(cat_ids[1]), int(cat_ids[2]))
133
+
116
134
  color_regex = re.compile(r"#\(#\d{6}\)")
117
- producer = re.sub(color_regex, "", str(article_details[1]["markdown"]))
118
- article_name = re.sub(color_regex, "", str(article_details[0]["markdown"]))
135
+ producer = re.sub(color_regex, "", str(
136
+ article_details[1].get("markdown", "")))
137
+ article_name = re.sub(color_regex, "", str(
138
+ article_details[0]["markdown"]))
119
139
 
120
- article = {"name": f"{producer} {article_name}", "id": article_id}
140
+ article["name"] = f"{producer} {article_name}"
141
+ article["id"] = article_id
121
142
 
122
143
  return article
123
144
 
@@ -170,6 +191,17 @@ class PicnicAPI:
170
191
  def get_categories(self, depth: int = 0):
171
192
  return self._get(f"/my_store?depth={depth}")["catalog"]
172
193
 
194
+ def get_category_by_ids(self, l2_id: int, l3_id: int):
195
+ path = "/pages/L2-category-page-root" + \
196
+ f"?category_id={l2_id}&l3_category_id={l3_id}"
197
+ data = self._get(path, add_picnic_headers=True)
198
+ nodes = find_nodes_by_content(
199
+ data, {"id": f"vertical-article-tiles-sub-header-{l3_id}"}, max_nodes=1)
200
+ if len(nodes) == 0:
201
+ raise KeyError("Could not find category with specified IDs")
202
+ return {"l2_id": l2_id, "l3_id": l3_id,
203
+ "name": nodes[0]["pml"]["component"]["accessibilityLabel"]}
204
+
173
205
  def print_categories(self, depth: int = 0):
174
206
  tree = "\n".join(_tree_generator(self.get_categories(depth=depth)))
175
207
  print(tree)
@@ -85,39 +85,66 @@ def get_image(id: str, size="regular", suffix="webp"):
85
85
  return f"{IMAGE_BASE_URL}/{id}/{size}.{suffix}"
86
86
 
87
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
+
88
123
  def _extract_search_results(raw_results, max_items: int = 10):
89
124
  """Extract search results from the nested dictionary structure returned by
90
125
  Picnic search. Number of max items can be defined to reduce excessive nested
91
126
  search"""
92
- search_results = []
93
127
 
94
128
  LOGGER.debug(f"Extracting search results from {raw_results}")
95
129
 
96
- def find_articles(node):
97
- if len(search_results) >= max_items:
98
- return
99
-
100
- content = node.get("content", {})
101
- if content.get("type") == "SELLING_UNIT_TILE" and "sellingUnit" in content:
102
- selling_unit = content["sellingUnit"]
103
- sole_article_ids = SOLE_ARTICLE_ID_PATTERN.findall(json.dumps(node))
104
- sole_article_id = sole_article_ids[0] if sole_article_ids else None
105
- result_entry = {
106
- **selling_unit,
107
- "sole_article_id": sole_article_id,
108
- }
109
- LOGGER.debug(f"Found article {result_entry}")
110
- search_results.append(result_entry)
111
-
112
- for child in node.get("children", []):
113
- find_articles(child)
114
-
115
- if "child" in node:
116
- find_articles(node.get("child"))
117
-
118
130
  body = raw_results.get("body", {})
119
- find_articles(body.get("child", {}))
131
+ nodes = find_nodes_by_content(body.get("child", {}), {
132
+ "type": "SELLING_UNIT_TILE", "sellingUnit": {}})
120
133
 
121
- LOGGER.debug(f"Found {len(search_results)}/{max_items} products after extraction")
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")
122
149
 
123
150
  return [{"items": search_results}]
@@ -8,8 +8,8 @@ from python_picnic_api2.client import DEFAULT_URL
8
8
  from python_picnic_api2.session import PicnicAuthError
9
9
 
10
10
  PICNIC_HEADERS = {
11
- "x-picnic-agent": "30100;1.15.272-15295;",
12
- "x-picnic-did": "3C417201548B2E3B",
11
+ "x-picnic-agent": "30100;1.206.1-#15408",
12
+ "x-picnic-did": "598F770380CA54B6",
13
13
  }
14
14
 
15
15
 
@@ -23,7 +23,8 @@ class TestClient(unittest.TestCase):
23
23
  return self.json_data
24
24
 
25
25
  def setUp(self) -> None:
26
- self.session_patcher = patch("python_picnic_api2.client.PicnicAPISession")
26
+ self.session_patcher = patch(
27
+ "python_picnic_api2.client.PicnicAPISession")
27
28
  self.session_mock = self.session_patcher.start()
28
29
  self.client = PicnicAPI(username="test@test.nl", password="test")
29
30
  self.expected_base_url = DEFAULT_URL.format("nl", "15")
@@ -106,6 +107,107 @@ class TestClient(unittest.TestCase):
106
107
  headers=PICNIC_HEADERS,
107
108
  )
108
109
 
110
+ def test_get_article(self):
111
+ self.session_mock().get.return_value = self.MockResponse(
112
+ {"body": {"child": {"child": {"children": [{
113
+ "id": "product-details-page-root-main-container",
114
+ "pml": {
115
+ "component": {
116
+ "children": [
117
+ {
118
+ "markdown": "#(#333333)Goede start halvarine#(#333333)",
119
+ },
120
+ {
121
+ "markdown": "Blue Band",
122
+ },
123
+
124
+ ]
125
+ }
126
+ }
127
+ }]}}}},
128
+ 200
129
+ )
130
+
131
+ article = self.client.get_article("p3f2qa")
132
+ self.session_mock().get.assert_called_with(
133
+ "https://storefront-prod.nl.picnicinternational.com/api/15/pages/product-details-page-root?id=p3f2qa&show_category_action=true",
134
+ headers=PICNIC_HEADERS,
135
+ )
136
+
137
+ self.assertEqual(
138
+ article, {'name': 'Blue Band Goede start halvarine', 'id': 'p3f2qa'})
139
+
140
+ def test_get_article_with_category(self):
141
+ self.session_mock().get.return_value = self.MockResponse(
142
+ {"body": {"child": {"child": {"children": [{
143
+ "id": "product-details-page-root-main-container",
144
+ "pml": {
145
+ "component": {
146
+ "children": [
147
+ {
148
+ "markdown": "#(#333333)Goede start halvarine#(#333333)",
149
+ },
150
+ {
151
+ "markdown": "Blue Band",
152
+ },
153
+
154
+ ]
155
+ }
156
+ }
157
+ },
158
+ {
159
+ "id": "category-button",
160
+ "pml": {"component": {"onPress": {"target": "app.picnic://categories/1000/l2/2000/l3/3000"}}}
161
+ }]}}}},
162
+ 200
163
+ )
164
+
165
+ category_patch = patch(
166
+ "python_picnic_api2.client.PicnicAPI.get_category_by_ids")
167
+ category_patch.start().return_value = {
168
+ "l2_id": 2000, "l3_id": 3000, "name": "Test"}
169
+
170
+ article = self.client.get_article("p3f2qa", True)
171
+
172
+ category_patch.stop()
173
+ self.session_mock().get.assert_called_with(
174
+ "https://storefront-prod.nl.picnicinternational.com/api/15/pages/product-details-page-root?id=p3f2qa&show_category_action=true",
175
+ headers=PICNIC_HEADERS,
176
+ )
177
+
178
+ self.assertEqual(
179
+ article, {'name': 'Blue Band Goede start halvarine', 'id': 'p3f2qa',
180
+ "category": {"l2_id": 2000, "l3_id": 3000, "name": "Test"}})
181
+
182
+ def test_get_article_with_unsupported_structure(self):
183
+ self.session_mock().get.return_value = self.MockResponse(
184
+ {"body": {"child": {"child": {"children": [{
185
+ "id": "unsupported-root-container",
186
+ "pml": {
187
+ "component": {
188
+ "children": [
189
+ {
190
+ "markdown": "#(#333333)Goede start halvarine#(#333333)",
191
+ },
192
+ {
193
+ "markdown": "Blue Band",
194
+ },
195
+
196
+ ]
197
+ }
198
+ }
199
+ }]}}}},
200
+ 200
201
+ )
202
+
203
+ article = self.client.get_article("p3f2qa")
204
+ self.session_mock().get.assert_called_with(
205
+ "https://storefront-prod.nl.picnicinternational.com/api/15/pages/product-details-page-root?id=p3f2qa&show_category_action=true",
206
+ headers=PICNIC_HEADERS,
207
+ )
208
+
209
+ assert article is None
210
+
109
211
  def test_get_article_by_gtin(self):
110
212
  self.client.get_article_by_gtin("123456789")
111
213
  self.session_mock().get.assert_called_with(
@@ -212,6 +314,30 @@ class TestClient(unittest.TestCase):
212
314
  {"type": "CATEGORY", "id": "purchases", "name": "Besteld"},
213
315
  )
214
316
 
317
+ def test_get_category_by_ids(self):
318
+ self.session_mock().get.return_value = self.MockResponse(
319
+ {"children": [
320
+ {
321
+ "id": "vertical-article-tiles-sub-header-22193",
322
+ "pml": {
323
+ "component": {
324
+ "accessibilityLabel": "Halvarine"
325
+ }
326
+ }
327
+ }
328
+ ]},
329
+ 200
330
+ )
331
+
332
+ category = self.client.get_category_by_ids(1000, 22193)
333
+ self.session_mock().get.assert_called_with(
334
+ f"{self.expected_base_url}/pages/L2-category-page-root" +
335
+ "?category_id=1000&l3_category_id=22193", headers=PICNIC_HEADERS
336
+ )
337
+
338
+ self.assertDictEqual(
339
+ category, {"name": "Halvarine", "l2_id": 1000, "l3_id": 22193})
340
+
215
341
  def test_get_auth_exception(self):
216
342
  self.session_mock().get.return_value = self.MockResponse(
217
343
  {"error": {"code": "AUTH_ERROR"}}, 400
@@ -197,7 +197,7 @@ wheels = [
197
197
 
198
198
  [[package]]
199
199
  name = "python-picnic-api2"
200
- version = "1.3.0"
200
+ version = "1.3.2"
201
201
  source = { editable = "." }
202
202
  dependencies = [
203
203
  { name = "requests" },