diff --git a/bring_api/bring.py b/bring_api/bring.py index 3e3303a..58afd8e 100644 --- a/bring_api/bring.py +++ b/bring_api/bring.py @@ -1466,3 +1466,82 @@ async def get_inspiration_filters(self) -> BringInspirationFiltersResponse: raise BringParseException( "Request failed during parsing of request response." ) from e + + async def get_user_recipes(self) -> list[BringTemplate]: + """Fetch all recipes and templates saved by the user, with full item details. + + Combines :meth:`get_inspirations` (``filter="mine"``) with + :meth:`get_template_content` to return complete templates whose + ``items`` list is populated with ingredients and staples. + + Returns + ------- + list[BringTemplate] + Full templates for every user-created recipe/template (ad entries + of type ``POST`` are excluded). Each template's ``items`` field + contains all ingredients; items with ``stock=True`` are staples. + + Raises + ------ + BringRequestException + If any request fails. + BringParseException + If parsing of any response fails. + BringAuthException + If the request fails due to invalid or expired authorization token. + + """ + inspirations = await self.get_inspirations("mine") + recipes: list[BringTemplate] = [] + for entry in inspirations.entries: + if entry.template_type == TemplateType.POST: + continue + summary = entry.content + if not summary.contentUuid: + recipes.append(summary) + continue + full = await self.get_template_content(summary.contentUuid) + # Detail endpoint may omit top-level metadata present in the summary + if not full.name: + full.name = summary.name or summary.title + if not full.linkOutUrl: + full.linkOutUrl = summary.linkOutUrl + if not full.imageUrl: + full.imageUrl = summary.imageUrl + recipes.append(full) + return recipes + + async def get_template_content(self, content_uuid: str) -> BringTemplate: + """Fetch full template content including ingredients by content UUID. + + Parameters + ---------- + content_uuid : str + The content UUID of the template (available as ``contentUuid`` on + entries returned by :meth:`get_inspirations`). + + Returns + ------- + BringTemplate + The full template including items and ingredients. + + Raises + ------ + BringRequestException + If the request fails. + BringParseException + If the parsing of the response fails. + BringAuthException + If the request fails due to invalid or expired authorization token. + + """ + url = self.url / "v2/bringtemplates/content" / content_uuid + try: + return BringTemplate.from_json(await self._request("GET", url)) + except MissingField as e: + raise BringMissingFieldException(e) from e + except JSONDecodeError as e: + _LOGGER.debug("Exception: Cannot parse response:", exc_info=True) + raise BringParseException( + "Request failed during parsing of request response." + ) from e diff --git a/smoke_test/test_2_methods.py b/smoke_test/test_2_methods.py index 1cc23db..64a8871 100644 --- a/smoke_test/test_2_methods.py +++ b/smoke_test/test_2_methods.py @@ -6,7 +6,12 @@ from dotenv import load_dotenv from bring_api.bring import Bring -from bring_api.types import BringItemOperation, BringList, BringNotificationType +from bring_api.types import ( + BringItemOperation, + BringList, + BringNotificationType, + BringTemplate, +) load_dotenv() @@ -197,3 +202,27 @@ async def test_article_language(self, bring: Bring, test_list: BringList) -> Non await bring.set_list_article_language(test_list.listUuid, "es-ES") await bring.get_list(test_list.listUuid) await bring.set_list_article_language(test_list.listUuid, "de-DE") + + async def test_get_template_content(self, bring: Bring) -> None: + """Test get_template_content returns a full template with items.""" + inspirations = await bring.get_inspirations("mine") + first = next( + e.content for e in inspirations.entries if e.content.contentUuid + ) + result = await bring.get_template_content(first.contentUuid) # type: ignore[arg-type] + assert isinstance(result, BringTemplate) + assert result.name + _LOGGER.info("Template: %s (%d items)", result.name, len(result.items)) + + async def test_get_user_recipes(self, bring: Bring) -> None: + """Test get_user_recipes returns all user recipes with full item details.""" + result = await bring.get_user_recipes() + assert isinstance(result, list) + assert len(result) > 0 + assert all(isinstance(r, BringTemplate) for r in result) + _LOGGER.info( + "Fetched %d recipes; first: %s (%d items)", + len(result), + result[0].name, + len(result[0].items), + ) diff --git a/tests/__snapshots__/test_get_template_content.ambr b/tests/__snapshots__/test_get_template_content.ambr new file mode 100644 index 0000000..f004ed2 --- /dev/null +++ b/tests/__snapshots__/test_get_template_content.ambr @@ -0,0 +1,4 @@ +# serializer version: 1 +# name: test_get_template_content + BringTemplate(name='Spaghetti Bolognese', author=None, tagline=None, linkOutUrl='https://example.com/recipe/spaghetti-bolognese/', imageUrl='https://bring-production.s3.amazonaws.com/bring/templates/00000000-0000-0000-0000-000000000001', imageWidth=None, imageHeight=None, items=[Ingredient(itemId='Spaghetti', stock=False, spec='500g', altIcon=None, altSection=None), Ingredient(itemId='Ground Beef', stock=False, spec='400g', altIcon=None, altSection=None), Ingredient(itemId='Canned Tomatoes', stock=False, spec='1 can', altIcon=None, altSection=None), Ingredient(itemId='Onion', stock=True, spec=None, altIcon=None, altSection=None)], requestQuantity=None, baseQuantity=4, time=None, ingredients=[], likeCount=5, tags=[], enableQuantityChange=True, uuid=None, contentUuid=None, contentVersion=None, campaign=None, title=None, importCount=None, template_type=, isPromoted=None, isAd=None, contentSrcUrl=None, attribution=None, attributionSubtitle=None, attributionIcon=None) +# --- diff --git a/tests/__snapshots__/test_get_user_recipes.ambr b/tests/__snapshots__/test_get_user_recipes.ambr new file mode 100644 index 0000000..e5bd6be --- /dev/null +++ b/tests/__snapshots__/test_get_user_recipes.ambr @@ -0,0 +1,7 @@ +# serializer version: 1 +# name: test_get_user_recipes + list([ + BringTemplate(name='Spaghetti Bolognese', author=None, tagline=None, linkOutUrl='https://example.com/recipe/spaghetti-bolognese/', imageUrl='https://bring-production.s3.amazonaws.com/bring/templates/00000000-0000-0000-0000-000000000001', imageWidth=None, imageHeight=None, items=[Ingredient(itemId='Spaghetti', stock=False, spec='500g', altIcon=None, altSection=None), Ingredient(itemId='Ground Beef', stock=False, spec='400g', altIcon=None, altSection=None), Ingredient(itemId='Canned Tomatoes', stock=False, spec='1 can', altIcon=None, altSection=None), Ingredient(itemId='Onion', stock=True, spec=None, altIcon=None, altSection=None)], requestQuantity=None, baseQuantity=4, time=None, ingredients=[], likeCount=5, tags=[], enableQuantityChange=True, uuid=None, contentUuid=None, contentVersion=None, campaign=None, title=None, importCount=None, template_type=, isPromoted=None, isAd=None, contentSrcUrl=None, attribution=None, attributionSubtitle=None, attributionIcon=None), + BringTemplate(name='Chicken Curry', author=None, tagline=None, linkOutUrl='https://example.com/recipe/chicken-curry/', imageUrl='https://bring-production.s3.amazonaws.com/bring/templates/00000000-0000-0000-0000-000000000002', imageWidth=None, imageHeight=None, items=[Ingredient(itemId='Chicken Breast', stock=False, spec='600g', altIcon=None, altSection=None), Ingredient(itemId='Coconut Milk', stock=False, spec='400ml', altIcon=None, altSection=None), Ingredient(itemId='Curry Paste', stock=False, spec='2 tbsp', altIcon=None, altSection=None), Ingredient(itemId='Rice', stock=True, spec=None, altIcon=None, altSection=None)], requestQuantity=None, baseQuantity=4, time=None, ingredients=[], likeCount=3, tags=[], enableQuantityChange=True, uuid=None, contentUuid=None, contentVersion=None, campaign=None, title=None, importCount=None, template_type=, isPromoted=None, isAd=None, contentSrcUrl=None, attribution=None, attributionSubtitle=None, attributionIcon=None), + ]) +# --- diff --git a/tests/conftest.py b/tests/conftest.py index e43ad1b..261aeb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -164,4 +164,16 @@ def aioclient_mock() -> Generator[aioresponses]: status=HTTPStatus.OK, body=load_fixture("inspiration_filters_response.json"), ) + m.get( + "https://api.getbring.com/rest/v2/bringtemplates/content/1f80e479-f04b-4dfa-98fb-bee670536fe8", + status=HTTPStatus.OK, + body=load_fixture("template_content_response.json"), + repeat=True, + ) + m.get( + "https://api.getbring.com/rest/v2/bringtemplates/content/9ec60716-ca6b-4511-ae78-925325c64e26", + status=HTTPStatus.OK, + body=load_fixture("template_content_response.json"), + repeat=True, + ) yield m diff --git a/tests/fixtures/template_content_response.json b/tests/fixtures/template_content_response.json new file mode 100644 index 0000000..012de52 --- /dev/null +++ b/tests/fixtures/template_content_response.json @@ -0,0 +1,17 @@ +{ + "name": "Spaghetti Bolognese", + "linkOutUrl": "https://example.com/recipe/spaghetti-bolognese/", + "imageUrl": "https://bring-production.s3.amazonaws.com/bring/templates/00000000-0000-0000-0000-000000000001", + "items": [ + {"itemId": "Spaghetti", "spec": "500g", "stock": false}, + {"itemId": "Ground Beef", "spec": "400g", "stock": false}, + {"itemId": "Canned Tomatoes", "spec": "1 can", "stock": false}, + {"itemId": "Onion", "stock": true} + ], + "baseQuantity": 4, + "nutrition": {}, + "ingredients": [], + "likeCount": 5, + "tags": [], + "enableQuantityChange": true +} diff --git a/tests/test_get_template_content.py b/tests/test_get_template_content.py new file mode 100644 index 0000000..61630f5 --- /dev/null +++ b/tests/test_get_template_content.py @@ -0,0 +1,17 @@ +"""Unit tests for get_template_content.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from bring_api import Bring + + +@pytest.mark.usefixtures("mocked") +async def test_get_template_content( + bring: Bring, + snapshot: SnapshotAssertion, +) -> None: + """Test get_template_content.""" + await bring.login() + result = await bring.get_template_content("1f80e479-f04b-4dfa-98fb-bee670536fe8") + assert result == snapshot diff --git a/tests/test_get_user_recipes.py b/tests/test_get_user_recipes.py new file mode 100644 index 0000000..409d4be --- /dev/null +++ b/tests/test_get_user_recipes.py @@ -0,0 +1,17 @@ +"""Unit tests for get_user_recipes.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from bring_api import Bring + + +@pytest.mark.usefixtures("mocked") +async def test_get_user_recipes( + bring: Bring, + snapshot: SnapshotAssertion, +) -> None: + """Test get_user_recipes.""" + await bring.login() + result = await bring.get_user_recipes() + assert result == snapshot