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.
@@ -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
- def remove_vary_header(response):
8
- if "Vary" in response.headers:
9
- del response.headers["Vary"] # Fuck it, it’s gone
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: requests_cache.ExpirationTime
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
- "response_hook": remove_vary_header,
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.token = None
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
- def _ensure_token_valid(self):
162
+ @property
163
+ def token(self):
90
164
  """
91
- Ensure a valid token exists. Gets a new token if one doesn't exist.
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.token:
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.token` attribute.
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.token = response.json()["access"]
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:
@@ -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('cache_options', None),
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, filing_account: str):
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
- filing_account (str): The optional filing account from the configuration.
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
- if filing_account:
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
- # Could be configured to use "!" for pending transactions status == 'pending'
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, filing_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
- filing_account (str): The optional filing account.
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, filing_account)
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
- filing_account = account.get("filing_account", None)
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, filing_account
303
+ transaction, status, asset_account, custom_metadata
305
304
  )
306
305
  entries.append(entry)
307
306
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: beancount-gocardless
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.12
@@ -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,,