bunq-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ """OAuth client management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from bunq.sdk.model.generated.endpoint import (
7
+ OauthCallbackUrlApiObject,
8
+ OauthClientApiObject,
9
+ )
10
+
11
+ from bunq_cli.config import (
12
+ get_oauth_client,
13
+ load_config,
14
+ remove_oauth_client,
15
+ save_config,
16
+ set_oauth_client,
17
+ )
18
+ from bunq_cli.main import Context, pass_context, yes_option
19
+ from bunq_cli.oauth import REDIRECT_URI
20
+ from bunq_cli.output import echo_output
21
+
22
+
23
+ @click.group(name="oauth-client")
24
+ def oauth_client() -> None:
25
+ """Manage the OAuth client for browser-based login."""
26
+
27
+
28
+ @oauth_client.command("register")
29
+ @pass_context
30
+ def register(ctx: Context) -> None:
31
+ """Register a new OAuth client and store its credentials.
32
+
33
+ Requires an active session (run 'bunq login --token' first); the OAuth
34
+ client is owned by the selected profile's user.
35
+ Automatically adds the CLI callback URL.
36
+ """
37
+ client = ctx.get_client(write=True) # noqa: F841 -- ensures SDK context is loaded
38
+
39
+ config = load_config(ctx.config_path)
40
+ existing = get_oauth_client(config)
41
+ if existing.get("client_id"):
42
+ raise click.ClickException(
43
+ "An OAuth client is already registered. "
44
+ "Run 'bunq oauth-client delete' first to replace it."
45
+ )
46
+
47
+ # Create the OAuth client
48
+ response = OauthClientApiObject.create(status="ACTIVE")
49
+ oauth_id = response.value
50
+
51
+ # Fetch the client details (client_id + secret)
52
+ detail = OauthClientApiObject.get(oauth_id).value
53
+ client_id = detail.client_id
54
+ client_secret = detail.secret
55
+
56
+ # Register the CLI callback URL
57
+ OauthCallbackUrlApiObject.create(oauth_id, url=REDIRECT_URI)
58
+
59
+ # Persist to config
60
+ config = set_oauth_client(config, client_id, client_secret, oauth_id, ctx.profile_name)
61
+ save_config(config, ctx.config_path)
62
+
63
+ if not ctx.quiet:
64
+ click.echo(f"OAuth client registered (ID: {oauth_id}).")
65
+ click.echo(f"Client ID: {client_id}")
66
+ click.echo(f"Callback URL: {REDIRECT_URI}")
67
+ click.echo()
68
+ click.echo("You can now log in with: bunq login")
69
+
70
+
71
+ @oauth_client.command("show")
72
+ @pass_context
73
+ def show(ctx: Context) -> None:
74
+ """Show the stored OAuth client credentials."""
75
+ config = load_config(ctx.config_path)
76
+ oauth = get_oauth_client(config)
77
+ if not oauth.get("client_id"):
78
+ raise click.ClickException(
79
+ "No OAuth client registered. Run 'bunq oauth-client register' first."
80
+ )
81
+ data = {
82
+ "oauth_client_id": oauth["oauth_client_id"],
83
+ "client_id": oauth["client_id"],
84
+ "client_secret": oauth["client_secret"],
85
+ "callback_url": REDIRECT_URI,
86
+ "profile": oauth.get("profile", ""),
87
+ }
88
+ echo_output(data, fmt=ctx.output_format)
89
+
90
+
91
+ @oauth_client.command("delete")
92
+ @yes_option
93
+ @pass_context
94
+ def delete(ctx: Context, yes: bool) -> None:
95
+ """Delete the OAuth client and remove stored credentials."""
96
+ config = load_config(ctx.config_path)
97
+ oauth = get_oauth_client(config)
98
+ oauth_id = oauth.get("oauth_client_id")
99
+ # The OAuth client lives under the user that registered it.
100
+ client = ctx.get_client(write=True, profile=oauth.get("profile")) # noqa: F841
101
+ ctx.confirm("Delete the OAuth client?", yes)
102
+
103
+ if oauth_id:
104
+ try:
105
+ OauthClientApiObject.update(oauth_id, status="CANCELLED")
106
+ except Exception:
107
+ pass # best-effort: client may already be gone
108
+
109
+ config = remove_oauth_client(config)
110
+ save_config(config, ctx.config_path)
111
+
112
+ if not ctx.quiet:
113
+ click.echo("OAuth client deleted.")
@@ -0,0 +1,90 @@
1
+ """Payments commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from bunq_cli.main import Context, pass_context, yes_option
8
+ from bunq_cli.output import echo_output
9
+
10
+ LIST_COLUMNS = ["id", "created", "amount", "currency", "counterparty", "description"]
11
+
12
+
13
+ @click.group()
14
+ def payments() -> None:
15
+ """Manage payments."""
16
+
17
+
18
+ @payments.command("list")
19
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
20
+ @click.option("--all", "fetch_all", is_flag=True, help="Fetch all pages.")
21
+ @click.option("--count", type=int, default=25, help="Number of results per page.")
22
+ @click.option("--older-id", type=int, default=None, help="Cursor: fetch results older than this ID.")
23
+ @click.option("--newer-id", type=int, default=None, help="Cursor: fetch results newer than this ID.")
24
+ @pass_context
25
+ def list_payments(
26
+ ctx: Context,
27
+ account_id: int | None,
28
+ fetch_all: bool,
29
+ count: int,
30
+ older_id: int | None,
31
+ newer_id: int | None,
32
+ ) -> None:
33
+ """List payments. Returns one page by default; use --all to fetch everything."""
34
+ client = ctx.get_client()
35
+ if fetch_all:
36
+ data = client.list_all_payments(account_id=account_id)
37
+ else:
38
+ data = client.list_payments(
39
+ account_id=account_id,
40
+ count=count,
41
+ older_id=older_id,
42
+ newer_id=newer_id,
43
+ )
44
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
45
+
46
+
47
+ @payments.command("get")
48
+ @click.argument("id", type=int)
49
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID.")
50
+ @pass_context
51
+ def get_payment(ctx: Context, id: int, account_id: int | None) -> None:
52
+ """Get a single payment by ID."""
53
+ client = ctx.get_client()
54
+ data = client.get_payment(id, account_id=account_id)
55
+ echo_output(data, fmt=ctx.output_format)
56
+
57
+
58
+ @payments.command("create")
59
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
60
+ @click.option("--amount", required=True, help="Amount to send (e.g., 25.00).")
61
+ @click.option("--currency", default="EUR", help="Currency code (default: EUR).")
62
+ @click.option("--recipient-iban", required=True, help="Recipient IBAN.")
63
+ @click.option("--recipient-name", required=True, help="Recipient name.")
64
+ @click.option("--description", required=True, help="Payment description.")
65
+ @yes_option
66
+ @pass_context
67
+ def create_payment(
68
+ ctx: Context,
69
+ account_id: int | None,
70
+ amount: str,
71
+ currency: str,
72
+ recipient_iban: str,
73
+ recipient_name: str,
74
+ description: str,
75
+ yes: bool,
76
+ ) -> None:
77
+ """Create a payment."""
78
+ client = ctx.get_client(write=True)
79
+ source = f"account {account_id}" if account_id else "the primary account"
80
+ ctx.confirm(f"Pay {amount} {currency} to {recipient_name} ({recipient_iban}) from {source}?", yes)
81
+ payment_id = client.create_payment(
82
+ amount=amount,
83
+ currency=currency,
84
+ recipient_iban=recipient_iban,
85
+ recipient_name=recipient_name,
86
+ description=description,
87
+ account_id=account_id,
88
+ )
89
+ if not ctx.quiet:
90
+ click.echo(f"Payment created (ID: {payment_id}).")
@@ -0,0 +1,91 @@
1
+ """Profile (client) management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ from bunq_cli.config import (
10
+ load_config,
11
+ resolve_profile,
12
+ save_config,
13
+ validate_profile_name,
14
+ )
15
+ from bunq_cli.main import Context, pass_context
16
+ from bunq_cli.output import echo_output
17
+
18
+
19
+ @click.group()
20
+ def profiles() -> None:
21
+ """Manage profiles (one per bunq login, e.g. per client)."""
22
+
23
+
24
+ @profiles.command("list")
25
+ @pass_context
26
+ def list_profiles(ctx: Context) -> None:
27
+ """List profiles."""
28
+ config = load_config(ctx.config_path)
29
+ default = config.get("default_profile")
30
+ data = [
31
+ {
32
+ "default": "*" if name == default else "",
33
+ "name": name,
34
+ "display_name": p.get("display_name", ""),
35
+ "environment": "sandbox" if p.get("sandbox") else "production",
36
+ "read_only": p.get("read_only", False),
37
+ "logged_in": Path(p["context_path"]).expanduser().exists(),
38
+ }
39
+ for name, p in config.get("profiles", {}).items()
40
+ ]
41
+ echo_output(data, fmt=ctx.output_format)
42
+
43
+
44
+ @profiles.command("use")
45
+ @click.argument("name")
46
+ @pass_context
47
+ def use_profile(ctx: Context, name: str) -> None:
48
+ """Set the default profile."""
49
+ config = load_config(ctx.config_path)
50
+ resolve_profile(config, name)
51
+ config["default_profile"] = name
52
+ save_config(config, ctx.config_path)
53
+ if not ctx.quiet:
54
+ click.echo(f"Default profile is now '{name}'.")
55
+
56
+
57
+ @profiles.command("rename")
58
+ @click.argument("old")
59
+ @click.argument("new")
60
+ @pass_context
61
+ def rename_profile(ctx: Context, old: str, new: str) -> None:
62
+ """Rename a profile."""
63
+ validate_profile_name(new)
64
+ config = load_config(ctx.config_path)
65
+ resolve_profile(config, old)
66
+ if new in config["profiles"]:
67
+ raise click.ClickException(f"Profile '{new}' already exists.")
68
+ config["profiles"][new] = config["profiles"].pop(old)
69
+ if config.get("default_profile") == old:
70
+ config["default_profile"] = new
71
+ if config.get("oauth", {}).get("profile") == old:
72
+ config["oauth"]["profile"] = new
73
+ save_config(config, ctx.config_path)
74
+ if not ctx.quiet:
75
+ click.echo(f"Profile '{old}' renamed to '{new}'.")
76
+
77
+
78
+ @profiles.command("set")
79
+ @click.argument("name")
80
+ @click.option("--read-only/--read-write", default=None, help="Block or allow commands that change data.")
81
+ @pass_context
82
+ def set_profile_options(ctx: Context, name: str, read_only: bool | None) -> None:
83
+ """Change profile settings."""
84
+ if read_only is None:
85
+ raise click.ClickException("Nothing to change. Use --read-only or --read-write.")
86
+ config = load_config(ctx.config_path)
87
+ resolve_profile(config, name)
88
+ config["profiles"][name]["read_only"] = read_only
89
+ save_config(config, ctx.config_path)
90
+ if not ctx.quiet:
91
+ click.echo(f"Profile '{name}' is now {'read-only' if read_only else 'read-write'}.")
@@ -0,0 +1,122 @@
1
+ """Payment request commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from bunq_cli.main import Context, pass_context, yes_option
8
+ from bunq_cli.output import echo_output
9
+
10
+ LIST_COLUMNS = ["id", "created", "amount", "currency", "counterparty", "description", "status"]
11
+
12
+
13
+ @click.group()
14
+ def requests() -> None:
15
+ """Manage payment requests."""
16
+
17
+
18
+ @requests.command("list")
19
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID.")
20
+ @pass_context
21
+ def list_requests(ctx: Context, account_id: int | None) -> None:
22
+ """List outgoing payment requests."""
23
+ client = ctx.get_client()
24
+ data = client.list_requests(account_id=account_id)
25
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
26
+
27
+
28
+ @requests.command("get")
29
+ @click.argument("id", type=int)
30
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID.")
31
+ @pass_context
32
+ def get_request(ctx: Context, id: int, account_id: int | None) -> None:
33
+ """Get a single outgoing payment request."""
34
+ client = ctx.get_client()
35
+ data = client.get_request(id, account_id=account_id)
36
+ echo_output(data, fmt=ctx.output_format)
37
+
38
+
39
+ @requests.command("create")
40
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID.")
41
+ @click.option("--amount", required=True, help="Amount to request (e.g., 35.00).")
42
+ @click.option("--currency", default="EUR", help="Currency code (default: EUR).")
43
+ @click.option("--counterparty-email", default=None, help="Recipient email address.")
44
+ @click.option("--counterparty-phone", default=None, help="Recipient phone number (e.g., +31612345678).")
45
+ @click.option("--description", required=True, help="Request description.")
46
+ @pass_context
47
+ def create_request(
48
+ ctx: Context,
49
+ account_id: int | None,
50
+ amount: str,
51
+ currency: str,
52
+ counterparty_email: str | None,
53
+ counterparty_phone: str | None,
54
+ description: str,
55
+ ) -> None:
56
+ """Create a payment request."""
57
+ if counterparty_email:
58
+ cp_type, cp_value = "EMAIL", counterparty_email
59
+ elif counterparty_phone:
60
+ cp_type, cp_value = "PHONE_NUMBER", counterparty_phone
61
+ else:
62
+ raise click.ClickException("Provide --counterparty-email or --counterparty-phone.")
63
+
64
+ client = ctx.get_client(write=True)
65
+ request_id = client.create_request(
66
+ amount=amount,
67
+ currency=currency,
68
+ counterparty_type=cp_type,
69
+ counterparty_value=cp_value,
70
+ description=description,
71
+ account_id=account_id,
72
+ )
73
+ if not ctx.quiet:
74
+ click.echo(f"Payment request created (ID: {request_id}).")
75
+
76
+
77
+ @requests.command("revoke")
78
+ @click.argument("id", type=int)
79
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID.")
80
+ @pass_context
81
+ def revoke_request(ctx: Context, id: int, account_id: int | None) -> None:
82
+ """Revoke a pending payment request."""
83
+ client = ctx.get_client(write=True)
84
+ client.revoke_request(id, account_id=account_id)
85
+ if not ctx.quiet:
86
+ click.echo(f"Request {id} revoked.")
87
+
88
+
89
+ @requests.command("incoming")
90
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID.")
91
+ @pass_context
92
+ def list_incoming(ctx: Context, account_id: int | None) -> None:
93
+ """List incoming payment requests."""
94
+ client = ctx.get_client()
95
+ data = client.list_incoming_requests(account_id=account_id)
96
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
97
+
98
+
99
+ @requests.command("accept")
100
+ @click.argument("id", type=int)
101
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID.")
102
+ @yes_option
103
+ @pass_context
104
+ def accept_request(ctx: Context, id: int, account_id: int | None, yes: bool) -> None:
105
+ """Accept (and pay) an incoming payment request."""
106
+ client = ctx.get_client(write=True)
107
+ ctx.confirm(f"Accept and pay incoming request {id}?", yes)
108
+ client.accept_request(id, account_id=account_id)
109
+ if not ctx.quiet:
110
+ click.echo(f"Request {id} accepted.")
111
+
112
+
113
+ @requests.command("reject")
114
+ @click.argument("id", type=int)
115
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID.")
116
+ @pass_context
117
+ def reject_request(ctx: Context, id: int, account_id: int | None) -> None:
118
+ """Reject an incoming payment request."""
119
+ client = ctx.get_client(write=True)
120
+ client.reject_request(id, account_id=account_id)
121
+ if not ctx.quiet:
122
+ click.echo(f"Request {id} rejected.")
@@ -0,0 +1,25 @@
1
+ """Sandbox helper commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from bunq_cli.client import create_sandbox_user
8
+ from bunq_cli.commands.login import login
9
+
10
+
11
+ @click.group()
12
+ def sandbox() -> None:
13
+ """Sandbox environment helpers."""
14
+
15
+
16
+ @sandbox.command("create-user")
17
+ @click.option("--login", "login_profile", metavar="PROFILE", default=None, help="Log in with the new user under this profile name.")
18
+ @click.pass_context
19
+ def create_user(click_ctx: click.Context, login_profile: str | None) -> None:
20
+ """Create a sandbox test user and print its API key."""
21
+ api_key = create_sandbox_user()
22
+ if login_profile:
23
+ click_ctx.invoke(login, profile_name=login_profile, token=api_key, sandbox=True)
24
+ else:
25
+ click.echo(api_key)
@@ -0,0 +1,108 @@
1
+ """Scheduled payment commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+
7
+ import click
8
+
9
+ from bunq_cli.main import Context, pass_context, yes_option
10
+ from bunq_cli.output import echo_output
11
+
12
+ LIST_COLUMNS = ["id", "status", "amount", "currency", "counterparty", "description", "every", "next"]
13
+ UNITS = ["ONCE", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY"]
14
+ DATETIME_FORMATS = ["%Y-%m-%d", "%Y-%m-%d %H:%M", "%Y-%m-%dT%H:%M"]
15
+
16
+
17
+ def _to_api_time(value: datetime) -> str:
18
+ # bunq expects UTC; naive input is treated as local time.
19
+ return value.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
20
+
21
+
22
+ @click.group("scheduled-payments")
23
+ def scheduled() -> None:
24
+ """Manage scheduled (recurring) payments."""
25
+
26
+
27
+ @scheduled.command("list")
28
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
29
+ @pass_context
30
+ def list_scheduled(ctx: Context, account_id: int | None) -> None:
31
+ """List scheduled payments."""
32
+ client = ctx.get_client()
33
+ data = client.list_scheduled_payments(account_id=account_id)
34
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
35
+
36
+
37
+ @scheduled.command("get")
38
+ @click.argument("id", type=int)
39
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
40
+ @pass_context
41
+ def get_scheduled(ctx: Context, id: int, account_id: int | None) -> None:
42
+ """Get a single scheduled payment."""
43
+ client = ctx.get_client()
44
+ data = client.get_scheduled_payment(id, account_id=account_id)
45
+ echo_output(data, fmt=ctx.output_format)
46
+
47
+
48
+ @scheduled.command("create")
49
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
50
+ @click.option("--amount", required=True, help="Amount per payment (e.g., 25.00).")
51
+ @click.option("--currency", default="EUR", help="Currency code (default: EUR).")
52
+ @click.option("--recipient-iban", required=True, help="Recipient IBAN.")
53
+ @click.option("--recipient-name", required=True, help="Recipient name.")
54
+ @click.option("--description", required=True, help="Payment description.")
55
+ @click.option("--schedule-date", "start", required=True, type=click.DateTime(DATETIME_FORMATS), help="First payment (local time, e.g. 2026-11-01 or '2026-11-01 09:00').")
56
+ @click.option("--end-date", "end", type=click.DateTime(DATETIME_FORMATS), default=None, help="Stop after this date (optional).")
57
+ @click.option("--frequency", "unit", type=click.Choice(UNITS, case_sensitive=False), default="ONCE", help="How often to pay (default: ONCE).")
58
+ @click.option("--every", type=click.IntRange(min=1), default=1, help="Repeat every N periods, e.g. --every 2 --frequency WEEKLY.")
59
+ @yes_option
60
+ @pass_context
61
+ def create_scheduled(
62
+ ctx: Context,
63
+ account_id: int | None,
64
+ amount: str,
65
+ currency: str,
66
+ recipient_iban: str,
67
+ recipient_name: str,
68
+ description: str,
69
+ start: datetime,
70
+ end: datetime | None,
71
+ unit: str,
72
+ every: int,
73
+ yes: bool,
74
+ ) -> None:
75
+ """Schedule a (recurring) payment."""
76
+ client = ctx.get_client(write=True)
77
+ source = f"account {account_id}" if account_id else "the primary account"
78
+ ctx.confirm(
79
+ f"Schedule {amount} {currency} to {recipient_name} ({recipient_iban}) from {source}, "
80
+ f"starting {start:%Y-%m-%d %H:%M}, {unit.upper()}?",
81
+ yes,
82
+ )
83
+ schedule_id = client.create_scheduled_payment(
84
+ amount=amount,
85
+ currency=currency,
86
+ recipient_iban=recipient_iban,
87
+ recipient_name=recipient_name,
88
+ description=description,
89
+ time_start=_to_api_time(start),
90
+ time_end=_to_api_time(end) if end else None,
91
+ recurrence_unit=unit.upper(),
92
+ recurrence_size=every,
93
+ account_id=account_id,
94
+ )
95
+ if not ctx.quiet:
96
+ click.echo(f"Scheduled payment created (ID: {schedule_id}).")
97
+
98
+
99
+ @scheduled.command("delete")
100
+ @click.argument("id", type=int)
101
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
102
+ @pass_context
103
+ def cancel_scheduled(ctx: Context, id: int, account_id: int | None) -> None:
104
+ """Cancel a scheduled payment."""
105
+ client = ctx.get_client(write=True)
106
+ client.cancel_scheduled_payment(id, account_id=account_id)
107
+ if not ctx.quiet:
108
+ click.echo(f"Scheduled payment {id} cancelled.")
@@ -0,0 +1,97 @@
1
+ """Account statement export commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from bunq_cli.main import Context, pass_context
11
+ from bunq_cli.output import echo_output
12
+
13
+ LIST_COLUMNS = ["id", "created", "date_start", "date_end", "format", "status"]
14
+
15
+
16
+ @click.group()
17
+ def statements() -> None:
18
+ """Export account statements."""
19
+
20
+
21
+ @statements.command("list")
22
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
23
+ @pass_context
24
+ def list_statements(ctx: Context, account_id: int | None) -> None:
25
+ """List statement exports."""
26
+ client = ctx.get_client()
27
+ data = client.list_statements(account_id=account_id)
28
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
29
+
30
+
31
+ @statements.command("create")
32
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
33
+ @click.option("--from", "date_start", required=True, type=click.DateTime(["%Y-%m-%d"]), help="Start date (YYYY-MM-DD).")
34
+ @click.option("--to", "date_end", required=True, type=click.DateTime(["%Y-%m-%d"]), help="End date (YYYY-MM-DD).")
35
+ @click.option(
36
+ "--format",
37
+ "statement_format",
38
+ type=click.Choice(["CSV", "PDF", "MT940"], case_sensitive=False),
39
+ default="CSV",
40
+ help="Statement format (default: CSV).",
41
+ )
42
+ @click.option(
43
+ "--regional-format",
44
+ type=click.Choice(["EUROPEAN", "UK_US"], case_sensitive=False),
45
+ default="EUROPEAN",
46
+ help="CSV separator style: EUROPEAN (;) or UK_US (,). Only used for CSV.",
47
+ )
48
+ @pass_context
49
+ def create_statement(
50
+ ctx: Context,
51
+ account_id: int | None,
52
+ date_start: datetime,
53
+ date_end: datetime,
54
+ statement_format: str,
55
+ regional_format: str,
56
+ ) -> None:
57
+ """Request a statement export. Download it with 'statements download'."""
58
+ statement_format = statement_format.upper()
59
+ client = ctx.get_client()
60
+ statement_id = client.create_statement(
61
+ statement_format=statement_format,
62
+ date_start=date_start.strftime("%Y-%m-%d"),
63
+ date_end=date_end.strftime("%Y-%m-%d"),
64
+ regional_format=regional_format.upper() if statement_format == "CSV" else None,
65
+ account_id=account_id,
66
+ )
67
+ if not ctx.quiet:
68
+ click.echo(f"Statement export created (ID: {statement_id}).")
69
+
70
+
71
+ @statements.command("download")
72
+ @click.argument("id", type=int)
73
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
74
+ @click.option("-o", "--output-file", type=click.Path(path_type=Path, dir_okay=False), required=True, help="File to write the statement to.")
75
+ @pass_context
76
+ def download_statement(ctx: Context, id: int, account_id: int | None, output_file: Path) -> None:
77
+ """Download a statement export."""
78
+ client = ctx.get_client()
79
+ if ctx.all_profiles:
80
+ if "{profile}" not in str(output_file):
81
+ raise click.UsageError("With --all-profiles, put {profile} in the output file name.")
82
+ output_file = Path(str(output_file).replace("{profile}", ctx.profile_name))
83
+ output_file.write_bytes(client.download_statement(id, account_id=account_id))
84
+ if not ctx.quiet:
85
+ click.echo(f"Statement saved to {output_file}.")
86
+
87
+
88
+ @statements.command("delete")
89
+ @click.argument("id", type=int)
90
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
91
+ @pass_context
92
+ def delete_statement(ctx: Context, id: int, account_id: int | None) -> None:
93
+ """Delete a statement export."""
94
+ client = ctx.get_client(write=True)
95
+ client.delete_statement(id, account_id=account_id)
96
+ if not ctx.quiet:
97
+ click.echo(f"Statement {id} deleted.")