python-picnic-api2 1.3.2__py3-none-any.whl → 1.3.4__py3-none-any.whl

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,6 +1,7 @@
1
1
  from .client import PicnicAPI
2
+ from .session import Picnic2FAError, Picnic2FARequired
2
3
 
3
- __all__ = ["PicnicAPI"]
4
+ __all__ = ["PicnicAPI", "Picnic2FAError", "Picnic2FARequired"]
4
5
  __title__ = "python-picnic-api"
5
6
  __version__ = "1.1.0"
6
7
  __author__ = "Mike Brink"
@@ -10,7 +10,12 @@ from .helper import (
10
10
  _url_generator,
11
11
  find_nodes_by_content,
12
12
  )
13
- from .session import PicnicAPISession, PicnicAuthError
13
+ from .session import (
14
+ Picnic2FAError,
15
+ Picnic2FARequired,
16
+ PicnicAPISession,
17
+ PicnicAuthError,
18
+ )
14
19
 
15
20
  DEFAULT_URL = "https://storefront-prod.{}.picnicinternational.com/api/{}"
16
21
  GLOBAL_GATEWAY_URL = "https://gateway-prod.global.picnicinternational.com"
@@ -60,14 +65,18 @@ class PicnicAPI:
60
65
 
61
66
  return response
62
67
 
63
- def _post(self, path: str, data=None, base_url_override=None):
68
+ def _post(
69
+ self, path: str, data=None, base_url_override=None, add_picnic_headers=False
70
+ ):
64
71
  url = (base_url_override if base_url_override else self._base_url) + path
65
- response = self.session.post(url, json=data).json()
72
+ kwargs = {"json": data}
73
+ if add_picnic_headers:
74
+ kwargs["headers"] = _HEADERS
75
+ response = self.session.post(url, **kwargs).json()
66
76
 
67
77
  if self._contains_auth_error(response):
68
78
  raise PicnicAuthError(
69
- f"Picnic authentication error: \
70
- {response['error'].get('message')}"
79
+ f"Picnic authentication error: {response['error'].get('message')}"
71
80
  )
72
81
 
73
82
  return response
@@ -80,12 +89,82 @@ class PicnicAPI:
80
89
  error_code = response.setdefault("error", {}).get("code")
81
90
  return error_code == "AUTH_ERROR" or error_code == "AUTH_INVALID_CRED"
82
91
 
92
+ @staticmethod
93
+ def _requires_2fa(response):
94
+ if not isinstance(response, dict):
95
+ return False
96
+
97
+ return "second_factor_authentication_required" in response \
98
+ and response["second_factor_authentication_required"] is True
99
+
83
100
  def login(self, username: str, password: str):
84
101
  path = "/user/login"
85
102
  secret = md5(password.encode("utf-8")).hexdigest()
86
103
  data = {"key": username, "secret": secret, "client_id": 30100}
87
104
 
88
- return self._post(path, data)
105
+ response = self._post(path, data, add_picnic_headers=True)
106
+
107
+ if self._requires_2fa(response):
108
+ raise Picnic2FARequired(
109
+ message=response.get("error", {}).get(
110
+ "message", "Two-factor authentication required"
111
+ ),
112
+ response=response,
113
+ )
114
+
115
+ return response
116
+
117
+ def _post_2fa(self, path: str, data=None):
118
+ """POST for 2FA endpoints that may return empty (204) or JSON error bodies."""
119
+ url = self._base_url + path
120
+ response = self.session.post(url, json=data, headers=_HEADERS)
121
+
122
+ if response.status_code == 204 or not response.content:
123
+ return None
124
+
125
+ json_body = response.json()
126
+
127
+ # This should not happen because password auth is already done
128
+ # at this point, but just in case.
129
+ if self._contains_auth_error(json_body):
130
+ raise PicnicAuthError(
131
+ f"Picnic authentication error: {json_body['error'].get('message')}"
132
+ )
133
+
134
+ error = json_body.get("error", {})
135
+ if error.get("code"):
136
+ raise Picnic2FAError(
137
+ message=error.get("message", "Two-factor authentication failed"),
138
+ code=error["code"],
139
+ )
140
+
141
+ return json_body
142
+
143
+ def generate_2fa_code(self, channel: str = "SMS"):
144
+ """Request a 2FA code to be sent via the specified channel.
145
+
146
+ Args:
147
+ channel: The delivery channel ("SMS" or "EMAIL").
148
+
149
+ Raises:
150
+ Picnic2FAError: If the server returns an error (e.g. invalid channel).
151
+ """
152
+ path = "/user/2fa/generate"
153
+ data = {"channel": channel}
154
+ self._post_2fa(path, data)
155
+
156
+ def verify_2fa_code(self, code: str):
157
+ """Verify the 2FA code to complete authentication.
158
+
159
+ Args:
160
+ code: The OTP code received via SMS or email.
161
+
162
+ Raises:
163
+ Picnic2FAError: If the OTP code is invalid.
164
+ """
165
+ path = "/user/2fa/verify"
166
+ data = {"otp": code}
167
+ self._post_2fa(path, data)
89
168
 
90
169
  def logged_in(self):
91
170
  return self.session.authenticated
@@ -189,7 +268,8 @@ class PicnicAPI:
189
268
  return self.get_deliveries(data=["CURRENT"])
190
269
 
191
270
  def get_categories(self, depth: int = 0):
192
- return self._get(f"/my_store?depth={depth}")["catalog"]
271
+ raise NotImplementedError("This endpoint has been removed by picnic\
272
+ and is no longer functional.")
193
273
 
194
274
  def get_category_by_ids(self, l2_id: int, l3_id: int):
195
275
  path = "/pages/L2-category-page-root" + \
@@ -5,6 +5,30 @@ class PicnicAuthError(Exception):
5
5
  """Indicates an error when authenticating to the Picnic API."""
6
6
 
7
7
 
8
+ class Picnic2FARequired(Exception):
9
+ """Indicates that two-factor authentication is required."""
10
+
11
+ def __init__(
12
+ self,
13
+ message: str = "Two-factor authentication required",
14
+ response: dict = None,
15
+ ):
16
+ super().__init__(message)
17
+ self.response = response or {}
18
+
19
+
20
+ class Picnic2FAError(Exception):
21
+ """Indicates an error during two-factor authentication (e.g. invalid OTP)."""
22
+
23
+ def __init__(
24
+ self,
25
+ message: str = "Two-factor authentication failed",
26
+ code: str = None,
27
+ ):
28
+ super().__init__(message)
29
+ self.code = code
30
+
31
+
8
32
  class PicnicAPISession(Session):
9
33
  AUTH_HEADER = "x-picnic-auth"
10
34
 
@@ -52,4 +76,4 @@ class PicnicAPISession(Session):
52
76
  return response
53
77
 
54
78
 
55
- __all__ = ["PicnicAuthError", "PicnicAPISession"]
79
+ __all__ = ["PicnicAuthError", "Picnic2FARequired", "Picnic2FAError", "PicnicAPISession"]
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-picnic-api2
3
- Version: 1.3.2
3
+ Version: 1.3.4
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>
7
7
  Maintainer-email: CodeSalat <pypi@codesalat.dev>
8
8
  License: Apache-2.0
9
9
  License-File: LICENSE.md
10
- Requires-Python: >=3.11
10
+ Requires-Python: >=3.13
11
11
  Requires-Dist: requests>=2.24.0
12
12
  Requires-Dist: typing-extensions>=4.12.2
13
13
  Description-Content-Type: text/x-rst
@@ -0,0 +1,8 @@
1
+ python_picnic_api2/__init__.py,sha256=_yT2GUMnhnNIGlYDzBxerDtlN08PzUOG00mhNCoPlGU,229
2
+ python_picnic_api2/client.py,sha256=Bxp4WrABcGL7lheDsJRu0gSabq7AcTdsADGuikFL0bI,10804
3
+ python_picnic_api2/helper.py,sha256=_cHhC1YCzZj7FlnR5tdvu825UOB5etcIdm23lezrw8Y,5022
4
+ python_picnic_api2/session.py,sha256=2nhlyJ2MXR1yHQRxw85B-KXF4zt8L5__KrI-T_VvURg,2388
5
+ python_picnic_api2-1.3.4.dist-info/METADATA,sha256=FOrKfeXQ9L110lcNOaX7oCHJvcquZyj4D3WrYyU-Y6c,3370
6
+ python_picnic_api2-1.3.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ python_picnic_api2-1.3.4.dist-info/licenses/LICENSE.md,sha256=CUvt2ilcBwKAfRAnoA4RNPTLcdUj3En21LPK9YkFMGg,11358
8
+ python_picnic_api2-1.3.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.29.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,8 +0,0 @@
1
- python_picnic_api2/__init__.py,sha256=smjQNoqBSj_opj6Kk5Q3e4q-rW5Optnpuo4YZuEngzs,135
2
- python_picnic_api2/client.py,sha256=oRGCTlzuz9xzBJUH4iL2jjQzqURxd-C0WK_DdJQbRjQ,8282
3
- python_picnic_api2/helper.py,sha256=_cHhC1YCzZj7FlnR5tdvu825UOB5etcIdm23lezrw8Y,5022
4
- python_picnic_api2/session.py,sha256=UVUaKNyyz2S6RQGE9o2CNXxcGiAn8Dy9TJv7PTl_JbY,1741
5
- python_picnic_api2-1.3.2.dist-info/METADATA,sha256=Q7-5cHhVtzG6W3T-ydSJlB3IAkNls0PrdHRVJmzj-00,3370
6
- python_picnic_api2-1.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- python_picnic_api2-1.3.2.dist-info/licenses/LICENSE.md,sha256=CUvt2ilcBwKAfRAnoA4RNPTLcdUj3En21LPK9YkFMGg,11358
8
- python_picnic_api2-1.3.2.dist-info/RECORD,,