from __future__ import annotations import argparse import json from datetime import date from pathlib import Path from typing import Any from .csv_importer import read_transaction_csv from .fints_connector import ( account_to_dict, config_from_args, date_range_from_args, display_accounts, display_transactions, fetch_transactions, list_accounts, ) from .http import post_json from .imap_importer import fetch_imap_observations from .server import run as run_local_agent_server def main() -> None: parser = argparse.ArgumentParser(prog="mousehold-agent") parser.add_argument("--api-url", default="http://127.0.0.1:8000") subcommands = parser.add_subparsers(dest="command", required=True) register = subcommands.add_parser("register") register.add_argument("--household-id", required=True) register.add_argument("--owner-user-id", required=True) register.add_argument("--display-name", default="Local connector agent") csv_sync = subcommands.add_parser("sync-csv") csv_sync.add_argument("--connection-id", required=True) csv_sync.add_argument("--household-id", required=True) csv_sync.add_argument("--source-type", choices=["bank_csv", "paypal_csv"], required=True) csv_sync.add_argument("--path", type=Path, required=True) csv_sync.add_argument("--source-account-id") csv_sync.add_argument("--owner-user-id") imap_sync = subcommands.add_parser("sync-imap") imap_sync.add_argument("--connection-id", required=True) imap_sync.add_argument("--household-id", required=True) imap_sync.add_argument("--owner-user-id") imap_sync.add_argument("--source-account-id") imap_sync.add_argument("--limit", type=int, default=25) demo = subcommands.add_parser("sync-demo") demo.add_argument("--connection-id", required=True) demo.add_argument("--household-id", required=True) demo.add_argument("--owner-user-id") demo.add_argument("--source-account-id") serve = subcommands.add_parser("serve") serve.add_argument("--host", default="127.0.0.1") serve.add_argument("--port", type=int, default=8765) fints_accounts = subcommands.add_parser("fints-accounts") _add_fints_common_args(fints_accounts) fints_accounts.add_argument("--with-balances", action="store_true") fints_accounts.add_argument("--json", action="store_true") fints_transactions = subcommands.add_parser("fints-transactions") _add_fints_common_args(fints_transactions) _add_fints_transaction_args(fints_transactions) fints_transactions.add_argument("--json", action="store_true") fints_sync = subcommands.add_parser("sync-fints") _add_fints_common_args(fints_sync) _add_fints_transaction_args(fints_sync) fints_sync.add_argument("--connection-id", required=True) fints_sync.add_argument("--household-id", required=True) fints_sync.add_argument("--owner-user-id") fints_sync.add_argument("--source-account-id") fints_sync.add_argument("--display", action="store_true") args = parser.parse_args() if args.command == "register": result = post_json( f"{args.api_url}/local-agent/register", { "household_id": args.household_id, "owner_user_id": args.owner_user_id, "display_name": args.display_name, }, ) elif args.command == "sync-csv": observations = read_transaction_csv( args.path, household_id=args.household_id, source_type=args.source_type, source_account_id=args.source_account_id, owner_user_id=args.owner_user_id, ) result = _sync(args.api_url, args.connection_id, observations) elif args.command == "sync-imap": observations = fetch_imap_observations( household_id=args.household_id, owner_user_id=args.owner_user_id, source_account_id=args.source_account_id, limit=args.limit, ) result = _sync(args.api_url, args.connection_id, observations) elif args.command == "sync-demo": result = _sync( args.api_url, args.connection_id, _demo_observations(args.household_id, args.owner_user_id, args.source_account_id), ) elif args.command == "serve": run_local_agent_server(host=args.host, port=args.port) return elif args.command == "fints-accounts": accounts = list_accounts(config_from_args(args), include_balances=args.with_balances) if args.json: result = accounts else: display_accounts(accounts) return elif args.command == "fints-transactions": start_date, end_date = date_range_from_args(args) fetched = fetch_transactions( config_from_args(args), iban=args.iban, start_date=start_date, end_date=end_date, include_pending=args.include_pending, ) if args.json: result = { "account": account_to_dict(fetched.account), "observations": fetched.observations, } else: display_transactions(fetched.observations) return elif args.command == "sync-fints": start_date, end_date = date_range_from_args(args) fetched = fetch_transactions( config_from_args(args), iban=args.iban, start_date=start_date, end_date=end_date, include_pending=args.include_pending, household_id=args.household_id, source_account_id=args.source_account_id, owner_user_id=args.owner_user_id, ) if args.display: display_transactions(fetched.observations) result = _sync(args.api_url, args.connection_id, fetched.observations) else: raise AssertionError(args.command) print(json.dumps(result, indent=2, sort_keys=True)) def _sync(api_url: str, connection_id: str, observations: list[dict[str, Any]]) -> dict[str, Any]: return post_json( f"{api_url}/observations/sync", { "connection_id": connection_id, "observations": observations, }, ) def _add_fints_common_args(parser: argparse.ArgumentParser) -> None: parser.add_argument("--profile", default="default") parser.add_argument("--state-dir", default="~/.config/mousehold/fints") parser.add_argument("--no-state", action="store_true") parser.add_argument("--no-bootstrap", action="store_true") parser.add_argument("--blz", help="Bank identifier/BLZ. Env fallback: MOUSEHOLD_FINTS_BLZ") parser.add_argument("--server", help="FinTS endpoint URL. Env fallback: MOUSEHOLD_FINTS_SERVER") parser.add_argument("--bank-user-id", help="FinTS login/user ID. Env fallback: MOUSEHOLD_FINTS_USER_ID") parser.add_argument("--customer-id", help="Optional customer ID. Env fallback: MOUSEHOLD_FINTS_CUSTOMER_ID") parser.add_argument("--pin", help="FinTS PIN. Prefer prompt or MOUSEHOLD_FINTS_PIN over shell history.") parser.add_argument("--pin-env", default="MOUSEHOLD_FINTS_PIN") parser.add_argument("--tan-mechanism", help="Optional TAN mechanism. Env fallback: MOUSEHOLD_FINTS_TAN_MECHANISM") parser.add_argument("--tan-medium", help="Optional TAN medium name. Env fallback: MOUSEHOLD_FINTS_TAN_MEDIUM") parser.add_argument("--product-id", help="Registered FinTS product ID. Env fallback: MOUSEHOLD_FINTS_PRODUCT_ID") parser.add_argument( "--product-version", help="Optional product version. Env fallback: MOUSEHOLD_FINTS_PRODUCT_VERSION", ) def _add_fints_transaction_args(parser: argparse.ArgumentParser) -> None: parser.add_argument("--iban", help="IBAN to fetch. Defaults to the first account returned by the bank.") parser.add_argument("--days", type=int, default=30) parser.add_argument("--start-date", help="YYYY-MM-DD") parser.add_argument("--end-date", help="YYYY-MM-DD") parser.add_argument("--include-pending", action="store_true") def _demo_observations( household_id: str, owner_user_id: str | None, source_account_id: str | None, ) -> list[dict[str, Any]]: today = date.today().isoformat() return [ { "household_id": household_id, "source_type": "bank_csv", "source_account_id": source_account_id, "owner_user_id": owner_user_id, "source_reference": "demo-bank-001", "provider_transaction_id": "demo-bank-001", "amount": "42.50", "currency": "EUR", "event_date": today, "merchant": "REWE", "description": "Groceries", "event_type": "expense", "status": "confirmed", }, { "household_id": household_id, "source_type": "paypal_email", "source_account_id": source_account_id, "owner_user_id": owner_user_id, "source_reference": "demo-paypal-email-001", "message_id": "demo-paypal-email-001", "amount": "18.99", "currency": "EUR", "event_date": today, "due_date": today, "merchant": "PayPal Merchant", "description": "PayPal purchase announced by email", "event_type": "promised_payment", "status": "outstanding", }, { "household_id": household_id, "source_type": "klarna_email", "source_account_id": source_account_id, "owner_user_id": owner_user_id, "source_reference": "demo-klarna-email-001", "message_id": "demo-klarna-email-001", "amount": "84.00", "currency": "EUR", "event_date": today, "due_date": today, "merchant": "Klarna Shop", "description": "Klarna pay-later obligation", "event_type": "promised_payment", "status": "outstanding", }, ]