9249f92db8
- Full resyncs mark mirror rows missing from the download as deleted;
YNAB omits deleted entities from non-delta responses, so deletions
made just before a full sync never reached the mirror.
- YNAB tools return errors as text instead of raising, since ToolNode
re-raises and would fail the whole chat turn. ynab_sql catches all
sqlglot errors (e.g. an unbalanced quote) and runs the SQL regenerated
from the validated tree, which also drops comments that broke the
LIMIT wrapper. Results keep duplicate column names.
- Sync fetches from YNAB before opening the DB transaction, discards the
payload if the other container synced meanwhile, re-checks freshness
under the lock so parallel tool calls share one sync, and backs off
for 60s after a failure.
- The mirror is pinned to one budget; a changed YNAB_BUDGET_ID errors
instead of mixing budgets, and an unset id stays on the budget the
first sync resolved from "last-used".
- Change log: no entries for empty month rollover rows, and id fields
are logged as names (category, payee, account). ynab_recent_changes
accepts the names it prints ("assignment") and plurals.
- Outputs sized to the 4000-char tool result cap; ynab_sql reuses the
shared admin message.
- YNAB API client moves back to utils/ynab_service.py, and the frontend
tool status labels list the new tools.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
38 lines
1.3 KiB
Python
38 lines
1.3 KiB
Python
"""YNAB API client used by the budget mirror (``blueprints.ynab.sync``)."""
|
|
|
|
import os
|
|
|
|
import httpx
|
|
|
|
YNAB_API_URL = "https://api.ynab.com/v1"
|
|
|
|
|
|
class YNABService:
|
|
"""Thin async client for the YNAB endpoints the mirror needs."""
|
|
|
|
def __init__(self):
|
|
self.access_token = os.getenv("YNAB_ACCESS_TOKEN", "")
|
|
if not self.access_token:
|
|
raise ValueError("YNAB_ACCESS_TOKEN environment variable is required")
|
|
|
|
async def fetch_budget(self, budget_id: str, last_knowledge: int | None) -> dict:
|
|
"""GET a whole budget, or only what changed since ``last_knowledge``.
|
|
|
|
Returns ``{"budget": {...}, "server_knowledge": int}``. Full responses
|
|
omit deleted entities; delta responses include them with
|
|
``deleted: true``.
|
|
"""
|
|
params = {}
|
|
if last_knowledge is not None:
|
|
params["last_knowledge_of_server"] = last_knowledge
|
|
async with httpx.AsyncClient(
|
|
timeout=httpx.Timeout(60.0, connect=10.0)
|
|
) as client:
|
|
response = await client.get(
|
|
f"{YNAB_API_URL}/budgets/{budget_id}",
|
|
params=params,
|
|
headers={"Authorization": f"Bearer {self.access_token}"},
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()["data"]
|