Skip to content
Open
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
79 changes: 79 additions & 0 deletions bring_api/bring.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 30 additions & 1 deletion smoke_test/test_2_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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),
)
4 changes: 4 additions & 0 deletions tests/__snapshots__/test_get_template_content.ambr
Original file line number Diff line number Diff line change
@@ -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=<TemplateType.TEMPLATE: 'TEMPLATE'>, isPromoted=None, isAd=None, contentSrcUrl=None, attribution=None, attributionSubtitle=None, attributionIcon=None)
# ---
7 changes: 7 additions & 0 deletions tests/__snapshots__/test_get_user_recipes.ambr
Original file line number Diff line number Diff line change
@@ -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=<TemplateType.TEMPLATE: 'TEMPLATE'>, 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=<TemplateType.TEMPLATE: 'TEMPLATE'>, isPromoted=None, isAd=None, contentSrcUrl=None, attribution=None, attributionSubtitle=None, attributionIcon=None),
])
# ---
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions tests/fixtures/template_content_response.json
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 17 additions & 0 deletions tests/test_get_template_content.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions tests/test_get_user_recipes.py
Original file line number Diff line number Diff line change
@@ -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
Loading