beancount-gocardless 0.1.7__py3-none-any.whl → 0.1.9__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.
@@ -1,368 +1,424 @@
1
- from datetime import timedelta, datetime
1
+ """
2
+ GoCardless Bank Account Data API Client
3
+ Clean, typed, auto-generated client with full caching and header stripping
4
+ """
5
+
6
+ import logging
7
+ from typing import Optional, Dict, Any, List
8
+ from datetime import datetime, timedelta
2
9
  import requests_cache
3
10
  import requests
4
- from typing import TypedDict, Optional
5
- import logging
11
+ from .models import (
12
+ Account,
13
+ AccountBalance,
14
+ AccountDetail,
15
+ AccountTransactions,
16
+ Institution,
17
+ Requisition,
18
+ EndUserAgreement,
19
+ ReconfirmationRetrieve,
20
+ PaginatedRequisitionList,
21
+ PaginatedEndUserAgreementList,
22
+ Integration,
23
+ IntegrationRetrieve,
24
+ SpectacularJWTObtain,
25
+ SpectacularJWTRefresh,
26
+ AccountInfo,
27
+ )
6
28
 
7
29
  logger = logging.getLogger(__name__)
8
30
 
9
31
 
10
- def cleanup_headers(response):
32
+ def strip_headers_hook(response, *args, **kwargs):
33
+ """
34
+ Strip response headers to reduce cache size and improve privacy.
35
+ Only preserves essential headers needed for proper operation.
36
+ """
11
37
  to_preserve = [
12
38
  "Content-Type",
13
39
  "Date",
14
40
  "Content-Encoding",
15
41
  "Content-Language",
16
- "ETag",
17
42
  "Last-Modified",
43
+ "Location",
18
44
  ]
19
45
  deleted = set()
20
46
  to_preserve_lower = [h.lower() for h in to_preserve]
21
- for header in response.headers.keys():
22
- if header.lower() not in to_preserve_lower:
23
- del response.headers[header]
47
+ header_keys_to_check = response.headers.copy().keys()
48
+ for header in header_keys_to_check:
49
+ if header.lower() in to_preserve_lower:
50
+ continue
51
+ else:
52
+ response.headers.pop(header, None)
24
53
  deleted.add(header)
25
- logger.info("Deleted headers: %s", ", ".join(deleted))
54
+ logger.debug("Deleted headers: %s", ", ".join(deleted))
26
55
  return response
27
56
 
28
57
 
29
- class CacheOptions(TypedDict, total=False):
30
- """
31
- Options for configuring requests-cache.
32
-
33
- Attributes:
34
- cache_name (str, optional): The name of the cache. Defaults to 'nordigen'. Can also be a path.
35
- backend (str, optional): The cache backend to use (e.g., 'sqlite', 'memory'). Defaults to 'sqlite'.
36
- expire_after (int, optional): The cache expiration time in seconds. Defaults to 86400 (24 hours).
37
- old_data_on_error (bool, optional): Whether to return old cached data on a request error. Defaults to False.
38
- """
39
-
40
- cache_name: requests_cache.StrOrPath
41
- backend: Optional[requests_cache.BackendSpecifier]
42
- expire_after: requests_cache.ExpirationTime
43
- old_data_on_error: bool
44
-
45
-
46
- class HttpServiceException(Exception):
47
- """
48
- Exception raised for HTTP service errors. This wraps HTTP errors from the underlying requests library.
49
-
50
- Attributes:
51
- error (str): The original error message.
52
- response_text (str, optional): The full response text from the server.
53
- """
54
-
55
- def __init__(self, error, response_text=None):
56
- self.error = error
57
- self.response_text = response_text
58
- super().__init__(f"{error}: {response_text}")
59
-
60
-
61
- class BaseService:
58
+ class GoCardlessClient:
62
59
  """
63
- Base class for HTTP services handling authentication and requests to the GoCardless Bank Account Data API.
64
-
65
- Attributes:
66
- BASE_URL (str): The base URL for the API.
67
- DEFAULT_CACHE_OPTIONS (CacheOptions): Default caching options.
68
- secret_id (str): Your GoCardless API secret ID.
69
- secret_key (str): Your GoCardless API secret key.
70
- token (str, optional): The current API access token.
71
- session (requests_cache.CachedSession): The cached requests session.
60
+ Clean, typed GoCardless Bank Account Data API client with full caching support.
72
61
  """
73
62
 
74
63
  BASE_URL = "https://bankaccountdata.gocardless.com/api/v2"
75
64
 
76
- DEFAULT_CACHE_OPTIONS: CacheOptions = {
77
- "cache_name": "nordigen",
78
- "backend": "sqlite",
79
- "expire_after": 0,
80
- "old_data_on_error": True,
81
- "match_headers": False,
82
- "cache_control": False,
83
- "response_hook": cleanup_headers,
84
- }
85
-
86
65
  def __init__(
87
66
  self,
88
67
  secret_id: str,
89
68
  secret_key: str,
90
- cache_options: Optional[CacheOptions],
69
+ cache_options: Optional[Dict[str, Any]] = None,
91
70
  ):
92
- """
93
- Initializes the BaseService.
94
-
95
- Args:
96
- secret_id (str): Your GoCardless API secret ID.
97
- secret_key (str): Your GoCardless API secret key.
98
- cache_options (CacheOptions, optional): Custom cache options. Merges with and overrides DEFAULT_CACHE_OPTIONS.
99
- """
71
+ logger.info("Initializing GoCardlessClient")
100
72
  self.secret_id = secret_id
101
73
  self.secret_key = secret_key
102
- self.token = None
103
- merged_options = {**self.DEFAULT_CACHE_OPTIONS, **(cache_options or {})}
104
- self.session = requests_cache.CachedSession(**merged_options)
74
+ self._token: Optional[str] = None
75
+
76
+ # Default cache options that match the original client
77
+ default_cache_options = {
78
+ "cache_name": "gocardless",
79
+ "backend": "sqlite",
80
+ "expire_after": 0,
81
+ "old_data_on_error": True,
82
+ "match_headers": False,
83
+ "cache_control": False,
84
+ }
85
+
86
+ # Merge with provided options
87
+ cache_config = {**default_cache_options, **(cache_options or {})}
88
+ logger.debug("Cache config: %s", cache_config)
89
+
90
+ # Create cached session with header stripping
91
+ self.session = requests_cache.CachedSession(**cache_config)
92
+ self.session.hooks["response"].append(strip_headers_hook)
93
+
94
+ def check_cache_status(self, method: str, url: str, params=None, data=None) -> dict:
95
+ """
96
+ Check cache status for a given request.
97
+ This mimics the original client's cache checking functionality.
98
+ """
99
+ headers = {"Authorization": f"Bearer {self._token}"} if self._token else {}
100
+
101
+ req = requests.Request(method, url, params=params, data=data, headers=headers)
102
+ prepared_request: requests.PreparedRequest = self.session.prepare_request(req)
103
+ cache = self.session.cache
104
+ cache_key = cache.create_key(prepared_request)
105
+ key_exists = cache.contains(cache_key)
106
+ is_expired = None
107
+
108
+ if key_exists:
109
+ try:
110
+ cached_response = cache.get_response(cache_key)
111
+ if cached_response:
112
+ is_expired = cached_response.is_expired
113
+ else:
114
+ key_exists = False
115
+ is_expired = True
116
+ except Exception as e:
117
+ logger.error(
118
+ f"Error checking expiration for cache key {cache_key}: {e}"
119
+ )
120
+ is_expired = None
105
121
 
106
- def _ensure_token_valid(self):
122
+ return {
123
+ "key_exists": key_exists,
124
+ "is_expired": is_expired,
125
+ "cache_key": cache_key,
126
+ }
127
+
128
+ @property
129
+ def token(self) -> str:
107
130
  """
108
- Ensure a valid token exists. Gets a new token if one doesn't exist.
109
- Nordigen tokens don't currently have a refresh mechanism, so this just gets a new one if needed.
131
+ Get or refresh access token.
110
132
  """
111
- if not self.token:
133
+ if not self._token:
112
134
  self.get_token()
135
+ return self._token
113
136
 
114
137
  def get_token(self):
115
138
  """
116
- Fetch a new API access token using credentials. Sets the `self.token` attribute.
117
-
118
- Raises:
119
- HttpServiceException: If the API request fails.
139
+ Fetch a new API access token using credentials.
120
140
  """
141
+ logger.debug("Fetching new access token")
121
142
  response = requests.post(
122
143
  f"{self.BASE_URL}/token/new/",
123
144
  data={"secret_id": self.secret_id, "secret_key": self.secret_key},
124
145
  )
125
- self._handle_response(response)
126
- self.token = response.json()["access"]
127
-
128
- def _handle_response(self, response):
129
- """
130
- Check response status and handle errors.
131
-
132
- Args:
133
- response (requests.Response): The response object from the API request.
134
-
135
- Raises:
136
- HttpServiceException: If the API request returns an error status code.
137
- """
138
- try:
139
- response.raise_for_status()
140
- except requests.exceptions.HTTPError as e:
141
- raise HttpServiceException(str(e), response.text)
142
-
143
- def _request(self, method, endpoint, params=None, data=None):
144
- """
145
- Execute an HTTP request with token handling and automatic retries.
146
+ response.raise_for_status()
147
+ self._token = response.json()["access"]
148
+ logger.debug("Access token obtained")
146
149
 
147
- Args:
148
- method (str): The HTTP method (e.g., "GET", "POST", "DELETE").
149
- endpoint (str): The API endpoint (relative to BASE_URL).
150
- params (dict, optional): URL parameters for the request.
151
- data (dict, optional): Data to send in the request body (for POST requests).
152
-
153
- Returns:
154
- requests.Response: The response object from the API request.
155
-
156
- Raises:
157
- HttpServiceException: If the API request fails.
158
- """
150
+ def _request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
151
+ """Make authenticated request with caching"""
159
152
  url = f"{self.BASE_URL}{endpoint}"
160
- self._ensure_token_valid()
161
- headers = {"Authorization": f"Bearer {self.token}"}
153
+ headers = kwargs.pop("headers", {})
154
+ headers["Authorization"] = f"Bearer {self.token}"
162
155
 
163
- response = self.session.request(
164
- method, url, headers=headers, params=params, data=data
156
+ # Check cache status for logging
157
+ status = self.check_cache_status(
158
+ method, url, kwargs.get("params"), kwargs.get("data")
159
+ )
160
+ logger.debug(
161
+ f"{endpoint}: {'expired' if status.get('is_expired') else 'cache ok'}"
165
162
  )
166
- logger.info("Response headers", response.headers)
167
163
 
168
- # Retry once if token expired (401 Unauthorized)
164
+ response = self.session.request(method, url, headers=headers, **kwargs)
165
+ logger.debug("Response headers: %s", response.headers)
166
+
167
+ # Handle 401 by refreshing token
169
168
  if response.status_code == 401:
170
- self.get_token() # Get a new token
171
- headers = {"Authorization": f"Bearer {self.token}"} # Update headers
172
- response = self.session.request(
173
- method, url, headers=headers, params=params, data=data
174
- )
169
+ self.get_token()
170
+ headers["Authorization"] = f"Bearer {self.token}"
171
+ response = self.session.request(method, url, headers=headers, **kwargs)
175
172
 
176
- self._handle_response(response)
173
+ response.raise_for_status()
177
174
  return response
178
175
 
179
- def _get(self, endpoint, params=None):
180
- """
181
- Perform a GET request and return the JSON response.
182
-
183
- Args:
184
- endpoint (str): The API endpoint.
185
- params (dict, optional): URL parameters.
186
-
187
- Returns:
188
- dict: The JSON response from the API.
189
- """
190
- return self._request("GET", endpoint, params=params).json()
191
-
192
- def _post(self, endpoint, data=None):
193
- """
194
- Perform a POST request and return the JSON response.
195
-
196
- Args:
197
- endpoint (str): The API endpoint.
198
- data (dict, optional): Data to send in the request body.
199
-
200
- Returns:
201
- dict: The JSON response from the API.
202
- """
203
- return self._request("POST", endpoint, data=data).json()
204
-
205
- def _delete(self, endpoint):
206
- """
207
- Perform a DELETE request and return the JSON response.
208
-
209
- Args:
210
- endpoint (str): The API endpoint.
211
-
212
- Returns:
213
- dict: The JSON response from the API.
214
- """
215
- return self._request("DELETE", endpoint).json()
216
-
217
-
218
- class NordigenClient(BaseService):
219
- """
220
- Client for interacting with the Nordigen API (GoCardless Bank Account Data).
221
-
222
- This class provides methods for listing banks, creating and managing requisitions (links),
223
- listing accounts, deleting links, and retrieving transactions. It inherits from `BaseService`
224
- to handle authentication and HTTP requests.
225
- """
226
-
227
- def list_banks(self, country="GB"):
228
- """
229
- List available institutions (banks) for a given country.
230
-
231
- Args:
232
- country (str, optional): The two-letter country code (ISO 3166). Defaults to "GB".
233
-
234
- Returns:
235
- list: A list of dictionaries, each containing the 'name' and 'id' of a bank.
236
-
237
- Example:
238
- ```python
239
- client = NordigenClient(...)
240
- banks = client.list_banks(country="US")
241
- for bank in banks:
242
- print(f"{bank['name']} (ID: {bank['id']})")
243
- ```
244
- """
245
- return [
246
- {"name": bank["name"], "id": bank["id"]}
247
- for bank in self._get("/institutions/", params={"country": country})
248
- ]
249
-
250
- def find_requisition_id(self, reference):
251
- """
252
- Find a requisition ID by its reference string.
253
-
254
- Args:
255
- reference (str): The unique reference string associated with the requisition.
256
-
257
- Returns:
258
- str or None: The requisition ID if found, otherwise None.
259
- """
260
- requisitions = self._get("/requisitions/")["results"]
261
- return next(
262
- (req["id"] for req in requisitions if req["reference"] == reference), None
176
+ def get(self, endpoint: str, params: Optional[Dict] = None) -> Dict[str, Any]:
177
+ """Make GET request"""
178
+ response = self._request("GET", endpoint, params=params)
179
+ return response.json()
180
+
181
+ def post(self, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
182
+ """Make POST request"""
183
+ response = self._request("POST", endpoint, data=data)
184
+ return response.json()
185
+
186
+ def delete(self, endpoint: str) -> Dict[str, Any]:
187
+ """Make DELETE request"""
188
+ response = self._request("DELETE", endpoint)
189
+ return response.json()
190
+
191
+ # Account methods
192
+ def get_account(self, account_id: str) -> Account:
193
+ """Get account metadata"""
194
+ logger.debug("Getting account metadata for %s", account_id)
195
+ data = self.get(f"/accounts/{account_id}/")
196
+ return Account(**data)
197
+
198
+ def get_account_balances(self, account_id: str) -> AccountBalance:
199
+ """Get account balances"""
200
+ logger.debug("Getting account balances for %s", account_id)
201
+ data = self.get(f"/accounts/{account_id}/balances/")
202
+ return AccountBalance(**data)
203
+
204
+ def get_account_details(self, account_id: str) -> AccountDetail:
205
+ """Get account details"""
206
+ logger.debug("Getting account details for %s", account_id)
207
+ data = self.get(f"/accounts/{account_id}/details/")
208
+ return AccountDetail(**data)
209
+
210
+ def get_account_transactions(
211
+ self, account_id: str, days_back: int = 180
212
+ ) -> AccountTransactions:
213
+ """Get account transactions"""
214
+ date_from = (datetime.now() - timedelta(days=days_back)).strftime("%Y-%m-%d")
215
+ date_to = datetime.now().strftime("%Y-%m-%d")
216
+ logger.debug(
217
+ "Fetching transactions for account %s from %s to %s",
218
+ account_id,
219
+ date_from,
220
+ date_to,
263
221
  )
264
222
 
265
- def create_link(self, reference, bank_id, redirect_url="http://localhost"):
266
- """
267
- Create a new bank link requisition.
268
-
269
- Args:
270
- reference (str): A unique reference string for this link.
271
- bank_id (str): The ID of the institution (bank) to link to.
272
- redirect_url (str, optional): The URL to redirect the user to after authentication. Defaults to "http://localhost".
273
-
274
- Returns:
275
- dict: A dictionary with the status of the operation. If successful, includes the link URL.
276
- - status: "exists" if a requisition with the given `reference` already exists, "created" otherwise
277
- - message: A descriptive message
278
- - link: (If status is "created") The URL to start the linking process.
279
-
280
- Example:
281
- ```python
282
- client = NordigenClient(...)
283
- result = client.create_link(reference="my-unique-ref", bank_id="SANDBOXFINANCE_SFIN0000")
284
- if result["status"] == "created":
285
- print(f"Redirect user to: {result['link']}")
286
- else:
287
- print(result["message"])
288
- ```
289
- """
290
- if self.find_requisition_id(reference):
291
- return {"status": "exists", "message": f"Link {reference} exists"}
292
-
293
- response = self._post(
294
- "/requisitions/",
295
- data={
296
- "redirect": redirect_url,
297
- "institution_id": bank_id,
298
- "reference": reference,
299
- },
223
+ data = self.get(
224
+ f"/accounts/{account_id}/transactions/",
225
+ params={"date_from": date_from, "date_to": date_to},
300
226
  )
301
- return {
302
- "status": "created",
303
- "link": response["link"],
304
- "message": f"Complete linking at: {response['link']}",
227
+ booked_count = len(data.get("transactions", {}).get("booked", []))
228
+ pending_count = len(data.get("transactions", {}).get("pending", []))
229
+ logger.debug(
230
+ "Fetched %d booked and %d pending transactions for account %s",
231
+ booked_count,
232
+ pending_count,
233
+ account_id,
234
+ )
235
+ return AccountTransactions(**data)
236
+
237
+ # Institutions methods
238
+ def get_institutions(self, country: Optional[str] = None) -> List[Institution]:
239
+ """Get institutions for a country"""
240
+ logger.debug("Getting institutions for country %s", country)
241
+ params = {"country": country} if country else {}
242
+ data = self.get("/institutions/", params=params)
243
+ logger.debug("Fetched %d institutions", len(data))
244
+ return [Institution(**inst) for inst in data]
245
+
246
+ def get_institution(self, institution_id: str) -> Institution:
247
+ """Get specific institution"""
248
+ data = self.get(f"/institutions/{institution_id}/")
249
+ return Institution(**data)
250
+
251
+ # Requisitions methods
252
+ def create_requisition(
253
+ self, redirect: str, institution_id: str, reference: str, **kwargs
254
+ ) -> Requisition:
255
+ """Create a new requisition"""
256
+ request_data = {
257
+ "redirect": redirect,
258
+ "institution_id": institution_id,
259
+ "reference": reference,
305
260
  }
261
+ request_data.update(kwargs)
262
+ data = self.post("/requisitions/", data=request_data)
263
+ return Requisition(**data)
264
+
265
+ def get_requisitions(self) -> List[Requisition]:
266
+ """Get all requisitions"""
267
+ logger.debug("Getting all requisitions")
268
+ data = self.get("/requisitions/")
269
+ logger.debug("Fetched %d requisitions", len(data.get("results", [])))
270
+ return [Requisition(**req) for req in data.get("results", [])]
271
+
272
+ def get_requisition(self, requisition_id: str) -> Requisition:
273
+ """Get specific requisition"""
274
+ data = self.get(f"/requisitions/{requisition_id}/")
275
+ return Requisition(**data)
276
+
277
+ def delete_requisition(self, requisition_id: str) -> Dict[str, Any]:
278
+ """Delete a requisition"""
279
+ return self.delete(f"/requisitions/{requisition_id}/")
280
+
281
+ # Agreements methods
282
+ def create_agreement(
283
+ self,
284
+ institution_id: str,
285
+ max_historical_days: int,
286
+ access_valid_for_days: int,
287
+ access_scope: List[str],
288
+ **kwargs,
289
+ ) -> EndUserAgreement:
290
+ """Create end user agreement"""
291
+ request_data = {
292
+ "institution_id": institution_id,
293
+ "max_historical_days": max_historical_days,
294
+ "access_valid_for_days": access_valid_for_days,
295
+ "access_scope": access_scope,
296
+ }
297
+ request_data.update(kwargs)
298
+ data = self.post("/agreements/enduser/", data=request_data)
299
+ return EndUserAgreement(**data)
300
+
301
+ def get_agreements(self) -> List[EndUserAgreement]:
302
+ """Get all agreements"""
303
+ data = self.get("/agreements/enduser/")
304
+ return [EndUserAgreement(**ag) for ag in data.get("results", [])]
305
+
306
+ def get_agreement(self, agreement_id: str) -> EndUserAgreement:
307
+ """Get specific agreement"""
308
+ data = self.get(f"/agreements/enduser/{agreement_id}/")
309
+ return EndUserAgreement(**data)
310
+
311
+ def accept_agreement(
312
+ self, agreement_id: str, user_agent: str, ip: str
313
+ ) -> Dict[str, Any]:
314
+ """Accept an agreement"""
315
+ data = self.post(
316
+ f"/agreements/enduser/{agreement_id}/accept/",
317
+ data={"user_agent": user_agent, "ip": ip},
318
+ )
319
+ return data
320
+
321
+ def reconfirm_agreement(
322
+ self, agreement_id: str, user_agent: str, ip: str
323
+ ) -> ReconfirmationRetrieve:
324
+ """Reconfirm an agreement"""
325
+ data = self.post(
326
+ f"/agreements/enduser/{agreement_id}/reconfirm/",
327
+ data={"user_agent": user_agent, "ip": ip},
328
+ )
329
+ return ReconfirmationRetrieve(**data)
306
330
 
307
- def list_accounts(self):
308
- """
309
- List all connected accounts with details (ID, institution, reference, IBAN, currency, name).
331
+ # Token management endpoints (usually handled internally)
332
+ def get_access_token(self) -> SpectacularJWTObtain:
333
+ """Get a new access token (usually handled internally)"""
334
+ data = self.post(
335
+ "/token/new/",
336
+ data={"secret_id": self.secret_id, "secret_key": self.secret_key},
337
+ )
338
+ return SpectacularJWTObtain(**data)
339
+
340
+ def refresh_access_token(self, refresh_token: str) -> SpectacularJWTRefresh:
341
+ """Refresh access token"""
342
+ data = self.post("/token/refresh/", data={"refresh": refresh_token})
343
+ return SpectacularJWTRefresh(**data)
344
+
345
+ # Integration endpoints
346
+ def get_integrations(self) -> List[Integration]:
347
+ """Get all integrations"""
348
+ data = self.get("/integrations/")
349
+ return [Integration(**integration) for integration in data]
350
+
351
+ def get_integration(self, integration_id: str) -> IntegrationRetrieve:
352
+ """Get specific integration"""
353
+ data = self.get(f"/integrations/{integration_id}/")
354
+ return IntegrationRetrieve(**data)
355
+
356
+ # Paginated endpoints with full response models
357
+ def get_requisitions_paginated(
358
+ self, limit: Optional[int] = None, offset: Optional[int] = None
359
+ ) -> PaginatedRequisitionList:
360
+ """Get paginated requisitions"""
361
+ params = {}
362
+ if limit:
363
+ params["limit"] = limit
364
+ if offset:
365
+ params["offset"] = offset
366
+ data = self.get("/requisitions/", params=params)
367
+ return PaginatedRequisitionList(**data)
368
+
369
+ def get_agreements_paginated(
370
+ self, limit: Optional[int] = None, offset: Optional[int] = None
371
+ ) -> PaginatedEndUserAgreementList:
372
+ """Get paginated agreements"""
373
+ params = {}
374
+ if limit:
375
+ params["limit"] = limit
376
+ if offset:
377
+ params["offset"] = offset
378
+ data = self.get("/agreements/enduser/", params=params)
379
+ return PaginatedEndUserAgreementList(**data)
380
+
381
+ # Convenience methods for common workflows
382
+ def list_banks(self, country: Optional[str] = None) -> List[str]:
383
+ """Quick way to list bank names for a country"""
384
+ institutions = self.get_institutions(country)
385
+ return [inst.name for inst in institutions]
386
+
387
+ def find_requisition_by_reference(self, reference: str) -> Optional[Requisition]:
388
+ """Find a requisition by its reference"""
389
+ requisitions = self.get_requisitions()
390
+ return next((req for req in requisitions if req.reference == reference), None)
391
+
392
+ def create_bank_link(
393
+ self, reference: str, bank_id: str, redirect_url: str = "http://localhost"
394
+ ) -> Optional[str]:
395
+ """Create bank link and return the URL"""
396
+ existing = self.find_requisition_by_reference(reference)
397
+ if existing:
398
+ return None
399
+
400
+ requisition = self.create_requisition(
401
+ redirect=redirect_url, institution_id=bank_id, reference=reference
402
+ )
403
+ return requisition.link
310
404
 
311
- Returns:
312
- list: A list of dictionaries, each representing a connected account.
313
- """
405
+ def get_all_accounts(self) -> List[AccountInfo]:
406
+ """Get all accounts from all requisitions"""
314
407
  accounts = []
315
- for req in self._get("/requisitions/")["results"]:
316
- for account_id in req["accounts"]:
317
- account = self._get(f"/accounts/{account_id}")
318
- details = self._get(f"/accounts/{account_id}/details")["account"]
319
-
320
- accounts.append(
321
- {
322
- "id": account_id,
323
- "institution_id": req.get("institution_id", ""),
324
- "reference": req["reference"],
325
- "iban": account.get("iban", ""),
326
- "currency": details.get("currency", ""),
327
- "name": details.get("name", "Unknown"),
328
- }
329
- )
408
+ for req in self.get_requisitions():
409
+ for account_id in req.accounts:
410
+ try:
411
+ account = self.get_account(account_id)
412
+ account_dict = account.model_dump()
413
+ account_dict.update(
414
+ {
415
+ "requisition_id": req.id,
416
+ "requisition_reference": req.reference,
417
+ "institution_id": req.institution_id,
418
+ }
419
+ )
420
+ accounts.append(account_dict)
421
+ except Exception:
422
+ # Skip accounts that can't be accessed
423
+ continue
330
424
  return accounts
331
-
332
- def delete_link(self, reference):
333
- """
334
- Delete a bank link (requisition) by its reference.
335
-
336
- Args:
337
- reference (str): The unique reference string of the link to delete.
338
-
339
- Returns:
340
- dict: A dictionary with the status and a message. Status can be "deleted" or "not_found".
341
- """
342
- req_id = self.find_requisition_id(reference)
343
- if not req_id:
344
- return {"status": "not_found", "message": f"Link {reference} not found"}
345
-
346
- self._delete(f"/requisitions/{req_id}")
347
- return {"status": "deleted", "message": f"Link {reference} removed"}
348
-
349
- def get_transactions(self, account_id, days_back=180):
350
- """
351
- Retrieve transactions for a given account.
352
-
353
- Args:
354
- account_id (str): The ID of the account.
355
- days_back (int, optional): The number of days back to retrieve transactions for. Defaults to 180.
356
-
357
- Returns:
358
- dict: The 'transactions' part of the API response, or an empty dict if no transactions are found.
359
- See the Nordigen API documentation for the structure of this data.
360
- """
361
- date_from = (datetime.now() - timedelta(days=days_back)).strftime("%Y-%m-%d")
362
- return self._get(
363
- f"/accounts/{account_id}/transactions/",
364
- params={
365
- "date_from": date_from,
366
- "date_to": datetime.now().strftime("%Y-%m-%d"),
367
- },
368
- ).get("transactions", [])