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.
@@ -1,22 +1,21 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-picnic-api2
3
- Version: 1.1.0
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
- Requires-Python: >=3.6,<4.0
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,<3.0.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
- from .helper import _tree_generator, _url_generator, _get_category_name, _extract_search_results
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, username: str = None, password: str = None,
15
- country_code: str = DEFAULT_COUNTRY_CODE, auth_token: str = None
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
- "x-picnic-agent": "30100;1.15.272-15295;",
41
- "x-picnic-did": "3C417201548B2E3B"
42
- } if add_picnic_headers else None
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 = (self._base_url if not base_url_override else base_url_override) + path
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(f"Picnic authentication error: {response['error'].get('message')}")
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 get_lists(self, list_id: str = None):
86
- if list_id:
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 get_sublist(self, list_id: str, sublist_id: str) -> list:
93
- """Get sublist.
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
- Args:
96
- list_id (str): ID of list, corresponding to requested sublist.
97
- sublist_id (str): ID of sublist.
115
+ if len(article_details) == 0:
116
+ return None
98
117
 
99
- Returns:
100
- list: Sublist result.
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
- def get_cart(self):
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
- def get_deliveries(self, summary: bool = False, data=None):
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
- return self._post("/deliveries/summary", data=data)
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 = "https://storefront-prod.nl.picnicinternational.com/static/images"
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.keys():
29
+ if "unit_quantity" in item:
29
30
  pre = f"{item['unit_quantity']} "
30
31
  after = ""
31
- if "display_price" in item.keys():
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) -> Optional[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) -> Optional[str]:
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
- "tile" in size if suffix == "webp" else True
78
- ), "webp format only supports tile sizes"
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 Picnic search.
87
- Number of max items can be defined to reduce excessive nested search"""
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 content.get("type") == "SELLING_UNIT_TILE" and "sellingUnit" in content:
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/3.9.0",
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 authentication token is set."""
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(PicnicAPISession, self).get(url, **kwargs)
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(PicnicAPISession, self).post(url, data, json, **kwargs)
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"