bilkatogo 0.2.0__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.
- bilkatogo-0.2.0/LICENSE +21 -0
- bilkatogo-0.2.0/PKG-INFO +258 -0
- bilkatogo-0.2.0/README.md +235 -0
- bilkatogo-0.2.0/pyproject.toml +111 -0
- bilkatogo-0.2.0/pyproject.toml.orig +84 -0
- bilkatogo-0.2.0/src/bilkatogo/__init__.py +58 -0
- bilkatogo-0.2.0/src/bilkatogo/__main__.py +21 -0
- bilkatogo-0.2.0/src/bilkatogo/cli.py +398 -0
- bilkatogo-0.2.0/src/bilkatogo/client.py +294 -0
- bilkatogo-0.2.0/src/bilkatogo/config.py +31 -0
- bilkatogo-0.2.0/src/bilkatogo/const.py +23 -0
- bilkatogo-0.2.0/src/bilkatogo/exceptions.py +22 -0
- bilkatogo-0.2.0/src/bilkatogo/models/__init__.py +14 -0
- bilkatogo-0.2.0/src/bilkatogo/models/base.py +18 -0
- bilkatogo-0.2.0/src/bilkatogo/models/money.py +6 -0
- bilkatogo-0.2.0/src/bilkatogo/models/order.py +166 -0
- bilkatogo-0.2.0/src/bilkatogo/models/order_details.py +187 -0
- bilkatogo-0.2.0/src/bilkatogo/models/product.py +77 -0
- bilkatogo-0.2.0/src/bilkatogo/py.typed +0 -0
- bilkatogo-0.2.0/src/bilkatogo/token_storage.py +40 -0
bilkatogo-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nick Nissen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bilkatogo-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bilkatogo
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Async client and CLI for tracking BilkaToGo orders.
|
|
5
|
+
Keywords: bilka,bilkatogo,danish,groceries,api,async
|
|
6
|
+
Author: Nick Nissen
|
|
7
|
+
Author-email: Nick Nissen <nickknissen@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Requires-Dist: httpx>=0.28.1
|
|
15
|
+
Requires-Dist: tzdata ; sys_platform == 'win32'
|
|
16
|
+
Requires-Dist: typer>=0.27.3,<0.28 ; extra == 'cli'
|
|
17
|
+
Requires-Python: >=3.14
|
|
18
|
+
Project-URL: Homepage, https://github.com/nickknissen/bilkatogo
|
|
19
|
+
Project-URL: Repository, https://github.com/nickknissen/bilkatogo
|
|
20
|
+
Project-URL: Issues, https://github.com/nickknissen/bilkatogo/issues
|
|
21
|
+
Provides-Extra: cli
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# bilkatogo
|
|
25
|
+
|
|
26
|
+
Track your [BilkaToGo](https://www.bilkatogo.dk) grocery orders: an async Python library, a
|
|
27
|
+
command line tool and a Home Assistant integration that follow your order from the moment it is
|
|
28
|
+
placed until you pick it up, including which products Bilka substituted.
|
|
29
|
+
|
|
30
|
+
> **Unofficial.** This project is not affiliated with or endorsed by Bilka or Salling Group. It
|
|
31
|
+
> uses the same private web API as the bilkatogo.dk website, logging in with your email and
|
|
32
|
+
> password. That API can change or break at any time.
|
|
33
|
+
|
|
34
|
+
The library, the CLI and the Home Assistant integration are released together and always share
|
|
35
|
+
the same version.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Library only (it depends on httpx, plus tzdata on Windows):
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pip install bilkatogo
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
With the command line tool:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pip install 'bilkatogo[cli]'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or run the CLI without installing it:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
uvx --from 'bilkatogo[cli]' bilkatogo --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Home Assistant: add the integration from the Home Assistant Marketplace (HACS) by searching
|
|
58
|
+
for **BilkaToGo**, or add `https://github.com/nickknissen/bilkatogo` as a custom repository of
|
|
59
|
+
type Integration. Restart Home Assistant afterwards.
|
|
60
|
+
|
|
61
|
+
## Command line
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
bilkatogo login # log in and store the session
|
|
65
|
+
bilkatogo orders --limit 10 # list your most recent orders
|
|
66
|
+
bilkatogo order # show the Current Order with its details
|
|
67
|
+
bilkatogo order 900000000001 # show one order by Order Number
|
|
68
|
+
bilkatogo pickup-points # list Pickup Points and their Store, * marks your Cart's
|
|
69
|
+
bilkatogo search skyr # Products with Price, stock and Offer at your Cart's Store
|
|
70
|
+
bilkatogo search skyr --pickup-point "Bilka Vejle" --limit 5
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Every command accepts `--json` for machine-readable output.
|
|
74
|
+
|
|
75
|
+
Credentials are looked up in this order:
|
|
76
|
+
|
|
77
|
+
1. `--email` and `--password`
|
|
78
|
+
2. the `BILKATOGO_EMAIL` and `BILKATOGO_PASSWORD` environment variables
|
|
79
|
+
3. `config.json` (with `email` and `password` keys) in the user config directory
|
|
80
|
+
(`~/.config/bilkatogo` on Linux, `%APPDATA%\bilkatogo` on Windows,
|
|
81
|
+
`~/Library/Application Support/bilkatogo` on macOS); set `BILKATOGO_CONFIG_DIR` to use
|
|
82
|
+
another directory
|
|
83
|
+
4. an interactive prompt
|
|
84
|
+
|
|
85
|
+
The session is stored as `session.json` in the same directory, so later commands don't log in
|
|
86
|
+
again.
|
|
87
|
+
|
|
88
|
+
The Current Order is the unfinished order with the earliest pickup slot or, when every order
|
|
89
|
+
is finished, the most recently finished one.
|
|
90
|
+
|
|
91
|
+
## Library
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import asyncio
|
|
95
|
+
|
|
96
|
+
from bilkatogo import BilkaToGoClient, FileTokenStorage
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def main() -> None:
|
|
100
|
+
async with BilkaToGoClient(
|
|
101
|
+
"you@example.com",
|
|
102
|
+
"your-password",
|
|
103
|
+
token_storage=FileTokenStorage("session.json"),
|
|
104
|
+
) as client:
|
|
105
|
+
for order in await client.get_orders(limit=5):
|
|
106
|
+
print(order.order_number, order.status, order.pickup_slot)
|
|
107
|
+
|
|
108
|
+
current = await client.get_current_order()
|
|
109
|
+
if current:
|
|
110
|
+
if current.pickup_code:
|
|
111
|
+
print("Pickup Code:", current.pickup_code.code)
|
|
112
|
+
for substitution in current.substitutions:
|
|
113
|
+
print(substitution.original_product, "->", substitution.replacement_product)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `BilkaToGoClient(email, password, *, token_storage=None, httpx_client=None)` is an async
|
|
120
|
+
context manager. Pass your own `httpx.AsyncClient` to control timeouts or proxies; the
|
|
121
|
+
client only closes clients it created itself.
|
|
122
|
+
- `login()`, `get_orders(limit=5)`, `get_order(order_number)` and `get_current_order()` are the
|
|
123
|
+
public operations. The client logs in on demand, refreshes the session before it expires and
|
|
124
|
+
logs in again when Bilka rejects it.
|
|
125
|
+
- `token_storage` is any `TokenStorage` (async `load()` and `save(data)`). `FileTokenStorage`
|
|
126
|
+
stores the session in a JSON file.
|
|
127
|
+
- Errors derive from `BilkaToGoError`: `InvalidCredentialsError`, `BilkaUnreachableError` and
|
|
128
|
+
`UnexpectedResponseError`.
|
|
129
|
+
- `Order.status` is an `OrderStatus` (Processing, Received, Packed, Picked Up, Not Collected,
|
|
130
|
+
Cancelled, Sold Out, Unknown), using the same words as the website. The raw Bilka status code
|
|
131
|
+
is kept in `status_code`. The mapping is listed in [GLOSSARY.md](GLOSSARY.md).
|
|
132
|
+
- `PickupSlot.starts_at` and `ends_at` are timezone-aware datetimes in Europe/Copenhagen.
|
|
133
|
+
- Amounts are `Decimal` kroner.
|
|
134
|
+
- `OrderDetails.order_total` is the Order Total, broken down into `deposit`, `service_fee`,
|
|
135
|
+
`packaging`, `delivery_fee` and `discount`. `ordered_total` is what the Order cost when it was
|
|
136
|
+
placed, looked up in the order history (`None` when it is not among the latest 20 Orders).
|
|
137
|
+
Pass an `Order` to `get_order` instead of its number to skip that lookup.
|
|
138
|
+
|
|
139
|
+
## Home Assistant
|
|
140
|
+
|
|
141
|
+
1. Install the integration (see [Installation](#installation)) and restart Home Assistant.
|
|
142
|
+
2. Go to **Settings > Devices & services > Add integration** and pick **BilkaToGo**.
|
|
143
|
+
3. Sign in with the email and password you use on bilkatogo.dk. Add the integration once per
|
|
144
|
+
account.
|
|
145
|
+
|
|
146
|
+
If the password changes, Home Assistant asks you to sign in again.
|
|
147
|
+
|
|
148
|
+
The integration polls Bilka every 60 minutes, and every 5 minutes from 3 hours before the
|
|
149
|
+
pickup slot starts until the order is picked up, not collected or cancelled.
|
|
150
|
+
|
|
151
|
+
### Entities
|
|
152
|
+
|
|
153
|
+
All entities describe the Current Order.
|
|
154
|
+
|
|
155
|
+
| Entity | State | Attributes |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| Order Status | Processing, Received, Packed, Picked Up, Not Collected, Cancelled, Sold Out or Unknown | Bilka status code |
|
|
158
|
+
| Substitutions | Number of substituted products | List of Original Product, Replacement Product and the Replacement Product's quantity |
|
|
159
|
+
| Missing Items | Number of products packed short without a replacement | List of Product, Article Number, ordered and missing quantity |
|
|
160
|
+
| Order Total | What you pay for the order now (DKK) | Order Number, Ordered Total, difference from the Ordered Total (negative when you pay less), Deposit, Service Fee, Packaging, Delivery Fee, Discount |
|
|
161
|
+
| Pickup Slot | Start of the pickup slot (timestamp, Europe/Copenhagen) | Order Number, Order Total (DKK), Pickup Slot end |
|
|
162
|
+
| Pickup Code | The code you enter at check-in | Order Number, Order Total (DKK) |
|
|
163
|
+
| Pickup Point | The Bilka store you pick up from | Order Number, Order Total (DKK) |
|
|
164
|
+
|
|
165
|
+
Substitutions, Missing Items, the Pickup Code and the Order Total breakdown (Deposit, Service
|
|
166
|
+
Fee, Packaging, Delivery Fee, Discount) are fetched once the order is packed. Before that the
|
|
167
|
+
Order Total is Bilka's estimate from the order history.
|
|
168
|
+
|
|
169
|
+
An Order Status of Unknown (`unrecognized`) means Bilka sent a status code the integration does
|
|
170
|
+
not know yet; the code is in the attribute. Without any order the entities have no state.
|
|
171
|
+
|
|
172
|
+
### Event `bilkatogo_substitution`
|
|
173
|
+
|
|
174
|
+
Fired once for every new substitution, with this data:
|
|
175
|
+
|
|
176
|
+
| Field | Example |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `order_number` | `900000000001` |
|
|
179
|
+
| `original_product` | `Arla minimælk 1 l` |
|
|
180
|
+
| `replacement_product` | `Øko minimælk 1 l` |
|
|
181
|
+
| `quantity` | `2` |
|
|
182
|
+
|
|
183
|
+
Substitutions that already fired are remembered across restarts and reloads. An automation
|
|
184
|
+
that sends a notification:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
automation:
|
|
188
|
+
- alias: BilkaToGo substitution
|
|
189
|
+
triggers:
|
|
190
|
+
- trigger: event
|
|
191
|
+
event_type: bilkatogo_substitution
|
|
192
|
+
actions:
|
|
193
|
+
- action: notify.notify
|
|
194
|
+
data:
|
|
195
|
+
message: >
|
|
196
|
+
{{ trigger.event.data.original_product }} was replaced by
|
|
197
|
+
{{ trigger.event.data.replacement_product }}
|
|
198
|
+
({{ trigger.event.data.quantity }}).
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Event `bilkatogo_missing_item`
|
|
202
|
+
|
|
203
|
+
Fired once for every new Missing Item, with this data:
|
|
204
|
+
|
|
205
|
+
| Field | Example |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `order_number` | `900000000003` |
|
|
208
|
+
| `product` | `Skyr` |
|
|
209
|
+
| `article_number` | `932058` |
|
|
210
|
+
| `ordered_quantity` | `4` |
|
|
211
|
+
| `missing_quantity` | `2` |
|
|
212
|
+
|
|
213
|
+
Missing Items that already fired are remembered across restarts and reloads. An automation
|
|
214
|
+
that sends a notification:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
automation:
|
|
218
|
+
- alias: BilkaToGo missing item
|
|
219
|
+
triggers:
|
|
220
|
+
- trigger: event
|
|
221
|
+
event_type: bilkatogo_missing_item
|
|
222
|
+
actions:
|
|
223
|
+
- action: notify.notify
|
|
224
|
+
data:
|
|
225
|
+
message: >
|
|
226
|
+
{{ trigger.event.data.product }}: {{ trigger.event.data.missing_quantity }}
|
|
227
|
+
of {{ trigger.event.data.ordered_quantity }} missing.
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
Library and CLI:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
uv run ruff format --check . && uv run ruff check . && uv run ty check && uv run pytest
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Home Assistant tests use their own dependency group, because
|
|
239
|
+
`pytest-homeassistant-custom-component` pins its own pytest and httpx. Keep them in a
|
|
240
|
+
separate virtual environment so the library environment stays untouched:
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
UV_PROJECT_ENVIRONMENT=.venv-ha uv run --no-default-groups --group ha pytest tests/ha
|
|
244
|
+
UV_PROJECT_ENVIRONMENT=.venv-ha uv run --no-default-groups --group ha ty check custom_components tests/ha
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Home Assistant does not run on Windows; use Linux, WSL or CI for these.
|
|
248
|
+
|
|
249
|
+
The Home Assistant tests run the real library against the fake Bilka backend in
|
|
250
|
+
`tests/fake_bilka.py`. The integration creates its httpx client with Home Assistant's
|
|
251
|
+
`create_async_httpx_client`, and `tests/ha/conftest.py` routes every request at the
|
|
252
|
+
transport level to the fake backend with `respx`, so nothing in the integration is patched.
|
|
253
|
+
|
|
254
|
+
Releasing is described in [AGENTS.md](AGENTS.md#releasing).
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# bilkatogo
|
|
2
|
+
|
|
3
|
+
Track your [BilkaToGo](https://www.bilkatogo.dk) grocery orders: an async Python library, a
|
|
4
|
+
command line tool and a Home Assistant integration that follow your order from the moment it is
|
|
5
|
+
placed until you pick it up, including which products Bilka substituted.
|
|
6
|
+
|
|
7
|
+
> **Unofficial.** This project is not affiliated with or endorsed by Bilka or Salling Group. It
|
|
8
|
+
> uses the same private web API as the bilkatogo.dk website, logging in with your email and
|
|
9
|
+
> password. That API can change or break at any time.
|
|
10
|
+
|
|
11
|
+
The library, the CLI and the Home Assistant integration are released together and always share
|
|
12
|
+
the same version.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Library only (it depends on httpx, plus tzdata on Windows):
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pip install bilkatogo
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
With the command line tool:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pip install 'bilkatogo[cli]'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or run the CLI without installing it:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
uvx --from 'bilkatogo[cli]' bilkatogo --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Home Assistant: add the integration from the Home Assistant Marketplace (HACS) by searching
|
|
35
|
+
for **BilkaToGo**, or add `https://github.com/nickknissen/bilkatogo` as a custom repository of
|
|
36
|
+
type Integration. Restart Home Assistant afterwards.
|
|
37
|
+
|
|
38
|
+
## Command line
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
bilkatogo login # log in and store the session
|
|
42
|
+
bilkatogo orders --limit 10 # list your most recent orders
|
|
43
|
+
bilkatogo order # show the Current Order with its details
|
|
44
|
+
bilkatogo order 900000000001 # show one order by Order Number
|
|
45
|
+
bilkatogo pickup-points # list Pickup Points and their Store, * marks your Cart's
|
|
46
|
+
bilkatogo search skyr # Products with Price, stock and Offer at your Cart's Store
|
|
47
|
+
bilkatogo search skyr --pickup-point "Bilka Vejle" --limit 5
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Every command accepts `--json` for machine-readable output.
|
|
51
|
+
|
|
52
|
+
Credentials are looked up in this order:
|
|
53
|
+
|
|
54
|
+
1. `--email` and `--password`
|
|
55
|
+
2. the `BILKATOGO_EMAIL` and `BILKATOGO_PASSWORD` environment variables
|
|
56
|
+
3. `config.json` (with `email` and `password` keys) in the user config directory
|
|
57
|
+
(`~/.config/bilkatogo` on Linux, `%APPDATA%\bilkatogo` on Windows,
|
|
58
|
+
`~/Library/Application Support/bilkatogo` on macOS); set `BILKATOGO_CONFIG_DIR` to use
|
|
59
|
+
another directory
|
|
60
|
+
4. an interactive prompt
|
|
61
|
+
|
|
62
|
+
The session is stored as `session.json` in the same directory, so later commands don't log in
|
|
63
|
+
again.
|
|
64
|
+
|
|
65
|
+
The Current Order is the unfinished order with the earliest pickup slot or, when every order
|
|
66
|
+
is finished, the most recently finished one.
|
|
67
|
+
|
|
68
|
+
## Library
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import asyncio
|
|
72
|
+
|
|
73
|
+
from bilkatogo import BilkaToGoClient, FileTokenStorage
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def main() -> None:
|
|
77
|
+
async with BilkaToGoClient(
|
|
78
|
+
"you@example.com",
|
|
79
|
+
"your-password",
|
|
80
|
+
token_storage=FileTokenStorage("session.json"),
|
|
81
|
+
) as client:
|
|
82
|
+
for order in await client.get_orders(limit=5):
|
|
83
|
+
print(order.order_number, order.status, order.pickup_slot)
|
|
84
|
+
|
|
85
|
+
current = await client.get_current_order()
|
|
86
|
+
if current:
|
|
87
|
+
if current.pickup_code:
|
|
88
|
+
print("Pickup Code:", current.pickup_code.code)
|
|
89
|
+
for substitution in current.substitutions:
|
|
90
|
+
print(substitution.original_product, "->", substitution.replacement_product)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
asyncio.run(main())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- `BilkaToGoClient(email, password, *, token_storage=None, httpx_client=None)` is an async
|
|
97
|
+
context manager. Pass your own `httpx.AsyncClient` to control timeouts or proxies; the
|
|
98
|
+
client only closes clients it created itself.
|
|
99
|
+
- `login()`, `get_orders(limit=5)`, `get_order(order_number)` and `get_current_order()` are the
|
|
100
|
+
public operations. The client logs in on demand, refreshes the session before it expires and
|
|
101
|
+
logs in again when Bilka rejects it.
|
|
102
|
+
- `token_storage` is any `TokenStorage` (async `load()` and `save(data)`). `FileTokenStorage`
|
|
103
|
+
stores the session in a JSON file.
|
|
104
|
+
- Errors derive from `BilkaToGoError`: `InvalidCredentialsError`, `BilkaUnreachableError` and
|
|
105
|
+
`UnexpectedResponseError`.
|
|
106
|
+
- `Order.status` is an `OrderStatus` (Processing, Received, Packed, Picked Up, Not Collected,
|
|
107
|
+
Cancelled, Sold Out, Unknown), using the same words as the website. The raw Bilka status code
|
|
108
|
+
is kept in `status_code`. The mapping is listed in [GLOSSARY.md](GLOSSARY.md).
|
|
109
|
+
- `PickupSlot.starts_at` and `ends_at` are timezone-aware datetimes in Europe/Copenhagen.
|
|
110
|
+
- Amounts are `Decimal` kroner.
|
|
111
|
+
- `OrderDetails.order_total` is the Order Total, broken down into `deposit`, `service_fee`,
|
|
112
|
+
`packaging`, `delivery_fee` and `discount`. `ordered_total` is what the Order cost when it was
|
|
113
|
+
placed, looked up in the order history (`None` when it is not among the latest 20 Orders).
|
|
114
|
+
Pass an `Order` to `get_order` instead of its number to skip that lookup.
|
|
115
|
+
|
|
116
|
+
## Home Assistant
|
|
117
|
+
|
|
118
|
+
1. Install the integration (see [Installation](#installation)) and restart Home Assistant.
|
|
119
|
+
2. Go to **Settings > Devices & services > Add integration** and pick **BilkaToGo**.
|
|
120
|
+
3. Sign in with the email and password you use on bilkatogo.dk. Add the integration once per
|
|
121
|
+
account.
|
|
122
|
+
|
|
123
|
+
If the password changes, Home Assistant asks you to sign in again.
|
|
124
|
+
|
|
125
|
+
The integration polls Bilka every 60 minutes, and every 5 minutes from 3 hours before the
|
|
126
|
+
pickup slot starts until the order is picked up, not collected or cancelled.
|
|
127
|
+
|
|
128
|
+
### Entities
|
|
129
|
+
|
|
130
|
+
All entities describe the Current Order.
|
|
131
|
+
|
|
132
|
+
| Entity | State | Attributes |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| Order Status | Processing, Received, Packed, Picked Up, Not Collected, Cancelled, Sold Out or Unknown | Bilka status code |
|
|
135
|
+
| Substitutions | Number of substituted products | List of Original Product, Replacement Product and the Replacement Product's quantity |
|
|
136
|
+
| Missing Items | Number of products packed short without a replacement | List of Product, Article Number, ordered and missing quantity |
|
|
137
|
+
| Order Total | What you pay for the order now (DKK) | Order Number, Ordered Total, difference from the Ordered Total (negative when you pay less), Deposit, Service Fee, Packaging, Delivery Fee, Discount |
|
|
138
|
+
| Pickup Slot | Start of the pickup slot (timestamp, Europe/Copenhagen) | Order Number, Order Total (DKK), Pickup Slot end |
|
|
139
|
+
| Pickup Code | The code you enter at check-in | Order Number, Order Total (DKK) |
|
|
140
|
+
| Pickup Point | The Bilka store you pick up from | Order Number, Order Total (DKK) |
|
|
141
|
+
|
|
142
|
+
Substitutions, Missing Items, the Pickup Code and the Order Total breakdown (Deposit, Service
|
|
143
|
+
Fee, Packaging, Delivery Fee, Discount) are fetched once the order is packed. Before that the
|
|
144
|
+
Order Total is Bilka's estimate from the order history.
|
|
145
|
+
|
|
146
|
+
An Order Status of Unknown (`unrecognized`) means Bilka sent a status code the integration does
|
|
147
|
+
not know yet; the code is in the attribute. Without any order the entities have no state.
|
|
148
|
+
|
|
149
|
+
### Event `bilkatogo_substitution`
|
|
150
|
+
|
|
151
|
+
Fired once for every new substitution, with this data:
|
|
152
|
+
|
|
153
|
+
| Field | Example |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `order_number` | `900000000001` |
|
|
156
|
+
| `original_product` | `Arla minimælk 1 l` |
|
|
157
|
+
| `replacement_product` | `Øko minimælk 1 l` |
|
|
158
|
+
| `quantity` | `2` |
|
|
159
|
+
|
|
160
|
+
Substitutions that already fired are remembered across restarts and reloads. An automation
|
|
161
|
+
that sends a notification:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
automation:
|
|
165
|
+
- alias: BilkaToGo substitution
|
|
166
|
+
triggers:
|
|
167
|
+
- trigger: event
|
|
168
|
+
event_type: bilkatogo_substitution
|
|
169
|
+
actions:
|
|
170
|
+
- action: notify.notify
|
|
171
|
+
data:
|
|
172
|
+
message: >
|
|
173
|
+
{{ trigger.event.data.original_product }} was replaced by
|
|
174
|
+
{{ trigger.event.data.replacement_product }}
|
|
175
|
+
({{ trigger.event.data.quantity }}).
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Event `bilkatogo_missing_item`
|
|
179
|
+
|
|
180
|
+
Fired once for every new Missing Item, with this data:
|
|
181
|
+
|
|
182
|
+
| Field | Example |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `order_number` | `900000000003` |
|
|
185
|
+
| `product` | `Skyr` |
|
|
186
|
+
| `article_number` | `932058` |
|
|
187
|
+
| `ordered_quantity` | `4` |
|
|
188
|
+
| `missing_quantity` | `2` |
|
|
189
|
+
|
|
190
|
+
Missing Items that already fired are remembered across restarts and reloads. An automation
|
|
191
|
+
that sends a notification:
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
automation:
|
|
195
|
+
- alias: BilkaToGo missing item
|
|
196
|
+
triggers:
|
|
197
|
+
- trigger: event
|
|
198
|
+
event_type: bilkatogo_missing_item
|
|
199
|
+
actions:
|
|
200
|
+
- action: notify.notify
|
|
201
|
+
data:
|
|
202
|
+
message: >
|
|
203
|
+
{{ trigger.event.data.product }}: {{ trigger.event.data.missing_quantity }}
|
|
204
|
+
of {{ trigger.event.data.ordered_quantity }} missing.
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
Library and CLI:
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
uv run ruff format --check . && uv run ruff check . && uv run ty check && uv run pytest
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Home Assistant tests use their own dependency group, because
|
|
216
|
+
`pytest-homeassistant-custom-component` pins its own pytest and httpx. Keep them in a
|
|
217
|
+
separate virtual environment so the library environment stays untouched:
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
UV_PROJECT_ENVIRONMENT=.venv-ha uv run --no-default-groups --group ha pytest tests/ha
|
|
221
|
+
UV_PROJECT_ENVIRONMENT=.venv-ha uv run --no-default-groups --group ha ty check custom_components tests/ha
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Home Assistant does not run on Windows; use Linux, WSL or CI for these.
|
|
225
|
+
|
|
226
|
+
The Home Assistant tests run the real library against the fake Bilka backend in
|
|
227
|
+
`tests/fake_bilka.py`. The integration creates its httpx client with Home Assistant's
|
|
228
|
+
`create_async_httpx_client`, and `tests/ha/conftest.py` routes every request at the
|
|
229
|
+
transport level to the fake backend with `respx`, so nothing in the integration is patched.
|
|
230
|
+
|
|
231
|
+
Releasing is described in [AGENTS.md](AGENTS.md#releasing).
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bilkatogo"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Async client and CLI for tracking BilkaToGo orders."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
keywords = [
|
|
9
|
+
"bilka",
|
|
10
|
+
"bilkatogo",
|
|
11
|
+
"danish",
|
|
12
|
+
"groceries",
|
|
13
|
+
"api",
|
|
14
|
+
"async",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Framework :: AsyncIO",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.14"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"httpx>=0.28.1",
|
|
25
|
+
"tzdata; sys_platform == 'win32'",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[project.authors]]
|
|
29
|
+
name = "Nick Nissen"
|
|
30
|
+
email = "nickknissen@gmail.com"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
cli = ["typer>=0.27.3,<0.28"]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
bilkatogo = "bilkatogo.__main__:main"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/nickknissen/bilkatogo"
|
|
40
|
+
Repository = "https://github.com/nickknissen/bilkatogo"
|
|
41
|
+
Issues = "https://github.com/nickknissen/bilkatogo/issues"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
45
|
+
build-backend = "uv_build"
|
|
46
|
+
|
|
47
|
+
[tool.uv]
|
|
48
|
+
default-groups = ["dev"]
|
|
49
|
+
conflicts = [[
|
|
50
|
+
{ group = "dev" },
|
|
51
|
+
{ group = "ha" },
|
|
52
|
+
]]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py314"
|
|
56
|
+
line-length = 100
|
|
57
|
+
extend-exclude = [
|
|
58
|
+
".worktrees",
|
|
59
|
+
"*.md",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = [
|
|
64
|
+
"E",
|
|
65
|
+
"F",
|
|
66
|
+
"W",
|
|
67
|
+
"I",
|
|
68
|
+
"UP",
|
|
69
|
+
"B",
|
|
70
|
+
"SIM",
|
|
71
|
+
"LOG",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|
|
76
|
+
asyncio_mode = "auto"
|
|
77
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
78
|
+
|
|
79
|
+
[tool.ty.environment]
|
|
80
|
+
python-version = "3.14"
|
|
81
|
+
|
|
82
|
+
[tool.ty.src]
|
|
83
|
+
include = [
|
|
84
|
+
"src/",
|
|
85
|
+
"tests/",
|
|
86
|
+
"release.py",
|
|
87
|
+
]
|
|
88
|
+
exclude = ["tests/ha/"]
|
|
89
|
+
|
|
90
|
+
[tool.ty.rules]
|
|
91
|
+
division-by-zero = "warn"
|
|
92
|
+
|
|
93
|
+
[[tool.ty.overrides]]
|
|
94
|
+
include = ["tests/"]
|
|
95
|
+
|
|
96
|
+
[tool.ty.overrides.rules]
|
|
97
|
+
invalid-assignment = "ignore"
|
|
98
|
+
|
|
99
|
+
[dependency-groups]
|
|
100
|
+
dev = [
|
|
101
|
+
"bilkatogo[cli]",
|
|
102
|
+
"pytest>=9.0.0",
|
|
103
|
+
"pytest-asyncio>=1.3.0",
|
|
104
|
+
"ruff>=0.16.1",
|
|
105
|
+
"ty>=0.0.92",
|
|
106
|
+
]
|
|
107
|
+
ha = [
|
|
108
|
+
"pytest-homeassistant-custom-component==0.13.316",
|
|
109
|
+
"respx>=0.22.0",
|
|
110
|
+
"ty>=0.0.92",
|
|
111
|
+
]
|