├── .gitignore ├── LICENSE ├── README.md ├── pyproject.toml ├── setup.cfg └── src └── python_bring_api ├── __init__.py ├── bring.py ├── exceptions.py └── types.py /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | src/python_bring_api.egg-info/ 3 | src/test*.py 4 | HOW-TO-UPLOAD.md 5 | HOW-TO-TEST.md 6 | test/ 7 | .venv 8 | __pycache__ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Elias Ball contact.eliasball@gmail.com 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bring Shopping Lists API 2 | 3 | An unofficial python package to access the Bring! shopping lists API. 4 | 5 | > This is a **minimal** python port of the [node-bring-api](https://github.com/foxriver76/node-bring-api) by [foxriver76](https://github.com/foxriver76). All credit goes to him for making this awesome API possible! 6 | 7 | ## Disclaimer 8 | 9 | The developers of this module are in no way endorsed by or affiliated with Bring! Labs AG, or any associated subsidiaries, logos or trademarks. 10 | 11 | ## Installation 12 | 13 | `pip install python-bring-api` 14 | 15 | ## Documentation 16 | 17 | See below for usage examples. See [Exceptions](#exceptions) for API-specific exceptions and mitigation strategies for common exceptions. 18 | 19 | ## Usage Example 20 | 21 | The API is available both sync and async, where sync is the default for simplicity. Both implementations of each function use the same async HTTP library `aiohttp` in the back. 22 | 23 | ### Sync 24 | 25 | ```python 26 | import logging 27 | import sys 28 | 29 | from python_bring_api.bring import Bring 30 | 31 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 32 | 33 | # Create Bring instance with email and password 34 | bring = Bring("MAIL", "PASSWORD") 35 | # Login 36 | bring.login() 37 | 38 | # Get information about all available shopping lists 39 | lists = bring.loadLists()["lists"] 40 | 41 | # Save an item with specifications to a certain shopping list 42 | bring.saveItem(lists[0]['listUuid'], 'Milk', 'low fat') 43 | 44 | # Save another item 45 | bring.saveItem(lists[0]['listUuid'], 'Carrots') 46 | 47 | # Get all the items of a list 48 | items = bring.getItems(lists[0]['listUuid']) 49 | print(items) 50 | 51 | # Check off an item 52 | bring.completeItem(lists[0]['listUuid'], 'Carrots') 53 | 54 | # Remove an item from a list 55 | bring.removeItem(lists[0]['listUuid'], 'Milk') 56 | ``` 57 | 58 | ### Async 59 | 60 | ```python 61 | import aiohttp 62 | import asyncio 63 | import logging 64 | import sys 65 | 66 | from python_bring_api.bring import Bring 67 | 68 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 69 | 70 | async def main(): 71 | async with aiohttp.ClientSession() as session: 72 | # Create Bring instance with email and password 73 | bring = Bring("MAIL", "PASSWORD", sessionAsync=session) 74 | # Login 75 | await bring.loginAsync() 76 | 77 | # Get information about all available shopping lists 78 | lists = (await bring.loadListsAsync())["lists"] 79 | 80 | # Save an item with specifications to a certain shopping list 81 | await bring.saveItemAsync(lists[0]['listUuid'], 'Milk', 'low fat') 82 | 83 | # Save another item 84 | await bring.saveItemAsync(lists[0]['listUuid'], 'Carrots') 85 | 86 | # Get all the items of a list 87 | items = await bring.getItemsAsync(lists[0]['listUuid']) 88 | print(items) 89 | 90 | # Check off an item 91 | await bring.completeItemAsync(lists[0]['listUuid'], 'Carrots') 92 | 93 | # Remove an item from a list 94 | await bring.removeItemAsync(lists[0]['listUuid'], 'Milk') 95 | 96 | asyncio.run(main()) 97 | ``` 98 | 99 | ## Exceptions 100 | In case something goes wrong during a request, several exceptions can be thrown. 101 | They will either be BringRequestException, BringParseException, or BringAuthException, depending on the context. All inherit from BringException. 102 | 103 | ### Another asyncio event loop is already running 104 | 105 | Because even the sync methods use async calls under the hood, you might encounter an error that another asyncio event loop is already running on the same thread. This is expected behavior according to the asyncio.run() [documentation](https://docs.python.org/3/library/asyncio-runner.html#asyncio.run). You cannot call the sync methods when another event loop is already running. When you are already inside an async function, you should use the async methods instead. 106 | 107 | ### Exception ignored: RuntimeError: Event loop is closed 108 | 109 | Due to a known issue in some versions of aiohttp when using Windows, you might encounter a similar error to this: 110 | 111 | ```python 112 | Exception ignored in: 113 | Traceback (most recent call last): 114 | File "C:\...\py38\lib\asyncio\proactor_events.py", line 116, in __del__ 115 | self.close() 116 | File "C:\...\py38\lib\asyncio\proactor_events.py", line 108, in close 117 | self._loop.call_soon(self._call_connection_lost, None) 118 | File "C:\...\py38\lib\asyncio\base_events.py", line 719, in call_soon 119 | self._check_closed() 120 | File "C:\...\py38\lib\asyncio\base_events.py", line 508, in _check_closed 121 | raise RuntimeError('Event loop is closed') 122 | RuntimeError: Event loop is closed 123 | ``` 124 | 125 | You can fix this according to [this](https://stackoverflow.com/questions/68123296/asyncio-throws-runtime-error-with-exception-ignored) stackoverflow answer by adding the following line of code before executing the library: 126 | ```python 127 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 128 | ``` 129 | 130 | ## Changelog 131 | 132 | ### 3.0.0 133 | 134 | Change backend library from requests to aiohttp, thanks to [@miaucl](https://github.com/miaucl)! 135 | This makes available async versions of all methods. 136 | 137 | Fix encoding of request data, thanks to [@miaucl](https://github.com/miaucl)! 138 | 139 | ### 2.1.0 140 | 141 | Add notify() method to send push notifications to other list members, thanks to [@tr4nt0r](https://github.com/tr4nt0r)! 142 | 143 | Add method to complete items, thanks to [@tr4nt0r](https://github.com/tr4nt0r)! 144 | 145 | Fix error handling in login method, thanks to [@tr4nt0r](https://github.com/tr4nt0r)! 146 | 147 | ### 2.0.0 148 | 149 | Add exceptions and typings, thanks to [@miaucl](https://github.com/miaucl)! 150 | 151 | Important: Unsuccessful HTTP status codes will now raise an exception. 152 | 153 | Module now requires Python version >= 3.8. 154 | 155 | ### 1.2.2 156 | 157 | Clean up unused code 🧹 158 | 159 | ### 1.2.1 160 | 161 | Fix encoding in login request, thanks to [@tony059](https://github.com/tony059)! 162 | 163 | ### 1.2.0 164 | 165 | Add function to update an item, thanks to [@Dielee](https://github.com/Dielee)! 166 | 167 | ### 1.1.2 168 | 169 | Add option to provide own headers, thanks to [@Dielee](https://github.com/Dielee)! 170 | 171 | ### 1.1.0 172 | 173 | Add item details endpoint, thanks to [@Dielee](https://github.com/Dielee)! 174 | 175 | ### 1.0.2 176 | 177 | Fixed error handling 178 | Added response return to login 179 | 180 | ### 1.0.1 181 | 182 | Add github repo 183 | 184 | ### 1.0.0 185 | 186 | Initial release 187 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = python-bring-api 3 | version = 3.0.0 4 | author = Elias Ball 5 | author_email = contact.eliasball@gmail.com 6 | description = Unofficial python package to access Bring! shopping lists API. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | license_files = LICENSE 10 | url = https://github.com/blue1stone/python-bring-api 11 | classifiers = 12 | Programming Language :: Python :: 3 13 | License :: OSI Approved :: MIT License 14 | Operating System :: OS Independent 15 | 16 | [options] 17 | package_dir = 18 | = src 19 | packages = find: 20 | python_requires = >=3.8 21 | install_requires = 22 | aiohttp 23 | 24 | [options.packages.find] 25 | where = src 26 | -------------------------------------------------------------------------------- /src/python_bring_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasball/python-bring-api/8043562b22be1f6421a8771774868b105b6ca375/src/python_bring_api/__init__.py -------------------------------------------------------------------------------- /src/python_bring_api/bring.py: -------------------------------------------------------------------------------- 1 | from json import JSONDecodeError 2 | import aiohttp 3 | import asyncio 4 | import traceback 5 | from typing import Dict 6 | 7 | from .types import BringNotificationType, BringAuthResponse, BringItemsResponse, BringListResponse, BringListItemsDetailsResponse 8 | from .exceptions import BringAuthException, BringRequestException, BringParseException 9 | 10 | import logging 11 | 12 | _LOGGER = logging.getLogger(__name__) 13 | 14 | class Bring: 15 | """ 16 | Unofficial Bring API interface. 17 | """ 18 | 19 | def __init__(self, mail: str, password: str, headers: Dict[str, str] = None, sessionAsync: aiohttp.ClientSession = None) -> None: 20 | self._session = sessionAsync 21 | 22 | self.mail = mail 23 | self.password = password 24 | self.uuid = '' 25 | self.publicUuid = '' 26 | 27 | self.url = 'https://api.getbring.com/rest/v2/' 28 | 29 | if headers: 30 | self.headers = headers 31 | else: 32 | self.headers = { 33 | 'Authorization': '', 34 | 'X-BRING-API-KEY': 'cof4Nc6D8saplXjE3h3HXqHH8m7VU2i1Gs0g85Sp', 35 | 'X-BRING-CLIENT-SOURCE': 'webApp', 36 | 'X-BRING-CLIENT': 'webApp', 37 | 'X-BRING-COUNTRY': 'DE', 38 | 'X-BRING-USER-UUID': '' 39 | } 40 | self.putHeaders = { 41 | 'Authorization': '', 42 | 'X-BRING-API-KEY': '', 43 | 'X-BRING-CLIENT-SOURCE': '', 44 | 'X-BRING-CLIENT': '', 45 | 'X-BRING-COUNTRY': '', 46 | 'X-BRING-USER-UUID': '', 47 | 'Content-Type': '' 48 | } 49 | self.postHeaders = { 50 | 'Authorization': '', 51 | 'X-BRING-API-KEY': '', 52 | 'X-BRING-CLIENT-SOURCE': '', 53 | 'X-BRING-CLIENT': '', 54 | 'X-BRING-COUNTRY': '', 55 | 'X-BRING-USER-UUID': '', 56 | 'Content-Type': '' 57 | } 58 | 59 | 60 | def login(self) -> BringAuthResponse: 61 | """ 62 | Try to login. 63 | 64 | Returns 65 | ------- 66 | Response 67 | The server response object. 68 | 69 | Raises 70 | ------ 71 | BringRequestException 72 | If the request fails. 73 | BringParseException 74 | If the parsing of the request response fails. 75 | BringAuthException 76 | If the login fails due to missing data in the API response. 77 | You should check your email and password. 78 | """ 79 | async def _async(): 80 | async with aiohttp.ClientSession() as session: 81 | self._session = session 82 | res = await self.loginAsync() 83 | self._session = None 84 | return res 85 | return asyncio.run(_async()) 86 | 87 | async def loginAsync(self) -> BringAuthResponse: 88 | """ 89 | Try to login. 90 | 91 | Returns 92 | ------- 93 | Response 94 | The server response object. 95 | 96 | Raises 97 | ------ 98 | BringRequestException 99 | If the request fails. 100 | BringParseException 101 | If the parsing of the request response fails. 102 | BringAuthException 103 | If the login fails due to missing data in the API response. 104 | You should check your email and password. 105 | """ 106 | data = { 107 | 'email': self.mail, 108 | 'password': self.password 109 | } 110 | 111 | try: 112 | url = f'{self.url}bringauth' 113 | async with self._session.post(url, data=data) as r: 114 | _LOGGER.debug(f'Response from %s: %s', url, r.status) 115 | 116 | if r.status == 401: 117 | try: 118 | errmsg = await r.json() 119 | except JSONDecodeError: 120 | _LOGGER.error(f'Exception: Cannot parse login request response:\n{traceback.format_exc()}') 121 | else: 122 | _LOGGER.error(f'Exception: Cannot login: {errmsg["message"]}') 123 | raise BringAuthException('Login failed due to authorization failure, please check your email and password.') 124 | elif r.status == 400: 125 | _LOGGER.error(f'Exception: Cannot login: {await r.text()}') 126 | raise BringAuthException('Login failed due to bad request, please check your email.') 127 | r.raise_for_status() 128 | 129 | try: 130 | data = await r.json() 131 | except JSONDecodeError as e: 132 | _LOGGER.error(f'Exception: Cannot login:\n{traceback.format_exc()}') 133 | raise BringParseException(f'Cannot parse login request response.') from e 134 | except asyncio.TimeoutError as e: 135 | _LOGGER.error(f'Exception: Cannot login:\n{traceback.format_exc()}') 136 | raise BringRequestException('Authentication failed due to connection timeout.') from e 137 | except aiohttp.ClientError as e: 138 | _LOGGER.error(f'Exception: Cannot login:\n{traceback.format_exc()}') 139 | raise BringRequestException(f'Authentication failed due to request exception.') from e 140 | 141 | if 'uuid' not in data or 'access_token' not in data: 142 | _LOGGER.error('Exception: Cannot login: Data missing in API response.') 143 | raise BringAuthException('Login failed due to missing data in the API response, please check your email and password.') 144 | 145 | self.uuid = data['uuid'] 146 | self.publicUuid = data.get('publicUuid', '') 147 | self.headers['X-BRING-USER-UUID'] = self.uuid 148 | self.headers['Authorization'] = f'Bearer {data["access_token"]}' 149 | self.putHeaders = { 150 | **self.headers, 151 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 152 | } 153 | self.postHeaders = { 154 | **self.headers, 155 | 'Content-Type': 'application/json; charset=UTF-8' 156 | } 157 | return data 158 | 159 | def loadLists(self) -> BringListResponse: 160 | """Load all shopping lists. 161 | 162 | Returns 163 | ------- 164 | dict 165 | 166 | The JSON response as a dict. 167 | 168 | Raises 169 | ------ 170 | BringRequestException 171 | If the request fails. 172 | BringParseException 173 | If the parsing of the request response fails. 174 | """ 175 | async def _async(): 176 | async with aiohttp.ClientSession() as session: 177 | self._session = session 178 | res = await self.loadListsAsync() 179 | self._session = None 180 | return res 181 | return asyncio.run(_async()) 182 | 183 | async def loadListsAsync(self) -> BringListResponse: 184 | """Load all shopping lists. 185 | 186 | Returns 187 | ------- 188 | dict 189 | 190 | The JSON response as a dict. 191 | 192 | Raises 193 | ------ 194 | BringRequestException 195 | If the request fails. 196 | BringParseException 197 | If the parsing of the request response fails. 198 | """ 199 | try: 200 | url = f'{self.url}bringusers/{self.uuid}/lists' 201 | async with self._session.get(url, headers=self.headers) as r: 202 | _LOGGER.debug(f'Response from %s: %s', url, r.status) 203 | r.raise_for_status() 204 | 205 | try: 206 | return await r.json() 207 | except JSONDecodeError as e: 208 | _LOGGER.error(f'Exception: Cannot get lists:\n{traceback.format_exc()}') 209 | raise BringParseException(f'Loading lists failed during parsing of request response.') from e 210 | except asyncio.TimeoutError as e: 211 | _LOGGER.error(f'Exception: Cannot get lists:\n{traceback.format_exc()}') 212 | raise BringRequestException('Loading list failed due to connection timeout.') from e 213 | except aiohttp.ClientError as e: 214 | _LOGGER.error(f'Exception: Cannot get lists:\n{traceback.format_exc()}') 215 | raise BringRequestException('Loading lists failed due to request exception.') from e 216 | 217 | def getItems(self, listUuid: str) -> BringItemsResponse: 218 | """ 219 | Get all items from a shopping list. 220 | 221 | Parameters 222 | ---------- 223 | listUuid : str 224 | A list uuid returned by loadLists() 225 | 226 | Returns 227 | ------- 228 | dict 229 | The JSON response as a dict. 230 | 231 | Raises 232 | ------ 233 | BringRequestException 234 | If the request fails. 235 | BringParseException 236 | If the parsing of the request response fails. 237 | """ 238 | async def _async(): 239 | async with aiohttp.ClientSession() as session: 240 | self._session = session 241 | res = await self.getItemsAsync(listUuid) 242 | self._session = None 243 | return res 244 | return asyncio.run(_async()) 245 | 246 | async def getItemsAsync(self, listUuid: str) -> BringItemsResponse: 247 | """ 248 | Get all items from a shopping list. 249 | 250 | Parameters 251 | ---------- 252 | listUuid : str 253 | A list uuid returned by loadLists() 254 | 255 | Returns 256 | ------- 257 | dict 258 | The JSON response as a dict. 259 | 260 | Raises 261 | ------ 262 | BringRequestException 263 | If the request fails. 264 | BringParseException 265 | If the parsing of the request response fails. 266 | """ 267 | try: 268 | url = f'{self.url}bringlists/{listUuid}' 269 | async with self._session.get(url, headers=self.headers) as r: 270 | _LOGGER.debug(f'Response from %s: %s', url, r.status) 271 | r.raise_for_status() 272 | 273 | try: 274 | return await r.json() 275 | except JSONDecodeError as e: 276 | _LOGGER.error(f'Exception: Cannot get items for list {listUuid}:\n{traceback.format_exc()}') 277 | raise BringParseException('Loading list items failed during parsing of request response.') from e 278 | except asyncio.TimeoutError as e: 279 | _LOGGER.error(f'Exception: Cannot get items for list {listUuid}:\n{traceback.format_exc()}') 280 | raise BringRequestException('Loading list items failed due to connection timeout.') from e 281 | except aiohttp.ClientError as e: 282 | _LOGGER.error(f'Exception: Cannot get items for list {listUuid}:\n{traceback.format_exc()}') 283 | raise BringRequestException('Loading list items failed due to request exception.') from e 284 | 285 | 286 | def getAllItemDetails(self, listUuid: str) -> BringListItemsDetailsResponse: 287 | """ 288 | Get all details from a shopping list. 289 | 290 | Parameters 291 | ---------- 292 | listUuid : str 293 | A list uuid returned by loadLists() 294 | 295 | Returns 296 | ------- 297 | list 298 | The JSON response as a list. A list of item details. 299 | Caution: This is NOT a list of the items currently marked as 'to buy'. See getItems() for that. 300 | 301 | Raises 302 | ------ 303 | BringRequestException 304 | If the request fails. 305 | BringParseException 306 | If the parsing of the request response fails. 307 | """ 308 | async def _async(): 309 | async with aiohttp.ClientSession() as session: 310 | self._session = session 311 | res = await self.getAllItemDetailsAsync(listUuid) 312 | self._session = None 313 | return res 314 | return asyncio.run(_async()) 315 | 316 | async def getAllItemDetailsAsync(self, listUuid: str) -> BringItemsResponse: 317 | """ 318 | Get all details from a shopping list. 319 | 320 | Parameters 321 | ---------- 322 | listUuid : str 323 | A list uuid returned by loadLists() 324 | 325 | Returns 326 | ------- 327 | list 328 | The JSON response as a list. A list of item details. 329 | Caution: This is NOT a list of the items currently marked as 'to buy'. See getItems() for that. 330 | 331 | Raises 332 | ------ 333 | BringRequestException 334 | If the request fails. 335 | BringParseException 336 | If the parsing of the request response fails. 337 | """ 338 | try: 339 | url = f'{self.url}bringlists/{listUuid}/details' 340 | async with self._session.get(url, headers=self.headers) as r: 341 | _LOGGER.debug(f'Response from %s: %s', url, r.status) 342 | r.raise_for_status() 343 | 344 | try: 345 | return await r.json() 346 | except JSONDecodeError as e: 347 | _LOGGER.error(f'Exception: Cannot get item details for list {listUuid}:\n{traceback.format_exc()}') 348 | raise BringParseException(f'Loading list details failed during parsing of request response.') from e 349 | except asyncio.TimeoutError as e: 350 | _LOGGER.error(f'Exception: Cannot get item details for list {listUuid}:\n{traceback.format_exc()}') 351 | raise BringRequestException('Loading list details failed due to connection timeout.') from e 352 | except aiohttp.ClientError as e: 353 | _LOGGER.error(f'Exception: Cannot get item details for list {listUuid}:\n{traceback.format_exc()}') 354 | raise BringRequestException('Loading list details failed due to request exception.') from e 355 | 356 | def saveItem(self, listUuid: str, itemName: str, specification='') -> aiohttp.ClientResponse: 357 | """ 358 | Save an item to a shopping list. 359 | 360 | Parameters 361 | ---------- 362 | listUuid : str 363 | A list uuid returned by loadLists() 364 | itemName : str 365 | The name of the item you want to save. 366 | specification : str, optional 367 | The details you want to add to the item. 368 | 369 | Returns 370 | ------- 371 | Response 372 | The server response object. 373 | 374 | Raises 375 | ------ 376 | BringRequestException 377 | If the request fails. 378 | """ 379 | async def _async(): 380 | async with aiohttp.ClientSession() as session: 381 | self._session = session 382 | res = await self.saveItemAsync(listUuid, itemName, specification) 383 | self._session = None 384 | return res 385 | return asyncio.run(_async()) 386 | 387 | async def saveItemAsync(self, listUuid: str, itemName: str, specification='') -> aiohttp.ClientResponse: 388 | """ 389 | Save an item to a shopping list. 390 | 391 | Parameters 392 | ---------- 393 | listUuid : str 394 | A list uuid returned by loadLists() 395 | itemName : str 396 | The name of the item you want to save. 397 | specification : str, optional 398 | The details you want to add to the item. 399 | 400 | Returns 401 | ------- 402 | Response 403 | The server response object. 404 | 405 | Raises 406 | ------ 407 | BringRequestException 408 | If the request fails. 409 | """ 410 | data = { 411 | 'purchase': itemName, 412 | 'specification': specification, 413 | } 414 | try: 415 | url = f'{self.url}bringlists/{listUuid}' 416 | async with self._session.put(url, headers=self.putHeaders, data=data) as r: 417 | _LOGGER.debug(f'Response from %s: %s', url, r.status) 418 | r.raise_for_status() 419 | return r 420 | except asyncio.TimeoutError as e: 421 | _LOGGER.error(f'Exception: Cannot save item {itemName} ({specification}) to list {listUuid}:\n{traceback.format_exc()}') 422 | raise BringRequestException(f'Saving item {itemName} ({specification}) to list {listUuid} failed due to connection timeout.') from e 423 | except aiohttp.ClientError as e: 424 | _LOGGER.error(f'Exception: Cannot save item {itemName} ({specification}) to list {listUuid}:\n{traceback.format_exc()}') 425 | raise BringRequestException(f'Saving item {itemName} ({specification}) to list {listUuid} failed due to request exception.') from e 426 | 427 | 428 | def updateItem(self, listUuid: str, itemName: str, specification='') -> aiohttp.ClientResponse: 429 | """ 430 | Update an existing list item. 431 | 432 | Parameters 433 | ---------- 434 | listUuid : str 435 | A list uuid returned by loadLists() 436 | itemName : str 437 | The name of the item you want to update. 438 | specification : str, optional 439 | The details you want to update on the item. 440 | 441 | Returns 442 | ------- 443 | Response 444 | The server response object. 445 | 446 | Raises 447 | ------ 448 | BringRequestException 449 | If the request fails. 450 | """ 451 | async def _async(): 452 | async with aiohttp.ClientSession() as session: 453 | self._session = session 454 | res = await self.updateItemAsync(listUuid, itemName, specification) 455 | self._session = None 456 | return res 457 | return asyncio.run(_async()) 458 | 459 | async def updateItemAsync(self, listUuid: str, itemName: str, specification='') -> aiohttp.ClientResponse: 460 | """ 461 | Update an existing list item. 462 | 463 | Parameters 464 | ---------- 465 | listUuid : str 466 | A list uuid returned by loadLists() 467 | itemName : str 468 | The name of the item you want to update. 469 | specification : str, optional 470 | The details you want to update on the item. 471 | 472 | Returns 473 | ------- 474 | Response 475 | The server response object. 476 | 477 | Raises 478 | ------ 479 | BringRequestException 480 | If the request fails. 481 | """ 482 | data = { 483 | 'purchase': itemName, 484 | 'specification': specification 485 | } 486 | try: 487 | url = f'{self.url}bringlists/{listUuid}' 488 | async with self._session.put(url, headers=self.putHeaders, data=data) as r: 489 | _LOGGER.debug(f'Response from %s: %s', url, r.status) 490 | r.raise_for_status() 491 | return r 492 | except asyncio.TimeoutError as e: 493 | _LOGGER.error(f'Exception: Cannot update item {itemName} ({specification}) to list {listUuid}:\n{traceback.format_exc()}') 494 | raise BringRequestException(f'Updating item {itemName} ({specification}) in list {listUuid} failed due to connection timeout.') from e 495 | except aiohttp.ClientError as e: 496 | _LOGGER.error(f'Exception: Cannot update item {itemName} ({specification}) to list {listUuid}:\n{traceback.format_exc()}') 497 | raise BringRequestException(f'Updating item {itemName} ({specification}) in list {listUuid} failed due to request exception.') from e 498 | 499 | 500 | def removeItem(self, listUuid: str, itemName: str) -> aiohttp.ClientResponse: 501 | """ 502 | Remove an item from a shopping list. 503 | 504 | Parameters 505 | ---------- 506 | listUuid : str 507 | A list uuid returned by loadLists() 508 | itemName : str 509 | The name of the item you want to remove. 510 | 511 | Returns 512 | ------- 513 | Response 514 | The server response object. 515 | 516 | Raises 517 | ------ 518 | BringRequestException 519 | If the request fails. 520 | """ 521 | async def _async(): 522 | async with aiohttp.ClientSession() as session: 523 | self._session = session 524 | res = await self.removeItemAsync(listUuid, itemName) 525 | self._session = None 526 | return res 527 | return asyncio.run(_async()) 528 | 529 | async def removeItemAsync(self, listUuid: str, itemName: str) -> aiohttp.ClientResponse: 530 | """ 531 | Remove an item from a shopping list. 532 | 533 | Parameters 534 | ---------- 535 | listUuid : str 536 | A list uuid returned by loadLists() 537 | itemName : str 538 | The name of the item you want to remove. 539 | 540 | Returns 541 | ------- 542 | Response 543 | The server response object. 544 | 545 | Raises 546 | ------ 547 | BringRequestException 548 | If the request fails. 549 | """ 550 | data = { 551 | 'remove': itemName, 552 | } 553 | try: 554 | url = f'{self.url}bringlists/{listUuid}' 555 | async with self._session.put(url, headers=self.putHeaders, data=data) as r: 556 | _LOGGER.debug(f'Response from %s: %s', url, r.status) 557 | r.raise_for_status() 558 | return r 559 | except asyncio.TimeoutError as e: 560 | _LOGGER.error(f'Exception: Cannot remove item {itemName} to list {listUuid}:\n{traceback.format_exc()}') 561 | raise BringRequestException(f'Removing item {itemName} from list {listUuid} failed due to connection timeout.') from e 562 | except aiohttp.ClientError as e: 563 | _LOGGER.error(f'Exception: Cannot remove item {itemName} to list {listUuid}:\n{traceback.format_exc()}') 564 | raise BringRequestException(f'Removing item {itemName} from list {listUuid} failed due to request exception.') from e 565 | 566 | def completeItem(self, listUuid: str, itemName: str) -> aiohttp.ClientResponse: 567 | """ 568 | Complete an item from a shopping list. This will add it to recent items. 569 | If it was not on the list, it will still be added to recent items. 570 | 571 | Parameters 572 | ---------- 573 | listUuid : str 574 | A list uuid returned by loadLists() 575 | itemName : str 576 | The name of the item you want to complete. 577 | 578 | Returns 579 | ------- 580 | Response 581 | The server response object. 582 | 583 | Raises 584 | ------ 585 | BringRequestException 586 | If the request fails. 587 | """ 588 | async def _async(): 589 | async with aiohttp.ClientSession() as session: 590 | self._session = session 591 | res = await self.completeItemAsync(listUuid, itemName) 592 | self._session = None 593 | return res 594 | return asyncio.run(_async()) 595 | 596 | 597 | async def completeItemAsync(self, listUuid: str, itemName: str) -> aiohttp.ClientResponse: 598 | """ 599 | Complete an item from a shopping list. This will add it to recent items. 600 | If it was not on the list, it will still be added to recent items. 601 | 602 | Parameters 603 | ---------- 604 | listUuid : str 605 | A list uuid returned by loadLists() 606 | itemName : str 607 | The name of the item you want to complete. 608 | 609 | Returns 610 | ------- 611 | Response 612 | The server response object. 613 | 614 | Raises 615 | ------ 616 | BringRequestException 617 | If the request fails. 618 | """ 619 | data = { 620 | 'recently': itemName 621 | } 622 | try: 623 | url = f'{self.url}bringlists/{listUuid}' 624 | async with self._session.put(url, headers=self.putHeaders, data=data) as r: 625 | _LOGGER.debug(f'Response from %s: %s', url, r.status) 626 | r.raise_for_status() 627 | return r 628 | except asyncio.TimeoutError as e: 629 | _LOGGER.error(f'Exception: Cannot complete item {itemName} to list {listUuid}:\n{traceback.format_exc()}') 630 | raise BringRequestException(f'Completing item {itemName} from list {listUuid} failed due to connection timeout.') from e 631 | except aiohttp.ClientError as e: 632 | _LOGGER.error(f'Exception: Cannot complete item {itemName} to list {listUuid}:\n{traceback.format_exc()}') 633 | raise BringRequestException(f'Completing item {itemName} from list {listUuid} failed due to request exception.') from e 634 | 635 | 636 | def notify(self, listUuid: str, notificationType: BringNotificationType, itemName: str = None) -> aiohttp.ClientResponse: 637 | """ 638 | Send a push notification to all other members of a shared list. 639 | 640 | Parameters 641 | ---------- 642 | listUuid : str 643 | A list uuid returned by loadLists() 644 | notificationType : BringNotificationType 645 | itemName : str, optional 646 | The text that **must** be included in the URGENT_MESSAGE BringNotificationType. 647 | 648 | Returns 649 | ------- 650 | Response 651 | The server response object. 652 | 653 | Raises 654 | ------ 655 | BringRequestException 656 | If the request fails. 657 | """ 658 | async def _async(): 659 | async with aiohttp.ClientSession() as session: 660 | self._session = session 661 | res = await self.notifyAsync(listUuid, notificationType, itemName) 662 | self._session = None 663 | return res 664 | return asyncio.run(_async()) 665 | 666 | 667 | async def notifyAsync(self, listUuid: str, notificationType: BringNotificationType, itemName: str = None) -> aiohttp.ClientResponse: 668 | """ 669 | Send a push notification to all other members of a shared list. 670 | 671 | Parameters 672 | ---------- 673 | listUuid : str 674 | A list uuid returned by loadLists() 675 | notificationType : BringNotificationType 676 | itemName : str, optional 677 | The text that **must** be included in the URGENT_MESSAGE BringNotificationType. 678 | 679 | Returns 680 | ------- 681 | Response 682 | The server response object. 683 | 684 | Raises 685 | ------ 686 | BringRequestException 687 | If the request fails. 688 | """ 689 | json = { 690 | 'arguments': [], 691 | 'listNotificationType': notificationType.value, 692 | 'senderPublicUserUuid': self.publicUuid 693 | } 694 | 695 | if not isinstance(notificationType, BringNotificationType): 696 | _LOGGER.error(f'Exception: notificationType {notificationType} not supported.') 697 | raise ValueError(f'notificationType {notificationType} not supported, must be of type BringNotificationType.') 698 | if notificationType is BringNotificationType.URGENT_MESSAGE: 699 | if not itemName or len(itemName) == 0 : 700 | _LOGGER.error('Exception: Argument itemName missing.') 701 | raise ValueError('notificationType is URGENT_MESSAGE but argument itemName missing.') 702 | else: 703 | json['arguments'] = [itemName] 704 | try: 705 | url = f'{self.url}bringnotifications/lists/{listUuid}' 706 | async with self._session.post(url, headers=self.postHeaders, json=json) as r: 707 | _LOGGER.debug(f'Response from %s: %s', url, r.status) 708 | r.raise_for_status() 709 | return r 710 | except asyncio.TimeoutError as e: 711 | _LOGGER.error(f'Exception: Cannot send notification {notificationType} for list {listUuid}:\n{traceback.format_exc()}') 712 | raise BringRequestException(f'Sending notification {notificationType} for list {listUuid} failed due to connection timeout.') from e 713 | except aiohttp.ClientError as e: 714 | _LOGGER.error(f'Exception: Cannot send notification {notificationType} for list {listUuid}:\n{traceback.format_exc()}') 715 | raise BringRequestException(f'Sending notification {notificationType} for list {listUuid} failed due to request exception.') from e 716 | -------------------------------------------------------------------------------- /src/python_bring_api/exceptions.py: -------------------------------------------------------------------------------- 1 | class BringException(Exception): 2 | """General exception occurred.""" 3 | 4 | pass 5 | 6 | class BringAuthException(BringException): 7 | """When an authentication error is encountered.""" 8 | 9 | pass 10 | 11 | class BringRequestException(BringException): 12 | """When the HTTP request fails.""" 13 | 14 | pass 15 | 16 | class BringParseException(BringException): 17 | """When parsing the response of a request fails.""" 18 | 19 | pass 20 | -------------------------------------------------------------------------------- /src/python_bring_api/types.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | from typing import List 3 | from enum import Enum 4 | 5 | class BringList(TypedDict): 6 | """A list class. Represents a single list.""" 7 | 8 | listUuid: str 9 | name: str 10 | theme: str 11 | 12 | class BringPurchase(TypedDict): 13 | """A purchase class. Represents a single item.""" 14 | 15 | name: str 16 | specification: str 17 | 18 | class BringListItemDetails(TypedDict): 19 | """An item details class. Includes several details of an item in the context of a list. 20 | Caution: This does not have to be an item that is currently marked as 'to buy'.""" 21 | 22 | uuid: str 23 | itemId: str 24 | listUuid: str 25 | userIconItemId: str 26 | userSectionId: str 27 | assignedTo: str 28 | imageUrl: str 29 | 30 | class BringAuthResponse(TypedDict): 31 | """An auth response class.""" 32 | 33 | uuid: str 34 | publicUuid: str 35 | email: str 36 | name: str 37 | photoPath: str 38 | bringListUUID: str 39 | access_token: str 40 | refresh_token: str 41 | token_type: str 42 | expires_in: int 43 | 44 | class BringListResponse(TypedDict): 45 | """A list response class.""" 46 | 47 | lists: List[BringList] 48 | 49 | class BringItemsResponse(TypedDict): 50 | """An items response class.""" 51 | 52 | uuid: str 53 | status: str 54 | purchase: List[BringPurchase] 55 | 56 | class BringListItemsDetailsResponse(List[BringListItemDetails]): 57 | """A response class of a list of item details.""" 58 | pass 59 | 60 | class BringNotificationType(Enum): 61 | """Notification type 62 | 63 | GOING_SHOPPING: "I'm going shopping! - Last chance for adjustments" 64 | CHANGED_LIST: "List changed - Check it out" 65 | SHOPPING_DONE: "Shopping done - you can relax" 66 | URGENT_MESSAGE: "Breaking news - Please get {itemName}! 67 | """ 68 | GOING_SHOPPING = "GOING_SHOPPING" 69 | CHANGED_LIST = "CHANGED_LIST" 70 | SHOPPING_DONE = "SHOPPING_DONE" 71 | URGENT_MESSAGE = "URGENT_MESSAGE" --------------------------------------------------------------------------------