Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bring_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BringUserUnknownException,
)
from .types import (
BringActivityResponse,
BringAuthResponse,
BringAuthTokenResponse,
BringItem,
Expand All @@ -30,6 +31,7 @@

__all__ = [
"Bring",
"BringActivityResponse",
"BringAuthException",
"BringAuthResponse",
"BringAuthTokenResponse",
Expand Down
60 changes: 60 additions & 0 deletions bring_api/bring.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
BringUserUnknownException,
)
from .types import (
BringActivityResponse,
BringAuthResponse,
BringAuthTokenResponse,
BringItem,
Expand Down Expand Up @@ -1477,3 +1478,62 @@ async def set_list_article_language(
raise BringRequestException(
"Set list article language failed due to request exception."
) from e

async def get_activity(self, list_uuid: UUID) -> BringActivityResponse:
"""Get activity for given list."""
try:
url = self.url / "v2/bringlists" / str(list_uuid) / "activity"
async with self._session.get(url, headers=self.headers) as r:
_LOGGER.debug(
"Response from %s [%s]: %s", url, r.status, await r.text()
)

if r.status == HTTPStatus.UNAUTHORIZED:
try:
errmsg = await r.json()
except (JSONDecodeError, aiohttp.ClientError):
_LOGGER.debug(
"Exception: Cannot parse request response:\n %s",
traceback.format_exc(),
)
else:
_LOGGER.debug(
"Exception: Cannot get list activity: %s", errmsg["message"]
)
raise BringAuthException(
"Loading list activity failed due to authorization failure, "
"the authorization token is invalid or expired."
)

r.raise_for_status()

try:
return BringActivityResponse.from_json(await r.text())
except (JSONDecodeError, KeyError) as e:
_LOGGER.debug(
"Exception: Cannot get activity for list %s:\n%s",
list_uuid,
traceback.format_exc(),
)
raise BringParseException(
"Loading list activity failed during parsing of request response."
) from e

except TimeoutError as e:
_LOGGER.debug(
"Exception: Cannot get activity for list %s:\n%s",
list_uuid,
traceback.format_exc(),
)
raise BringRequestException(
"Loading list activity failed due to connection timeout."
) from e
except aiohttp.ClientError as e:
_LOGGER.debug(
"Exception: Cannot get activity for list %s:\n%s",
list_uuid,
traceback.format_exc(),
)
raise BringRequestException(
"Loading list activity failed due to request exception."
) from e
38 changes: 38 additions & 0 deletions bring_api/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Bring API types."""

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum, StrEnum
from typing import Literal, NotRequired, TypedDict
from uuid import UUID
Expand Down Expand Up @@ -231,3 +232,40 @@ class BringAuthTokenResponse(DataClassORJSONMixin):
refresh_token: str
token_type: str
expires_in: int


class ActivityType(StrEnum):
"""Activity type."""

LIST_ITEMS_CHANGED = "LIST_ITEMS_CHANGED"
LIST_ITEMS_ADDED = "LIST_ITEMS_ADDED"
LIST_ITEMS_REMOVED = "LIST_ITEMS_REMOVED"


@dataclass
class ActivityContent:
"""An activity content entry."""

uuid: UUID
sessionDate: datetime
publicUserUuid: UUID
items: list[BringPurchase] = field(default_factory=list)
purchase: list[BringPurchase] = field(default_factory=list)
recently: list[BringPurchase] = field(default_factory=list)


@dataclass
class Activity:
"""An activity entry."""

type: ActivityType
content: ActivityContent


@dataclass(kw_only=True)
class BringActivityResponse(DataClassORJSONMixin):
"""A list activity."""

timeline: list[Activity] = field(default_factory=list)
timestamp: datetime
totalEvents: int
5 changes: 4 additions & 1 deletion tests/__snapshots__/test_bring.ambr
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# serializer version: 1
# name: TestGetActivity.test_get_activity
BringActivityResponse(timeline=[Activity(type=<ActivityType.LIST_ITEMS_CHANGED: 'LIST_ITEMS_CHANGED'>, content=ActivityContent(uuid=UUID('673594a9-f92d-4cb6-adf1-d2f7a83207a4'), sessionDate=datetime.datetime(2025, 1, 1, 3, 9, 33, 36000, tzinfo=datetime.timezone.utc), publicUserUuid=UUID('98615d7e-0a7d-4a7e-8f73-a9cbb9f1bc32'), items=[], purchase=[BringPurchase(uuid=UUID('658a3770-1a03-4ee0-94a6-10362a642377'), itemId='Gurke', specification='', attributes=[])], recently=[BringPurchase(uuid=UUID('1ed22d3d-f19b-4530-a518-19872da3fd3e'), itemId='Milch', specification='', attributes=[])])), Activity(type=<ActivityType.LIST_ITEMS_ADDED: 'LIST_ITEMS_ADDED'>, content=ActivityContent(uuid=UUID('9a16635c-dea2-4e00-904a-c5034f9cfecf'), sessionDate=datetime.datetime(2025, 1, 1, 2, 54, 57, 656000, tzinfo=datetime.timezone.utc), publicUserUuid=UUID('6743a171-247d-46d0-bc06-baf31194f949'), items=[BringPurchase(uuid=UUID('66a633a2-ae09-47bf-8845-3c0198480544'), itemId='Joghurt', specification='', attributes=[])], purchase=[], recently=[])), Activity(type=<ActivityType.LIST_ITEMS_REMOVED: 'LIST_ITEMS_REMOVED'>, content=ActivityContent(uuid=UUID('303dedf6-d4b2-4d25-a8cd-1c7967b84fcb'), sessionDate=datetime.datetime(2025, 1, 1, 3, 9, 12, 380000, tzinfo=datetime.timezone.utc), publicUserUuid=UUID('6d79d10b-70b2-443f-9f7e-0b02e670c402'), items=[BringPurchase(uuid=UUID('2ba8ddb6-01c6-4b0b-a89d-f3da6b291528'), itemId='Tofu', specification='', attributes=[])], purchase=[], recently=[]))], timestamp=datetime.datetime(2025, 1, 1, 3, 9, 33, 36000, tzinfo=datetime.timezone.utc), totalEvents=2)
# ---
# name: TestGetAllItemDetails.test_get_all_item_details
BringListItemsDetailsResponse(items=[BringListItemDetails(uuid=UUID('bfb5634c-d219-4d66-b68e-1388e54f0bb0'), itemId='Milchreis', listUuid=UUID('00000000-0000-0000-0000-000000000000'), userIconItemId='Reis', userSectionId='Getreideprodukte', assignedTo='', imageUrl=''), BringListItemDetails(uuid=UUID('0056b23c-7fc3-44da-8c34-426f8b632220'), itemId='Zitronensaft', listUuid=UUID('00000000-0000-0000-0000-000000000000'), userIconItemId='Zitrone', userSectionId='Zutaten & Gewürze', assignedTo='', imageUrl='')])
# ---
# name: TestGetAllUserSettings.test_get_all_user_settings
BringUserSettingsResponse(usersettings=[BringUserSettingsEntry(key='autoPush', value='ON'), BringUserSettingsEntry(key='purchaseStyle', value='grouped'), BringUserSettingsEntry(key='premiumHideSponsoredCategories', value='OFF'), BringUserSettingsEntry(key='premiumHideInspirationsBadge', value='OFF'), BringUserSettingsEntry(key='premiumHideOffersBadge', value='OFF'), BringUserSettingsEntry(key='premiumHideOffersOnMain', value='OFF'), BringUserSettingsEntry(key='defaultListUUID', value='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx'), BringUserSettingsEntry(key='discountActivatorOnMainEnabled', value='OFF'), BringUserSettingsEntry(key='onboardClient', value='android')], userlistsettings=[BringUserListSettingEntry(listUuid='00000000-00000000-00000000-00000000', usersettings=[BringUserSettingsEntry(key='listSectionOrder', value='["Früchte & Gemüse","Brot & Gebäck","Milch & Käse","Fleisch & Fisch","Zutaten & Gewürze","Fertig- & Tiefkühlprodukte","Getreideprodukte","Snacks & Süsswaren","Getränke & Tabak","Haushalt & Gesundheit","Pflege & Gesundheit","Tierbedarf","Baumarkt & Garten","Eigene Artikel"]'), BringUserSettingsEntry(key='listArticleLanguage', value='de-DE')])])
BringUserSettingsResponse(usersettings=[BringUserSettingsEntry(key='autoPush', value='ON'), BringUserSettingsEntry(key='purchaseStyle', value='grouped'), BringUserSettingsEntry(key='premiumHideSponsoredCategories', value='OFF'), BringUserSettingsEntry(key='premiumHideInspirationsBadge', value='OFF'), BringUserSettingsEntry(key='premiumHideOffersBadge', value='OFF'), BringUserSettingsEntry(key='premiumHideOffersOnMain', value='OFF'), BringUserSettingsEntry(key='defaultListUUID', value='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx'), BringUserSettingsEntry(key='discountActivatorOnMainEnabled', value='OFF'), BringUserSettingsEntry(key='onboardClient', value='android')], userlistsettings=[BringUserListSettingEntry(listUuid='00000000-0000-0000-0000-000000000000', usersettings=[BringUserSettingsEntry(key='listSectionOrder', value='["Früchte & Gemüse","Brot & Gebäck","Milch & Käse","Fleisch & Fisch","Zutaten & Gewürze","Fertig- & Tiefkühlprodukte","Getreideprodukte","Snacks & Süsswaren","Getränke & Tabak","Haushalt & Gesundheit","Pflege & Gesundheit","Tierbedarf","Baumarkt & Garten","Eigene Artikel"]'), BringUserSettingsEntry(key='listArticleLanguage', value='de-DE')])])
# ---
# name: TestGetList.test_get_list
BringItemsResponse(uuid=UUID('00000000-0000-0000-0000-000000000000'), status='SHARED', items=Items(purchase=[BringPurchase(uuid=UUID('43bdd5a2-740a-4230-8b27-d0bbde886da7'), itemId='Paprika', specification='grün', attributes=[]), BringPurchase(uuid=UUID('2de9d1c0-c211-4129-b6c5-c1260c3fc735'), itemId='Zucchetti', specification='gelb', attributes=[])], recently=[BringPurchase(uuid=UUID('5681ed79-c8e4-4c8b-95ec-112999d016c0'), itemId='Paprika', specification='rot', attributes=[]), BringPurchase(uuid=UUID('01eea2cd-f433-4263-ad08-3d71317c4298'), itemId='Pouletbrüstli', specification='', attributes=[])]))
Expand Down
69 changes: 66 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from bring_api.bring import Bring

load_dotenv()
UUID = "00000000-00000000-00000000-00000000"
UUID = "00000000-0000-0000-0000-000000000000"

BRING_LOGIN_RESPONSE = {
"uuid": UUID,
Expand All @@ -24,8 +24,8 @@
}

BRING_USER_ACCOUNT_RESPONSE = {
"userUuid": "00000000-00000000-00000000-00000000",
"publicUserUuid": "00000000-00000000-00000000-00000000",
"userUuid": "00000000-0000-0000-0000-000000000000",
"publicUserUuid": "00000000-0000-0000-0000-000000000000",
"email": "{email}",
"emailVerified": True,
"name": "{user_name}",
Expand Down Expand Up @@ -136,6 +136,69 @@
"expires_in": 604799,
}

BRING_GET_ACTIVITY_RESPONSE = {
"timeline": [
{
"type": "LIST_ITEMS_CHANGED",
"content": {
"uuid": "673594a9-f92d-4cb6-adf1-d2f7a83207a4",
"purchase": [
{
"uuid": "658a3770-1a03-4ee0-94a6-10362a642377",
"itemId": "Gurke",
"specification": "",
"attributes": [],
}
],
"recently": [
{
"uuid": "1ed22d3d-f19b-4530-a518-19872da3fd3e",
"itemId": "Milch",
"specification": "",
"attributes": [],
}
],
"sessionDate": "2025-01-01T03:09:33.036Z",
"publicUserUuid": "98615d7e-0a7d-4a7e-8f73-a9cbb9f1bc32",
},
},
{
"type": "LIST_ITEMS_ADDED",
"content": {
"uuid": "9a16635c-dea2-4e00-904a-c5034f9cfecf",
"items": [
{
"uuid": "66a633a2-ae09-47bf-8845-3c0198480544",
"itemId": "Joghurt",
"specification": "",
"attributes": [],
},
],
"sessionDate": "2025-01-01T02:54:57.656Z",
"publicUserUuid": "6743a171-247d-46d0-bc06-baf31194f949",
},
},
{
"type": "LIST_ITEMS_REMOVED",
"content": {
"uuid": "303dedf6-d4b2-4d25-a8cd-1c7967b84fcb",
"items": [
{
"uuid": "2ba8ddb6-01c6-4b0b-a89d-f3da6b291528",
"itemId": "Tofu",
"specification": "",
"attributes": [],
}
],
"sessionDate": "2025-01-01T03:09:12.380Z",
"publicUserUuid": "6d79d10b-70b2-443f-9f7e-0b02e670c402",
},
},
],
"timestamp": "2025-01-01T03:09:33.036Z",
"totalEvents": 2,
}


@pytest.fixture(name="headers")
async def headers() -> str:
Expand Down
75 changes: 75 additions & 0 deletions tests/test_bring.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)

from .conftest import (
BRING_GET_ACTIVITY_RESPONSE,
BRING_GET_ALL_ITEM_DETAILS_RESPONSE,
BRING_GET_LIST_RESPONSE,
BRING_LOAD_LISTS_RESPONSE,
Expand Down Expand Up @@ -1538,3 +1539,77 @@ async def test_value_error(self, bring):

with pytest.raises(ValueError):
await bring.set_list_article_language(UUID, "es-CO")


class TestGetActivity:
"""Tests for get_activity method."""

async def test_get_activity(
self,
bring,
mocked,
monkeypatch,
snapshot: SnapshotAssertion,
):
"""Test get_activity."""

mocked.get(
f"https://api.getbring.com/rest/v2/bringlists/{UUID}/activity",
status=HTTPStatus.OK,
payload=BRING_GET_ACTIVITY_RESPONSE,
)
monkeypatch.setattr(bring, "uuid", UUID)

activity = await bring.get_activity(uuid.UUID(UUID))

assert activity == snapshot

@pytest.mark.parametrize(
"exception",
[
asyncio.TimeoutError,
aiohttp.ClientError,
],
)
async def test_request_exception(self, mocked, bring, exception):
"""Test request exceptions."""

mocked.get(
f"https://api.getbring.com/rest/v2/bringlists/{UUID}/activity",
exception=exception,
)

with pytest.raises(BringRequestException):
await bring.get_activity(uuid.UUID(UUID))

async def test_auth_exception(self, mocked, bring):
"""Test request exceptions."""

mocked.get(
f"https://api.getbring.com/rest/v2/bringlists/{UUID}/activity",
status=HTTPStatus.UNAUTHORIZED,
payload={"message": ""},
)

with pytest.raises(BringAuthException):
await bring.get_activity(uuid.UUID(UUID))

@pytest.mark.parametrize(
("status", "exception"),
[
(HTTPStatus.OK, BringParseException),
(HTTPStatus.UNAUTHORIZED, BringAuthException),
],
)
async def test_parse_exception(self, mocked, bring, status, exception):
"""Test request exceptions."""

mocked.get(
f"https://api.getbring.com/rest/v2/bringlists/{UUID}/activity",
status=status,
body="not json",
content_type="application/json",
)

with pytest.raises(exception):
await bring.get_activity(uuid.UUID(UUID))
Loading