feat: lazily find-or-create the Bestellungen Deck board and status stacks
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V57jSQPqwkGG8BuAXg59X5
This commit is contained in:
parent
dfe76db184
commit
2313c3c490
|
|
@ -485,6 +485,39 @@ async def _deck_request(method: str, path: str, json_body: dict = None):
|
||||||
|
|
||||||
_deck_cache: dict = {}
|
_deck_cache: dict = {}
|
||||||
|
|
||||||
|
_DECK_BOARD_TITLE = "Bestellungen"
|
||||||
|
_DECK_STACK_TITLES = ["Offen", "In Arbeit", "Erledigt"]
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_deck_board() -> dict:
|
||||||
|
"""Finds (or lazily creates) the 'Bestellungen' board and its three
|
||||||
|
status stacks. Result is cached in _deck_cache after the first call -
|
||||||
|
board/stack ids don't change at runtime, and finding/creating them
|
||||||
|
costs several sequential Deck API calls."""
|
||||||
|
if "board_id" in _deck_cache:
|
||||||
|
return _deck_cache
|
||||||
|
|
||||||
|
boards = await _deck_request("GET", "/boards")
|
||||||
|
board = next((b for b in boards if b["title"] == _DECK_BOARD_TITLE), None)
|
||||||
|
if board is None:
|
||||||
|
board = await _deck_request("POST", "/boards", {"title": _DECK_BOARD_TITLE, "color": "0082C9"})
|
||||||
|
board_id = board["id"]
|
||||||
|
|
||||||
|
stacks = await _deck_request("GET", f"/boards/{board_id}/stacks")
|
||||||
|
existing_by_title = {s["title"]: s["id"] for s in stacks}
|
||||||
|
|
||||||
|
stack_ids = {}
|
||||||
|
for order, title in enumerate(_DECK_STACK_TITLES):
|
||||||
|
if title in existing_by_title:
|
||||||
|
stack_ids[title] = existing_by_title[title]
|
||||||
|
else:
|
||||||
|
created = await _deck_request("POST", f"/boards/{board_id}/stacks", {"title": title, "order": order})
|
||||||
|
stack_ids[title] = created["id"]
|
||||||
|
|
||||||
|
_deck_cache["board_id"] = board_id
|
||||||
|
_deck_cache["stacks"] = stack_ids
|
||||||
|
return _deck_cache
|
||||||
|
|
||||||
|
|
||||||
def _decode_mime_words(value: str) -> str:
|
def _decode_mime_words(value: str) -> str:
|
||||||
parts = decode_header(value)
|
parts = decode_header(value)
|
||||||
|
|
|
||||||
|
|
@ -85,3 +85,59 @@ async def test_deck_request_raises_on_error_status():
|
||||||
with patch.object(main.aiohttp, "ClientSession", return_value=fake_session):
|
with patch.object(main.aiohttp, "ClientSession", return_value=fake_session):
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
await main._deck_request("GET", "/boards")
|
await main._deck_request("GET", "/boards")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ensure_deck_board_creates_board_and_stacks_when_missing():
|
||||||
|
titles_created = []
|
||||||
|
|
||||||
|
async def fake_deck_request(method, path, json_body=None):
|
||||||
|
if (method, path) == ("GET", "/boards"):
|
||||||
|
return []
|
||||||
|
if (method, path) == ("POST", "/boards"):
|
||||||
|
return {"id": 10, "title": "Bestellungen"}
|
||||||
|
if (method, path) == ("GET", "/boards/10/stacks"):
|
||||||
|
return []
|
||||||
|
if (method, path) == ("POST", "/boards/10/stacks"):
|
||||||
|
titles_created.append(json_body["title"])
|
||||||
|
return {"id": 100 + len(titles_created), "title": json_body["title"]}
|
||||||
|
raise AssertionError(f"unexpected call {method} {path}")
|
||||||
|
|
||||||
|
with patch.object(main, "_deck_request", new=AsyncMock(side_effect=fake_deck_request)):
|
||||||
|
board = await main._ensure_deck_board()
|
||||||
|
|
||||||
|
assert board["board_id"] == 10
|
||||||
|
assert titles_created == ["Offen", "In Arbeit", "Erledigt"]
|
||||||
|
assert set(board["stacks"].keys()) == {"Offen", "In Arbeit", "Erledigt"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ensure_deck_board_reuses_existing_board_and_stacks():
|
||||||
|
async def fake_deck_request(method, path, json_body=None):
|
||||||
|
if (method, path) == ("GET", "/boards"):
|
||||||
|
return [{"id": 10, "title": "Bestellungen"}]
|
||||||
|
if (method, path) == ("GET", "/boards/10/stacks"):
|
||||||
|
return [
|
||||||
|
{"id": 1, "title": "Offen", "cards": []},
|
||||||
|
{"id": 2, "title": "In Arbeit", "cards": []},
|
||||||
|
{"id": 3, "title": "Erledigt", "cards": []},
|
||||||
|
]
|
||||||
|
raise AssertionError(f"unexpected call {method} {path}")
|
||||||
|
|
||||||
|
with patch.object(main, "_deck_request", new=AsyncMock(side_effect=fake_deck_request)) as mock_req:
|
||||||
|
board = await main._ensure_deck_board()
|
||||||
|
|
||||||
|
assert board == {"board_id": 10, "stacks": {"Offen": 1, "In Arbeit": 2, "Erledigt": 3}}
|
||||||
|
assert mock_req.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ensure_deck_board_caches_after_first_lookup():
|
||||||
|
main._deck_cache["board_id"] = 10
|
||||||
|
main._deck_cache["stacks"] = {"Offen": 1, "In Arbeit": 2, "Erledigt": 3}
|
||||||
|
|
||||||
|
with patch.object(main, "_deck_request", new=AsyncMock()) as mock_req:
|
||||||
|
board = await main._ensure_deck_board()
|
||||||
|
|
||||||
|
mock_req.assert_not_called()
|
||||||
|
assert board["board_id"] == 10
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue