first ruggedy draft

This commit is contained in:
2026-06-28 13:48:15 +02:00
parent c916f64fcb
commit 1053f6a2a3
78 changed files with 20905 additions and 2 deletions

View File

@@ -0,0 +1,250 @@
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",
},
]