feat(agent): read-only Firefly III API client
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
"""Minimal read-only client for the Firefly III REST API."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class FireflyError(Exception):
|
||||||
|
"""Firefly III returned an unexpected response."""
|
||||||
|
|
||||||
|
|
||||||
|
class FireflyClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
token: str,
|
||||||
|
transport: httpx.BaseTransport | None = None,
|
||||||
|
) -> None:
|
||||||
|
self._http = httpx.Client(
|
||||||
|
base_url=base_url,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
timeout=30.0,
|
||||||
|
transport=transport,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get(self, path: str, params: dict | None = None) -> dict:
|
||||||
|
response = self._http.get(path, params=params)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise FireflyError(
|
||||||
|
f"GET {path} -> {response.status_code}: {response.text[:200]}"
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def about(self) -> dict:
|
||||||
|
return self._get("/api/v1/about")["data"]
|
||||||
|
|
||||||
|
def accounts(self, type: str = "asset") -> list[dict]:
|
||||||
|
results: list[dict] = []
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
body = self._get("/api/v1/accounts", params={"type": type, "page": page})
|
||||||
|
results.extend(body["data"])
|
||||||
|
pagination = body.get("meta", {}).get("pagination", {})
|
||||||
|
if page >= pagination.get("total_pages", 1):
|
||||||
|
return results
|
||||||
|
page += 1
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import httpx
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from agent.firefly import FireflyClient, FireflyError
|
||||||
|
|
||||||
|
|
||||||
|
def make_client(handler) -> FireflyClient:
|
||||||
|
return FireflyClient(
|
||||||
|
"http://firefly.test", "tok", transport=httpx.MockTransport(handler)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_about_sends_bearer_token_and_returns_data():
|
||||||
|
def handler(request: httpx.Request) -> httpx.Response:
|
||||||
|
assert request.headers["Authorization"] == "Bearer tok"
|
||||||
|
assert request.headers["Accept"] == "application/json"
|
||||||
|
assert request.url.path == "/api/v1/about"
|
||||||
|
return httpx.Response(200, json={"data": {"version": "6.1.1"}})
|
||||||
|
|
||||||
|
assert make_client(handler).about() == {"version": "6.1.1"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_200_raises_firefly_error_with_status():
|
||||||
|
def handler(request: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(401, json={"message": "Unauthenticated"})
|
||||||
|
|
||||||
|
with pytest.raises(FireflyError, match="401"):
|
||||||
|
make_client(handler).about()
|
||||||
|
|
||||||
|
|
||||||
|
def test_accounts_walks_all_pages():
|
||||||
|
def handler(request: httpx.Request) -> httpx.Response:
|
||||||
|
assert request.url.path == "/api/v1/accounts"
|
||||||
|
assert request.url.params["type"] == "asset"
|
||||||
|
page = int(request.url.params.get("page", "1"))
|
||||||
|
data = {
|
||||||
|
1: [{"id": "1", "attributes": {"name": "NFCU Checking"}}],
|
||||||
|
2: [{"id": "2", "attributes": {"name": "Venmo"}}],
|
||||||
|
}[page]
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"data": data,
|
||||||
|
"meta": {"pagination": {"total_pages": 2, "current_page": page}},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
accounts = make_client(handler).accounts()
|
||||||
|
assert [a["attributes"]["name"] for a in accounts] == ["NFCU Checking", "Venmo"]
|
||||||
Reference in New Issue
Block a user