beancount-gocardless 0.1.0__py3-none-any.whl → 0.1.1__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.
- beancount_gocardless/__init__.py +2 -0
- beancount_gocardless/cli.py +12 -1
- beancount_gocardless/client.py +185 -16
- beancount_gocardless/importer.py +178 -16
- beancount_gocardless-0.1.1.dist-info/METADATA +37 -0
- beancount_gocardless-0.1.1.dist-info/RECORD +8 -0
- beancount_gocardless-0.1.0.dist-info/METADATA +0 -18
- beancount_gocardless-0.1.0.dist-info/RECORD +0 -8
- {beancount_gocardless-0.1.0.dist-info → beancount_gocardless-0.1.1.dist-info}/LICENSE +0 -0
- {beancount_gocardless-0.1.0.dist-info → beancount_gocardless-0.1.1.dist-info}/WHEEL +0 -0
beancount_gocardless/__init__.py
CHANGED
beancount_gocardless/cli.py
CHANGED
|
@@ -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:
|
|
@@ -60,7 +71,7 @@ def main():
|
|
|
60
71
|
accounts = client.list_accounts()
|
|
61
72
|
for a in accounts:
|
|
62
73
|
print(
|
|
63
|
-
f"{a[
|
|
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":
|
beancount_gocardless/client.py
CHANGED
|
@@ -5,6 +5,16 @@ from typing import Protocol, 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
|
-
"""
|
|
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,7 +37,17 @@ class HttpServiceException(Exception):
|
|
|
21
37
|
|
|
22
38
|
|
|
23
39
|
class BaseService:
|
|
24
|
-
"""
|
|
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
|
|
|
@@ -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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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/",
|
beancount_gocardless/importer.py
CHANGED
|
@@ -4,18 +4,32 @@ import beangulp
|
|
|
4
4
|
import yaml
|
|
5
5
|
from beancount.core import amount, data, flags
|
|
6
6
|
from beancount.core.number import D
|
|
7
|
-
from .client import NordigenClient
|
|
7
|
+
from .client import NordigenClient # Assuming client.py is in the same directory
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class NordigenImporter(beangulp.Importer):
|
|
11
|
-
"""
|
|
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"],
|
|
@@ -26,28 +40,74 @@ class NordigenImporter(beangulp.Importer):
|
|
|
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
|
-
|
|
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
|
-
"""
|
|
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(
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
|
|
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,37 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: beancount-gocardless
|
|
3
|
+
Version: 0.1.1
|
|
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), a command-line interface (CLI) tool for authorization, and a Beancount importer for seamless integration with your financial data. It streamlines the process of fetching bank account data via the GoCardless API and incorporating it into your Beancount ledger.
|
|
22
|
+
|
|
23
|
+
**Key Features:**
|
|
24
|
+
|
|
25
|
+
* **GoCardless API Client:** A client for interacting with the GoCardless API. The client has built-in caching via `requests-cache`, which helps during testing as GoCardLess API limits the number of requests to 4/day/account.
|
|
26
|
+
* **GoCardLess CLI:** A command-line interface to manage authorization with the GoCardless API. The CLI supports:
|
|
27
|
+
* Listing available banks in a specified country (default: GB).
|
|
28
|
+
* Creating a link to a specific bank using its ID.
|
|
29
|
+
* Listing authorized accounts.
|
|
30
|
+
* Deleting an existing link.
|
|
31
|
+
* Uses environment variables (`NORDIGEN_SECRET_ID`, `NORDIGEN_SECRET_KEY`) or command-line arguments for API credentials.
|
|
32
|
+
* **Beancount Importer:** A `beangulp.Importer` implementation to easily import transactions fetched from the GoCardless API directly into your Beancount ledger.
|
|
33
|
+
|
|
34
|
+
**Installation:**
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install beancount-gocardless
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
beancount_gocardless/__init__.py,sha256=-taSw3dkNHhkBQrKeJMKUt-qIitinGevRW4hSqxHp58,73
|
|
2
|
+
beancount_gocardless/cli.py,sha256=QK3m9urC0VLwzMMqMT4owlrsKQ6f7feZT7a7bWX79Po,2432
|
|
3
|
+
beancount_gocardless/client.py,sha256=lz-vCeca78cig0V5iNk_uUVHlvtE3tBbw1KkcwY6n1U,12307
|
|
4
|
+
beancount_gocardless/importer.py,sha256=2UqSAbfa8d2UBZxTewmyyQ7G3Hm1b_4x7VuXWonoa8w,11459
|
|
5
|
+
beancount_gocardless-0.1.1.dist-info/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
|
|
6
|
+
beancount_gocardless-0.1.1.dist-info/METADATA,sha256=nc5mVlnMwp1MljMyJ-hzmMGfPZ2bGFZa2155fiMsw_4,1798
|
|
7
|
+
beancount_gocardless-0.1.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
8
|
+
beancount_gocardless-0.1.1.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,,
|
|
File without changes
|
|
File without changes
|