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