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.
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/ISSUE_TEMPLATE/config.yml +2 -2
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/workflows/ci.yaml +2 -2
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/workflows/it.yaml +2 -2
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/workflows/release.yml +4 -4
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/PKG-INFO +1 -1
- python_picnic_api2-1.3.2/README.md +104 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/integration_tests/test_client.py +6 -3
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/pyproject.toml +1 -1
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/src/python_picnic_api2/client.py +44 -12
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/src/python_picnic_api2/helper.py +52 -25
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/tests/test_client.py +129 -3
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/uv.lock +1 -1
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.devcontainer/devcontainer.json +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.env.example +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/dependabot.yml +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.github/release.yml +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/.gitignore +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/LICENSE.md +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/README.rst +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/codecov.yml +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/integration_tests/__init__.py +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/integration_tests/test_helper.py +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/integration_tests/test_session.py +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/src/python_picnic_api2/__init__.py +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/src/python_picnic_api2/session.py +0 -0
- {python_picnic_api2-1.3.0 → python_picnic_api2-1.3.2}/tests/__init__.py +0 -0
- {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://
|
|
11
|
-
about: Please use the Home Assistant
|
|
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@
|
|
20
|
+
- uses: actions/checkout@v5
|
|
21
21
|
|
|
22
22
|
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
-
uses: actions/setup-python@
|
|
23
|
+
uses: actions/setup-python@v6
|
|
24
24
|
with:
|
|
25
25
|
python-version: ${{ matrix.python-version }}
|
|
26
26
|
|
|
@@ -4,10 +4,10 @@ jobs:
|
|
|
4
4
|
build:
|
|
5
5
|
runs-on: ubuntu-latest
|
|
6
6
|
steps:
|
|
7
|
-
- uses: actions/checkout@
|
|
7
|
+
- uses: actions/checkout@v5
|
|
8
8
|
|
|
9
9
|
- name: Set up Python 3.11
|
|
10
|
-
uses: actions/setup-python@
|
|
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@
|
|
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.
|
|
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.
|
|
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
|
-
|
|
56
|
-
|
|
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(
|
|
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
|
|
|
@@ -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.
|
|
20
|
-
"x-picnic-did": "
|
|
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,
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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(
|
|
118
|
-
|
|
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
|
|
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
|
-
|
|
131
|
+
nodes = find_nodes_by_content(body.get("child", {}), {
|
|
132
|
+
"type": "SELLING_UNIT_TILE", "sellingUnit": {}})
|
|
120
133
|
|
|
121
|
-
|
|
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.
|
|
12
|
-
"x-picnic-did": "
|
|
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(
|
|
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
|
|
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
|