diff --git a/fava_client.py b/fava_client.py index 2eb3e00..5d59763 100644 --- a/fava_client.py +++ b/fava_client.py @@ -491,6 +491,61 @@ class FavaClient: logger.error(f"Fava connection error: {e}") raise + async def query_bql(self, query_string: str) -> Dict[str, Any]: + """ + Execute arbitrary Beancount Query Language (BQL) query. + + This is a general-purpose method for executing BQL queries against Fava/Beancount. + Use this for efficient aggregations, filtering, and data retrieval. + + Args: + query_string: BQL query (e.g., "SELECT account, sum(position) WHERE account ~ 'User-abc'") + + Returns: + { + "rows": [[col1, col2, ...], ...], + "types": [{"name": "col1", "type": "str"}, ...], + "column_names": ["col1", "col2", ...] + } + + Example: + result = await fava.query_bql("SELECT account, sum(position) WHERE account ~ 'User-abc'") + for row in result["rows"]: + account, balance = row + print(f"{account}: {balance}") + + See: + https://beancount.github.io/docs/beancount_query_language.html + """ + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.get( + f"{self.base_url}/query", + params={"query_string": query_string} + ) + response.raise_for_status() + result = response.json() + + # Fava returns: {"data": {"rows": [...], "types": [...]}} + data = result.get("data", {}) + rows = data.get("rows", []) + types = data.get("types", []) + column_names = [t.get("name") for t in types] + + return { + "rows": rows, + "types": types, + "column_names": column_names + } + + except httpx.HTTPStatusError as e: + logger.error(f"BQL query error: {e.response.status_code} - {e.response.text}") + logger.error(f"Query was: {query_string}") + raise + except httpx.RequestError as e: + logger.error(f"Fava connection error: {e}") + raise + async def get_account_transactions( self, account_name: str,