python-picnic-api2 1.1.0__tar.gz → 1.2.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.1.0 → python_picnic_api2-1.2.2}/PKG-INFO +7 -8
- python_picnic_api2-1.2.2/pyproject.toml +51 -0
- {python_picnic_api2-1.1.0 → python_picnic_api2-1.2.2}/python_picnic_api2/client.py +54 -38
- {python_picnic_api2-1.1.0 → python_picnic_api2-1.2.2}/python_picnic_api2/helper.py +24 -18
- {python_picnic_api2-1.1.0 → python_picnic_api2-1.2.2}/python_picnic_api2/session.py +6 -5
- python_picnic_api2-1.1.0/pyproject.toml +0 -22
- {python_picnic_api2-1.1.0 → python_picnic_api2-1.2.2}/LICENSE.md +0 -0
- {python_picnic_api2-1.1.0 → python_picnic_api2-1.2.2}/README.rst +0 -0
- {python_picnic_api2-1.1.0 → python_picnic_api2-1.2.2}/python_picnic_api2/__init__.py +0 -0
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-picnic-api2
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary:
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: Mike Brink
|
|
7
7
|
Author-email: mjh.brink@icloud.com
|
|
8
|
-
|
|
8
|
+
Maintainer: CodeSalat
|
|
9
|
+
Maintainer-email: pypi@codesalat.dev
|
|
10
|
+
Requires-Python: >=3.11
|
|
9
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
12
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Requires-Dist: requests (>=2.24.0
|
|
16
|
+
Requires-Dist: requests (>=2.24.0)
|
|
17
|
+
Requires-Dist: typing_extensions (>=4.12.2)
|
|
18
|
+
Project-URL: Homepage, https://github.com/codesalatdev/python-picnic-api
|
|
20
19
|
Project-URL: Repository, https://github.com/codesalatdev/python-picnic-api
|
|
21
20
|
Description-Content-Type: text/x-rst
|
|
22
21
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "python-picnic-api2"
|
|
3
|
+
version = "1.2.2"
|
|
4
|
+
description = ""
|
|
5
|
+
readme = "README.rst"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
maintainers = [
|
|
8
|
+
{ name = "CodeSalat", email = "pypi@codesalat.dev"}
|
|
9
|
+
]
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Mike Brink", email = "mjh.brink@icloud.com"},
|
|
12
|
+
{ name = "CodeSalat", email = "pypi@codesalat.dev"}
|
|
13
|
+
]
|
|
14
|
+
urls = {homepage = "https://github.com/codesalatdev/python-picnic-api", repository = "https://github.com/codesalatdev/python-picnic-api"}
|
|
15
|
+
requires-python = ">=3.11"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"requests>=2.24.0",
|
|
18
|
+
"typing_extensions>=4.12.2"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.poetry.group.dev.dependencies]
|
|
22
|
+
pytest = "^8.3"
|
|
23
|
+
ruff = "^0.9.6"
|
|
24
|
+
python-dotenv = "^0.15.0"
|
|
25
|
+
|
|
26
|
+
[tool.ruff]
|
|
27
|
+
line-length = 88
|
|
28
|
+
indent-width = 4
|
|
29
|
+
target-version = "py311"
|
|
30
|
+
|
|
31
|
+
[tool.ruff.lint]
|
|
32
|
+
select = [
|
|
33
|
+
# pycodestyle
|
|
34
|
+
"E",
|
|
35
|
+
# Pyflakes
|
|
36
|
+
"F",
|
|
37
|
+
# pyupgrade
|
|
38
|
+
"UP",
|
|
39
|
+
# flake8-bugbear
|
|
40
|
+
"B",
|
|
41
|
+
# flake8-simplify
|
|
42
|
+
"SIM",
|
|
43
|
+
# isort
|
|
44
|
+
"I",
|
|
45
|
+
# flake8-fixme
|
|
46
|
+
"FIX"
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["poetry-core"]
|
|
51
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from hashlib import md5
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import typing_extensions
|
|
5
|
+
|
|
6
|
+
from .helper import (
|
|
7
|
+
_extract_search_results,
|
|
8
|
+
_tree_generator,
|
|
9
|
+
_url_generator,
|
|
10
|
+
)
|
|
4
11
|
from .session import PicnicAPISession, PicnicAuthError
|
|
5
12
|
|
|
6
13
|
DEFAULT_URL = "https://storefront-prod.{}.picnicinternational.com/api/{}"
|
|
@@ -11,8 +18,11 @@ DEFAULT_API_VERSION = "15"
|
|
|
11
18
|
|
|
12
19
|
class PicnicAPI:
|
|
13
20
|
def __init__(
|
|
14
|
-
self,
|
|
15
|
-
|
|
21
|
+
self,
|
|
22
|
+
username: str = None,
|
|
23
|
+
password: str = None,
|
|
24
|
+
country_code: str = DEFAULT_COUNTRY_CODE,
|
|
25
|
+
auth_token: str = None,
|
|
16
26
|
):
|
|
17
27
|
self._country_code = country_code
|
|
18
28
|
self._base_url = _url_generator(
|
|
@@ -36,10 +46,14 @@ class PicnicAPI:
|
|
|
36
46
|
url = self._base_url + path
|
|
37
47
|
|
|
38
48
|
# Make the request, add special picnic headers if needed
|
|
39
|
-
headers =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
headers = (
|
|
50
|
+
{
|
|
51
|
+
"x-picnic-agent": "30100;1.15.272-15295;",
|
|
52
|
+
"x-picnic-did": "3C417201548B2E3B",
|
|
53
|
+
}
|
|
54
|
+
if add_picnic_headers
|
|
55
|
+
else None
|
|
56
|
+
)
|
|
43
57
|
response = self.session.get(url, headers=headers).json()
|
|
44
58
|
|
|
45
59
|
if self._contains_auth_error(response):
|
|
@@ -48,11 +62,14 @@ class PicnicAPI:
|
|
|
48
62
|
return response
|
|
49
63
|
|
|
50
64
|
def _post(self, path: str, data=None, base_url_override=None):
|
|
51
|
-
url = (
|
|
65
|
+
url = (base_url_override if base_url_override else self._base_url) + path
|
|
52
66
|
response = self.session.post(url, json=data).json()
|
|
53
67
|
|
|
54
68
|
if self._contains_auth_error(response):
|
|
55
|
-
raise PicnicAuthError(
|
|
69
|
+
raise PicnicAuthError(
|
|
70
|
+
f"Picnic authentication error: \
|
|
71
|
+
{response['error'].get('message')}"
|
|
72
|
+
)
|
|
56
73
|
|
|
57
74
|
return response
|
|
58
75
|
|
|
@@ -82,36 +99,28 @@ class PicnicAPI:
|
|
|
82
99
|
raw_results = self._get(path, add_picnic_headers=True)
|
|
83
100
|
return _extract_search_results(raw_results)
|
|
84
101
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
path = "/lists/" + list_id
|
|
88
|
-
else:
|
|
89
|
-
path = "/lists"
|
|
90
|
-
return self._get(path)
|
|
102
|
+
def get_cart(self):
|
|
103
|
+
return self._get("/cart")
|
|
91
104
|
|
|
92
|
-
def
|
|
93
|
-
|
|
105
|
+
def get_article(self, article_id: str, add_category_name=False):
|
|
106
|
+
if add_category_name:
|
|
107
|
+
raise NotImplementedError()
|
|
108
|
+
path = f"/pages/product-details-page-root?id={article_id}"
|
|
109
|
+
data = self._get(path, add_picnic_headers=True)
|
|
110
|
+
article_details = []
|
|
111
|
+
for block in data["body"]["child"]["child"]["children"]:
|
|
112
|
+
if block["id"] == "product-details-page-root-main-container":
|
|
113
|
+
article_details = block["pml"]["component"]["children"]
|
|
94
114
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
sublist_id (str): ID of sublist.
|
|
115
|
+
if len(article_details) == 0:
|
|
116
|
+
return None
|
|
98
117
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"""
|
|
102
|
-
return self._get(f"/lists/{list_id}?sublist={sublist_id}")
|
|
118
|
+
color_regex = re.compile(r"#\(#\d{6}\)")
|
|
119
|
+
producer = re.sub(color_regex, "", str(article_details[1]["markdown"]))
|
|
120
|
+
article_name = re.sub(color_regex, "", str(article_details[0]["markdown"]))
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
return self._get("/cart")
|
|
122
|
+
article = {"name": f"{producer} {article_name}", "id": article_id}
|
|
106
123
|
|
|
107
|
-
def get_article(self, article_id: str, add_category_name=False):
|
|
108
|
-
path = "/articles/" + article_id
|
|
109
|
-
article = self._get(path)
|
|
110
|
-
if add_category_name and "category_link" in article:
|
|
111
|
-
self.initialize_high_level_categories()
|
|
112
|
-
article.update(
|
|
113
|
-
category_name=_get_category_name(article['category_link'], self.high_level_categories)
|
|
114
|
-
)
|
|
115
124
|
return article
|
|
116
125
|
|
|
117
126
|
def get_article_category(self, article_id: str):
|
|
@@ -144,11 +153,18 @@ class PicnicAPI:
|
|
|
144
153
|
path = "/deliveries/" + delivery_id + "/position"
|
|
145
154
|
return self._get(path, add_picnic_headers=True)
|
|
146
155
|
|
|
147
|
-
|
|
156
|
+
@typing_extensions.deprecated(
|
|
157
|
+
"""The option to show unsummarized deliveries was removed by picnic.
|
|
158
|
+
The optional parameter 'summary' will be removed in the future and default
|
|
159
|
+
to True.
|
|
160
|
+
You can ignore this warning if you do not pass the 'summary' argument to
|
|
161
|
+
this function."""
|
|
162
|
+
)
|
|
163
|
+
def get_deliveries(self, summary: bool = True, data: list = None):
|
|
148
164
|
data = [] if data is None else data
|
|
149
|
-
if summary:
|
|
150
|
-
|
|
151
|
-
return self._post("/deliveries", data=data)
|
|
165
|
+
if not summary:
|
|
166
|
+
raise NotImplementedError()
|
|
167
|
+
return self._post("/deliveries/summary", data=data)
|
|
152
168
|
|
|
153
169
|
def get_current_deliveries(self):
|
|
154
170
|
return self.get_deliveries(data=["CURRENT"])
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import re
|
|
3
|
-
from typing import List, Dict, Any, Optional
|
|
4
3
|
|
|
5
4
|
# prefix components:
|
|
6
5
|
space = " "
|
|
@@ -10,7 +9,9 @@ tee = "├── "
|
|
|
10
9
|
last = "└── "
|
|
11
10
|
|
|
12
11
|
IMAGE_SIZES = ["small", "medium", "regular", "large", "extra-large"]
|
|
13
|
-
IMAGE_BASE_URL =
|
|
12
|
+
IMAGE_BASE_URL = (
|
|
13
|
+
"https://storefront-prod.nl.picnicinternational.com/static/images"
|
|
14
|
+
)
|
|
14
15
|
|
|
15
16
|
SOLE_ARTICLE_ID_PATTERN = re.compile(r'"sole_article_id":"(\w+)"')
|
|
16
17
|
|
|
@@ -22,14 +23,14 @@ def _tree_generator(response: list, prefix: str = ""):
|
|
|
22
23
|
"""
|
|
23
24
|
# response each get pointers that are ├── with a final └── :
|
|
24
25
|
pointers = [tee] * (len(response) - 1) + [last]
|
|
25
|
-
for pointer, item in zip(pointers, response):
|
|
26
|
+
for pointer, item in zip(pointers, response, strict=False):
|
|
26
27
|
if "name" in item: # print the item
|
|
27
28
|
pre = ""
|
|
28
|
-
if "unit_quantity" in item
|
|
29
|
+
if "unit_quantity" in item:
|
|
29
30
|
pre = f"{item['unit_quantity']} "
|
|
30
31
|
after = ""
|
|
31
|
-
if "display_price" in item
|
|
32
|
-
after = f" €{int(item['display_price'])/100.0:.2f}"
|
|
32
|
+
if "display_price" in item:
|
|
33
|
+
after = f" €{int(item['display_price']) / 100.0:.2f}"
|
|
33
34
|
|
|
34
35
|
yield prefix + pointer + pre + item["name"] + after
|
|
35
36
|
if "items" in item: # extend the prefix and recurse:
|
|
@@ -42,7 +43,7 @@ def _url_generator(url: str, country_code: str, api_version: str):
|
|
|
42
43
|
return url.format(country_code.lower(), api_version)
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
def _get_category_id_from_link(category_link: str) ->
|
|
46
|
+
def _get_category_id_from_link(category_link: str) -> str | None:
|
|
46
47
|
pattern = r"categories/(\d+)"
|
|
47
48
|
first_number = re.search(pattern, category_link)
|
|
48
49
|
if first_number:
|
|
@@ -52,7 +53,7 @@ def _get_category_id_from_link(category_link: str) -> Optional[str]:
|
|
|
52
53
|
return None
|
|
53
54
|
|
|
54
55
|
|
|
55
|
-
def _get_category_name(category_link: str, categories: list) ->
|
|
56
|
+
def _get_category_name(category_link: str, categories: list) -> str | None:
|
|
56
57
|
category_id = _get_category_id_from_link(category_link)
|
|
57
58
|
if category_id:
|
|
58
59
|
category = next(
|
|
@@ -73,26 +74,31 @@ def get_recipe_image(id: str, size="regular"):
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
def get_image(id: str, size="regular", suffix="webp"):
|
|
76
|
-
assert (
|
|
77
|
-
"
|
|
78
|
-
)
|
|
77
|
+
assert "tile" in size if suffix == "webp" else True, (
|
|
78
|
+
"webp format only supports tile sizes"
|
|
79
|
+
)
|
|
79
80
|
assert suffix in ["webp", "png"], "suffix must be webp or png"
|
|
80
81
|
sizes = IMAGE_SIZES + [f"tile-{size}" for size in IMAGE_SIZES]
|
|
81
82
|
|
|
82
83
|
assert size in sizes, "size must be one of: " + ", ".join(sizes)
|
|
83
84
|
return f"{IMAGE_BASE_URL}/{id}/{size}.{suffix}"
|
|
84
85
|
|
|
86
|
+
|
|
85
87
|
def _extract_search_results(raw_results, max_items: int = 10):
|
|
86
|
-
"""Extract search results from the nested dictionary structure returned by
|
|
87
|
-
Number of max items can be defined to reduce excessive nested
|
|
88
|
+
"""Extract search results from the nested dictionary structure returned by
|
|
89
|
+
Picnic search. Number of max items can be defined to reduce excessive nested
|
|
90
|
+
search"""
|
|
88
91
|
search_results = []
|
|
89
|
-
|
|
92
|
+
|
|
90
93
|
def find_articles(node):
|
|
91
94
|
if len(search_results) >= max_items:
|
|
92
95
|
return
|
|
93
|
-
|
|
96
|
+
|
|
94
97
|
content = node.get("content", {})
|
|
95
|
-
if
|
|
98
|
+
if (
|
|
99
|
+
content.get("type") == "SELLING_UNIT_TILE"
|
|
100
|
+
and "sellingUnit" in content
|
|
101
|
+
):
|
|
96
102
|
selling_unit = content["sellingUnit"]
|
|
97
103
|
sole_article_ids = SOLE_ARTICLE_ID_PATTERN.findall(json.dumps(node))
|
|
98
104
|
sole_article_id = sole_article_ids[0] if sole_article_ids else None
|
|
@@ -101,11 +107,11 @@ def _extract_search_results(raw_results, max_items: int = 10):
|
|
|
101
107
|
"sole_article_id": sole_article_id,
|
|
102
108
|
}
|
|
103
109
|
search_results.append(result_entry)
|
|
104
|
-
|
|
110
|
+
|
|
105
111
|
for child in node.get("children", []):
|
|
106
112
|
find_articles(child)
|
|
107
113
|
|
|
108
114
|
body = raw_results.get("body", {})
|
|
109
115
|
find_articles(body.get("child", {}))
|
|
110
|
-
|
|
116
|
+
|
|
111
117
|
return [{"items": search_results}]
|
|
@@ -14,15 +14,16 @@ class PicnicAPISession(Session):
|
|
|
14
14
|
|
|
15
15
|
self.headers.update(
|
|
16
16
|
{
|
|
17
|
-
"User-Agent": "okhttp/
|
|
17
|
+
"User-Agent": "okhttp/4.9.0",
|
|
18
18
|
"Content-Type": "application/json; charset=UTF-8",
|
|
19
|
-
self.AUTH_HEADER: self._auth_token
|
|
19
|
+
self.AUTH_HEADER: self._auth_token,
|
|
20
20
|
}
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
@property
|
|
24
24
|
def authenticated(self):
|
|
25
|
-
"""Returns whether the user is authenticated by checking if the
|
|
25
|
+
"""Returns whether the user is authenticated by checking if the
|
|
26
|
+
authentication token is set."""
|
|
26
27
|
return bool(self._auth_token)
|
|
27
28
|
|
|
28
29
|
@property
|
|
@@ -38,14 +39,14 @@ class PicnicAPISession(Session):
|
|
|
38
39
|
|
|
39
40
|
def get(self, url, **kwargs) -> Response:
|
|
40
41
|
"""Do a GET request and update the auth token if set."""
|
|
41
|
-
response = super(
|
|
42
|
+
response = super().get(url, **kwargs)
|
|
42
43
|
self._update_auth_token(response.headers.get(self.AUTH_HEADER))
|
|
43
44
|
|
|
44
45
|
return response
|
|
45
46
|
|
|
46
47
|
def post(self, url, data=None, json=None, **kwargs) -> Response:
|
|
47
48
|
"""Do a POST request and update the auth token if set."""
|
|
48
|
-
response = super(
|
|
49
|
+
response = super().post(url, data, json, **kwargs)
|
|
49
50
|
self._update_auth_token(response.headers.get(self.AUTH_HEADER))
|
|
50
51
|
|
|
51
52
|
return response
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "python-picnic-api2"
|
|
3
|
-
version = "1.1.0"
|
|
4
|
-
description = ""
|
|
5
|
-
license = "Apache-2.0"
|
|
6
|
-
authors = ["Mike Brink <mjh.brink@icloud.com>", "Noah Groß <pypi@codesalat.dev>"]
|
|
7
|
-
readme = "README.rst"
|
|
8
|
-
repository = "https://github.com/codesalatdev/python-picnic-api"
|
|
9
|
-
|
|
10
|
-
[tool.poetry.dependencies]
|
|
11
|
-
python = "^3.6"
|
|
12
|
-
requests = "^2.24.0"
|
|
13
|
-
|
|
14
|
-
[tool.poetry.dev-dependencies]
|
|
15
|
-
pytest = "^5.2"
|
|
16
|
-
flake8 = "^3.8.3"
|
|
17
|
-
black = {version = "^19.10b0", allow-prereleases = true}
|
|
18
|
-
python-dotenv = "^0.15.0"
|
|
19
|
-
|
|
20
|
-
[build-system]
|
|
21
|
-
requires = ["poetry-core"]
|
|
22
|
-
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|