251 lines
9.9 KiB
Python
251 lines
9.9 KiB
Python
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",
|
|
},
|
|
]
|