beancount-gocardless 0.1.0__py3-none-any.whl → 0.1.2__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,2 @@
1
+ from .client import NordigenClient # noqa: F401
2
+ from .importer import NordigenImporter # noqa: F401
@@ -5,6 +5,12 @@ from .client import NordigenClient
5
5
 
6
6
 
7
7
  def parse_args():
8
+ """
9
+ Parses command-line arguments.
10
+
11
+ Returns:
12
+ argparse.Namespace: An object containing the parsed arguments.
13
+ """
8
14
  parser = argparse.ArgumentParser(description="Nordigen CLI Utility")
9
15
  parser.add_argument(
10
16
  "mode",
@@ -32,6 +38,11 @@ def parse_args():
32
38
 
33
39
 
34
40
  def main():
41
+ """
42
+ The main entry point for the CLI.
43
+
44
+ Executes the specified operation based on the parsed command-line arguments.
45
+ """
35
46
  args = parse_args()
36
47
 
37
48
  if not args.secret_id or not args.secret_key:
@@ -44,7 +55,7 @@ def main():
44
55
  client = NordigenClient(
45
56
  args.secret_id,
46
57
  args.secret_key,
47
- {"expire_after": 3600 * 24},
58
+ {},
48
59
  )
49
60
 
50
61
  if args.mode == "list_banks":
@@ -60,7 +71,7 @@ def main():
60
71
  accounts = client.list_accounts()
61
72
  for a in accounts:
62
73
  print(
63
- f"{a["institution_id"]} {a['name']}: {a['iban']} {a['currency']} ({a['reference']}/{a['id']})"
74
+ f"{a['institution_id']} {a['name']}: {a['iban']} {a['currency']} ({a['reference']}/{a['id']})"
64
75
  )
65
76
 
66
77
  elif args.mode == "delete_link":
@@ -1,10 +1,20 @@
1
- from datetime import date, timedelta, datetime
1
+ from datetime import timedelta, datetime
2
2
  import requests_cache
3
3
  import requests
4
- from typing import Protocol, TypedDict, Optional
4
+ from typing import TypedDict, Optional
5
5
 
6
6
 
7
7
  class CacheOptions(TypedDict, total=False):
8
+ """
9
+ Options for configuring requests-cache.
10
+
11
+ Attributes:
12
+ cache_name (str, optional): The name of the cache. Defaults to 'nordigen'. Can also be a path.
13
+ backend (str, optional): The cache backend to use (e.g., 'sqlite', 'memory'). Defaults to 'sqlite'.
14
+ expire_after (int, optional): The cache expiration time in seconds. Defaults to 86400 (24 hours).
15
+ old_data_on_error (bool, optional): Whether to return old cached data on a request error. Defaults to False.
16
+ """
17
+
8
18
  cache_name: requests_cache.StrOrPath
9
19
  backend: Optional[requests_cache.BackendSpecifier]
10
20
  expire_after: requests_cache.ExpirationTime
@@ -12,7 +22,13 @@ class CacheOptions(TypedDict, total=False):
12
22
 
13
23
 
14
24
  class HttpServiceException(Exception):
15
- """Exception raised for HTTP service errors."""
25
+ """
26
+ Exception raised for HTTP service errors. This wraps HTTP errors from the underlying requests library.
27
+
28
+ Attributes:
29
+ error (str): The original error message.
30
+ response_text (str, optional): The full response text from the server.
31
+ """
16
32
 
17
33
  def __init__(self, error, response_text=None):
18
34
  self.error = error
@@ -21,14 +37,24 @@ class HttpServiceException(Exception):
21
37
 
22
38
 
23
39
  class BaseService:
24
- """Base class for HTTP services handling authentication and requests."""
40
+ """
41
+ Base class for HTTP services handling authentication and requests to the GoCardless Bank Account Data API.
42
+
43
+ Attributes:
44
+ BASE_URL (str): The base URL for the API.
45
+ DEFAULT_CACHE_OPTIONS (CacheOptions): Default caching options.
46
+ secret_id (str): Your GoCardless API secret ID.
47
+ secret_key (str): Your GoCardless API secret key.
48
+ token (str, optional): The current API access token.
49
+ session (requests_cache.CachedSession): The cached requests session.
50
+ """
25
51
 
26
52
  BASE_URL = "https://bankaccountdata.gocardless.com/api/v2"
27
53
 
28
54
  DEFAULT_CACHE_OPTIONS: CacheOptions = {
29
55
  "cache_name": "nordigen",
30
56
  "backend": "sqlite",
31
- "expire_after": 3600 * 24,
57
+ "expire_after": 0,
32
58
  "old_data_on_error": False,
33
59
  }
34
60
 
@@ -38,6 +64,14 @@ class BaseService:
38
64
  secret_key: str,
39
65
  cache_options: Optional[CacheOptions],
40
66
  ):
67
+ """
68
+ Initializes the BaseService.
69
+
70
+ Args:
71
+ secret_id (str): Your GoCardless API secret ID.
72
+ secret_key (str): Your GoCardless API secret key.
73
+ cache_options (CacheOptions, optional): Custom cache options. Merges with and overrides DEFAULT_CACHE_OPTIONS.
74
+ """
41
75
  self.secret_id = secret_id
42
76
  self.secret_key = secret_key
43
77
  self.token = None
@@ -45,12 +79,20 @@ class BaseService:
45
79
  self.session = requests_cache.CachedSession(**merged_options)
46
80
 
47
81
  def _ensure_token_valid(self):
48
- """Ensure a valid token exists (no-op here as Nordigen doesn't provide refresh tokens)."""
82
+ """
83
+ Ensure a valid token exists. Gets a new token if one doesn't exist.
84
+ Nordigen tokens don't currently have a refresh mechanism, so this just gets a new one if needed.
85
+ """
49
86
  if not self.token:
50
87
  self.get_token()
51
88
 
52
89
  def get_token(self):
53
- """Fetch a new API token using credentials."""
90
+ """
91
+ Fetch a new API access token using credentials. Sets the `self.token` attribute.
92
+
93
+ Raises:
94
+ HttpServiceException: If the API request fails.
95
+ """
54
96
  response = requests.post(
55
97
  f"{self.BASE_URL}/token/new/",
56
98
  data={"secret_id": self.secret_id, "secret_key": self.secret_key},
@@ -59,14 +101,36 @@ class BaseService:
59
101
  self.token = response.json()["access"]
60
102
 
61
103
  def _handle_response(self, response):
62
- """Check response status and handle errors."""
104
+ """
105
+ Check response status and handle errors.
106
+
107
+ Args:
108
+ response (requests.Response): The response object from the API request.
109
+
110
+ Raises:
111
+ HttpServiceException: If the API request returns an error status code.
112
+ """
63
113
  try:
64
114
  response.raise_for_status()
65
115
  except requests.exceptions.HTTPError as e:
66
116
  raise HttpServiceException(str(e), response.text)
67
117
 
68
118
  def _request(self, method, endpoint, params=None, data=None):
69
- """Execute an HTTP request with token handling."""
119
+ """
120
+ Execute an HTTP request with token handling and automatic retries.
121
+
122
+ Args:
123
+ method (str): The HTTP method (e.g., "GET", "POST", "DELETE").
124
+ endpoint (str): The API endpoint (relative to BASE_URL).
125
+ params (dict, optional): URL parameters for the request.
126
+ data (dict, optional): Data to send in the request body (for POST requests).
127
+
128
+ Returns:
129
+ requests.Response: The response object from the API request.
130
+
131
+ Raises:
132
+ HttpServiceException: If the API request fails.
133
+ """
70
134
  url = f"{self.BASE_URL}{endpoint}"
71
135
  self._ensure_token_valid()
72
136
  headers = {"Authorization": f"Bearer {self.token}"}
@@ -75,10 +139,10 @@ class BaseService:
75
139
  method, url, headers=headers, params=params, data=data
76
140
  )
77
141
 
78
- # Retry once if token expired
142
+ # Retry once if token expired (401 Unauthorized)
79
143
  if response.status_code == 401:
80
- self.get_token()
81
- headers = {"Authorization": f"Bearer {self.token}"}
144
+ self.get_token() # Get a new token
145
+ headers = {"Authorization": f"Bearer {self.token}"} # Update headers
82
146
  response = self.session.request(
83
147
  method, url, headers=headers, params=params, data=data
84
148
  )
@@ -87,34 +151,116 @@ class BaseService:
87
151
  return response
88
152
 
89
153
  def _get(self, endpoint, params=None):
154
+ """
155
+ Perform a GET request and return the JSON response.
156
+
157
+ Args:
158
+ endpoint (str): The API endpoint.
159
+ params (dict, optional): URL parameters.
160
+
161
+ Returns:
162
+ dict: The JSON response from the API.
163
+ """
90
164
  return self._request("GET", endpoint, params=params).json()
91
165
 
92
166
  def _post(self, endpoint, data=None):
167
+ """
168
+ Perform a POST request and return the JSON response.
169
+
170
+ Args:
171
+ endpoint (str): The API endpoint.
172
+ data (dict, optional): Data to send in the request body.
173
+
174
+ Returns:
175
+ dict: The JSON response from the API.
176
+ """
93
177
  return self._request("POST", endpoint, data=data).json()
94
178
 
95
179
  def _delete(self, endpoint):
180
+ """
181
+ Perform a DELETE request and return the JSON response.
182
+
183
+ Args:
184
+ endpoint (str): The API endpoint.
185
+
186
+ Returns:
187
+ dict: The JSON response from the API.
188
+ """
96
189
  return self._request("DELETE", endpoint).json()
97
190
 
98
191
 
99
192
  class NordigenClient(BaseService):
100
- """Client for interacting with the Nordigen API."""
193
+ """
194
+ Client for interacting with the Nordigen API (GoCardless Bank Account Data).
195
+
196
+ This class provides methods for listing banks, creating and managing requisitions (links),
197
+ listing accounts, deleting links, and retrieving transactions. It inherits from `BaseService`
198
+ to handle authentication and HTTP requests.
199
+ """
101
200
 
102
201
  def list_banks(self, country="GB"):
103
- """List available institutions for a country."""
202
+ """
203
+ List available institutions (banks) for a given country.
204
+
205
+ Args:
206
+ country (str, optional): The two-letter country code (ISO 3166). Defaults to "GB".
207
+
208
+ Returns:
209
+ list: A list of dictionaries, each containing the 'name' and 'id' of a bank.
210
+
211
+ Example:
212
+ ```python
213
+ client = NordigenClient(...)
214
+ banks = client.list_banks(country="US")
215
+ for bank in banks:
216
+ print(f"{bank['name']} (ID: {bank['id']})")
217
+ ```
218
+ """
104
219
  return [
105
220
  {"name": bank["name"], "id": bank["id"]}
106
221
  for bank in self._get("/institutions/", params={"country": country})
107
222
  ]
108
223
 
109
224
  def find_requisition_id(self, reference):
110
- """Find requisition ID by reference."""
225
+ """
226
+ Find a requisition ID by its reference string.
227
+
228
+ Args:
229
+ reference (str): The unique reference string associated with the requisition.
230
+
231
+ Returns:
232
+ str or None: The requisition ID if found, otherwise None.
233
+ """
111
234
  requisitions = self._get("/requisitions/")["results"]
112
235
  return next(
113
236
  (req["id"] for req in requisitions if req["reference"] == reference), None
114
237
  )
115
238
 
116
239
  def create_link(self, reference, bank_id, redirect_url="http://localhost"):
117
- """Create a new bank link requisition."""
240
+ """
241
+ Create a new bank link requisition.
242
+
243
+ Args:
244
+ reference (str): A unique reference string for this link.
245
+ bank_id (str): The ID of the institution (bank) to link to.
246
+ redirect_url (str, optional): The URL to redirect the user to after authentication. Defaults to "http://localhost".
247
+
248
+ Returns:
249
+ dict: A dictionary with the status of the operation. If successful, includes the link URL.
250
+ - status: "exists" if a requisition with the given `reference` already exists, "created" otherwise
251
+ - message: A descriptive message
252
+ - link: (If status is "created") The URL to start the linking process.
253
+
254
+ Example:
255
+ ```python
256
+ client = NordigenClient(...)
257
+ result = client.create_link(reference="my-unique-ref", bank_id="SANDBOXFINANCE_SFIN0000")
258
+ if result["status"] == "created":
259
+ print(f"Redirect user to: {result['link']}")
260
+ else:
261
+ print(result["message"])
262
+ ```
263
+ """
118
264
  if self.find_requisition_id(reference):
119
265
  return {"status": "exists", "message": f"Link {reference} exists"}
120
266
 
@@ -133,7 +279,12 @@ class NordigenClient(BaseService):
133
279
  }
134
280
 
135
281
  def list_accounts(self):
136
- """List all connected accounts with details."""
282
+ """
283
+ List all connected accounts with details (ID, institution, reference, IBAN, currency, name).
284
+
285
+ Returns:
286
+ list: A list of dictionaries, each representing a connected account.
287
+ """
137
288
  accounts = []
138
289
  for req in self._get("/requisitions/")["results"]:
139
290
  for account_id in req["accounts"]:
@@ -153,7 +304,15 @@ class NordigenClient(BaseService):
153
304
  return accounts
154
305
 
155
306
  def delete_link(self, reference):
156
- """Delete a bank link by reference."""
307
+ """
308
+ Delete a bank link (requisition) by its reference.
309
+
310
+ Args:
311
+ reference (str): The unique reference string of the link to delete.
312
+
313
+ Returns:
314
+ dict: A dictionary with the status and a message. Status can be "deleted" or "not_found".
315
+ """
157
316
  req_id = self.find_requisition_id(reference)
158
317
  if not req_id:
159
318
  return {"status": "not_found", "message": f"Link {reference} not found"}
@@ -162,7 +321,17 @@ class NordigenClient(BaseService):
162
321
  return {"status": "deleted", "message": f"Link {reference} removed"}
163
322
 
164
323
  def get_transactions(self, account_id, days_back=180):
165
- """Retrieve transactions for an account."""
324
+ """
325
+ Retrieve transactions for a given account.
326
+
327
+ Args:
328
+ account_id (str): The ID of the account.
329
+ days_back (int, optional): The number of days back to retrieve transactions for. Defaults to 180.
330
+
331
+ Returns:
332
+ dict: The 'transactions' part of the API response, or an empty dict if no transactions are found.
333
+ See the Nordigen API documentation for the structure of this data.
334
+ """
166
335
  date_from = (datetime.now() - timedelta(days=days_back)).strftime("%Y-%m-%d")
167
336
  return self._get(
168
337
  f"/accounts/{account_id}/transactions/",
@@ -1,4 +1,4 @@
1
- from datetime import date, timedelta, datetime
1
+ from datetime import date
2
2
  from os import path
3
3
  import beangulp
4
4
  import yaml
@@ -8,46 +8,106 @@ from .client import NordigenClient
8
8
 
9
9
 
10
10
  class NordigenImporter(beangulp.Importer):
11
- """An importer for Nordigen API with improved structure and extensibility."""
11
+ """
12
+ An importer for Nordigen API with improved structure and extensibility.
13
+
14
+ Attributes:
15
+ config (dict): Configuration loaded from the YAML file.
16
+ _client (NordigenClient): Instance of the Nordigen API client.
17
+
18
+ """
12
19
 
13
20
  def __init__(self):
21
+ """Initialize the NordigenImporter."""
14
22
  self.config = None
15
23
  self._client = None
16
24
 
17
25
  @property
18
26
  def client(self):
27
+ """
28
+ Lazily initializes and returns the Nordigen API client.
29
+
30
+ Returns:
31
+ NordigenClient: The initialized Nordigen API client.
32
+ """
19
33
  if not self._client:
20
34
  self._client = NordigenClient(
21
35
  self.config["secret_id"],
22
36
  self.config["secret_key"],
23
- cache_options={"expire_after": 3600 * 24, "old_data_on_error": True},
37
+ cache_options=self.config.get('cache_options', None),
24
38
  )
25
39
 
26
40
  return self._client
27
41
 
28
42
  def identify(self, filepath: str) -> bool:
43
+ """
44
+ Identifies if the given file is a Nordigen configuration file.
45
+
46
+ Args:
47
+ filepath (str): The path to the file.
48
+
49
+ Returns:
50
+ bool: True if the file is a Nordigen configuration file, False otherwise.
51
+ """
29
52
  return path.basename(filepath).endswith("nordigen.yaml")
30
53
 
31
54
  def account(self, filepath: str) -> str:
32
- return ""
55
+ """
56
+ Returns an empty string as account (not directly used in this importer).
57
+
58
+ Args:
59
+ filepath (str): The path to the file. Not used in this implementation.
60
+
61
+ Returns:
62
+ str: An empty string.
63
+ """
64
+ return "" # We get the account from the config file
33
65
 
34
66
  def load_config(self, filepath: str):
35
- """Load configuration from YAML file."""
67
+ """
68
+ Loads configuration from the specified YAML file.
69
+
70
+ Args:
71
+ filepath (str): The path to the YAML configuration file.
72
+
73
+ Returns:
74
+ dict: The loaded configuration dictionary. Also sets the `self.config` attribute.
75
+ """
36
76
  with open(filepath, "r") as f:
37
77
  raw_config = f.read()
38
- expanded_config = path.expandvars(raw_config)
78
+ expanded_config = path.expandvars(
79
+ raw_config
80
+ ) # Handle environment variables
39
81
  self.config = yaml.safe_load(expanded_config)
40
82
 
41
83
  return self.config
42
84
 
43
85
  def get_transactions_data(self, account_id):
44
- """Get transactions data either from API or debug files."""
86
+ """
87
+ Retrieves transaction data for a given account ID from the Nordigen API.
88
+
89
+ Args:
90
+ account_id (str): The Nordigen account ID.
91
+
92
+ Returns:
93
+ dict: The transaction data returned by the API.
94
+ """
45
95
  transactions_data = self.client.get_transactions(account_id)
46
96
 
47
97
  return transactions_data
48
98
 
49
99
  def get_all_transactions(self, transactions_data):
50
- """Combine and sort booked and pending transactions."""
100
+ """
101
+ Combines booked and pending transactions and sorts them by date.
102
+
103
+ Args:
104
+ transactions_data (dict): The transaction data from the API,
105
+ containing 'booked' and 'pending' lists.
106
+
107
+ Returns:
108
+ list: A sorted list of tuples, where each tuple contains
109
+ a transaction dictionary and its status ('booked' or 'pending').
110
+ """
51
111
  all_transactions = [
52
112
  (tx, "booked") for tx in transactions_data.get("booked", [])
53
113
  ] + [(tx, "pending") for tx in transactions_data.get("pending", [])]
@@ -57,7 +117,18 @@ class NordigenImporter(beangulp.Importer):
57
117
  )
58
118
 
59
119
  def add_metadata(self, transaction, filing_account: str):
60
- """Extract metadata from transaction - overridable method."""
120
+ """
121
+ Extracts metadata from a transaction and returns it as a dictionary.
122
+
123
+ This method can be overridden in subclasses to customize metadata extraction.
124
+
125
+ Args:
126
+ transaction (dict): The transaction data from the API.
127
+ filing_account (str): The optional filing account from the configuration.
128
+
129
+ Returns:
130
+ dict: A dictionary of metadata key-value pairs.
131
+ """
61
132
  metakv = {}
62
133
 
63
134
  # Transaction ID
@@ -91,7 +162,17 @@ class NordigenImporter(beangulp.Importer):
91
162
  return metakv
92
163
 
93
164
  def get_narration(self, transaction):
94
- """Extract narration from transaction - overridable method."""
165
+ """
166
+ Extracts the narration from a transaction.
167
+
168
+ This method can be overridden in subclasses to customize narration extraction.
169
+
170
+ Args:
171
+ transaction (dict): The transaction data from the API.
172
+
173
+ Returns:
174
+ str: The extracted narration.
175
+ """
95
176
  narration = ""
96
177
 
97
178
  if "remittanceInformationUnstructured" in transaction:
@@ -103,23 +184,70 @@ class NordigenImporter(beangulp.Importer):
103
184
  return narration
104
185
 
105
186
  def get_payee(self, transaction):
106
- """Extract payee from transaction - overridable method."""
187
+ """
188
+ Extracts the payee from a transaction.
189
+
190
+ This method can be overridden in subclasses to customize payee extraction. The default
191
+ implementation returns an empty string.
192
+
193
+ Args:
194
+ transaction (dict): The transaction data from the API.
195
+
196
+ Returns:
197
+ str: The extracted payee (or an empty string by default).
198
+
199
+ """
107
200
  return ""
108
201
 
109
202
  def get_transaction_date(self, transaction):
110
- """Extract transaction date - overridable method."""
203
+ """
204
+ Extracts the transaction date from a transaction. Prefers 'valueDate',
205
+ falls back to 'bookingDate'.
206
+
207
+ This method can be overridden in subclasses to customize date extraction.
208
+
209
+ Args:
210
+ transaction (dict): The transaction data from the API.
211
+
212
+ Returns:
213
+ date: The extracted transaction date, or None if no date is found.
214
+ """
111
215
  date_str = transaction.get("valueDate") or transaction.get("bookingDate")
112
216
  return date.fromisoformat(date_str) if date_str else None
113
217
 
114
218
  def get_transaction_status(self, status):
115
- """Determine transaction status flag - overridable method."""
219
+ """
220
+ Determines the Beancount transaction flag based on the transaction status.
221
+
222
+ This method can be overridden in subclasses to customize flag assignment. The default
223
+ implementation returns FLAG_OKAY for all transactions.
224
+
225
+ Args:
226
+ status (str): The transaction status ('booked' or 'pending').
227
+
228
+ Returns:
229
+ str: The Beancount transaction flag.
230
+ """
116
231
  # Could be configured to use "!" for pending transactions status == 'pending'
117
232
  return flags.FLAG_OKAY
118
233
 
119
234
  def create_transaction_entry(
120
235
  self, transaction, status, asset_account, filing_account
121
236
  ):
122
- """Create a Beancount transaction entry - overridable method."""
237
+ """
238
+ Creates a Beancount transaction entry from a Nordigen transaction.
239
+
240
+ This method can be overridden in subclasses to customize entry creation.
241
+
242
+ Args:
243
+ transaction (dict): The transaction data from the API.
244
+ status (str): The transaction status ('booked' or 'pending').
245
+ asset_account (str): The Beancount asset account.
246
+ filing_account (str): The optional filing account.
247
+
248
+ Returns:
249
+ data.Transaction: The created Beancount transaction entry.
250
+ """
123
251
  metakv = self.add_metadata(transaction, filing_account)
124
252
  meta = data.new_metadata("", 0, metakv)
125
253
 
@@ -155,7 +283,16 @@ class NordigenImporter(beangulp.Importer):
155
283
  )
156
284
 
157
285
  def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
158
- """Extract entries from Nordigen transactions."""
286
+ """
287
+ Extracts Beancount entries from Nordigen transactions.
288
+
289
+ Args:
290
+ filepath (str): The path to the YAML configuration file.
291
+ existing (data.Entries): Existing Beancount entries (not used in this implementation).
292
+
293
+ Returns:
294
+ data.Entries: A list of Beancount transaction entries.
295
+ """
159
296
  self.load_config(filepath)
160
297
 
161
298
  entries = []
@@ -176,8 +313,33 @@ class NordigenImporter(beangulp.Importer):
176
313
  return entries
177
314
 
178
315
  def cmp(self, entry1: data.Transaction, entry2: data.Transaction):
179
- return (
316
+ """
317
+ Compares two transactions based on their 'nordref' metadata.
318
+
319
+ Used for sorting transactions. This assumes that 'nordref' is a unique
320
+ identifier for each transaction.
321
+
322
+ Args:
323
+ entry1 (data.Transaction): The first transaction.
324
+ entry2 (data.Transaction): The second transaction.
325
+
326
+ Returns:
327
+ int: -1 if entry1 < entry2, 0 if entry1 == entry2, 1 if entry1 > entry2.
328
+ Returns 0 if 'nordref' is not present in both.
329
+ """
330
+ if (
180
331
  "nordref" in entry1.meta
181
332
  and "nordref" in entry2.meta
182
333
  and entry1.meta["nordref"] == entry2.meta["nordref"]
183
- )
334
+ ):
335
+ return 0 # Consider them equal if nordref matches
336
+ elif (
337
+ "nordref" in entry1.meta
338
+ and "nordref" in entry2.meta
339
+ and entry1.meta["nordref"] < entry2.meta["nordref"]
340
+ ):
341
+ return -1
342
+ elif "nordref" in entry1.meta and "nordref" in entry2.meta:
343
+ return 1
344
+ else:
345
+ return 0
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.3
2
+ Name: beancount-gocardless
3
+ Version: 0.1.2
4
+ Summary:
5
+ License: MIT
6
+ Requires-Python: >=3.12
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Dist: beancount
12
+ Requires-Dist: beangulp
13
+ Requires-Dist: pyyaml
14
+ Requires-Dist: requests
15
+ Requires-Dist: requests-cache
16
+ Description-Content-Type: text/markdown
17
+
18
+ beancount-gocardless
19
+ ====================
20
+
21
+ This package provides a basic client for interacting with the GoCardless API (formerly Nordigen) and importing your data into Beancount.
22
+
23
+ This project was inspired by https://github.com/tarioch/beancounttools
24
+
25
+ Full documentation available at https://beancount-gocardless.readthedocs.io/en/latest/
26
+
27
+
28
+ **Key Features:**
29
+
30
+ - **GoCardless API Client:** A client for interacting with the GoCardless API. The client has built-in caching via `requests-cache`.
31
+ - **GoCardLess CLI**\: A command-line interface to manage authorization with the GoCardless API:
32
+
33
+ - Listing available banks in a specified country (default: GB).
34
+ - Creating a link to a specific bank using its ID.
35
+ - Listing authorized accounts.
36
+ - Deleting an existing link.
37
+ - Uses environment variables (`NORDIGEN_SECRET_ID`, `NORDIGEN_SECRET_KEY`) or command-line arguments for API credentials.
38
+ - **Beancount Importer:** A `beangulp.Importer` implementation to easily import transactions fetched from the GoCardless API directly into your Beancount ledger.
39
+
40
+ You'll need to create a GoCardLess account on https://bankaccountdata.gocardless.com/overview/ to get your credentials.
41
+
42
+ **Installation:**
43
+
44
+ ```bash
45
+ pip install beancount-gocardless
46
+ ```
47
+
48
+ **Usage**
49
+ ```yaml
50
+ #### nordigen.yaml
51
+ secret_id: $NORDIGEN_SECRET_ID
52
+ secret_key: $NORDIGEN_SECRET_KEY
53
+
54
+ cache_options: # by default, no caching if cache_options is not provided
55
+ cache_name: "nordigen"
56
+ backend: "sqlite"
57
+ expire_after: 3600
58
+ old_data_on_error: true
59
+
60
+ accounts:
61
+ - id: <REDACTED_UUID>
62
+ asset_account: "Assets:Banks:Revolut:Checking"
63
+ ```
64
+
65
+ ```python
66
+ #### my.import
67
+ #!/usr/bin/env python
68
+
69
+ import beangulp
70
+ from beancount_gocardless import NordigenImporter
71
+ from smart_importer import apply_hooks, PredictPostings, PredictPayees
72
+
73
+ importers = [
74
+ apply_hooks(
75
+ NordigenImporter(),
76
+ [
77
+ PredictPostings(),
78
+ PredictPayees(),
79
+ ],
80
+ )
81
+ ]
82
+
83
+ if __name__ == "__main__":
84
+ ingest = beangulp.Ingest(importers)
85
+ ingest()
86
+ ```
87
+
88
+ Import your data from Nordigen's API
89
+ ```bash
90
+ python my.import extract ./nordigen.yaml --existing ./ledger.bean
91
+ ```
92
+
@@ -0,0 +1,8 @@
1
+ beancount_gocardless/__init__.py,sha256=Rf2-pfuaXaXPwBu3yEn2uXyOQ6uLyGxljJ5hoTCss5Y,100
2
+ beancount_gocardless/cli.py,sha256=ZdsdknScEOlUq_7rI0ixzN1UDh1dgUokzTzO_3WySqY,2407
3
+ beancount_gocardless/client.py,sha256=zjWgagMK9LbV0lDsQ4W-FSxGxoj-KYKKmgOlx2XZsKk,12283
4
+ beancount_gocardless/importer.py,sha256=6VBGkwU2arRukXGkHuKtINGy7ocOmM1NM48l8jLPbTs,11375
5
+ beancount_gocardless-0.1.2.dist-info/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
6
+ beancount_gocardless-0.1.2.dist-info/METADATA,sha256=JbiSAL5dC2fGnHmzqh3HOpuhc4cGNjXov1oUJWaeeD4,2634
7
+ beancount_gocardless-0.1.2.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
8
+ beancount_gocardless-0.1.2.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: beancount-gocardless
3
- Version: 0.1.0
4
- Summary:
5
- License: MIT
6
- Requires-Python: >=3.12
7
- Classifier: License :: OSI Approved :: MIT License
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.12
10
- Classifier: Programming Language :: Python :: 3.13
11
- Requires-Dist: beancount
12
- Requires-Dist: beangulp
13
- Requires-Dist: pyyaml
14
- Requires-Dist: requests
15
- Requires-Dist: requests-cache
16
- Description-Content-Type: text/markdown
17
-
18
-
@@ -1,8 +0,0 @@
1
- beancount_gocardless/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- beancount_gocardless/cli.py,sha256=qCZZ-KUl9yzUWRP2Qw3aiwsnHKqSWYjKsz2aMFyXn60,2160
3
- beancount_gocardless/client.py,sha256=rjm1ey-uQkQYHTMVG_EFxvaVagez4tw96VOHfOWZZGE,6142
4
- beancount_gocardless/importer.py,sha256=zYQ9zFnjTyIxk1T0utycOlyBOXbs5pNemp_5gmc5N6A,6362
5
- beancount_gocardless-0.1.0.dist-info/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
6
- beancount_gocardless-0.1.0.dist-info/METADATA,sha256=wOVGuun-w9RuKIO2dKc_o_PSarP6Xpwjgy6Ig1YXfL0,479
7
- beancount_gocardless-0.1.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
8
- beancount_gocardless-0.1.0.dist-info/RECORD,,