python-picnic-api2 1.3.2__tar.gz → 1.3.3__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.3/.envrc +1 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/.github/workflows/ci.yaml +2 -2
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/.github/workflows/it.yaml +3 -3
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/.github/workflows/release.yml +4 -4
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/PKG-INFO +2 -2
- python_picnic_api2-1.3.3/flake.lock +60 -0
- python_picnic_api2-1.3.3/flake.nix +19 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/integration_tests/test_client.py +4 -8
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/pyproject.toml +3 -3
- python_picnic_api2-1.3.3/src/python_picnic_api2/__init__.py +7 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/src/python_picnic_api2/client.py +87 -7
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/src/python_picnic_api2/session.py +25 -1
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/tests/test_client.py +93 -23
- python_picnic_api2-1.3.3/uv.lock +234 -0
- python_picnic_api2-1.3.2/src/python_picnic_api2/__init__.py +0 -6
- python_picnic_api2-1.3.2/uv.lock +0 -324
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/.devcontainer/devcontainer.json +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/.env.example +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/.github/dependabot.yml +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/.github/release.yml +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/.gitignore +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/LICENSE.md +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/README.md +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/README.rst +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/codecov.yml +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/integration_tests/__init__.py +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/integration_tests/test_helper.py +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/integration_tests/test_session.py +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/src/python_picnic_api2/helper.py +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/tests/__init__.py +0 -0
- {python_picnic_api2-1.3.2 → python_picnic_api2-1.3.3}/tests/test_session.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
use flake
|
|
@@ -11,13 +11,13 @@ jobs:
|
|
|
11
11
|
strategy:
|
|
12
12
|
fail-fast: false
|
|
13
13
|
matrix:
|
|
14
|
-
python-version: [3.
|
|
14
|
+
python-version: [3.13,3.14]
|
|
15
15
|
uv-version: [0.6.6]
|
|
16
16
|
|
|
17
17
|
runs-on: ubuntu-latest
|
|
18
18
|
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
21
|
|
|
22
22
|
- name: Set up Python ${{ matrix.python-version }}
|
|
23
23
|
uses: actions/setup-python@v6
|
|
@@ -11,12 +11,12 @@ jobs:
|
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
15
|
|
|
16
|
-
- name: Set up Python 3.
|
|
16
|
+
- name: Set up Python 3.13
|
|
17
17
|
uses: actions/setup-python@v6
|
|
18
18
|
with:
|
|
19
|
-
python-version: 3.
|
|
19
|
+
python-version: 3.13
|
|
20
20
|
|
|
21
21
|
- name: Install uv 0.6.6
|
|
22
22
|
run: |
|
|
@@ -4,12 +4,12 @@ jobs:
|
|
|
4
4
|
build:
|
|
5
5
|
runs-on: ubuntu-latest
|
|
6
6
|
steps:
|
|
7
|
-
- uses: actions/checkout@
|
|
7
|
+
- uses: actions/checkout@v6
|
|
8
8
|
|
|
9
|
-
- name: Set up Python 3.
|
|
9
|
+
- name: Set up Python 3.13
|
|
10
10
|
uses: actions/setup-python@v6
|
|
11
11
|
with:
|
|
12
|
-
python-version: 3.
|
|
12
|
+
python-version: 3.13
|
|
13
13
|
|
|
14
14
|
- name: Install uv 0.6.6
|
|
15
15
|
run: |
|
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
run: uv build
|
|
27
27
|
|
|
28
28
|
- name: Upload artifacts
|
|
29
|
-
uses: actions/upload-artifact@
|
|
29
|
+
uses: actions/upload-artifact@v6
|
|
30
30
|
with:
|
|
31
31
|
name: release-dists
|
|
32
32
|
path: dist/
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-picnic-api2
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
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,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nodes": {
|
|
3
|
+
"flake-utils": {
|
|
4
|
+
"inputs": {
|
|
5
|
+
"systems": "systems"
|
|
6
|
+
},
|
|
7
|
+
"locked": {
|
|
8
|
+
"lastModified": 1731533236,
|
|
9
|
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
10
|
+
"owner": "numtide",
|
|
11
|
+
"repo": "flake-utils",
|
|
12
|
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
13
|
+
"type": "github"
|
|
14
|
+
},
|
|
15
|
+
"original": {
|
|
16
|
+
"owner": "numtide",
|
|
17
|
+
"repo": "flake-utils",
|
|
18
|
+
"type": "github"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"nixpkgs": {
|
|
22
|
+
"locked": {
|
|
23
|
+
"lastModified": 1757194111,
|
|
24
|
+
"narHash": "sha256-4I5rftBv5fUuLkGJ5TZ+LWBz8c+tZ/ZEkUJ/uB0QCOM=",
|
|
25
|
+
"owner": "nixos",
|
|
26
|
+
"repo": "nixpkgs",
|
|
27
|
+
"rev": "64028f19ae7ae87174d168286073ec0dc2b61395",
|
|
28
|
+
"type": "github"
|
|
29
|
+
},
|
|
30
|
+
"original": {
|
|
31
|
+
"owner": "nixos",
|
|
32
|
+
"repo": "nixpkgs",
|
|
33
|
+
"type": "github"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"root": {
|
|
37
|
+
"inputs": {
|
|
38
|
+
"flake-utils": "flake-utils",
|
|
39
|
+
"nixpkgs": "nixpkgs"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"systems": {
|
|
43
|
+
"locked": {
|
|
44
|
+
"lastModified": 1681028828,
|
|
45
|
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
46
|
+
"owner": "nix-systems",
|
|
47
|
+
"repo": "default",
|
|
48
|
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
49
|
+
"type": "github"
|
|
50
|
+
},
|
|
51
|
+
"original": {
|
|
52
|
+
"owner": "nix-systems",
|
|
53
|
+
"repo": "default",
|
|
54
|
+
"type": "github"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"root": "root",
|
|
59
|
+
"version": 7
|
|
60
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
inputs = {
|
|
3
|
+
nixpkgs.url = "github:nixos/nixpkgs";
|
|
4
|
+
flake-utils.url = "github:numtide/flake-utils";
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
outputs = { self, nixpkgs, flake-utils }:
|
|
8
|
+
flake-utils.lib.eachDefaultSystem (system:
|
|
9
|
+
let pkgs = nixpkgs.legacyPackages.${system};
|
|
10
|
+
in {
|
|
11
|
+
devShell = pkgs.mkShell { buildInputs = with pkgs; [
|
|
12
|
+
python313Packages.requests
|
|
13
|
+
python313Packages.python-lsp-ruff
|
|
14
|
+
python313Packages.ruff
|
|
15
|
+
python313Packages.typing-extensions
|
|
16
|
+
];
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -147,15 +147,11 @@ def test_get_current_deliveries():
|
|
|
147
147
|
|
|
148
148
|
|
|
149
149
|
def test_get_categories():
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
with pytest.raises(NotImplementedError):
|
|
151
|
+
picnic.get_categories()
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
def test_print_categories(capsys):
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
assert isinstance(captured.out, str)
|
|
159
|
-
|
|
155
|
+
with pytest.raises(NotImplementedError):
|
|
156
|
+
picnic.print_categories()
|
|
160
157
|
|
|
161
|
-
# TODO: add test for re-logging
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-picnic-api2"
|
|
3
|
-
version = "1.3.
|
|
3
|
+
version = "1.3.3"
|
|
4
4
|
description = ""
|
|
5
5
|
readme = "README.rst"
|
|
6
6
|
license = {text = "Apache-2.0"}
|
|
@@ -12,7 +12,7 @@ authors = [
|
|
|
12
12
|
{ name = "CodeSalat", email = "pypi@codesalat.dev"}
|
|
13
13
|
]
|
|
14
14
|
urls = {homepage = "https://github.com/codesalatdev/python-picnic-api", repository = "https://github.com/codesalatdev/python-picnic-api"}
|
|
15
|
-
requires-python = ">=3.
|
|
15
|
+
requires-python = ">=3.13"
|
|
16
16
|
dependencies = [
|
|
17
17
|
"requests>=2.24.0",
|
|
18
18
|
"typing_extensions>=4.12.2"
|
|
@@ -21,7 +21,7 @@ dependencies = [
|
|
|
21
21
|
[tool.ruff]
|
|
22
22
|
line-length = 88
|
|
23
23
|
indent-width = 4
|
|
24
|
-
target-version = "
|
|
24
|
+
target-version = "py313"
|
|
25
25
|
|
|
26
26
|
[tool.ruff.lint]
|
|
27
27
|
select = [
|
|
@@ -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
|
+
error_code = response.get("error", {}).get("code")
|
|
98
|
+
return error_code == "TWO_FACTOR_AUTHENTICATION_REQUIRED"
|
|
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" + \
|
|
@@ -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"]
|
|
@@ -5,7 +5,11 @@ import pytest
|
|
|
5
5
|
|
|
6
6
|
from python_picnic_api2 import PicnicAPI
|
|
7
7
|
from python_picnic_api2.client import DEFAULT_URL
|
|
8
|
-
from python_picnic_api2.session import
|
|
8
|
+
from python_picnic_api2.session import (
|
|
9
|
+
Picnic2FAError,
|
|
10
|
+
Picnic2FARequired,
|
|
11
|
+
PicnicAuthError,
|
|
12
|
+
)
|
|
9
13
|
|
|
10
14
|
PICNIC_HEADERS = {
|
|
11
15
|
"x-picnic-agent": "30100;1.206.1-#15408",
|
|
@@ -15,9 +19,10 @@ PICNIC_HEADERS = {
|
|
|
15
19
|
|
|
16
20
|
class TestClient(unittest.TestCase):
|
|
17
21
|
class MockResponse:
|
|
18
|
-
def __init__(self, json_data, status_code):
|
|
22
|
+
def __init__(self, json_data, status_code, content=b"data"):
|
|
19
23
|
self.json_data = json_data
|
|
20
24
|
self.status_code = status_code
|
|
25
|
+
self.content = content
|
|
21
26
|
|
|
22
27
|
def json(self):
|
|
23
28
|
return self.json_data
|
|
@@ -42,6 +47,7 @@ class TestClient(unittest.TestCase):
|
|
|
42
47
|
"secret": "098f6bcd4621d373cade4e832627b4f6",
|
|
43
48
|
"client_id": 30100,
|
|
44
49
|
},
|
|
50
|
+
headers=PICNIC_HEADERS,
|
|
45
51
|
)
|
|
46
52
|
|
|
47
53
|
def test_login_auth_token(self):
|
|
@@ -292,27 +298,8 @@ class TestClient(unittest.TestCase):
|
|
|
292
298
|
)
|
|
293
299
|
|
|
294
300
|
def test_get_categories(self):
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
"type": "MY_STORE",
|
|
298
|
-
"catalog": [
|
|
299
|
-
{"type": "CATEGORY", "id": "purchases", "name": "Besteld"},
|
|
300
|
-
{"type": "CATEGORY", "id": "promotions", "name": "Acties"},
|
|
301
|
-
],
|
|
302
|
-
"user": {},
|
|
303
|
-
},
|
|
304
|
-
200,
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
categories = self.client.get_categories()
|
|
308
|
-
self.session_mock().get.assert_called_with(
|
|
309
|
-
self.expected_base_url + "/my_store?depth=0", headers=None
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
self.assertDictEqual(
|
|
313
|
-
categories[0],
|
|
314
|
-
{"type": "CATEGORY", "id": "purchases", "name": "Besteld"},
|
|
315
|
-
)
|
|
301
|
+
with pytest.raises(NotImplementedError):
|
|
302
|
+
self.client.get_categories()
|
|
316
303
|
|
|
317
304
|
def test_get_category_by_ids(self):
|
|
318
305
|
self.session_mock().get.return_value = self.MockResponse(
|
|
@@ -353,3 +340,86 @@ class TestClient(unittest.TestCase):
|
|
|
353
340
|
|
|
354
341
|
with self.assertRaises(PicnicAuthError):
|
|
355
342
|
self.client.clear_cart()
|
|
343
|
+
|
|
344
|
+
def test_login_requires_2fa(self):
|
|
345
|
+
response = {
|
|
346
|
+
"error": {
|
|
347
|
+
"code": "TWO_FACTOR_AUTHENTICATION_REQUIRED",
|
|
348
|
+
"message": "User must verify their second factor",
|
|
349
|
+
"details": {},
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
self.session_mock().post.return_value = self.MockResponse(response, 200)
|
|
353
|
+
|
|
354
|
+
client = PicnicAPI()
|
|
355
|
+
with self.assertRaises(Picnic2FARequired) as ctx:
|
|
356
|
+
client.login("test-user", "test-password")
|
|
357
|
+
self.assertEqual(
|
|
358
|
+
str(ctx.exception), "User must verify their second factor"
|
|
359
|
+
)
|
|
360
|
+
self.assertEqual(ctx.exception.response, response)
|
|
361
|
+
|
|
362
|
+
def test_generate_2fa_code(self):
|
|
363
|
+
self.session_mock().post.return_value = self.MockResponse(
|
|
364
|
+
None, 204, content=b""
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
result = self.client.generate_2fa_code()
|
|
368
|
+
self.session_mock().post.assert_called_with(
|
|
369
|
+
self.expected_base_url + "/user/2fa/generate",
|
|
370
|
+
json={"channel": "SMS"},
|
|
371
|
+
headers=PICNIC_HEADERS,
|
|
372
|
+
)
|
|
373
|
+
self.assertIsNone(result)
|
|
374
|
+
|
|
375
|
+
def test_generate_2fa_code_email(self):
|
|
376
|
+
self.session_mock().post.return_value = self.MockResponse(
|
|
377
|
+
None, 204, content=b""
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
self.client.generate_2fa_code(channel="EMAIL")
|
|
381
|
+
self.session_mock().post.assert_called_with(
|
|
382
|
+
self.expected_base_url + "/user/2fa/generate",
|
|
383
|
+
json={"channel": "EMAIL"},
|
|
384
|
+
headers=PICNIC_HEADERS,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def test_verify_2fa_code_success(self):
|
|
388
|
+
self.session_mock().post.return_value = self.MockResponse(
|
|
389
|
+
None, 204, content=b""
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
result = self.client.verify_2fa_code("123456")
|
|
393
|
+
self.session_mock().post.assert_called_with(
|
|
394
|
+
self.expected_base_url + "/user/2fa/verify",
|
|
395
|
+
json={"otp": "123456"},
|
|
396
|
+
headers=PICNIC_HEADERS,
|
|
397
|
+
)
|
|
398
|
+
self.assertIsNone(result)
|
|
399
|
+
|
|
400
|
+
def test_verify_2fa_code_invalid(self):
|
|
401
|
+
response = {
|
|
402
|
+
"error": {
|
|
403
|
+
"code": "OTP_NOT_VALID",
|
|
404
|
+
"message": "Otp is not valid",
|
|
405
|
+
"details": {},
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
self.session_mock().post.return_value = self.MockResponse(response, 200)
|
|
409
|
+
|
|
410
|
+
with self.assertRaises(Picnic2FAError) as ctx:
|
|
411
|
+
self.client.verify_2fa_code("000000")
|
|
412
|
+
self.assertEqual(str(ctx.exception), "Otp is not valid")
|
|
413
|
+
self.assertEqual(ctx.exception.code, "OTP_NOT_VALID")
|
|
414
|
+
|
|
415
|
+
def test_2fa_auth_error(self):
|
|
416
|
+
response = {
|
|
417
|
+
"error": {
|
|
418
|
+
"code": "AUTH_ERROR",
|
|
419
|
+
"message": "Authentication failed.",
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
self.session_mock().post.return_value = self.MockResponse(response, 400)
|
|
423
|
+
|
|
424
|
+
with self.assertRaises(PicnicAuthError):
|
|
425
|
+
self.client.verify_2fa_code("123456")
|