beancount-gocardless 0.1.13__py3-none-any.whl → 0.1.14__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.
@@ -1,139 +1,640 @@
1
- import argparse
2
- import sys
1
+ """
2
+ Interactive CLI for GoCardless Bank Account Data API.
3
+
4
+ A polished, modern CLI experience with arrow-key navigation.
5
+ Uses rich for beautiful output and questionary for interactive prompts.
6
+ """
7
+
8
+ from typing import Optional, Union
9
+ from datetime import datetime
3
10
  import os
4
- import logging
11
+ import sys
12
+ import argparse
5
13
 
6
- from beancount_gocardless.models import AccountInfo
7
- from beancount_gocardless.client import GoCardlessClient
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.text import Text
17
+ from rich.table import Table
18
+ from rich import box
19
+ import questionary
8
20
 
21
+ from .client import GoCardlessClient
22
+ from .mock_client import MockGoCardlessClient
23
+ from .models import AccountInfo, Institution
24
+ from .utils import load_dotenv
9
25
 
10
- logging.basicConfig(level=os.environ.get("LOGLEVEL", logging.INFO))
11
- logger = logging.getLogger(__name__)
12
26
 
27
+ class CLI:
28
+ """Interactive CLI for managing GoCardless bank connections."""
13
29
 
14
- def display_account(index: int, account: AccountInfo) -> None:
15
- """Helper function to display account information."""
16
- iban = account.get("iban", "no-iban")
17
- currency = "no-currency" # not in AccountInfo
18
- name = account.get("name", "no-name")
19
- institution_id = account.get("institution_id", "no-institution")
20
- requisition_ref = account.get("requisition_reference", "no-ref")
21
- account_id = account.get("id", "no-id")
22
- logger.info(
23
- f"{index}: {institution_id} {name}: {iban} {currency} ({requisition_ref}/{account_id})"
24
- )
30
+ def __init__(
31
+ self,
32
+ secret_id: Optional[str] = None,
33
+ secret_key: Optional[str] = None,
34
+ mock: bool = False,
35
+ env_file: Optional[str] = None,
36
+ ):
37
+ self.console = Console()
38
+ self.mock = mock
25
39
 
40
+ if env_file:
41
+ load_dotenv(env_file)
26
42
 
27
- def parse_args():
28
- parser = argparse.ArgumentParser(description="GoCardless CLI Utility")
29
- parser.add_argument(
30
- "mode",
31
- choices=[
32
- "list_banks",
33
- "create_link",
34
- "list_accounts",
35
- "delete_link",
36
- "balance",
37
- ],
38
- help="Operation mode",
43
+ self.secret_id = secret_id or os.getenv("GOCARDLESS_SECRET_ID")
44
+ self.secret_key = secret_key or os.getenv("GOCARDLESS_SECRET_KEY")
45
+ self._init_client()
46
+
47
+ def _init_client(self) -> None:
48
+ """Initialize the GoCardless client (real or mock)."""
49
+ if self.mock:
50
+ self.console.print("[dim]Using mock client[/dim]")
51
+ self.client: Union[GoCardlessClient, MockGoCardlessClient] = (
52
+ MockGoCardlessClient(
53
+ "mock-id",
54
+ "mock-key",
55
+ )
56
+ )
57
+ else:
58
+ if not self.secret_id or not self.secret_key:
59
+ self.console.print(
60
+ "[red]Error: Secret ID and Secret Key are required[/red]\n"
61
+ "Set GOCARDLESS_SECRET_ID and GOCARDLESS_SECRET_KEY environment variables\n"
62
+ "or pass --secret-id and --secret-key arguments."
63
+ )
64
+ sys.exit(1)
65
+ self.client = GoCardlessClient(self.secret_id, self.secret_key)
66
+
67
+ def _print_header(self, title: str) -> None:
68
+ """Print a styled header."""
69
+ self.console.print()
70
+ self.console.print(
71
+ Panel(
72
+ Text(title, style="bold"),
73
+ box=box.ROUNDED,
74
+ border_style="blue",
75
+ )
76
+ )
77
+ self.console.print()
78
+
79
+ def _print_success(self, message: str) -> None:
80
+ """Print a success message."""
81
+ self.console.print(f"[green]✓[/green] {message}")
82
+
83
+ def _print_error(self, message: str) -> None:
84
+ """Print an error message."""
85
+ self.console.print(f"[red]✗[/red] {message}")
86
+
87
+ def _print_info(self, message: str) -> None:
88
+ """Print an info message."""
89
+ self.console.print(f"[dim]{message}[/dim]")
90
+
91
+ def _format_expiry_status(self, account: AccountInfo) -> str:
92
+ """Format expiry status for display in account list."""
93
+ is_expired = account.get("is_expired", False)
94
+ if is_expired:
95
+ return "[EXPIRED]"
96
+
97
+ access_valid_until = account.get("access_valid_until")
98
+ if access_valid_until:
99
+ try:
100
+ expiry = datetime.fromisoformat(
101
+ access_valid_until.replace("Z", "+00:00")
102
+ )
103
+ days_remaining = (expiry - datetime.now(expiry.tzinfo)).days
104
+ if days_remaining <= 7 and days_remaining >= 0:
105
+ return f"[{days_remaining}d left]"
106
+ except (ValueError, TypeError):
107
+ pass
108
+ return ""
109
+
110
+ def _show_expiry_details(self, account: AccountInfo) -> None:
111
+ """Show detailed expiry information in a table."""
112
+ access_valid_until = account.get("access_valid_until")
113
+ is_expired = account.get("is_expired", False)
114
+ status = account.get("requisition_status", "Unknown")
115
+
116
+ table = Table(box=box.ROUNDED, show_header=False, border_style="blue")
117
+ table.add_column("Property", style="cyan")
118
+ table.add_column("Value")
119
+
120
+ table.add_row("Status", status)
121
+
122
+ if access_valid_until:
123
+ try:
124
+ expiry = datetime.fromisoformat(
125
+ access_valid_until.replace("Z", "+00:00")
126
+ )
127
+ days_remaining = (expiry - datetime.now(expiry.tzinfo)).days
128
+ expiry_str = expiry.strftime("%Y-%m-%d %H:%M")
129
+
130
+ if is_expired:
131
+ table.add_row("Access", f"[red]Expired on {expiry_str}[/red]")
132
+ elif days_remaining < 0:
133
+ table.add_row("Access", "[green]Valid[/green]")
134
+ elif days_remaining <= 7:
135
+ table.add_row(
136
+ "Access",
137
+ f"[yellow]Expires in {days_remaining} days ({expiry_str})[/yellow]",
138
+ )
139
+ else:
140
+ table.add_row(
141
+ "Access",
142
+ f"[green]Valid until {expiry_str} ({days_remaining} days left)[/green]",
143
+ )
144
+ except (ValueError, TypeError):
145
+ table.add_row("Access", "Unknown")
146
+ else:
147
+ table.add_row("Access", "Not available")
148
+
149
+ self.console.print(table)
150
+
151
+ def run(self) -> None:
152
+ """Main entry point for the interactive CLI."""
153
+ self._print_header("GoCardless Bank Manager")
154
+
155
+ while True:
156
+ action = questionary.select(
157
+ "What would you like to do?",
158
+ choices=[
159
+ questionary.Choice("List accounts", value="list"),
160
+ questionary.Choice("Add account", value="add"),
161
+ questionary.Choice("List banks (browse)", value="banks"),
162
+ questionary.Choice("Exit", value="exit"),
163
+ ],
164
+ pointer=">",
165
+ ).ask()
166
+
167
+ if action is None or action == "exit":
168
+ self.console.print("\n[dim]Goodbye![/dim]")
169
+ break
170
+
171
+ try:
172
+ if action == "list":
173
+ self.list_accounts_interactive()
174
+ elif action == "add":
175
+ self.add_account_interactive()
176
+ elif action == "banks":
177
+ self.list_banks_interactive()
178
+ except KeyboardInterrupt:
179
+ self.console.print("\n[dim]Cancelled[/dim]")
180
+ continue
181
+ except Exception as e:
182
+ self._print_error(f"Error: {e}")
183
+ continue
184
+
185
+ self.console.print()
186
+ continue_choice = questionary.select(
187
+ "What next?",
188
+ choices=[
189
+ questionary.Choice("Continue", value="continue"),
190
+ questionary.Choice("Exit", value="exit"),
191
+ ],
192
+ default="continue",
193
+ pointer=">",
194
+ ).ask()
195
+
196
+ if continue_choice == "exit":
197
+ self.console.print("\n[dim]Goodbye![/dim]")
198
+ break
199
+
200
+ def list_accounts_interactive(self) -> None:
201
+ """List all connected accounts with arrow-key selection."""
202
+ self._print_header("Connected Accounts")
203
+
204
+ accounts = self.client.list_accounts()
205
+
206
+ if not accounts:
207
+ self.console.print("[dim]No accounts found.[/dim]")
208
+ return
209
+
210
+ account_map: dict[str, dict] = {}
211
+ choices: list[str] = []
212
+ for acc in accounts:
213
+ iban = acc.get("iban", "no-iban")
214
+ name = acc.get("name", "no-name")
215
+ institution = acc.get("institution_id", "unknown")
216
+ expiry_status = self._format_expiry_status(acc)
217
+ display = f"{institution} - {name} ({iban}){expiry_status}"
218
+ account_map[display] = acc
219
+ choices.append(display)
220
+
221
+ choices.append("Back")
222
+
223
+ selected = questionary.select(
224
+ "Select an account:",
225
+ choices=choices,
226
+ pointer=">",
227
+ ).ask()
228
+
229
+ if selected is None or selected == "Back":
230
+ return
231
+
232
+ self._show_account_menu(account_map[selected])
233
+
234
+ def _show_account_menu(self, account: AccountInfo) -> None:
235
+ """Show the action menu for a selected account."""
236
+ account_id = account.get("id", "unknown")
237
+ iban = account.get("iban", "no-iban")
238
+ name = account.get("name", "no-name")
239
+ institution = account.get("institution_id", "unknown")
240
+ requisition_ref = account.get("requisition_reference", "no-ref")
241
+ is_expired = account.get("is_expired", False)
242
+
243
+ self.console.print()
244
+ table = Table(box=box.ROUNDED, show_header=False, border_style="blue")
245
+ table.add_column("Property", style="cyan")
246
+ table.add_column("Value")
247
+ table.add_row("ID", account_id)
248
+ table.add_row("Name", name)
249
+ table.add_row("IBAN", iban)
250
+ table.add_row("Institution", institution)
251
+ table.add_row("Reference", requisition_ref)
252
+ self.console.print(table)
253
+
254
+ self._show_expiry_details(account)
255
+ self.console.print()
256
+
257
+ choices = [
258
+ questionary.Choice("View balance", value="balance"),
259
+ ]
260
+
261
+ if is_expired:
262
+ choices.append(questionary.Choice("[Renew connection]", value="renew"))
263
+
264
+ choices.extend(
265
+ [
266
+ questionary.Choice("Delete link", value="delete"),
267
+ questionary.Choice("← Back", value="back"),
268
+ ]
269
+ )
270
+
271
+ action = questionary.select(
272
+ "Choose an action:",
273
+ choices=choices,
274
+ pointer=">",
275
+ ).ask()
276
+
277
+ if action == "balance":
278
+ self._view_balance(account_id)
279
+ elif action == "renew":
280
+ if institution:
281
+ self._renew_connection(requisition_ref, institution)
282
+ else:
283
+ self._print_error("Cannot renew: institution ID not found")
284
+ elif action == "delete":
285
+ self._delete_link(requisition_ref)
286
+
287
+ def _renew_connection(self, reference: str, institution_id: Optional[str]) -> None:
288
+ if not institution_id:
289
+ self._print_error("Cannot renew: institution ID not available")
290
+ return
291
+
292
+ if self.mock:
293
+ self._print_error("Mock client does not support renewing connections")
294
+ return
295
+
296
+ confirm = questionary.confirm(
297
+ "Create new authorization link to replace the expired one?",
298
+ default=False,
299
+ ).ask()
300
+
301
+ if not confirm:
302
+ self._print_info("Renewal cancelled")
303
+ return
304
+
305
+ try:
306
+ old_req = self.client.find_requisition_by_reference(reference)
307
+ link = self.client.create_bank_link(reference, institution_id)
308
+
309
+ if link:
310
+ self._print_success("New bank link created!")
311
+ self.console.print()
312
+ self.console.print(
313
+ Panel(
314
+ f"[bold]Authorization Link:[/bold]\n\n{link}",
315
+ box=box.ROUNDED,
316
+ border_style="green",
317
+ )
318
+ )
319
+ self.console.print()
320
+ self.console.print(
321
+ "[dim]Open this link in your browser to authorize the connection.[/dim]"
322
+ )
323
+
324
+ if old_req:
325
+ self.console.print()
326
+ delete_old = questionary.confirm(
327
+ "Have you authorized the new link? Delete the old expired one now?",
328
+ default=False,
329
+ ).ask()
330
+ if delete_old:
331
+ self.client.delete_requisition(old_req.id)
332
+ self._print_success("Old expired link deleted")
333
+ else:
334
+ self._print_error("Could not create new bank link")
335
+
336
+ except Exception as e:
337
+ self._print_error(f"Error renewing connection: {e}")
338
+
339
+ def _view_balance(self, account_id: str) -> None:
340
+ """View balance for an account."""
341
+ try:
342
+ balances = self.client.get_account_balances(account_id)
343
+
344
+ self.console.print()
345
+ table = Table(
346
+ title="Account Balances",
347
+ box=box.ROUNDED,
348
+ border_style="green",
349
+ )
350
+ table.add_column("Type", style="cyan")
351
+ table.add_column("Amount", style="green")
352
+ table.add_column("Currency")
353
+
354
+ for balance in balances.balances:
355
+ amount = balance.balance_amount.amount
356
+ currency = balance.balance_amount.currency
357
+ table.add_row(balance.balance_type, amount, currency)
358
+
359
+ self.console.print(table)
360
+
361
+ except Exception as e:
362
+ self._print_error(f"Could not fetch balance: {e}")
363
+
364
+ def _delete_link(self, reference: str) -> None:
365
+ """Delete a bank link by reference."""
366
+ if self.mock:
367
+ self._print_error("Mock client does not support deleting links")
368
+ return
369
+
370
+ confirm = questionary.confirm(
371
+ f"Are you sure you want to delete the link '{reference}'?",
372
+ default=False,
373
+ ).ask()
374
+
375
+ if not confirm:
376
+ self._print_info("Deletion cancelled")
377
+ return
378
+
379
+ try:
380
+ req = self.client.find_requisition_by_reference(reference)
381
+ if req:
382
+ self.client.delete_requisition(req.id)
383
+ self._print_success(f"Deleted link '{reference}'")
384
+ else:
385
+ self._print_error(f"No link found with reference '{reference}'")
386
+ except Exception as e:
387
+ self._print_error(f"Could not delete link: {e}")
388
+
389
+ def list_banks_interactive(self) -> None:
390
+ """List and browse available banks by country."""
391
+ self._print_header("Browse Banks")
392
+
393
+ country = self._select_country()
394
+ if not country:
395
+ return
396
+
397
+ self._print_info(f"Loading banks for {country}...")
398
+
399
+ try:
400
+ institutions = self.client.get_institutions(country)
401
+ except Exception as e:
402
+ self._print_error(f"Could not load banks: {e}")
403
+ return
404
+
405
+ if not institutions:
406
+ self._print_error(f"No banks found for country {country}")
407
+ return
408
+
409
+ choices = []
410
+ for inst in institutions:
411
+ display = f"{inst.name}"
412
+ if inst.bic:
413
+ display += f" (BIC: {inst.bic})"
414
+ choices.append(questionary.Choice(display, value=inst))
415
+
416
+ choices.append(questionary.Choice("← Back", value=None))
417
+
418
+ self.console.print(f"\n[dim]Found {len(institutions)} banks.[/dim]\n")
419
+
420
+ selected = questionary.select(
421
+ "Select a bank to view details:",
422
+ choices=choices,
423
+ pointer=">",
424
+ ).ask()
425
+
426
+ if selected is None:
427
+ return
428
+
429
+ self._show_bank_details(selected)
430
+
431
+ def _show_bank_details(self, institution: Institution) -> None:
432
+ """Show details for a selected bank."""
433
+ self.console.print()
434
+ table = Table(box=box.ROUNDED, show_header=False, border_style="blue")
435
+ table.add_column("Property", style="cyan")
436
+ table.add_column("Value")
437
+ table.add_row("Name", institution.name)
438
+ table.add_row("ID", institution.id)
439
+ table.add_row("BIC", institution.bic or "N/A")
440
+ table.add_row(
441
+ "Countries",
442
+ ", ".join(institution.countries) if institution.countries else "N/A",
443
+ )
444
+ table.add_row("Transaction Days", institution.transaction_total_days or "N/A")
445
+ self.console.print(table)
446
+ self.console.print()
447
+
448
+ action = questionary.select(
449
+ "What would you like to do?",
450
+ choices=[
451
+ questionary.Choice("Create link for this bank", value="link"),
452
+ questionary.Choice("← Back to bank list", value="back"),
453
+ ],
454
+ pointer=">",
455
+ ).ask()
456
+
457
+ if action == "link":
458
+ reference = questionary.text(
459
+ "Enter a unique reference for this connection:",
460
+ default="my-bank",
461
+ ).ask()
462
+ if reference:
463
+ self._create_bank_link(reference, institution.id)
464
+
465
+ def add_account_interactive(self) -> None:
466
+ """Add a new bank account interactively."""
467
+ self._print_header("Add New Account")
468
+
469
+ if self.mock:
470
+ self._print_error("Mock client does not support adding accounts")
471
+ return
472
+
473
+ while True:
474
+ country = self._select_country()
475
+ if not country:
476
+ return
477
+
478
+ institution = self._select_bank(country)
479
+ if institution is None:
480
+ return
481
+
482
+ reference = questionary.text(
483
+ "Enter a unique reference for this connection:",
484
+ default="my-bank",
485
+ ).ask()
486
+
487
+ if not reference:
488
+ self._print_info("Cancelled")
489
+ return
490
+
491
+ self._create_bank_link(reference, institution.id)
492
+ return
493
+
494
+ def _select_country(self) -> Optional[str]:
495
+ """Let user select a country from common options."""
496
+ country_map = {
497
+ "United Kingdom": "GB",
498
+ "France": "FR",
499
+ "Germany": "DE",
500
+ "Spain": "ES",
501
+ "Italy": "IT",
502
+ "Netherlands": "NL",
503
+ "Belgium": "BE",
504
+ "Portugal": "PT",
505
+ "Austria": "AT",
506
+ "Ireland": "IE",
507
+ "Other (enter code)": "other",
508
+ "Back": None,
509
+ }
510
+
511
+ selected = questionary.autocomplete(
512
+ "Select your country (type to filter):",
513
+ choices=list(country_map.keys()),
514
+ ignore_case=True,
515
+ ).ask()
516
+
517
+ if selected is None:
518
+ return None
519
+
520
+ value = country_map.get(selected)
521
+ if value is None:
522
+ return None
523
+
524
+ if value == "other":
525
+ country = questionary.text(
526
+ "Enter 2-letter country code (e.g., US, CA, AU):",
527
+ ).ask()
528
+ return country.upper() if country else None
529
+
530
+ return value
531
+
532
+ def _select_bank(self, country: str) -> Optional[Institution]:
533
+ """Search and select a bank for the given country."""
534
+ self.console.print(f"\n[dim]Loading banks for {country}...[/dim]")
535
+
536
+ try:
537
+ institutions = self.client.get_institutions(country)
538
+ except Exception as e:
539
+ self._print_error(f"Could not load banks: {e}")
540
+ return None
541
+
542
+ if not institutions:
543
+ self._print_error(f"No banks found for country {country}")
544
+ return None
545
+
546
+ bank_map: dict[str, Institution] = {}
547
+ choices: list[str] = []
548
+ for inst in institutions:
549
+ display = f"{inst.name}"
550
+ if inst.bic:
551
+ display += f" (BIC: {inst.bic})"
552
+ bank_map[display] = inst
553
+ choices.append(display)
554
+
555
+ choices.append("Back to country selection")
556
+
557
+ self.console.print(f"\n[dim]Loaded {len(institutions)} banks[/dim]\n")
558
+
559
+ selected = questionary.autocomplete(
560
+ "Select your bank (type to filter by name or BIC):",
561
+ choices=choices,
562
+ ignore_case=True,
563
+ ).ask()
564
+
565
+ if selected is None or selected == "Back to country selection":
566
+ return None
567
+
568
+ return bank_map.get(selected)
569
+
570
+ def _create_bank_link(self, reference: str, bank_id: str) -> None:
571
+ """Create a bank link and display the authorization URL."""
572
+ try:
573
+ existing = self.client.find_requisition_by_reference(reference)
574
+ if existing:
575
+ self._print_error(f"A link with reference '{reference}' already exists")
576
+ return
577
+
578
+ link = self.client.create_bank_link(reference, bank_id)
579
+
580
+ if link:
581
+ self._print_success("Bank link created successfully!")
582
+ self.console.print()
583
+ self.console.print(
584
+ Panel(
585
+ f"[bold]Authorization Link:[/bold]\n\n{link}",
586
+ box=box.ROUNDED,
587
+ border_style="green",
588
+ )
589
+ )
590
+ self.console.print()
591
+ self.console.print(
592
+ "[dim]Open this link in your browser to authorize the connection.[/dim]"
593
+ )
594
+ self.console.print(
595
+ "[dim]After authorization, your account will appear in the list.[/dim]"
596
+ )
597
+ else:
598
+ self._print_error("Could not create bank link")
599
+
600
+ except Exception as e:
601
+ self._print_error(f"Error creating link: {e}")
602
+
603
+
604
+ def main():
605
+ """Entry point for the interactive CLI."""
606
+ parser = argparse.ArgumentParser(
607
+ description="Interactive CLI for GoCardless Bank Account Data",
39
608
  )
40
609
  parser.add_argument(
41
- "--secret_id",
610
+ "--secret-id",
42
611
  default=os.getenv("GOCARDLESS_SECRET_ID"),
43
612
  help="API secret ID (defaults to env var GOCARDLESS_SECRET_ID)",
44
613
  )
45
614
  parser.add_argument(
46
- "--secret_key",
615
+ "--secret-key",
47
616
  default=os.getenv("GOCARDLESS_SECRET_KEY"),
48
617
  help="API secret key (defaults to env var GOCARDLESS_SECRET_KEY)",
49
618
  )
50
619
  parser.add_argument(
51
- "--country", default="GB", help="Country code for listing banks"
52
- )
53
- parser.add_argument(
54
- "--reference", default="beancount", help="Unique reference for bank linking"
55
- )
56
- parser.add_argument("--bank", help="Bank ID for linking")
57
- parser.add_argument("--account", help="Account ID for operations")
58
- parser.add_argument("--cache", action="store_true", help="Enable caching")
59
- parser.add_argument(
60
- "--cache_backend", default="sqlite", help="Cache backend (sqlite, memory, etc.)"
61
- )
62
- parser.add_argument(
63
- "--cache_expire",
64
- type=int,
65
- default=0,
66
- help="Cache expiration in seconds (0 = never expire)",
620
+ "--mock",
621
+ action="store_true",
622
+ help="Use mock client with fixture data (for testing)",
67
623
  )
68
624
  parser.add_argument(
69
- "--cache_name", default="gocardless", help="Cache name/database file"
625
+ "--env-file",
626
+ help="Path to a .env file to load environment variables from",
70
627
  )
71
628
 
72
- return parser.parse_args()
629
+ args = parser.parse_args()
73
630
 
74
-
75
- def main():
76
- args = parse_args()
77
-
78
- if not args.secret_id or not args.secret_key:
79
- logger.error(
80
- "Error: Secret ID and Secret Key are required (pass as args or set env vars GOCARDLESS_SECRET_ID and GOCARDLESS_SECRET_KEY)",
81
- )
82
- sys.exit(1)
83
-
84
- try:
85
- logger.debug("Initializing GoCardlessClient")
86
-
87
- cache_options = (
88
- {
89
- "backend": args.cache_backend,
90
- "expire_after": args.cache_expire,
91
- "cache_name": args.cache_name,
92
- }
93
- if args.cache
94
- else {}
95
- )
96
-
97
- client = GoCardlessClient(args.secret_id, args.secret_key, cache_options)
98
-
99
- if args.mode == "list_banks":
100
- banks = client.list_banks(args.country)
101
- for bank in banks:
102
- logger.info(bank)
103
- elif args.mode == "create_link":
104
- if not args.bank:
105
- logger.error("Error: --bank is required for create_link")
106
- sys.exit(1)
107
- link = client.create_bank_link(args.reference, args.bank)
108
- if link:
109
- logger.info(f"Bank link created: {link}")
110
- else:
111
- logger.info(f"Link already exists for reference '{args.reference}'")
112
- elif args.mode == "list_accounts":
113
- accounts = client.list_accounts()
114
- for i, account in enumerate(accounts, 1):
115
- display_account(i, account)
116
- elif args.mode == "delete_link":
117
- req = client.find_requisition_by_reference(args.reference)
118
- if req:
119
- client.delete_requisition(req.id)
120
- logger.info(f"Deleted requisition '{args.reference}'")
121
- else:
122
- logger.error(f"No requisition found with reference '{args.reference}'")
123
- sys.exit(1)
124
- elif args.mode == "balance":
125
- if not args.account:
126
- logger.error("Error: --account is required for balance")
127
- sys.exit(1)
128
- balances = client.get_account_balances(args.account)
129
- for balance in balances.balances:
130
- logger.info(
131
- f"{balance.balance_type}: {balance.balance_amount.amount} {balance.balance_amount.currency}"
132
- )
133
-
134
- except Exception as e:
135
- logger.error(f"Error: {e}")
136
- sys.exit(1)
631
+ cli = CLI(
632
+ secret_id=args.secret_id,
633
+ secret_key=args.secret_key,
634
+ mock=args.mock,
635
+ env_file=args.env_file,
636
+ )
637
+ cli.run()
137
638
 
138
639
 
139
640
  if __name__ == "__main__":