python-picnic-api2 1.2.4__tar.gz → 1.3.1__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.2.4 → python_picnic_api2-1.3.1}/.github/dependabot.yml +8 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/.github/workflows/ci.yaml +2 -2
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/.github/workflows/it.yaml +2 -2
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/.github/workflows/release.yml +2 -2
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/PKG-INFO +1 -1
- python_picnic_api2-1.3.1/README.md +102 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/integration_tests/test_client.py +11 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/pyproject.toml +1 -1
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/src/python_picnic_api2/client.py +25 -9
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/tests/test_client.py +17 -1
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/uv.lock +1 -1
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/.devcontainer/devcontainer.json +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/.env.example +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/.github/release.yml +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/.gitignore +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/LICENSE.md +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/README.rst +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/codecov.yml +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/integration_tests/__init__.py +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/integration_tests/test_helper.py +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/integration_tests/test_session.py +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/src/python_picnic_api2/__init__.py +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/src/python_picnic_api2/helper.py +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/src/python_picnic_api2/session.py +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/tests/__init__.py +0 -0
- {python_picnic_api2-1.2.4 → python_picnic_api2-1.3.1}/tests/test_session.py +0 -0
|
@@ -17,10 +17,10 @@ jobs:
|
|
|
17
17
|
runs-on: ubuntu-latest
|
|
18
18
|
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
21
|
|
|
22
22
|
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
-
uses: actions/setup-python@
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
24
|
with:
|
|
25
25
|
python-version: ${{ matrix.python-version }}
|
|
26
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-picnic-api2
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
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>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Python-Picnic-API
|
|
2
|
+
|
|
3
|
+
**This library is undergoing rapid changes as is the Picnic API itself. It is mainly intended for use within Home Assistant, but there are integration tests running regularly checking for failures in features not used bu the Home Assistant integration**
|
|
4
|
+
|
|
5
|
+
Fork of the Unofficial Python wrapper for the [Picnic](https://picnic.app) API. While not all API methods have been implemented yet, you'll find most of what you need to build a working application are available.
|
|
6
|
+
|
|
7
|
+
This library is not affiliated with Picnic and retrieves data from the endpoints of the mobile application. **Use at your own risk.**
|
|
8
|
+
|
|
9
|
+
## Credits
|
|
10
|
+
|
|
11
|
+
A big thanks to @MikeBrink for building the first versions of this library.
|
|
12
|
+
|
|
13
|
+
@maartenpaul and @thijmen-j continously provided fixes that were then merged into this fork.
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
16
|
+
|
|
17
|
+
The easiest way to install is directly from pip:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
$ pip install python-picnic-api2
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then create a new instance of `PicnicAPI` and login using your credentials:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from python_picnic_api2 import PicnicAPI
|
|
27
|
+
|
|
28
|
+
picnic = PicnicAPI(username='username', password='password', country_code="NL")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The country_code parameter defaults to `NL`, but you have to change it if you live in a different country than the Netherlands (ISO 3166-1 Alpha-2). This obviously only works for countries that picnic services.
|
|
32
|
+
|
|
33
|
+
## Searching for an article
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
picnic.search('coffee')
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
[{'items': [{'id': 's1019822', 'name': 'Lavazza Caffè Crema e Aroma Bohnen', 'decorators': [], 'display_price': 1799, 'image_id': 'aecbf7d3b018025ec78daf5a1099b6842a860a2e3faeceec777c13d708ce442c', 'max_count': 99, 'unit_quantity': '1kg', 'sole_article_id': None}, ... ]}]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Get article by ID
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
picnic.get_article("s1019822")
|
|
47
|
+
```
|
|
48
|
+
```python
|
|
49
|
+
{'name': 'Lavazza Caffè Crema e Aroma Bohnen', 'id': 's1019822'}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Get article by GTIN (EAN)
|
|
53
|
+
```python
|
|
54
|
+
picnic.get_article_by_gtin("8000070025400")
|
|
55
|
+
```
|
|
56
|
+
```python
|
|
57
|
+
{'name': 'Lavazza Caffè Crema e Aroma Bohnen', 'id': 's1019822'}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Check cart
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
picnic.get_cart()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
{'type': 'ORDER', 'id': 'shopping_cart', 'items': [{'type': 'ORDER_LINE', 'id': '1470', 'items': [{'type': 'ORDER_ARTICLE', 'id': 's1019822', 'name': 'Lavazza Caffè Crema e Aroma Bohnen',...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Manipulating your cart
|
|
71
|
+
All of these methods will return the shopping cart.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# Add product with ID "s1019822" 2x
|
|
75
|
+
picnic.add_product("s1019822", 2)
|
|
76
|
+
|
|
77
|
+
# Remove product with ID "s1019822" 1x
|
|
78
|
+
picnic.remove_product("s1019822")
|
|
79
|
+
|
|
80
|
+
# Clear your cart
|
|
81
|
+
picnic.clear_cart()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## See upcoming deliveries
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
picnic.get_current_deliveries()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
[{'delivery_id': 'XXYYZZ', 'creation_time': '2025-04-28T08:08:41.666+02:00', 'slot': {'slot_id': 'XXYYZZ', 'hub_id': '...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## See available delivery slots
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
picnic.get_delivery_slots()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
{'delivery_slots': [{'slot_id': 'XXYYZZ', 'hub_id': 'YYY', 'fc_id': 'FCX', 'window_start': '2025-04-29T17:15:00.000+02:00', 'window_end': '2025-04-29T19:15:00.000+02:00'...
|
|
102
|
+
```
|
|
@@ -56,6 +56,17 @@ def test_get_article_with_category_name():
|
|
|
56
56
|
picnic.get_article("s1018620", add_category_name=True)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def test_get_article_by_gtin():
|
|
60
|
+
response = picnic.get_article_by_gtin("4311501044209")
|
|
61
|
+
assert response["id"] == "s1018620"
|
|
62
|
+
assert response["name"] == "Gut&Günstig H-Milch 3,5%"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_get_article_by_gtin_unknown():
|
|
66
|
+
response = picnic.get_article_by_gtin("4311501040000")
|
|
67
|
+
assert response is None
|
|
68
|
+
|
|
69
|
+
|
|
59
70
|
def test_get_cart():
|
|
60
71
|
response = picnic.get_cart()
|
|
61
72
|
assert isinstance(response, dict)
|
|
@@ -15,6 +15,10 @@ DEFAULT_URL = "https://storefront-prod.{}.picnicinternational.com/api/{}"
|
|
|
15
15
|
GLOBAL_GATEWAY_URL = "https://gateway-prod.global.picnicinternational.com"
|
|
16
16
|
DEFAULT_COUNTRY_CODE = "NL"
|
|
17
17
|
DEFAULT_API_VERSION = "15"
|
|
18
|
+
_HEADERS = {
|
|
19
|
+
"x-picnic-agent": "30100;1.15.272-15295;",
|
|
20
|
+
"x-picnic-did": "3C417201548B2E3B",
|
|
21
|
+
}
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
class PicnicAPI:
|
|
@@ -47,14 +51,7 @@ class PicnicAPI:
|
|
|
47
51
|
url = self._base_url + path
|
|
48
52
|
|
|
49
53
|
# Make the request, add special picnic headers if needed
|
|
50
|
-
headers =
|
|
51
|
-
{
|
|
52
|
-
"x-picnic-agent": "30100;1.15.272-15295;",
|
|
53
|
-
"x-picnic-did": "3C417201548B2E3B",
|
|
54
|
-
}
|
|
55
|
-
if add_picnic_headers
|
|
56
|
-
else None
|
|
57
|
-
)
|
|
54
|
+
headers = _HEADERS if add_picnic_headers else None
|
|
58
55
|
response = self.session.get(url, headers=headers).json()
|
|
59
56
|
|
|
60
57
|
if self._contains_auth_error(response):
|
|
@@ -117,7 +114,7 @@ class PicnicAPI:
|
|
|
117
114
|
return None
|
|
118
115
|
|
|
119
116
|
color_regex = re.compile(r"#\(#\d{6}\)")
|
|
120
|
-
producer = re.sub(color_regex, "", str(article_details[1]
|
|
117
|
+
producer = re.sub(color_regex, "", str(article_details[1].get("markdown", "")))
|
|
121
118
|
article_name = re.sub(color_regex, "", str(article_details[0]["markdown"]))
|
|
122
119
|
|
|
123
120
|
article = {"name": f"{producer} {article_name}", "id": article_id}
|
|
@@ -177,5 +174,24 @@ class PicnicAPI:
|
|
|
177
174
|
tree = "\n".join(_tree_generator(self.get_categories(depth=depth)))
|
|
178
175
|
print(tree)
|
|
179
176
|
|
|
177
|
+
def get_article_by_gtin(self, etan: str, maxRedirects: int = 5):
|
|
178
|
+
# Finds the article ID for a gtin/ean (barcode).
|
|
179
|
+
|
|
180
|
+
url = "https://picnic.app/" + self._country_code.lower() + "/qr/gtin/" + etan
|
|
181
|
+
while maxRedirects > 0:
|
|
182
|
+
if url == "http://picnic.app/nl/link/store/storefront":
|
|
183
|
+
# gtin unknown
|
|
184
|
+
return None
|
|
185
|
+
r = self.session.get(url, headers=_HEADERS, allow_redirects=False)
|
|
186
|
+
maxRedirects -= 1
|
|
187
|
+
if ";id=" in r.url:
|
|
188
|
+
# found the article id
|
|
189
|
+
return self.get_article(r.url.split(";id=", 1)[1])
|
|
190
|
+
if "Location" not in r.headers:
|
|
191
|
+
# article id not found but also no futher redirect
|
|
192
|
+
return None
|
|
193
|
+
url = r.headers["Location"]
|
|
194
|
+
return None
|
|
195
|
+
|
|
180
196
|
|
|
181
197
|
__all__ = ["PicnicAPI"]
|
|
@@ -23,7 +23,8 @@ class TestClient(unittest.TestCase):
|
|
|
23
23
|
return self.json_data
|
|
24
24
|
|
|
25
25
|
def setUp(self) -> None:
|
|
26
|
-
self.session_patcher = patch(
|
|
26
|
+
self.session_patcher = patch(
|
|
27
|
+
"python_picnic_api2.client.PicnicAPISession")
|
|
27
28
|
self.session_mock = self.session_patcher.start()
|
|
28
29
|
self.client = PicnicAPI(username="test@test.nl", password="test")
|
|
29
30
|
self.expected_base_url = DEFAULT_URL.format("nl", "15")
|
|
@@ -106,6 +107,21 @@ class TestClient(unittest.TestCase):
|
|
|
106
107
|
headers=PICNIC_HEADERS,
|
|
107
108
|
)
|
|
108
109
|
|
|
110
|
+
def test_get_article(self):
|
|
111
|
+
self.client.get_article("p3f2qa")
|
|
112
|
+
self.session_mock().get.assert_called_with(
|
|
113
|
+
"https://storefront-prod.nl.picnicinternational.com/api/15/pages/product-details-page-root?id=p3f2qa",
|
|
114
|
+
headers=PICNIC_HEADERS,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def test_get_article_by_gtin(self):
|
|
118
|
+
self.client.get_article_by_gtin("123456789")
|
|
119
|
+
self.session_mock().get.assert_called_with(
|
|
120
|
+
"https://picnic.app/nl/qr/gtin/123456789",
|
|
121
|
+
headers=PICNIC_HEADERS,
|
|
122
|
+
allow_redirects=False,
|
|
123
|
+
)
|
|
124
|
+
|
|
109
125
|
def test_get_cart(self):
|
|
110
126
|
self.client.get_cart()
|
|
111
127
|
self.session_mock().get.assert_called_with(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|