beancount-gocardless 0.1.6__py3-none-any.whl → 0.1.8__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/client.py +91 -14
- beancount_gocardless/importer.py +11 -12
- {beancount_gocardless-0.1.6.dist-info → beancount_gocardless-0.1.8.dist-info}/METADATA +1 -1
- beancount_gocardless-0.1.8.dist-info/RECORD +9 -0
- beancount_gocardless-0.1.6.dist-info/RECORD +0 -9
- {beancount_gocardless-0.1.6.dist-info → beancount_gocardless-0.1.8.dist-info}/LICENSE +0 -0
- {beancount_gocardless-0.1.6.dist-info → beancount_gocardless-0.1.8.dist-info}/WHEEL +0 -0
- {beancount_gocardless-0.1.6.dist-info → beancount_gocardless-0.1.8.dist-info}/entry_points.txt +0 -0
beancount_gocardless/client.py
CHANGED
|
@@ -2,11 +2,29 @@ from datetime import timedelta, datetime
|
|
|
2
2
|
import requests_cache
|
|
3
3
|
import requests
|
|
4
4
|
from typing import TypedDict, Optional
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def strip_headers_hook(response, *args, **kwargs):
|
|
11
|
+
to_preserve = [
|
|
12
|
+
"Content-Type",
|
|
13
|
+
"Date",
|
|
14
|
+
"Content-Encoding",
|
|
15
|
+
"Content-Language",
|
|
16
|
+
"Last-Modified",
|
|
17
|
+
]
|
|
18
|
+
deleted = set()
|
|
19
|
+
to_preserve_lower = [h.lower() for h in to_preserve]
|
|
20
|
+
header_keys_to_check = response.headers.copy().keys()
|
|
21
|
+
for header in header_keys_to_check:
|
|
22
|
+
if header.lower() in to_preserve_lower:
|
|
23
|
+
continue
|
|
24
|
+
else:
|
|
25
|
+
response.headers.pop(header, None)
|
|
26
|
+
deleted.add(header)
|
|
27
|
+
logger.debug("Deleted headers: %s", ", ".join(deleted))
|
|
10
28
|
return response
|
|
11
29
|
|
|
12
30
|
|
|
@@ -23,7 +41,7 @@ class CacheOptions(TypedDict, total=False):
|
|
|
23
41
|
|
|
24
42
|
cache_name: requests_cache.StrOrPath
|
|
25
43
|
backend: Optional[requests_cache.BackendSpecifier]
|
|
26
|
-
expire_after:
|
|
44
|
+
expire_after: int
|
|
27
45
|
old_data_on_error: bool
|
|
28
46
|
|
|
29
47
|
|
|
@@ -63,7 +81,7 @@ class BaseService:
|
|
|
63
81
|
"expire_after": 0,
|
|
64
82
|
"old_data_on_error": True,
|
|
65
83
|
"match_headers": False,
|
|
66
|
-
"
|
|
84
|
+
"cache_control": False,
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
def __init__(
|
|
@@ -82,21 +100,78 @@ class BaseService:
|
|
|
82
100
|
"""
|
|
83
101
|
self.secret_id = secret_id
|
|
84
102
|
self.secret_key = secret_key
|
|
85
|
-
self.
|
|
103
|
+
self._token = None
|
|
86
104
|
merged_options = {**self.DEFAULT_CACHE_OPTIONS, **(cache_options or {})}
|
|
87
105
|
self.session = requests_cache.CachedSession(**merged_options)
|
|
106
|
+
self.session.hooks["response"].append(strip_headers_hook)
|
|
107
|
+
|
|
108
|
+
def check_cache_status(self, method: str, url: str, params=None, data=None) -> dict:
|
|
109
|
+
"""
|
|
110
|
+
Attempts to predict the cache status for a given request.
|
|
111
|
+
|
|
112
|
+
NOTE: This is an approximation and relies on internal mechanisms
|
|
113
|
+
that might change. It also performs I/O to check the cache.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
method (str): HTTP method ("GET", "POST", etc.).
|
|
117
|
+
endpoint (str): API endpoint.
|
|
118
|
+
params (dict, optional): URL parameters.
|
|
119
|
+
data (dict, optional): Request body data.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
dict: Information about the potential cache state:
|
|
123
|
+
{'key_exists': bool, 'is_expired': Optional[bool], 'cache_key': str}
|
|
124
|
+
'is_expired' is None if the key doesn't exist or expiration
|
|
125
|
+
cannot be reliably determined without full retrieval.
|
|
126
|
+
"""
|
|
127
|
+
headers = {"Authorization": f"Bearer {self.token}"}
|
|
128
|
+
|
|
129
|
+
req = requests.Request(method, url, params=params, data=data, headers=headers)
|
|
130
|
+
prepared_request: requests.PreparedRequest = self.session.prepare_request(req)
|
|
131
|
+
cache = self.session.cache
|
|
132
|
+
cache_key = cache.create_key(prepared_request)
|
|
133
|
+
key_exists = cache.contains(cache_key)
|
|
134
|
+
is_expired = None
|
|
135
|
+
|
|
136
|
+
if key_exists:
|
|
137
|
+
try:
|
|
138
|
+
# Try to get the response object without triggering expiration side effects
|
|
139
|
+
# Note: This still reads from the cache backend (I/O)
|
|
140
|
+
cached_response = cache.get_response(cache_key)
|
|
141
|
+
if cached_response:
|
|
142
|
+
# is_expired is a property calculated on the CachedResponse
|
|
143
|
+
is_expired = cached_response.is_expired
|
|
144
|
+
else:
|
|
145
|
+
# get_response might return None if item expired and configured to delete
|
|
146
|
+
# Or if backend consistency issue. Treat as expired/absent.
|
|
147
|
+
key_exists = False # Correct the state if get_response fails
|
|
148
|
+
is_expired = True # Assume expired if get_response returns None for existing key
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(
|
|
151
|
+
f"Error checking expiration for cache key {cache_key}: {e}"
|
|
152
|
+
)
|
|
153
|
+
# Cannot determine expiration reliably
|
|
154
|
+
is_expired = None # Mark as unknown
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"key_exists": key_exists,
|
|
158
|
+
"is_expired": is_expired,
|
|
159
|
+
"cache_key": cache_key,
|
|
160
|
+
}
|
|
88
161
|
|
|
89
|
-
|
|
162
|
+
@property
|
|
163
|
+
def token(self):
|
|
90
164
|
"""
|
|
91
|
-
Ensure a
|
|
165
|
+
Ensure a token exists. Gets a new token if one doesn't exist.
|
|
92
166
|
Nordigen tokens don't currently have a refresh mechanism, so this just gets a new one if needed.
|
|
93
167
|
"""
|
|
94
|
-
if not self.
|
|
168
|
+
if not self._token:
|
|
95
169
|
self.get_token()
|
|
170
|
+
return self._token
|
|
96
171
|
|
|
97
172
|
def get_token(self):
|
|
98
173
|
"""
|
|
99
|
-
Fetch a new API access token using credentials. Sets the `self.
|
|
174
|
+
Fetch a new API access token using credentials. Sets the `self._token` attribute.
|
|
100
175
|
|
|
101
176
|
Raises:
|
|
102
177
|
HttpServiceException: If the API request fails.
|
|
@@ -106,7 +181,7 @@ class BaseService:
|
|
|
106
181
|
data={"secret_id": self.secret_id, "secret_key": self.secret_key},
|
|
107
182
|
)
|
|
108
183
|
self._handle_response(response)
|
|
109
|
-
self.
|
|
184
|
+
self._token = response.json()["access"]
|
|
110
185
|
|
|
111
186
|
def _handle_response(self, response):
|
|
112
187
|
"""
|
|
@@ -140,12 +215,14 @@ class BaseService:
|
|
|
140
215
|
HttpServiceException: If the API request fails.
|
|
141
216
|
"""
|
|
142
217
|
url = f"{self.BASE_URL}{endpoint}"
|
|
143
|
-
self._ensure_token_valid()
|
|
144
218
|
headers = {"Authorization": f"Bearer {self.token}"}
|
|
145
219
|
|
|
220
|
+
status = self.check_cache_status(method, url, params, data)
|
|
221
|
+
logger.debug(f"{endpoint}: {'expired' if status["is_expired"] else 'cache ok'}")
|
|
146
222
|
response = self.session.request(
|
|
147
223
|
method, url, headers=headers, params=params, data=data
|
|
148
224
|
)
|
|
225
|
+
logger.info("Response headers", response.headers)
|
|
149
226
|
|
|
150
227
|
# Retry once if token expired (401 Unauthorized)
|
|
151
228
|
if response.status_code == 401:
|
beancount_gocardless/importer.py
CHANGED
|
@@ -34,7 +34,7 @@ class NordigenImporter(beangulp.Importer):
|
|
|
34
34
|
self._client = NordigenClient(
|
|
35
35
|
self.config["secret_id"],
|
|
36
36
|
self.config["secret_key"],
|
|
37
|
-
cache_options=self.config.get(
|
|
37
|
+
cache_options=self.config.get("cache_options", None),
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
return self._client
|
|
@@ -116,7 +116,7 @@ class NordigenImporter(beangulp.Importer):
|
|
|
116
116
|
key=lambda x: x[0].get("valueDate") or x[0].get("bookingDate"),
|
|
117
117
|
)
|
|
118
118
|
|
|
119
|
-
def add_metadata(self, transaction,
|
|
119
|
+
def add_metadata(self, transaction, custom_metadata):
|
|
120
120
|
"""
|
|
121
121
|
Extracts metadata from a transaction and returns it as a dictionary.
|
|
122
122
|
|
|
@@ -124,7 +124,7 @@ class NordigenImporter(beangulp.Importer):
|
|
|
124
124
|
|
|
125
125
|
Args:
|
|
126
126
|
transaction (dict): The transaction data from the API.
|
|
127
|
-
|
|
127
|
+
custom_metadata (dict): Custom metadata from the config file.
|
|
128
128
|
|
|
129
129
|
Returns:
|
|
130
130
|
dict: A dictionary of metadata key-value pairs.
|
|
@@ -151,8 +151,7 @@ class NordigenImporter(beangulp.Importer):
|
|
|
151
151
|
if transaction.get("bookingDate"):
|
|
152
152
|
metakv["bookingDate"] = transaction["bookingDate"]
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
metakv["filing_account"] = filing_account
|
|
154
|
+
metakv.update(custom_metadata)
|
|
156
155
|
|
|
157
156
|
return metakv
|
|
158
157
|
|
|
@@ -223,11 +222,10 @@ class NordigenImporter(beangulp.Importer):
|
|
|
223
222
|
Returns:
|
|
224
223
|
str: The Beancount transaction flag.
|
|
225
224
|
"""
|
|
226
|
-
|
|
227
|
-
return flags.FLAG_OKAY
|
|
225
|
+
return flags.FLAG_OKAY if status == "booked" else flags.FLAG_WARNING
|
|
228
226
|
|
|
229
227
|
def create_transaction_entry(
|
|
230
|
-
self, transaction, status, asset_account,
|
|
228
|
+
self, transaction, status, asset_account, custom_metadata
|
|
231
229
|
):
|
|
232
230
|
"""
|
|
233
231
|
Creates a Beancount transaction entry from a Nordigen transaction.
|
|
@@ -238,12 +236,12 @@ class NordigenImporter(beangulp.Importer):
|
|
|
238
236
|
transaction (dict): The transaction data from the API.
|
|
239
237
|
status (str): The transaction status ('booked' or 'pending').
|
|
240
238
|
asset_account (str): The Beancount asset account.
|
|
241
|
-
|
|
239
|
+
custom_metadata (dict): Custom metadata from config
|
|
242
240
|
|
|
243
241
|
Returns:
|
|
244
242
|
data.Transaction: The created Beancount transaction entry.
|
|
245
243
|
"""
|
|
246
|
-
metakv = self.add_metadata(transaction,
|
|
244
|
+
metakv = self.add_metadata(transaction, custom_metadata)
|
|
247
245
|
meta = data.new_metadata("", 0, metakv)
|
|
248
246
|
|
|
249
247
|
trx_date = self.get_transaction_date(transaction)
|
|
@@ -294,14 +292,15 @@ class NordigenImporter(beangulp.Importer):
|
|
|
294
292
|
for account in self.config["accounts"]:
|
|
295
293
|
account_id = account["id"]
|
|
296
294
|
asset_account = account["asset_account"]
|
|
297
|
-
|
|
295
|
+
# Use get() with a default empty dict for custom_metadata
|
|
296
|
+
custom_metadata = account.get("metadata", {})
|
|
298
297
|
|
|
299
298
|
transactions_data = self.get_transactions_data(account_id)
|
|
300
299
|
all_transactions = self.get_all_transactions(transactions_data)
|
|
301
300
|
|
|
302
301
|
for transaction, status in all_transactions:
|
|
303
302
|
entry = self.create_transaction_entry(
|
|
304
|
-
transaction, status, asset_account,
|
|
303
|
+
transaction, status, asset_account, custom_metadata
|
|
305
304
|
)
|
|
306
305
|
entries.append(entry)
|
|
307
306
|
|
|
@@ -0,0 +1,9 @@
|
|
|
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=g2Px7mcM34ibEAVzbxUtCTVrGWO1sm_CmCL-PXySHro,15764
|
|
4
|
+
beancount_gocardless/importer.py,sha256=tjNKPCYFddR62YDAXm6rfLqcVNeiGKWbVAEyoPWPekg,11151
|
|
5
|
+
beancount_gocardless-0.1.8.dist-info/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
|
|
6
|
+
beancount_gocardless-0.1.8.dist-info/METADATA,sha256=-WXcAySDk-rankLsjnn9keWLVrrscByJdwl2n1h1bkw,2634
|
|
7
|
+
beancount_gocardless-0.1.8.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
8
|
+
beancount_gocardless-0.1.8.dist-info/entry_points.txt,sha256=fmhiRcNVrum0p30f5YNqvIYVEPXYsS5cP1xNkVmdn8k,70
|
|
9
|
+
beancount_gocardless-0.1.8.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
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=PHZQH3O122cqYsYZDdb1NEoYuS5NBJc0ZteostnMUbM,12511
|
|
4
|
-
beancount_gocardless/importer.py,sha256=TcwHnXFLBER6RGhd0siaK4kD6ToSdmvWODwh_ZfT6e4,11183
|
|
5
|
-
beancount_gocardless-0.1.6.dist-info/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
|
|
6
|
-
beancount_gocardless-0.1.6.dist-info/METADATA,sha256=WZ5uXpof3dWLDVQFwM_7UX5i_I6y78tJ6A1oMHlE2s8,2634
|
|
7
|
-
beancount_gocardless-0.1.6.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
8
|
-
beancount_gocardless-0.1.6.dist-info/entry_points.txt,sha256=fmhiRcNVrum0p30f5YNqvIYVEPXYsS5cP1xNkVmdn8k,70
|
|
9
|
-
beancount_gocardless-0.1.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{beancount_gocardless-0.1.6.dist-info → beancount_gocardless-0.1.8.dist-info}/entry_points.txt
RENAMED
|
File without changes
|