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.
File without changes
@@ -0,0 +1,37 @@
1
+ """Accounts commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from bunq_cli.main import Context, pass_context
8
+ from bunq_cli.output import echo_output
9
+
10
+ LIST_COLUMNS = ["id", "description", "type", "currency", "balance", "iban", "status"]
11
+
12
+
13
+ @click.group()
14
+ def accounts() -> None:
15
+ """Manage monetary accounts."""
16
+
17
+
18
+ @accounts.command("list")
19
+ @click.option("--include-inactive", is_flag=True, help="Include closed accounts.")
20
+ @pass_context
21
+ def list_accounts(ctx: Context, include_inactive: bool) -> None:
22
+ """List all monetary accounts."""
23
+ client = ctx.get_client()
24
+ data = client.list_accounts()
25
+ if not include_inactive:
26
+ data = [a for a in data if a.get("status") == "ACTIVE"]
27
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
28
+
29
+
30
+ @accounts.command("get")
31
+ @click.argument("id", type=int)
32
+ @pass_context
33
+ def get_account(ctx: Context, id: int) -> None:
34
+ """Get a single monetary account by ID."""
35
+ client = ctx.get_client()
36
+ data = client.get_account(id)
37
+ echo_output(data, fmt=ctx.output_format)
@@ -0,0 +1,75 @@
1
+ """bunq.me payment link commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from bunq_cli.main import Context, pass_context
8
+ from bunq_cli.output import echo_output
9
+
10
+ LIST_COLUMNS = ["id", "created", "status", "amount", "currency", "description", "url"]
11
+
12
+
13
+ @click.group()
14
+ def bunqme() -> None:
15
+ """Manage bunq.me payment links."""
16
+
17
+
18
+ @bunqme.command("list")
19
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
20
+ @pass_context
21
+ def list_tabs(ctx: Context, account_id: int | None) -> None:
22
+ """List bunq.me payment links."""
23
+ client = ctx.get_client()
24
+ data = client.list_bunqme_tabs(account_id=account_id)
25
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
26
+
27
+
28
+ @bunqme.command("get")
29
+ @click.argument("id", type=int)
30
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
31
+ @pass_context
32
+ def get_tab(ctx: Context, id: int, account_id: int | None) -> None:
33
+ """Get a single bunq.me payment link."""
34
+ client = ctx.get_client()
35
+ data = client.get_bunqme_tab(id, account_id=account_id)
36
+ echo_output(data, fmt=ctx.output_format)
37
+
38
+
39
+ @bunqme.command("create")
40
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
41
+ @click.option("--amount", required=True, help="Amount to request (e.g., 15.00).")
42
+ @click.option("--currency", default="EUR", help="Currency code (default: EUR).")
43
+ @click.option("--description", required=True, help="Description shown to the payer.")
44
+ @click.option("--redirect-url", default=None, help="URL to send the payer to after paying.")
45
+ @pass_context
46
+ def create_tab(
47
+ ctx: Context,
48
+ account_id: int | None,
49
+ amount: str,
50
+ currency: str,
51
+ description: str,
52
+ redirect_url: str | None,
53
+ ) -> None:
54
+ """Create a bunq.me payment link."""
55
+ client = ctx.get_client(write=True)
56
+ data = client.create_bunqme_tab(
57
+ amount=amount,
58
+ currency=currency,
59
+ description=description,
60
+ redirect_url=redirect_url,
61
+ account_id=account_id,
62
+ )
63
+ echo_output(data, fmt=ctx.output_format)
64
+
65
+
66
+ @bunqme.command("cancel")
67
+ @click.argument("id", type=int)
68
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
69
+ @pass_context
70
+ def cancel_tab(ctx: Context, id: int, account_id: int | None) -> None:
71
+ """Cancel a bunq.me payment link."""
72
+ client = ctx.get_client(write=True)
73
+ client.cancel_bunqme_tab(id, account_id=account_id)
74
+ if not ctx.quiet:
75
+ click.echo(f"bunq.me link {id} cancelled.")
@@ -0,0 +1,122 @@
1
+ """Callbacks (notification filter) 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", "category", "notification_target"]
11
+
12
+ CATEGORIES = [
13
+ "MUTATION",
14
+ "PAYMENT",
15
+ "INSTANT_PAYMENT",
16
+ "PAYMENT_DELAYED",
17
+ "PAYMENT_SUSPENDED",
18
+ "PAYMENT_UPDATED",
19
+ "DRAFT_PAYMENT",
20
+ "REQUEST",
21
+ "SCHEDULE_RESULT",
22
+ "SCHEDULE_STATUS",
23
+ "IDEAL",
24
+ "SOFORT",
25
+ "CARD_TRANSACTION_SUCCESSFUL",
26
+ "CARD_TRANSACTION_FAILED",
27
+ "CARD_TRANSACTION_UPDATED",
28
+ "CARD_TRANSACTION_REFUND",
29
+ "CARD_TRANSACTION_TOKENIZED",
30
+ "CARD_DYNAMIC_CURRENCY_CONVERSION_WARNING",
31
+ "CARD_EXPIRY",
32
+ "CARD_REPLACEMENT",
33
+ "CARD_COUNTRY_PERMISSION",
34
+ "CARD_TOKENIZATION",
35
+ "MASTERCARD_IDENTITY_CHECK",
36
+ "MONETARY_ACCOUNT_BALANCE",
37
+ "MONETARY_ACCOUNT_JOINT",
38
+ "CO_OWNER_INVITE",
39
+ "APPROVAL_REQUEST_USER",
40
+ "APPROVAL_REQUEST_MONETARY_ACCOUNT",
41
+ "WHITELIST",
42
+ "WHITELIST_RESULT",
43
+ "TAB_RESULT",
44
+ "BUNQME_TAB",
45
+ "BUNQME_TAB_RESULT_INQUIRY",
46
+ "BUNQME_TAB_RESULT_RESPONSE",
47
+ "OAUTH",
48
+ ]
49
+
50
+
51
+ @click.group()
52
+ def callbacks() -> None:
53
+ """Manage notification filters (webhooks)."""
54
+
55
+
56
+ @callbacks.command("list")
57
+ @pass_context
58
+ def list_callbacks(ctx: Context) -> None:
59
+ """List all notification filters."""
60
+ client = ctx.get_client()
61
+ data = client.list_callbacks()
62
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
63
+
64
+
65
+ @callbacks.command("create")
66
+ @click.option("--url", required=True, help="Callback URL.")
67
+ @click.option(
68
+ "--category",
69
+ "categories",
70
+ multiple=True,
71
+ type=click.Choice(CATEGORIES, case_sensitive=False),
72
+ required=True,
73
+ help="Event category (can be specified multiple times).",
74
+ )
75
+ @pass_context
76
+ def create_callback(ctx: Context, url: str, categories: tuple[str, ...]) -> None:
77
+ """Create notification filters for one or more event categories."""
78
+ if not url.lower().startswith("https://"):
79
+ raise click.BadParameter("bunq requires an HTTPS callback URL.", param_hint="--url")
80
+ client = ctx.get_client(write=True)
81
+ data = client.create_callback(url, [c.upper() for c in categories])
82
+ if not ctx.quiet:
83
+ click.echo(f"Created {len(categories)} notification filter(s).")
84
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
85
+
86
+
87
+ @callbacks.command("delete")
88
+ @yes_option
89
+ @pass_context
90
+ def delete_callbacks(ctx: Context, yes: bool) -> None:
91
+ """Remove all notification filters."""
92
+ client = ctx.get_client(write=True)
93
+ ctx.confirm("Remove ALL notification filters?", yes)
94
+ client.delete_all_callbacks()
95
+ if not ctx.quiet:
96
+ click.echo("All notification filters removed.")
97
+
98
+
99
+ @callbacks.command("failures")
100
+ @pass_context
101
+ def list_failures(ctx: Context) -> None:
102
+ """List callbacks that failed after all retries."""
103
+ client = ctx.get_client()
104
+ data = client.list_callback_failures()
105
+ echo_output(
106
+ data,
107
+ fmt=ctx.output_format,
108
+ columns=["id", "created", "category", "event_type", "object_id", "response_code"],
109
+ )
110
+
111
+
112
+ @callbacks.command("retry")
113
+ @click.argument("ids", type=int, nargs=-1, required=True)
114
+ @pass_context
115
+ def retry_failures(ctx: Context, ids: tuple[int, ...]) -> None:
116
+ """Retry failed callbacks by failure ID (max 100)."""
117
+ if len(ids) > 100:
118
+ raise click.BadParameter("At most 100 IDs can be retried at once.", param_hint="IDS")
119
+ client = ctx.get_client(write=True)
120
+ client.retry_callback_failures(list(ids))
121
+ if not ctx.quiet:
122
+ click.echo(f"Retrying {len(ids)} callback(s).")
@@ -0,0 +1,122 @@
1
+ """Cards commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from bunq_cli.main import Context, pass_context
8
+ from bunq_cli.output import echo_output
9
+
10
+ LIST_COLUMNS = ["id", "type", "product_type", "status", "second_line", "expiry_date", "card_limit"]
11
+
12
+
13
+ @click.group()
14
+ def cards() -> None:
15
+ """Manage cards."""
16
+
17
+
18
+ @cards.command("list")
19
+ @pass_context
20
+ def list_cards(ctx: Context) -> None:
21
+ """List all cards."""
22
+ client = ctx.get_client()
23
+ data = client.list_cards()
24
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
25
+
26
+
27
+ @cards.command("get")
28
+ @click.argument("id", type=int)
29
+ @pass_context
30
+ def get_card(ctx: Context, id: int) -> None:
31
+ """Get a single card by ID."""
32
+ client = ctx.get_client()
33
+ data = client.get_card(id)
34
+ echo_output(data, fmt=ctx.output_format)
35
+
36
+
37
+ @cards.command("freeze")
38
+ @click.argument("id", type=int)
39
+ @pass_context
40
+ def freeze_card(ctx: Context, id: int) -> None:
41
+ """Temporarily freeze a card."""
42
+ client = ctx.get_client(write=True)
43
+ client.update_card(id, status="DEACTIVATED")
44
+ if not ctx.quiet:
45
+ click.echo(f"Card {id} frozen.")
46
+
47
+
48
+ @cards.command("unfreeze")
49
+ @click.argument("id", type=int)
50
+ @pass_context
51
+ def unfreeze_card(ctx: Context, id: int) -> None:
52
+ """Unfreeze a frozen card."""
53
+ client = ctx.get_client(write=True)
54
+ client.update_card(id, status="ACTIVE")
55
+ if not ctx.quiet:
56
+ click.echo(f"Card {id} unfrozen.")
57
+
58
+
59
+ @cards.command("update")
60
+ @click.argument("id", type=int)
61
+ @click.option("--daily-limit", type=float, default=None, help="Daily spending limit (e.g., 500.00).")
62
+ @click.option("--atm-limit", type=float, default=None, help="Daily ATM withdrawal limit (e.g., 250.00).")
63
+ @click.option("--country-permission", "countries", multiple=True, help="Country code the card may be used in (repeatable, replaces the current list).")
64
+ @click.option("--change-pin", is_flag=True, help="Prompt for a new PIN code.")
65
+ @pass_context
66
+ def update_card(
67
+ ctx: Context,
68
+ id: int,
69
+ daily_limit: float | None,
70
+ atm_limit: float | None,
71
+ countries: tuple[str, ...],
72
+ change_pin: bool,
73
+ ) -> None:
74
+ """Update card settings."""
75
+ from bunq.sdk.model.generated.object_ import AmountObject, CardCountryPermissionObject
76
+
77
+ kwargs = {}
78
+ if daily_limit is not None:
79
+ kwargs["card_limit"] = AmountObject(f"{daily_limit:.2f}", "EUR")
80
+ if atm_limit is not None:
81
+ kwargs["card_limit_atm"] = AmountObject(f"{atm_limit:.2f}", "EUR")
82
+ if countries:
83
+ kwargs["country_permission"] = [
84
+ CardCountryPermissionObject(country=c.upper()) for c in countries
85
+ ]
86
+ if change_pin:
87
+ kwargs["pin_code"] = click.prompt("New PIN", hide_input=True, confirmation_prompt=True)
88
+
89
+ if not kwargs:
90
+ raise click.ClickException(
91
+ "No updates specified. Use --daily-limit, --atm-limit, --country-permission or --change-pin."
92
+ )
93
+
94
+ client = ctx.get_client(write=True)
95
+ data = client.update_card(id, **kwargs)
96
+ echo_output(data, fmt=ctx.output_format)
97
+
98
+
99
+ TRANSACTION_COLUMNS = ["id", "created", "amount", "currency", "merchant", "card_id", "decision", "status"]
100
+
101
+
102
+ @cards.command("transactions")
103
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
104
+ @click.option("--count", type=int, default=25, help="Number of results.")
105
+ @click.option("--older-id", type=int, default=None, help="Cursor: fetch results older than this ID.")
106
+ @pass_context
107
+ def list_transactions(ctx: Context, account_id: int | None, count: int, older_id: int | None) -> None:
108
+ """List card transactions (authorisations, declines, refunds)."""
109
+ client = ctx.get_client()
110
+ data = client.list_card_transactions(account_id=account_id, count=count, older_id=older_id)
111
+ echo_output(data, fmt=ctx.output_format, columns=TRANSACTION_COLUMNS)
112
+
113
+
114
+ @cards.command("transaction")
115
+ @click.argument("id", type=int)
116
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
117
+ @pass_context
118
+ def get_transaction(ctx: Context, id: int, account_id: int | None) -> None:
119
+ """Get a single card transaction."""
120
+ client = ctx.get_client()
121
+ data = client.get_card_transaction(id, account_id=account_id)
122
+ echo_output(data, fmt=ctx.output_format)
@@ -0,0 +1,79 @@
1
+ """Draft payment commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from bunq_cli.main import Context, pass_context
8
+ from bunq_cli.output import echo_output
9
+
10
+ LIST_COLUMNS = ["id", "created", "status", "amount", "currency", "counterparty", "description"]
11
+
12
+
13
+ @click.group("draft-payments")
14
+ def drafts() -> None:
15
+ """Manage draft payments (approved in the bunq app)."""
16
+
17
+
18
+ @drafts.command("list")
19
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
20
+ @pass_context
21
+ def list_drafts(ctx: Context, account_id: int | None) -> None:
22
+ """List draft payments."""
23
+ client = ctx.get_client()
24
+ data = client.list_draft_payments(account_id=account_id)
25
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
26
+
27
+
28
+ @drafts.command("get")
29
+ @click.argument("id", type=int)
30
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
31
+ @pass_context
32
+ def get_draft(ctx: Context, id: int, account_id: int | None) -> None:
33
+ """Get a single draft payment."""
34
+ client = ctx.get_client()
35
+ data = client.get_draft_payment(id, account_id=account_id)
36
+ echo_output(data, fmt=ctx.output_format)
37
+
38
+
39
+ @drafts.command("create")
40
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
41
+ @click.option("--amount", required=True, help="Amount to send (e.g., 25.00).")
42
+ @click.option("--currency", default="EUR", help="Currency code (default: EUR).")
43
+ @click.option("--recipient-iban", required=True, help="Recipient IBAN.")
44
+ @click.option("--recipient-name", required=True, help="Recipient name.")
45
+ @click.option("--description", required=True, help="Payment description.")
46
+ @pass_context
47
+ def create_draft(
48
+ ctx: Context,
49
+ account_id: int | None,
50
+ amount: str,
51
+ currency: str,
52
+ recipient_iban: str,
53
+ recipient_name: str,
54
+ description: str,
55
+ ) -> None:
56
+ """Create a draft payment. It is only executed after approval in the bunq app."""
57
+ client = ctx.get_client(write=True)
58
+ draft_id = client.create_draft_payment(
59
+ amount=amount,
60
+ currency=currency,
61
+ recipient_iban=recipient_iban,
62
+ recipient_name=recipient_name,
63
+ description=description,
64
+ account_id=account_id,
65
+ )
66
+ if not ctx.quiet:
67
+ click.echo(f"Draft payment created (ID: {draft_id}). Approve it in the bunq app.")
68
+
69
+
70
+ @drafts.command("reject")
71
+ @click.argument("id", type=int)
72
+ @click.option("--account", "account_id", type=int, default=None, help="Monetary account ID (default: primary).")
73
+ @pass_context
74
+ def reject_draft(ctx: Context, id: int, account_id: int | None) -> None:
75
+ """Reject (withdraw) a pending draft payment."""
76
+ client = ctx.get_client(write=True)
77
+ client.reject_draft_payment(id, account_id=account_id)
78
+ if not ctx.quiet:
79
+ click.echo(f"Draft payment {id} rejected.")
@@ -0,0 +1,37 @@
1
+ """Event (activity feed) commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from bunq_cli.main import Context, pass_context
8
+ from bunq_cli.output import echo_output
9
+
10
+ LIST_COLUMNS = ["id", "created", "action", "object_type", "object_id", "account_id", "status"]
11
+
12
+
13
+ @click.group()
14
+ def events() -> None:
15
+ """View the account activity feed."""
16
+
17
+
18
+ @events.command("list")
19
+ @click.option("--account", "account_id", type=int, default=None, help="Only events for this monetary account.")
20
+ @click.option("--count", type=int, default=25, help="Number of results.")
21
+ @click.option("--older-id", type=int, default=None, help="Cursor: fetch results older than this ID.")
22
+ @pass_context
23
+ def list_events(ctx: Context, account_id: int | None, count: int, older_id: int | None) -> None:
24
+ """List recent events."""
25
+ client = ctx.get_client()
26
+ data = client.list_events(account_id=account_id, count=count, older_id=older_id)
27
+ echo_output(data, fmt=ctx.output_format, columns=LIST_COLUMNS)
28
+
29
+
30
+ @events.command("get")
31
+ @click.argument("id", type=int)
32
+ @pass_context
33
+ def get_event(ctx: Context, id: int) -> None:
34
+ """Get a single event, including the object it refers to."""
35
+ client = ctx.get_client()
36
+ data = client.get_event(id)
37
+ echo_output(data, fmt=ctx.output_format)
@@ -0,0 +1,131 @@
1
+ """Login and logout commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from bunq_cli.client import create_context, current_user_name, destroy_context
11
+ from bunq_cli.config import (
12
+ CONTEXTS_DIR,
13
+ load_config,
14
+ remove_profile,
15
+ resolve_profile,
16
+ save_config,
17
+ set_profile,
18
+ validate_profile_name,
19
+ )
20
+ from bunq_cli.main import Context, pass_context
21
+ from bunq_cli.oauth import oauth_login
22
+
23
+
24
+ def _validate_ips(ctx: click.Context, param: click.Parameter, value: tuple[str, ...]) -> list[str]:
25
+ for ip in value:
26
+ if ip != "*":
27
+ try:
28
+ ipaddress.ip_address(ip)
29
+ except ValueError:
30
+ raise click.BadParameter(f"{ip!r} is not an IP address or '*'.") from None
31
+ return list(value)
32
+
33
+
34
+ def _validate_profile(ctx: click.Context, param: click.Parameter, value: str | None) -> str | None:
35
+ return validate_profile_name(value) if value else None
36
+
37
+
38
+ def _contexts_dir() -> Path:
39
+ CONTEXTS_DIR.mkdir(parents=True, exist_ok=True)
40
+ try:
41
+ CONTEXTS_DIR.chmod(0o700)
42
+ except OSError:
43
+ pass
44
+ return CONTEXTS_DIR
45
+
46
+
47
+ @click.command()
48
+ @click.option("--profile", "profile_name", default=None, callback=_validate_profile, help="Profile name to store this login under (e.g. the client's name).")
49
+ @click.option("--token", default=None, help="API key (skip OAuth and use this token directly).")
50
+ @click.option("--sandbox", is_flag=True, help="Use the sandbox environment.")
51
+ @click.option(
52
+ "--allowed-ip",
53
+ "allowed_ips",
54
+ multiple=True,
55
+ default=["*"],
56
+ show_default=True,
57
+ callback=_validate_ips,
58
+ help="IP address the API key may be used from (repeatable). With '*', bunq assigns "
59
+ "the IP you log in from, or allows any IP for wildcard API keys.",
60
+ )
61
+ @click.option("--read-only/--read-write", default=None, help="Block commands that change data for this profile.")
62
+ @pass_context
63
+ def login(
64
+ ctx: Context,
65
+ profile_name: str | None,
66
+ token: str | None,
67
+ sandbox: bool,
68
+ allowed_ips: list[str],
69
+ read_only: bool | None,
70
+ ) -> None:
71
+ """Log in to a bunq account and store it as a profile.
72
+
73
+ Without --token, opens your browser for OAuth authorization.
74
+ With --token, uses the API key directly.
75
+ """
76
+ sandbox = sandbox or ctx.sandbox
77
+ config = load_config(ctx.config_path)
78
+ profiles = config.get("profiles", {})
79
+ name = profile_name or ctx.profile
80
+ if name is None and profiles:
81
+ # Guessing could overwrite another client's login.
82
+ raise click.UsageError(
83
+ f"Pass --profile NAME (existing profiles: {', '.join(profiles)})."
84
+ )
85
+ name = validate_profile_name(name or "default")
86
+
87
+ if token is None:
88
+ token = oauth_login(sandbox=sandbox, config=config)
89
+
90
+ context_path = profiles.get(name, {}).get("context_path") or str(_contexts_dir() / f"{name}.conf")
91
+ create_context(
92
+ api_key=token,
93
+ context_path=Path(context_path).expanduser(),
94
+ sandbox=sandbox,
95
+ permitted_ips=allowed_ips,
96
+ )
97
+
98
+ config = set_profile(
99
+ config,
100
+ name,
101
+ context_path=context_path,
102
+ sandbox=sandbox,
103
+ display_name=current_user_name(),
104
+ read_only=read_only,
105
+ )
106
+ save_config(config, ctx.config_path)
107
+
108
+ if not ctx.quiet:
109
+ env_name = "sandbox" if sandbox else "production"
110
+ who = config["profiles"][name].get("display_name") or "unknown user"
111
+ click.echo(f"Logged in to bunq ({env_name}) as {who}, profile '{name}'.")
112
+
113
+
114
+ @click.command()
115
+ @click.option("--profile", "profile_name", default=None, help="Profile to log out (default: the selected profile).")
116
+ @click.option("--all", "all_profiles", is_flag=True, help="Log out of every profile.")
117
+ @pass_context
118
+ def logout(ctx: Context, profile_name: str | None, all_profiles: bool) -> None:
119
+ """Close the session and remove a profile's stored credentials."""
120
+ config = load_config(ctx.config_path)
121
+ if all_profiles:
122
+ names = list(config.get("profiles", {}))
123
+ else:
124
+ names = [resolve_profile(config, profile_name or ctx.profile)[0]]
125
+
126
+ for name in names:
127
+ destroy_context(Path(config["profiles"][name]["context_path"]).expanduser())
128
+ config = remove_profile(config, name)
129
+ if not ctx.quiet:
130
+ click.echo(f"Logged out of profile '{name}'.")
131
+ save_config(config, ctx.config_path)