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.
- python_picnic_api2/__init__.py +2 -1
- python_picnic_api2/client.py +87 -7
- python_picnic_api2/session.py +25 -1
- {python_picnic_api2-1.3.2.dist-info → python_picnic_api2-1.3.4.dist-info}/METADATA +2 -2
- python_picnic_api2-1.3.4.dist-info/RECORD +8 -0
- {python_picnic_api2-1.3.2.dist-info → python_picnic_api2-1.3.4.dist-info}/WHEEL +1 -1
- python_picnic_api2-1.3.2.dist-info/RECORD +0 -8
- {python_picnic_api2-1.3.2.dist-info → python_picnic_api2-1.3.4.dist-info}/licenses/LICENSE.md +0 -0
python_picnic_api2/__init__.py
CHANGED
python_picnic_api2/client.py
CHANGED
|
@@ -10,7 +10,12 @@ from .helper import (
|
|
|
10
10
|
_url_generator,
|
|
11
11
|
find_nodes_by_content,
|
|
12
12
|
)
|
|
13
|
-
from .session import
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" + \
|
python_picnic_api2/session.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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,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,,
|
{python_picnic_api2-1.3.2.dist-info → python_picnic_api2-1.3.4.dist-info}/licenses/LICENSE.md
RENAMED
|
File without changes
|