beancount-gocardless 0.1.9__py3-none-any.whl → 0.1.10__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.
@@ -5,6 +5,5 @@ Generated from swagger.json to provide a clean, typed interface.
5
5
 
6
6
  from .client import GoCardlessClient
7
7
  from .importer import GoCardLessImporter
8
- from .models import *
9
8
 
10
9
  __all__ = ["GoCardlessClient", "GoCardLessImporter"]
@@ -2,7 +2,7 @@ import argparse
2
2
  import sys
3
3
  import os
4
4
  import logging
5
- from beancount_gocardless.client import GoCardlessClient
5
+
6
6
  from beancount_gocardless.models import AccountInfo
7
7
 
8
8
 
@@ -83,21 +83,7 @@ def main():
83
83
  try:
84
84
  logger.debug("Initializing GoCardlessClient")
85
85
 
86
- # Build cache options
87
- cache_options = None
88
- if args.cache:
89
- cache_options = {
90
- "cache_name": args.cache_name,
91
- "backend": args.cache_backend,
92
- "expire_after": args.cache_expire,
93
- "old_data_on_error": True,
94
- "match_headers": False,
95
- "cache_control": False,
96
- }
97
-
98
- # client = GoCardlessClient(
99
- # args.secret_id, args.secret_key, cache_options=cache_options
100
- # )
86
+ # TODO: Implement client initialization with cache options
101
87
 
102
88
  except Exception as e:
103
89
  logger.error(f"Error: {e}")
@@ -8,7 +8,7 @@ from beancount.core import amount, data, flags
8
8
  from beancount.core.number import D
9
9
 
10
10
  from .client import GoCardlessClient
11
- from .models import BankTransaction
11
+ from .models import BankTransaction, GoCardlessConfig
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -34,14 +34,14 @@ class GoCardLessImporter(beangulp.Importer):
34
34
  An importer for GoCardless API with improved structure and extensibility.
35
35
 
36
36
  Attributes:
37
- config (Optional[Dict[str, Any]]): Configuration loaded from the YAML file.
37
+ config (Optional[GoCardlessConfig]): Configuration loaded from the YAML file.
38
38
  _client (Optional[GoCardlessClient]): Instance of the GoCardless API client.
39
39
  """
40
40
 
41
41
  def __init__(self) -> None:
42
42
  """Initialize the GoCardLessImporter."""
43
43
  logger.debug("Initializing GoCardLessImporter")
44
- self.config: Optional[Dict[str, Any]] = None
44
+ self.config: Optional[GoCardlessConfig] = None
45
45
  self._client: Optional[GoCardlessClient] = None
46
46
 
47
47
  @property
@@ -59,9 +59,9 @@ class GoCardLessImporter(beangulp.Importer):
59
59
  if not self.config:
60
60
  raise ValueError("Config not loaded. Call load_config() first.")
61
61
  self._client = GoCardlessClient(
62
- self.config["secret_id"],
63
- self.config["secret_key"],
64
- cache_options=self.config.get("cache_options", None),
62
+ self.config.secret_id,
63
+ self.config.secret_key,
64
+ cache_options=self.config.cache_options or None,
65
65
  )
66
66
 
67
67
  return self._client
@@ -93,7 +93,7 @@ class GoCardLessImporter(beangulp.Importer):
93
93
  logger.debug("Returning account for %s: ''", filepath)
94
94
  return "" # We get the account from the config file
95
95
 
96
- def load_config(self, filepath: str) -> Optional[Dict[str, Any]]:
96
+ def load_config(self, filepath: str) -> Optional[GoCardlessConfig]:
97
97
  """
98
98
  Loads configuration from the specified YAML file.
99
99
 
@@ -101,35 +101,35 @@ class GoCardLessImporter(beangulp.Importer):
101
101
  filepath (str): The path to the YAML configuration file.
102
102
 
103
103
  Returns:
104
- Dict[str, Any]: The loaded configuration dictionary. Also sets the `self.config` attribute.
104
+ GoCardlessConfig: The loaded configuration. Also sets the `self.config` attribute.
105
105
  """
106
106
  logger.debug("Loading config from %s", filepath)
107
107
  with open(filepath, "r") as f:
108
108
  raw_config = f.read()
109
- expanded_config = path.expandvars(
110
- raw_config
111
- ) # Handle environment variables
112
- self.config = yaml.safe_load(expanded_config)
109
+ expanded_config = path.expandvars(raw_config)
110
+ self.config = GoCardlessConfig(**yaml.safe_load(expanded_config))
113
111
 
114
112
  return self.config
115
113
 
116
114
  def get_all_transactions(
117
- self, booked: List[BankTransaction], pending: List[BankTransaction]
115
+ self, transactions_dict: Dict[str, List[BankTransaction]], types: List[str]
118
116
  ) -> List[Tuple[BankTransaction, str]]:
119
117
  """
120
- Combines booked and pending transactions and sorts them by date.
118
+ Combines transactions of specified types and sorts them by date.
121
119
 
122
120
  Args:
123
- booked (List[BankTransaction]): The booked transaction data from the API.
124
- pending (List[BankTransaction]): The pending transaction data from the API.
121
+ transactions_dict (Dict[str, List[BankTransaction]]): Transactions by type.
122
+ types (List[str]): Types to include.
125
123
 
126
124
  Returns:
127
- List[Tuple[BankTransaction, str]]: A sorted list of tuples, where each tuple contains
128
- a transaction and its status ('booked' or 'pending').
125
+ List[Tuple[BankTransaction, str]]: Sorted list of (transaction, type) tuples.
129
126
  """
130
- all_transactions = [(tx, "booked") for tx in booked] + [
131
- (tx, "pending") for tx in pending
132
- ]
127
+ all_transactions = []
128
+ for tx_type in types:
129
+ if tx_type in transactions_dict:
130
+ all_transactions.extend(
131
+ [(tx, tx_type) for tx in transactions_dict[tx_type]]
132
+ )
133
133
  return sorted(
134
134
  all_transactions,
135
135
  key=lambda x: x[0].value_date or x[0].booking_date or "",
@@ -348,27 +348,31 @@ class GoCardLessImporter(beangulp.Importer):
348
348
  raise ValueError("No config loaded from YAML file")
349
349
 
350
350
  entries: data.Entries = []
351
- accounts = self.config.get("accounts", [])
351
+ accounts = self.config.accounts
352
352
  total_transactions = 0
353
353
  logger.info("Processing %d accounts", len(accounts))
354
354
  for account in accounts:
355
- account_id = account["id"]
356
- asset_account = account["asset_account"]
357
- # Use get() with a default empty dict for custom_metadata
358
- custom_metadata = account.get("metadata", {})
355
+ account_id = account.id
356
+ asset_account = account.asset_account
357
+ custom_metadata = account.metadata
359
358
 
360
359
  logger.debug("Fetching transactions for account %s", account_id)
361
360
  account_transactions = self.client.get_account_transactions(account_id)
362
- booked = account_transactions.transactions.get("booked", [])
363
- pending = account_transactions.transactions.get("pending", [])
364
- all_transactions = self.get_all_transactions(booked, pending)
361
+ transactions_dict = account_transactions.transactions
362
+ all_transactions = self.get_all_transactions(
363
+ transactions_dict, account.transaction_types
364
+ )
365
+ booked_count = len(transactions_dict.get("booked", []))
366
+ pending_count = len(transactions_dict.get("pending", []))
365
367
  logger.debug(
366
368
  "Fetched %d booked and %d pending transactions for account %s",
367
- len(booked),
368
- len(pending),
369
+ booked_count,
370
+ pending_count,
369
371
  account_id,
370
372
  )
371
- total_transactions += len(booked) + len(pending)
373
+ total_transactions += sum(
374
+ len(transactions_dict.get(t, [])) for t in account.transaction_types
375
+ )
372
376
 
373
377
  skipped = 0
374
378
  for transaction, status in all_transactions:
@@ -4,7 +4,7 @@ Complete coverage of all schemas from swagger.json
4
4
  """
5
5
 
6
6
  from typing import Optional, List, Dict, Any, TypedDict
7
- from pydantic import BaseModel, Field, ConfigDict
7
+ from pydantic import BaseModel, Field, ConfigDict, validator
8
8
  from pydantic.alias_generators import to_camel
9
9
  from enum import Enum
10
10
 
@@ -536,3 +536,26 @@ class IntegrationRetrieve(BaseModel):
536
536
  supported_payments: Optional[Dict[str, Any]] = None
537
537
  supported_features: Optional[List[str]] = None
538
538
  identification_codes: Optional[List[str]] = None
539
+
540
+
541
+ class AccountConfig(BaseModel):
542
+ id: str
543
+ asset_account: str
544
+ metadata: Dict[str, Any] = {}
545
+ transaction_types: List[str] = ["booked", "pending"]
546
+
547
+ @validator("transaction_types")
548
+ def validate_transaction_types(cls, v):
549
+ allowed = {"booked", "pending"}
550
+ if not set(v).issubset(allowed):
551
+ raise ValueError(
552
+ f"Invalid transaction types: {v}. Must be subset of {allowed}"
553
+ )
554
+ return v
555
+
556
+
557
+ class GoCardlessConfig(BaseModel):
558
+ secret_id: str
559
+ secret_key: str
560
+ cache_options: Dict[str, Any] = {}
561
+ accounts: List[AccountConfig]
@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
18
18
  class ActionMessage(Message):
19
19
  """Custom message to signal actions within the TUI."""
20
20
 
21
- def __init__(self, action: str, payload: dict = None) -> None:
21
+ def __init__(self, action: str, payload: Optional[dict] = None) -> None:
22
22
  super().__init__()
23
23
  self.action = action
24
24
  self.payload = payload or {}
@@ -47,16 +47,13 @@ class BaseSubView(Static):
47
47
  """Base class for sub-views with a back button."""
48
48
 
49
49
  def compose(self) -> ComposeResult:
50
- yield Vertical(
51
- self.compose_content(),
52
- Button("Back to Menu", id="back_to_menu", classes="back_button"),
53
- )
50
+ yield from self.compose_content()
54
51
 
55
52
  def compose_content(self) -> ComposeResult:
56
53
  # To be overridden by subclasses
57
54
  yield Static("Content goes here")
58
55
 
59
- def on_button_pressed(self, event: Button.Pressed) -> None:
56
+ async def on_button_pressed(self, event: Button.Pressed) -> None:
60
57
  if event.button.id == "back_to_menu":
61
58
  self.post_message(ActionMessage("show_menu"))
62
59
  else:
@@ -110,7 +107,10 @@ class BanksView(BaseSubView):
110
107
  try:
111
108
  if not self.all_banks:
112
109
  self.status_message.update("Loading all banks...")
113
- self.all_banks = self.client.list_banks(country=None)
110
+ self.all_banks = [
111
+ inst.model_dump()
112
+ for inst in self.client.get_institutions(country=None)
113
+ ]
114
114
 
115
115
  self.banks_table.update(
116
116
  f"Loading accounts... {name_filter} {country_code} {len(self.all_banks)}"
@@ -164,7 +164,7 @@ class BanksView(BaseSubView):
164
164
  country_code=country_code if not omit_country else None,
165
165
  )
166
166
 
167
- def on_button_pressed(self, event: Button.Pressed) -> None:
167
+ async def on_button_pressed(self, event: Button.Pressed) -> None:
168
168
  """Handle button presses within this view."""
169
169
  if event.button.id == "back_to_menu":
170
170
  self.post_message(ActionMessage("show_menu"))
@@ -205,7 +205,7 @@ class AccountsView(BaseSubView):
205
205
  """Loads and displays the linked accounts with detailed information."""
206
206
  try:
207
207
  self.status_message.update("Loading accounts...")
208
- accounts = self.client.list_accounts()
208
+ accounts = self.client.get_all_accounts()
209
209
  if not accounts:
210
210
  self.accounts_table.update("No accounts found.")
211
211
  self.status_message.update("")
@@ -260,7 +260,7 @@ class AccountsView(BaseSubView):
260
260
  except Exception as e:
261
261
  self.status_message.update(Text(f"Unexpected error: {e}", style="bold red"))
262
262
 
263
- def on_button_pressed(self, event: Button.Pressed) -> None:
263
+ async def on_button_pressed(self, event: Button.Pressed) -> None:
264
264
  """Handle button presses within this view."""
265
265
  if event.button.id == "back_to_menu":
266
266
  self.post_message(ActionMessage("show_menu"))
@@ -294,6 +294,7 @@ class BalanceView(BaseSubView):
294
294
  # The initial Static message that was here is removed
295
295
  id="balance_content_area",
296
296
  )
297
+ yield Button("Back to Menu", id="back_to_menu", classes="back_button")
297
298
 
298
299
  async def on_mount(self) -> None:
299
300
  # Query elements only after they are mounted
@@ -309,7 +310,7 @@ class BalanceView(BaseSubView):
309
310
  ):
310
311
  try:
311
312
  # status_message.update("Loading accounts...") # Message set by on_mount or can be refined here
312
- self.accounts_data = self.client.list_accounts()
313
+ self.accounts_data = self.client.get_all_accounts()
313
314
  await (
314
315
  content_area.remove_children()
315
316
  ) # Clear "Loading..." or previous content from content_area
@@ -391,7 +392,7 @@ class BalanceView(BaseSubView):
391
392
  return
392
393
 
393
394
  account_to_check = self.accounts_data[idx]
394
- acct_id = account_to_check["account_id"]
395
+ acct_id = account_to_check["id"]
395
396
 
396
397
  status_message.update(
397
398
  f"Fetching balances for {account_to_check['name']} ({acct_id})..."
@@ -399,7 +400,7 @@ class BalanceView(BaseSubView):
399
400
  # Optionally, display a "Fetching..." message in content_area as well
400
401
  await content_area.mount(Static("Fetching balances..."))
401
402
 
402
- balances_data = self.client.get_balances(account_id=acct_id)
403
+ balances_data = self.client.get_account_balances(acct_id)
403
404
  await content_area.remove_children() # Clear "Fetching..." message
404
405
 
405
406
  table = Table(
@@ -408,13 +409,12 @@ class BalanceView(BaseSubView):
408
409
  table.add_column("Type", overflow="fold")
409
410
  table.add_column("Amount")
410
411
  table.add_column("Currency")
411
- if "balances" in balances_data and balances_data["balances"]:
412
- for b in balances_data["balances"]:
413
- balance_amount_info = b.get("balanceAmount", {})
412
+ if balances_data.balances:
413
+ for b in balances_data.balances:
414
414
  table.add_row(
415
- b.get("balanceType", "N/A"),
416
- str(balance_amount_info.get("amount", "N/A")),
417
- balance_amount_info.get("currency", "N/A"),
415
+ b.balance_type,
416
+ b.balance_amount.amount,
417
+ b.balance_amount.currency,
418
418
  )
419
419
  else:
420
420
  table.add_row("N/A", "No balance data found", "N/A")
@@ -496,9 +496,10 @@ class LinkView(BaseSubView):
496
496
  Static(id="link_result_message", classes="result_message_area"),
497
497
  id="link_form_area",
498
498
  )
499
+ yield Button("Back to Menu", id="back_to_menu", classes="back_button")
499
500
 
500
501
  async def on_mount(self) -> None:
501
- await self.query_one("#bank_id_input", Input).focus()
502
+ self.query_one("#bank_id_input", Input).focus()
502
503
 
503
504
  async def on_button_pressed(self, event: Button.Pressed) -> None:
504
505
  if event.button.id == "back_to_menu":
@@ -527,7 +528,20 @@ class LinkView(BaseSubView):
527
528
  create_button.disabled = True
528
529
  result_message_widget.update("Creating link...")
529
530
 
530
- link_info = self.client.create_link(reference, bank_id) # API call
531
+ link_url = self.client.create_bank_link(reference, bank_id) # API call
532
+
533
+ if link_url:
534
+ link_info = {
535
+ "status": "created",
536
+ "message": "Link created successfully",
537
+ "link": link_url,
538
+ }
539
+ else:
540
+ link_info = {
541
+ "status": "exists",
542
+ "message": "Link already exists for this reference",
543
+ "link": None,
544
+ }
531
545
 
532
546
  msg_parts = [
533
547
  Text(f"Status: {link_info.get('status', 'N/A')}\n", style="bold")
@@ -577,9 +591,10 @@ class DeleteLinkView(BaseSubView):
577
591
  Static(id="delete_link_result_message", classes="result_message_area"),
578
592
  id="delete_link_form_area",
579
593
  )
594
+ yield Button("Back to Menu", id="back_to_menu", classes="back_button")
580
595
 
581
596
  async def on_mount(self) -> None:
582
- await self.query_one("#del_ref_input", Input).focus()
597
+ self.query_one("#del_ref_input", Input).focus()
583
598
 
584
599
  async def on_button_pressed(self, event: Button.Pressed) -> None:
585
600
  if event.button.id == "back_to_menu":
@@ -607,14 +622,17 @@ class DeleteLinkView(BaseSubView):
607
622
  f"Deleting link with reference '{reference}'..."
608
623
  )
609
624
 
610
- response = self.client.delete_link(reference) # API call
625
+ req = self.client.find_requisition_by_reference(reference)
626
+ if req:
627
+ self.client.delete_requisition(req.id) # API call
628
+ result = {"status": "deleted", "message": "Link deleted successfully"}
629
+ else:
630
+ result = {"status": "not_found", "message": "Link not found"}
611
631
 
612
- style = (
613
- "bold green" if response.get("status") == "deleted" else "bold yellow"
614
- )
632
+ style = "bold green" if result["status"] == "deleted" else "bold yellow"
615
633
  result_message_widget.update(
616
634
  Text(
617
- f"Status: {response.get('status', 'N/A')}\nMessage: {response.get('message', 'N/A')}",
635
+ f"Status: {result['status']}\nMessage: {result['message']}",
618
636
  style=style,
619
637
  )
620
638
  )
@@ -767,5 +785,5 @@ def main():
767
785
  app.run()
768
786
 
769
787
 
770
- # if __name__ == "__main__":
771
- # main()
788
+ if __name__ == "__main__":
789
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beancount-gocardless
3
- Version: 0.1.9
3
+ Version: 0.1.10
4
4
  License-Expression: Unlicense
5
5
  License-File: LICENSE
6
6
  Requires-Python: <4,>=3.12
@@ -20,6 +20,9 @@ Requires-Dist: sphinx; extra == 'dev'
20
20
  Requires-Dist: sphinx-rtd-theme; extra == 'dev'
21
21
  Provides-Extra: lint
22
22
  Requires-Dist: ruff>=0.9.8; extra == 'lint'
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest; extra == 'test'
25
+ Requires-Dist: pytest-asyncio; extra == 'test'
23
26
  Description-Content-Type: text/markdown
24
27
 
25
28
  beancount-gocardless
@@ -94,7 +97,8 @@ cache_options: # by default, no caching if cache_options is not provided
94
97
 
95
98
  accounts:
96
99
  - id: <REDACTED_UUID>
97
- asset_account: "Assets:Banks:Revolut:Checking"
100
+ asset_account: "Assets:Banks:Revolut:Checking"
101
+ transaction_types: ["booked", "pending"] # optional, defaults to both
98
102
  ```
99
103
 
100
104
  ```python
@@ -0,0 +1,12 @@
1
+ beancount_gocardless/__init__.py,sha256=JVJivGs-o5yY8YGqTYwn1cXY7aiA76lUyc95s1Ye-j0,253
2
+ beancount_gocardless/cli.py,sha256=VDtOhhY5wyuxtXokKlbl1RKZteLR919lParyyspE_qk,2850
3
+ beancount_gocardless/client.py,sha256=eBr3VVrnQIFMwRBmoIC8VN9JvtsDhGZge75nYW7Fyns,15402
4
+ beancount_gocardless/importer.py,sha256=4sh66Vmi8eWEXcmkUIq3UJcqjyZNUitZpkp1-ZcNyCU,16688
5
+ beancount_gocardless/models.py,sha256=H6sqkheJUVpmNaB1U5C6EL0j0y1xfiHC-msq5F2uWBk,18283
6
+ beancount_gocardless/tui.py,sha256=fPI60Tu9n44ZBYLjWbdx88EX4i3kp6S4BQ9p5L9Wtjw,30659
7
+ beancount_gocardless/openapi/swagger.json,sha256=t8TLbt0l2UOyorZX8JyoG4XO2qtXDRYUlsllM3ckyG4,264957
8
+ beancount_gocardless-0.1.10.dist-info/METADATA,sha256=htZDSUk8ne4wb33NSzmHGAsXxBG2tlmhu3kVqxZvOLY,4124
9
+ beancount_gocardless-0.1.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
+ beancount_gocardless-0.1.10.dist-info/entry_points.txt,sha256=_l8n11k9nGPaSDou4atu5NkV0Ojf-qjW2zIIGPzmmgA,128
11
+ beancount_gocardless-0.1.10.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
12
+ beancount_gocardless-0.1.10.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- from textual.app import App, ComposeResult
2
- from textual.widgets import Header, Footer, Label
3
-
4
-
5
- raise Exception
6
-
7
-
8
- class MinimalApp(App):
9
- TITLE = "Minimal Test App"
10
-
11
- def compose(self) -> ComposeResult:
12
- yield Header()
13
- yield Label("If you see this, MinimalApp from tui.py is running!")
14
- yield Footer()
15
-
16
-
17
- app = MinimalApp()
@@ -1,13 +0,0 @@
1
- beancount_gocardless/__init__.py,sha256=HNLcqMQMk27W16-tkWfU31C_1lDfwRHj6PR1j1UYs_M,275
2
- beancount_gocardless/cli.py,sha256=OLfpesyTQLIqhdtipoGW9gZ2sdC6_BuMvOIs8Zf5yvw,3358
3
- beancount_gocardless/client.py,sha256=eBr3VVrnQIFMwRBmoIC8VN9JvtsDhGZge75nYW7Fyns,15402
4
- beancount_gocardless/importer.py,sha256=JRkuc40ux49y3UKVx-HgwxGkbmxROSIXuiSwFT9Obi0,16621
5
- beancount_gocardless/models.py,sha256=RlqUitg2kaykP4WQVhHkhtF187NFFOLjrxpC3ncWCB8,17661
6
- beancount_gocardless/tui.py,sha256=qAPL9zSNF5lJ43PiBRWRJ7nyrRdtziSgVoDoXDsCsq8,29947
7
- beancount_gocardless/tui2.py,sha256=eQWh-KbqsoiqgZKLvDDpPov1DX1w9aV2cdIHZzvrJck,350
8
- beancount_gocardless/openapi/swagger.json,sha256=t8TLbt0l2UOyorZX8JyoG4XO2qtXDRYUlsllM3ckyG4,264957
9
- beancount_gocardless-0.1.9.dist-info/METADATA,sha256=AuaDbJLfw0f1sXNtsXF4P79-ARlEtjnALxQFUedBrLg,3937
10
- beancount_gocardless-0.1.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
- beancount_gocardless-0.1.9.dist-info/entry_points.txt,sha256=_l8n11k9nGPaSDou4atu5NkV0Ojf-qjW2zIIGPzmmgA,128
12
- beancount_gocardless-0.1.9.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
13
- beancount_gocardless-0.1.9.dist-info/RECORD,,