Enables Fava integration for entry management
Adds functionality to interact with Fava for managing Beancount entries, including fetching, updating, and deleting entries directly from the Beancount ledger. This allows for approving/rejecting pending entries via the API by modifying the source file through Fava. The changes include: - Adds methods to the Fava client for fetching all journal entries, retrieving entry context (source and hash), updating the entry source, and deleting entries. - Updates the pending entries API to use the Fava journal endpoint instead of querying transactions. - Implements entry approval and rejection using the new Fava client methods to modify the underlying Beancount file.
This commit is contained in:
parent
57e6b3de1d
commit
cfca10b782
2 changed files with 298 additions and 84 deletions
143
fava_client.py
143
fava_client.py
|
|
@ -525,6 +525,149 @@ class FavaClient:
|
|||
limit=limit
|
||||
)
|
||||
|
||||
async def get_journal_entries(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all journal entries from Fava (with entry hashes).
|
||||
|
||||
Returns:
|
||||
List of all entries (transactions, opens, closes, etc.) with entry_hash field.
|
||||
|
||||
Example:
|
||||
entries = await fava.get_journal_entries()
|
||||
# Each entry has: entry_hash, date, flag, narration, tags, links, etc.
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(f"{self.base_url}/journal")
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
return result.get("data", [])
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Fava journal error: {e.response.status_code} - {e.response.text}")
|
||||
raise
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Fava connection error: {e}")
|
||||
raise
|
||||
|
||||
async def get_entry_context(self, entry_hash: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get entry context including source text and sha256sum.
|
||||
|
||||
Args:
|
||||
entry_hash: Entry hash from get_journal_entries()
|
||||
|
||||
Returns:
|
||||
{
|
||||
"entry": {...}, # Serialized entry
|
||||
"slice": "2025-01-15 ! \"Description\"...", # Beancount source text
|
||||
"sha256sum": "abc123...", # For concurrency control
|
||||
"balances_before": {...},
|
||||
"balances_after": {...}
|
||||
}
|
||||
|
||||
Example:
|
||||
context = await fava.get_entry_context("abc123")
|
||||
source = context["slice"]
|
||||
sha256sum = context["sha256sum"]
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/context",
|
||||
params={"entry_hash": entry_hash}
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
return result.get("data", {})
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Fava context error: {e.response.status_code} - {e.response.text}")
|
||||
raise
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Fava connection error: {e}")
|
||||
raise
|
||||
|
||||
async def update_entry_source(self, entry_hash: str, new_source: str, sha256sum: str) -> str:
|
||||
"""
|
||||
Update an entry's source text (e.g., change flag from ! to *).
|
||||
|
||||
Args:
|
||||
entry_hash: Entry hash
|
||||
new_source: Modified Beancount source text
|
||||
sha256sum: Current sha256sum from get_entry_context() for concurrency control
|
||||
|
||||
Returns:
|
||||
New sha256sum after update
|
||||
|
||||
Example:
|
||||
# Get context
|
||||
context = await fava.get_entry_context("abc123")
|
||||
source = context["slice"]
|
||||
sha256 = context["sha256sum"]
|
||||
|
||||
# Change flag
|
||||
new_source = source.replace("2025-01-15 !", "2025-01-15 *")
|
||||
|
||||
# Update
|
||||
new_sha256 = await fava.update_entry_source("abc123", new_source, sha256)
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.put(
|
||||
f"{self.base_url}/source_slice",
|
||||
json={
|
||||
"entry_hash": entry_hash,
|
||||
"source": new_source,
|
||||
"sha256sum": sha256sum
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
return result.get("data", "")
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Fava update error: {e.response.status_code} - {e.response.text}")
|
||||
raise
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Fava connection error: {e}")
|
||||
raise
|
||||
|
||||
async def delete_entry(self, entry_hash: str, sha256sum: str) -> str:
|
||||
"""
|
||||
Delete an entry from the Beancount file.
|
||||
|
||||
Args:
|
||||
entry_hash: Entry hash
|
||||
sha256sum: Current sha256sum for concurrency control
|
||||
|
||||
Returns:
|
||||
Success message
|
||||
|
||||
Example:
|
||||
context = await fava.get_entry_context("abc123")
|
||||
await fava.delete_entry("abc123", context["sha256sum"])
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.delete(
|
||||
f"{self.base_url}/source_slice",
|
||||
json={
|
||||
"entry_hash": entry_hash,
|
||||
"sha256sum": sha256sum
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
return result.get("data", "")
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Fava delete error: {e.response.status_code} - {e.response.text}")
|
||||
raise
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Fava connection error: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# Singleton instance (configured from settings)
|
||||
_fava_client: Optional[FavaClient] = None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue