Rename Castle Accounting extension to Libra
Full identifier rename: module path lnbits.extensions.castle →
lnbits.extensions.libra, DB ext_castle → ext_libra, URL prefix
/castle/ → /libra/, manifest id castle → libra, fava ledger slug
default castle-ledger → libra-ledger, Beancount source metadata
castle-api → libra-api and link prefixes castle-{entry,tx}- →
libra-{entry,tx}-, column castle_wallet_id → libra_wallet_id, all
Python/JS/HTML identifiers (castle_ext, CastleSettings,
castle_reference, castleWalletConfigured, etc.).
Display name "Castle Accounting" → "Libra" (the scales/balance
metaphor — fits double-entry bookkeeping).
No backward compat: production hosts will be force-updated. Old
castle-prefixed Beancount metadata in existing Fava ledgers is
historical; new entries use libra-* prefixes going forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9c577c740c
commit
c174cda48d
44 changed files with 953 additions and 953 deletions
|
|
@ -1,11 +1,11 @@
|
|||
"""
|
||||
Account Synchronization Module
|
||||
|
||||
Syncs accounts from Beancount (source of truth) to Castle DB (metadata store).
|
||||
Syncs accounts from Beancount (source of truth) to Libra DB (metadata store).
|
||||
|
||||
This implements the hybrid approach:
|
||||
- Beancount owns account existence (Open directives)
|
||||
- Castle DB stores permissions and user associations
|
||||
- Libra DB stores permissions and user associations
|
||||
- Background sync keeps them in sync
|
||||
|
||||
Related: ACCOUNTS-TABLE-REMOVAL-FEASIBILITY.md - Phase 2 implementation
|
||||
|
|
@ -89,14 +89,14 @@ def extract_user_id_from_account_name(account_name: str) -> Optional[str]:
|
|||
|
||||
async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
||||
"""
|
||||
Sync accounts from Beancount to Castle DB.
|
||||
Sync accounts from Beancount to Libra DB.
|
||||
|
||||
This ensures Castle DB has metadata entries for all accounts that exist
|
||||
This ensures Libra DB has metadata entries for all accounts that exist
|
||||
in Beancount, enabling permissions and user associations to work properly.
|
||||
|
||||
New behavior (soft delete + virtual parents):
|
||||
- Accounts in Beancount but not in Castle DB: Added as active
|
||||
- Accounts in Castle DB but not in Beancount: Marked as inactive (soft delete)
|
||||
- Accounts in Beancount but not in Libra DB: Added as active
|
||||
- Accounts in Libra DB but not in Beancount: Marked as inactive (soft delete)
|
||||
- Inactive accounts that return to Beancount: Reactivated
|
||||
- Missing intermediate parents: Auto-created as virtual accounts
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
|||
dict with sync statistics:
|
||||
{
|
||||
"total_beancount_accounts": 150,
|
||||
"total_castle_accounts": 148,
|
||||
"total_libra_accounts": 148,
|
||||
"accounts_added": 2,
|
||||
"accounts_updated": 0,
|
||||
"accounts_skipped": 148,
|
||||
|
|
@ -123,7 +123,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
|||
"errors": []
|
||||
}
|
||||
"""
|
||||
logger.info("Starting account sync from Beancount to Castle DB")
|
||||
logger.info("Starting account sync from Beancount to Libra DB")
|
||||
|
||||
fava = get_fava_client()
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
|||
logger.error(f"Failed to fetch accounts from Beancount: {e}")
|
||||
return {
|
||||
"total_beancount_accounts": 0,
|
||||
"total_castle_accounts": 0,
|
||||
"total_libra_accounts": 0,
|
||||
"accounts_added": 0,
|
||||
"accounts_updated": 0,
|
||||
"accounts_skipped": 0,
|
||||
|
|
@ -143,16 +143,16 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
|||
"errors": [str(e)],
|
||||
}
|
||||
|
||||
# Get all accounts from Castle DB (including inactive ones for sync)
|
||||
castle_accounts = await get_all_accounts(include_inactive=True)
|
||||
# Get all accounts from Libra DB (including inactive ones for sync)
|
||||
libra_accounts = await get_all_accounts(include_inactive=True)
|
||||
|
||||
# Build lookup maps
|
||||
beancount_account_names = {acc["account"] for acc in beancount_accounts}
|
||||
castle_accounts_by_name = {acc.name: acc for acc in castle_accounts}
|
||||
libra_accounts_by_name = {acc.name: acc for acc in libra_accounts}
|
||||
|
||||
stats = {
|
||||
"total_beancount_accounts": len(beancount_accounts),
|
||||
"total_castle_accounts": len(castle_accounts),
|
||||
"total_libra_accounts": len(libra_accounts),
|
||||
"accounts_added": 0,
|
||||
"accounts_updated": 0,
|
||||
"accounts_skipped": 0,
|
||||
|
|
@ -162,15 +162,15 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
|||
"errors": [],
|
||||
}
|
||||
|
||||
# Step 1: Sync accounts from Beancount to Castle DB
|
||||
# Step 1: Sync accounts from Beancount to Libra DB
|
||||
for bc_account in beancount_accounts:
|
||||
account_name = bc_account["account"]
|
||||
|
||||
try:
|
||||
existing = castle_accounts_by_name.get(account_name)
|
||||
existing = libra_accounts_by_name.get(account_name)
|
||||
|
||||
if existing:
|
||||
# Account exists in Castle DB
|
||||
# Account exists in Libra DB
|
||||
# Check if it needs to be reactivated
|
||||
if not existing.is_active:
|
||||
await update_account_is_active(existing.id, True)
|
||||
|
|
@ -181,7 +181,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
|||
logger.debug(f"Account already active: {account_name}")
|
||||
continue
|
||||
|
||||
# Create new account in Castle DB
|
||||
# Create new account in Libra DB
|
||||
account_type = infer_account_type_from_name(account_name)
|
||||
user_id = extract_user_id_from_account_name(account_name)
|
||||
|
||||
|
|
@ -207,25 +207,25 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
|||
logger.error(error_msg)
|
||||
stats["errors"].append(error_msg)
|
||||
|
||||
# Step 2: Mark orphaned accounts (in Castle DB but not in Beancount) as inactive
|
||||
# Step 2: Mark orphaned accounts (in Libra DB but not in Beancount) as inactive
|
||||
# SKIP virtual accounts (they're intentionally metadata-only)
|
||||
for castle_account in castle_accounts:
|
||||
if castle_account.is_virtual:
|
||||
for libra_account in libra_accounts:
|
||||
if libra_account.is_virtual:
|
||||
# Virtual accounts are metadata-only, never deactivate them
|
||||
continue
|
||||
|
||||
if castle_account.name not in beancount_account_names:
|
||||
if libra_account.name not in beancount_account_names:
|
||||
# Account no longer exists in Beancount
|
||||
if castle_account.is_active:
|
||||
if libra_account.is_active:
|
||||
try:
|
||||
await update_account_is_active(castle_account.id, False)
|
||||
await update_account_is_active(libra_account.id, False)
|
||||
stats["accounts_deactivated"] += 1
|
||||
logger.info(
|
||||
f"Deactivated orphaned account: {castle_account.name}"
|
||||
f"Deactivated orphaned account: {libra_account.name}"
|
||||
)
|
||||
except Exception as e:
|
||||
error_msg = (
|
||||
f"Failed to deactivate account {castle_account.name}: {e}"
|
||||
f"Failed to deactivate account {libra_account.name}: {e}"
|
||||
)
|
||||
logger.error(error_msg)
|
||||
stats["errors"].append(error_msg)
|
||||
|
|
@ -236,8 +236,8 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
|||
|
||||
# IMPORTANT: Re-fetch accounts from DB after Step 1 added new accounts
|
||||
# Otherwise we'll be checking against stale data and miss newly synced children
|
||||
current_castle_accounts = await get_all_accounts(include_inactive=True)
|
||||
all_account_names = {acc.name for acc in current_castle_accounts}
|
||||
current_libra_accounts = await get_all_accounts(include_inactive=True)
|
||||
all_account_names = {acc.name for acc in current_libra_accounts}
|
||||
|
||||
for bc_account in beancount_accounts:
|
||||
account_name = bc_account["account"]
|
||||
|
|
@ -287,9 +287,9 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
|||
|
||||
async def sync_single_account_from_beancount(account_name: str) -> bool:
|
||||
"""
|
||||
Sync a single account from Beancount to Castle DB.
|
||||
Sync a single account from Beancount to Libra DB.
|
||||
|
||||
Useful for ensuring a specific account exists in Castle DB before
|
||||
Useful for ensuring a specific account exists in Libra DB before
|
||||
granting permissions on it.
|
||||
|
||||
Args:
|
||||
|
|
@ -318,7 +318,7 @@ async def sync_single_account_from_beancount(account_name: str) -> bool:
|
|||
logger.error(f"Account not found in Beancount: {account_name}")
|
||||
return False
|
||||
|
||||
# Create in Castle DB
|
||||
# Create in Libra DB
|
||||
account_type = infer_account_type_from_name(account_name)
|
||||
user_id = extract_user_id_from_account_name(account_name)
|
||||
|
||||
|
|
@ -343,9 +343,9 @@ async def sync_single_account_from_beancount(account_name: str) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
async def ensure_account_exists_in_castle(account_name: str) -> bool:
|
||||
async def ensure_account_exists_in_libra(account_name: str) -> bool:
|
||||
"""
|
||||
Ensure account exists in Castle DB, creating from Beancount if needed.
|
||||
Ensure account exists in Libra DB, creating from Beancount if needed.
|
||||
|
||||
This is the recommended function to call before granting permissions.
|
||||
|
||||
|
|
@ -355,7 +355,7 @@ async def ensure_account_exists_in_castle(account_name: str) -> bool:
|
|||
Returns:
|
||||
True if account exists (or was created), False if failed
|
||||
"""
|
||||
# Check Castle DB first
|
||||
# Check Libra DB first
|
||||
existing = await get_account_by_name(account_name)
|
||||
if existing:
|
||||
return True
|
||||
|
|
@ -367,9 +367,9 @@ async def ensure_account_exists_in_castle(account_name: str) -> bool:
|
|||
# Background sync task (can be scheduled with cron or async scheduler)
|
||||
async def scheduled_account_sync():
|
||||
"""
|
||||
Scheduled task to sync accounts from Beancount to Castle DB.
|
||||
Scheduled task to sync accounts from Beancount to Libra DB.
|
||||
|
||||
Run this periodically (e.g., every hour) to keep Castle DB in sync with Beancount.
|
||||
Run this periodically (e.g., every hour) to keep Libra DB in sync with Beancount.
|
||||
|
||||
Example with APScheduler:
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue