birtrashclient 0.1.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.
- birtrashclient-0.1.0/.github/workflows/workflow.yml +33 -0
- birtrashclient-0.1.0/.gitignore +19 -0
- birtrashclient-0.1.0/PKG-INFO +77 -0
- birtrashclient-0.1.0/README.md +59 -0
- birtrashclient-0.1.0/birtrashclient/__init__.py +9 -0
- birtrashclient-0.1.0/birtrashclient/client.py +247 -0
- birtrashclient-0.1.0/pyproject.toml +27 -0
- birtrashclient-0.1.0/test_integration.py +53 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- run: pip install hatch
|
|
16
|
+
- run: hatch build
|
|
17
|
+
- uses: actions/upload-artifact@v4
|
|
18
|
+
with:
|
|
19
|
+
name: dist
|
|
20
|
+
path: dist/
|
|
21
|
+
|
|
22
|
+
publish:
|
|
23
|
+
needs: build
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
environment: pypi
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/download-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: birtrashclient
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async client library for the BIR Trash Collection API
|
|
5
|
+
Project-URL: Homepage, https://github.com/eirikgrindevoll/birtrashclient
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/eirikgrindevoll/birtrashclient/issues
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Framework :: AsyncIO
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Home Automation
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: aiohttp>=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# birtrashclient
|
|
20
|
+
|
|
21
|
+
Async Python client for the [BIR](https://bir.no) Trash Collection API.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install birtrashclient
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
from birtrashclient import BirTrashClient
|
|
34
|
+
|
|
35
|
+
async def main():
|
|
36
|
+
client = BirTrashClient(app_id="your_app_id", contractor_id="your_contractor_id")
|
|
37
|
+
await client.authenticate()
|
|
38
|
+
|
|
39
|
+
address_id = await client.search_address("Storgata 1, Bergen")
|
|
40
|
+
calendar = await client.get_calendar(address_id, "2024-01-01", "2024-12-31")
|
|
41
|
+
|
|
42
|
+
for entry in calendar:
|
|
43
|
+
print(entry)
|
|
44
|
+
|
|
45
|
+
await client.close()
|
|
46
|
+
|
|
47
|
+
asyncio.run(main())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### `BirTrashClient(app_id, contractor_id, ...)`
|
|
53
|
+
|
|
54
|
+
| Parameter | Type | Default | Description |
|
|
55
|
+
|-----------|------|---------|-------------|
|
|
56
|
+
| `app_id` | `str` | required | Application ID for API authentication |
|
|
57
|
+
| `contractor_id` | `str` | required | Contractor ID for API authentication |
|
|
58
|
+
| `request_timeout` | `int` | `10` | HTTP timeout in seconds |
|
|
59
|
+
| `session` | `aiohttp.ClientSession` | `None` | Optional session to reuse |
|
|
60
|
+
| `retries` | `int` | `3` | Retries on transient server errors |
|
|
61
|
+
| `backoff_factor` | `float` | `2.0` | Exponential backoff base multiplier |
|
|
62
|
+
|
|
63
|
+
### Methods
|
|
64
|
+
|
|
65
|
+
- `authenticate()` — Fetch and store an auth token
|
|
66
|
+
- `search_address(address)` — Resolve a street address to a property ID
|
|
67
|
+
- `get_calendar(address_id, from_date, to_date)` — Get pickup schedule (dates as `YYYY-MM-DD`)
|
|
68
|
+
- `close()` — Close the underlying HTTP session
|
|
69
|
+
|
|
70
|
+
## Exceptions
|
|
71
|
+
|
|
72
|
+
- `BirTrashAuthError` — Authentication failure
|
|
73
|
+
- `BirTrashConnectionError` — Connection or HTTP error after retries
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# birtrashclient
|
|
2
|
+
|
|
3
|
+
Async Python client for the [BIR](https://bir.no) Trash Collection API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install birtrashclient
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import asyncio
|
|
15
|
+
from birtrashclient import BirTrashClient
|
|
16
|
+
|
|
17
|
+
async def main():
|
|
18
|
+
client = BirTrashClient(app_id="your_app_id", contractor_id="your_contractor_id")
|
|
19
|
+
await client.authenticate()
|
|
20
|
+
|
|
21
|
+
address_id = await client.search_address("Storgata 1, Bergen")
|
|
22
|
+
calendar = await client.get_calendar(address_id, "2024-01-01", "2024-12-31")
|
|
23
|
+
|
|
24
|
+
for entry in calendar:
|
|
25
|
+
print(entry)
|
|
26
|
+
|
|
27
|
+
await client.close()
|
|
28
|
+
|
|
29
|
+
asyncio.run(main())
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
### `BirTrashClient(app_id, contractor_id, ...)`
|
|
35
|
+
|
|
36
|
+
| Parameter | Type | Default | Description |
|
|
37
|
+
|-----------|------|---------|-------------|
|
|
38
|
+
| `app_id` | `str` | required | Application ID for API authentication |
|
|
39
|
+
| `contractor_id` | `str` | required | Contractor ID for API authentication |
|
|
40
|
+
| `request_timeout` | `int` | `10` | HTTP timeout in seconds |
|
|
41
|
+
| `session` | `aiohttp.ClientSession` | `None` | Optional session to reuse |
|
|
42
|
+
| `retries` | `int` | `3` | Retries on transient server errors |
|
|
43
|
+
| `backoff_factor` | `float` | `2.0` | Exponential backoff base multiplier |
|
|
44
|
+
|
|
45
|
+
### Methods
|
|
46
|
+
|
|
47
|
+
- `authenticate()` — Fetch and store an auth token
|
|
48
|
+
- `search_address(address)` — Resolve a street address to a property ID
|
|
49
|
+
- `get_calendar(address_id, from_date, to_date)` — Get pickup schedule (dates as `YYYY-MM-DD`)
|
|
50
|
+
- `close()` — Close the underlying HTTP session
|
|
51
|
+
|
|
52
|
+
## Exceptions
|
|
53
|
+
|
|
54
|
+
- `BirTrashAuthError` — Authentication failure
|
|
55
|
+
- `BirTrashConnectionError` — Connection or HTTP error after retries
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Client library for the BIR Trash Collection API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
TRANSIENT_STATUS_CODES = {500, 502, 503, 504}
|
|
15
|
+
DEFAULT_RETRIES = 3
|
|
16
|
+
DEFAULT_BACKOFF = 2.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BirTrashAuthError(Exception):
|
|
20
|
+
"""Exception raised when authentication fails."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BirTrashConnectionError(Exception):
|
|
24
|
+
"""Exception raised when a connection error occurs."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BirTrashClient:
|
|
28
|
+
"""Async client for the BIR Trash Collection API."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
app_id: str,
|
|
33
|
+
contractor_id: str,
|
|
34
|
+
request_timeout: int = 10,
|
|
35
|
+
session: aiohttp.ClientSession | None = None,
|
|
36
|
+
retries: int = DEFAULT_RETRIES,
|
|
37
|
+
backoff_factor: float = DEFAULT_BACKOFF,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize the client.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
app_id: The application ID for API authentication.
|
|
43
|
+
contractor_id: The contractor ID for API authentication.
|
|
44
|
+
request_timeout: Timeout in seconds for HTTP requests.
|
|
45
|
+
session: An optional aiohttp ClientSession to reuse.
|
|
46
|
+
retries: Number of retries for transient server errors.
|
|
47
|
+
backoff_factor: Base delay multiplier for exponential backoff.
|
|
48
|
+
"""
|
|
49
|
+
self.base_url = "https://webservice.bir.no/api"
|
|
50
|
+
self.request_timeout = aiohttp.ClientTimeout(total=request_timeout)
|
|
51
|
+
self.app_id = app_id
|
|
52
|
+
self.contractor_id = contractor_id
|
|
53
|
+
self.token: str | None = None
|
|
54
|
+
self._session = session
|
|
55
|
+
self._close_session = False
|
|
56
|
+
self.retries = retries
|
|
57
|
+
self.backoff_factor = backoff_factor
|
|
58
|
+
|
|
59
|
+
async def _get_session(self) -> aiohttp.ClientSession:
|
|
60
|
+
"""Return the active session, creating one if needed."""
|
|
61
|
+
if self._session is None or self._session.closed:
|
|
62
|
+
self._session = aiohttp.ClientSession()
|
|
63
|
+
self._close_session = True
|
|
64
|
+
return self._session
|
|
65
|
+
|
|
66
|
+
async def _request_with_retry(
|
|
67
|
+
self,
|
|
68
|
+
method: str,
|
|
69
|
+
url: str,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> Any:
|
|
72
|
+
"""Execute an HTTP request with retry logic for transient errors.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
method: The HTTP method (get, post, etc.).
|
|
76
|
+
url: The full URL to request.
|
|
77
|
+
**kwargs: Additional arguments passed to the aiohttp request.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The parsed JSON response.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
BirTrashAuthError: On 401 after re-authentication also fails.
|
|
84
|
+
BirTrashConnectionError: After all retries are exhausted.
|
|
85
|
+
"""
|
|
86
|
+
session = await self._get_session()
|
|
87
|
+
last_exception: Exception | None = None
|
|
88
|
+
|
|
89
|
+
for attempt in range(self.retries + 1):
|
|
90
|
+
try:
|
|
91
|
+
async with session.request(
|
|
92
|
+
method,
|
|
93
|
+
url,
|
|
94
|
+
timeout=self.request_timeout,
|
|
95
|
+
**kwargs,
|
|
96
|
+
) as response:
|
|
97
|
+
if response.status == 401:
|
|
98
|
+
_LOGGER.debug(
|
|
99
|
+
"Token expired (attempt %d), re-authenticating",
|
|
100
|
+
attempt + 1,
|
|
101
|
+
)
|
|
102
|
+
await self.authenticate()
|
|
103
|
+
if "headers" in kwargs and kwargs["headers"]:
|
|
104
|
+
kwargs["headers"]["Token"] = self.token
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if response.status in TRANSIENT_STATUS_CODES:
|
|
108
|
+
delay = self.backoff_factor * (2 ** attempt)
|
|
109
|
+
_LOGGER.warning(
|
|
110
|
+
"Server returned %d (attempt %d/%d), "
|
|
111
|
+
"retrying in %.1f s",
|
|
112
|
+
response.status,
|
|
113
|
+
attempt + 1,
|
|
114
|
+
self.retries + 1,
|
|
115
|
+
delay,
|
|
116
|
+
)
|
|
117
|
+
last_exception = BirTrashConnectionError(
|
|
118
|
+
f"Server returned {response.status}"
|
|
119
|
+
)
|
|
120
|
+
await asyncio.sleep(delay)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
return await response.json()
|
|
125
|
+
|
|
126
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
|
|
127
|
+
delay = self.backoff_factor * (2 ** attempt)
|
|
128
|
+
_LOGGER.warning(
|
|
129
|
+
"Request failed (attempt %d/%d): %s, retrying in %.1f s",
|
|
130
|
+
attempt + 1,
|
|
131
|
+
self.retries + 1,
|
|
132
|
+
err,
|
|
133
|
+
delay,
|
|
134
|
+
)
|
|
135
|
+
last_exception = err
|
|
136
|
+
if attempt < self.retries:
|
|
137
|
+
await asyncio.sleep(delay)
|
|
138
|
+
|
|
139
|
+
raise BirTrashConnectionError(
|
|
140
|
+
f"Request failed after {self.retries + 1} attempts"
|
|
141
|
+
) from last_exception
|
|
142
|
+
|
|
143
|
+
async def authenticate(self) -> None:
|
|
144
|
+
"""Authenticate with the BIR API and store the token.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
BirTrashAuthError: If the authentication request fails.
|
|
148
|
+
"""
|
|
149
|
+
session = await self._get_session()
|
|
150
|
+
try:
|
|
151
|
+
async with session.post(
|
|
152
|
+
f"{self.base_url}/login",
|
|
153
|
+
json={
|
|
154
|
+
"applikasjonsId": self.app_id,
|
|
155
|
+
"oppdragsgiverId": self.contractor_id,
|
|
156
|
+
},
|
|
157
|
+
timeout=self.request_timeout,
|
|
158
|
+
) as response:
|
|
159
|
+
response.raise_for_status()
|
|
160
|
+
self.token = response.headers["Token"]
|
|
161
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
|
|
162
|
+
raise BirTrashAuthError(
|
|
163
|
+
f"Authentication failed: {err}"
|
|
164
|
+
) from err
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _normalize_address(address: str) -> str:
|
|
168
|
+
"""Insert a space between street number and unit letter if missing.
|
|
169
|
+
|
|
170
|
+
Converts e.g. '46J' -> '46 J' to match the API's expected format.
|
|
171
|
+
"""
|
|
172
|
+
return re.sub(r"(\d)([A-Za-z])$", r"\1 \2", address.strip())
|
|
173
|
+
|
|
174
|
+
async def search_addresses(self, address: str) -> list[dict[str, Any]]:
|
|
175
|
+
"""Search for an address and return all matching properties.
|
|
176
|
+
|
|
177
|
+
Useful for Home Assistant config flows where the user selects
|
|
178
|
+
from a list of matching properties (e.g. multiple units at one
|
|
179
|
+
street number).
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
address: The street address to search for.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
A list of property dicts, each containing at minimum 'id'
|
|
186
|
+
and 'adresse' keys.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
BirTrashConnectionError: If the request fails after retries.
|
|
190
|
+
"""
|
|
191
|
+
return await self._request_with_retry(
|
|
192
|
+
"get",
|
|
193
|
+
f"{self.base_url}/eiendommer",
|
|
194
|
+
params={"adresse": self._normalize_address(address)},
|
|
195
|
+
headers={"Token": self.token},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
async def search_address(self, address: str) -> str:
|
|
199
|
+
"""Search for an address and return the corresponding property ID.
|
|
200
|
+
|
|
201
|
+
The address is normalized before searching — a space is inserted
|
|
202
|
+
between the street number and unit letter if missing
|
|
203
|
+
(e.g. '46J' becomes '46 J').
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
address: The street address to search for.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
The property ID string for the first matching property.
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
BirTrashConnectionError: If the request fails after retries.
|
|
213
|
+
"""
|
|
214
|
+
result = await self.search_addresses(address)
|
|
215
|
+
return result[0].get("id")
|
|
216
|
+
|
|
217
|
+
async def get_calendar(
|
|
218
|
+
self, address_id: str, from_date: str, to_date: str
|
|
219
|
+
) -> list[dict[str, Any]]:
|
|
220
|
+
"""Get the pickup calendar for the provided address ID.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
address_id: The property ID to query.
|
|
224
|
+
from_date: The start date in YYYY-MM-DD format.
|
|
225
|
+
to_date: The end date in YYYY-MM-DD format.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
A list of pickup schedule dictionaries.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
BirTrashConnectionError: If the request fails after retries.
|
|
232
|
+
"""
|
|
233
|
+
return await self._request_with_retry(
|
|
234
|
+
"get",
|
|
235
|
+
f"{self.base_url}/tomminger",
|
|
236
|
+
params={
|
|
237
|
+
"datoFra": from_date,
|
|
238
|
+
"datoTil": to_date,
|
|
239
|
+
"eiendomId": address_id,
|
|
240
|
+
},
|
|
241
|
+
headers={"Token": self.token},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
async def close(self) -> None:
|
|
245
|
+
"""Close the underlying session if we own it."""
|
|
246
|
+
if self._session and self._close_session:
|
|
247
|
+
await self._session.close()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "birtrashclient"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Async client library for the BIR Trash Collection API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"aiohttp>=3.9",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Framework :: AsyncIO",
|
|
22
|
+
"Topic :: Home Automation",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
"Homepage" = "https://github.com/eirikgrindevoll/birtrashclient"
|
|
27
|
+
"Bug Tracker" = "https://github.com/eirikgrindevoll/birtrashclient/issues"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Integration test against the real BIR API.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
BIR_APP_ID=xxx BIR_CONTRACTOR_ID=yyy BIR_ADDRESS="Storgata 1" python test_integration.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
from datetime import date, timedelta
|
|
10
|
+
|
|
11
|
+
from birtrashclient import BirTrashAuthError, BirTrashClient, BirTrashConnectionError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def main() -> None:
|
|
15
|
+
app_id = os.environ.get("BIR_APP_ID")
|
|
16
|
+
contractor_id = os.environ.get("BIR_CONTRACTOR_ID")
|
|
17
|
+
address = os.environ.get("BIR_ADDRESS")
|
|
18
|
+
|
|
19
|
+
if not app_id or not contractor_id or not address:
|
|
20
|
+
raise SystemExit(
|
|
21
|
+
"Set BIR_APP_ID, BIR_CONTRACTOR_ID, and BIR_ADDRESS environment variables."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
client = BirTrashClient(app_id=app_id, contractor_id=contractor_id)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
print("Authenticating...")
|
|
28
|
+
await client.authenticate()
|
|
29
|
+
print(f" Token: {client.token[:20]}...")
|
|
30
|
+
|
|
31
|
+
print(f"\nSearching for address: {address!r}")
|
|
32
|
+
address_id = await client.search_address(address)
|
|
33
|
+
print(f" Property ID: {address_id}")
|
|
34
|
+
|
|
35
|
+
from_date = date.today().isoformat()
|
|
36
|
+
to_date = (date.today() + timedelta(days=90)).isoformat()
|
|
37
|
+
print(f"\nFetching calendar ({from_date} -> {to_date})...")
|
|
38
|
+
calendar = await client.get_calendar(address_id, from_date, to_date)
|
|
39
|
+
print(f" {len(calendar)} pickup(s) found:")
|
|
40
|
+
for entry in calendar:
|
|
41
|
+
print(f" {entry}")
|
|
42
|
+
|
|
43
|
+
except BirTrashAuthError as err:
|
|
44
|
+
print(f"Auth error: {err}")
|
|
45
|
+
raise SystemExit(1)
|
|
46
|
+
except BirTrashConnectionError as err:
|
|
47
|
+
print(f"Connection error: {err}")
|
|
48
|
+
raise SystemExit(1)
|
|
49
|
+
finally:
|
|
50
|
+
await client.close()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
asyncio.run(main())
|