AkahuSync/FireflySync/firefly_client.py

80 lines
3.1 KiB
Python
Raw Permalink Normal View History

2026-05-31 23:18:27 +00:00
from __future__ import annotations
from typing import Any, Iterable
from urllib.parse import urljoin
import requests
class FireflyClient:
def __init__(self, base_url: str, api_token: str, page_limit: int = 200):
self.base_url = self._normalize_base_url(base_url)
self.page_limit = page_limit
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_token}",
"Accept": "application/json",
"Content-Type": "application/json",
})
def list_accounts(self, types: list[str] | None = None) -> list[dict[str, Any]]:
params: dict[str, Any] | None = None
if types:
params = {"type": ",".join(types)}
return list(self._list_all("accounts", params=params))
def get_account(self, account_id: str) -> dict[str, Any]:
return self._request("GET", f"accounts/{account_id}")
def create_account(self, payload: dict[str, Any]) -> dict[str, Any]:
return self._request("POST", "accounts", json=payload)
def update_account(self, account_id: str, payload: dict[str, Any]) -> dict[str, Any]:
return self._request("PUT", f"accounts/{account_id}", json=payload)
def list_categories(self) -> list[dict[str, Any]]:
return list(self._list_all("categories"))
def create_category(self, payload: dict[str, Any]) -> dict[str, Any]:
return self._request("POST", "categories", json=payload)
def create_transaction(self, payload: dict[str, Any]) -> dict[str, Any]:
return self._request("POST", "transactions", json=payload)
def search_transactions(self, query: str) -> dict[str, Any]:
return self._request("GET", "search/transactions", params={"query": query})
def _list_all(self, path: str, params: dict[str, Any] | None = None) -> Iterable[dict[str, Any]]:
page = 1
while True:
merged = {"page": page, "limit": self.page_limit}
if params:
merged.update(params)
payload = self._request("GET", path, params=merged)
data = payload.get("data") or []
if not data:
break
for item in data:
yield item
if len(data) < self.page_limit:
break
page += 1
def _request(self, method: str, path: str, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None) -> dict[str, Any]:
url = urljoin(f"{self.base_url}/", path)
response = self.session.request(method, url, params=params, json=json, timeout=30)
if response.status_code >= 400:
detail = response.text.strip()
raise RuntimeError(f"Firefly API {method} {url} failed: {response.status_code} {detail}")
if not response.content:
return {}
return response.json()
@staticmethod
def _normalize_base_url(base_url: str) -> str:
base = base_url.rstrip("/")
if base.endswith("/api/v1"):
return base
if base.endswith("/api"):
return f"{base}/v1"
return f"{base}/api/v1"