From b3a39401b58cae87124ae8a6bfee55024bb5386c Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Fri, 25 Mar 2022 18:04:17 +0000 Subject: [PATCH] Implement serde for JSON deserialisation --- handler/sinks/nordigen.py | 96 +++++++++++++++++++++------------------ handler/ux/commands.py | 23 +++++++++- 2 files changed, 73 insertions(+), 46 deletions(-) diff --git a/handler/sinks/nordigen.py b/handler/sinks/nordigen.py index 4c2ac94..1ebe03b 100644 --- a/handler/sinks/nordigen.py +++ b/handler/sinks/nordigen.py @@ -5,6 +5,8 @@ from twisted.internet.task import LoopingCall import requests from simplejson.errors import JSONDecodeError from json import dumps, loads +from lib.serde.nordigen import TXRoot, AccessToken, Institutions, Agreement, Requisitions, AccountDetails +from serde import ValidationError # Project imports from settings import settings @@ -57,15 +59,13 @@ class Nordigen(util.Base): path = f"{settings.Nordigen.Base}/token/new/" r = requests.post(path, headers=headers, data=dumps(data)) try: - parsed = r.json() - except JSONDecodeError: - self.log.error(f"Error parsing access token response: {r.content}") - return False - if "access" in parsed: - self.token = parsed["access"] - self.log.info("Refreshed access token") - else: - self.log.error(f"Access token not in response: {parsed}") + obj = AccessToken.from_json(r.content) + except ValidationError as err: + self.log.error(f"Validation error: {err}") + return + parsed = obj.to_dict() + self.token = parsed["access"] + self.log.info("Refreshed access token") if not self.authed: self.__authed__() self.authed = True @@ -84,10 +84,17 @@ class Nordigen(util.Base): path = f"{settings.Nordigen.Base}/institutions/?country={country}" r = requests.get(path, headers=headers) try: - parsed = r.json() + parsed_pre = r.json() except JSONDecodeError: self.log.error(f"Error parsing institutions response: {r.content}") return False + parsed = {"institutions": parsed_pre} + try: + obj = Institutions.from_dict(parsed) + except ValidationError as err: + self.log.error(f"Validation error: {err}") + return + parsed = obj.to_dict()["institutions"] new_list = [] if filter_name: for i in parsed: @@ -96,21 +103,6 @@ class Nordigen(util.Base): return new_list return parsed - def create_agreement(self, institution_id): - """Create an agreement to access an institution. - :param institution_id: ID of the institution - """ - headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"} - path = f"{settings.Nordigen.Base}/agreements/enduser" - data = {"institution_id": institution_id} - r = requests.post(path, headers=headers, data=dumps(data)) - try: - parsed = r.json() - except JSONDecodeError: - self.log.error(f"Error parsing agreement response: {r.content}") - return False - return parsed - def build_link(self, institution_id): """Create a link to access an institution. :param institution_id: ID of the institution @@ -120,10 +112,11 @@ class Nordigen(util.Base): data = {"institution_id": institution_id, "redirect": settings.Nordigen.CallbackURL} r = requests.post(path, headers=headers, data=data) try: - parsed = r.json() - except JSONDecodeError: - self.log.error(f"Error parsing link response: {r.content}") - return False + obj = Agreement.from_json(r.content) + except ValidationError as err: + self.log.error(f"Validation error: {err}") + return + parsed = obj.to_dict() if "link" in parsed: return parsed["link"] return False @@ -150,14 +143,16 @@ class Nordigen(util.Base): path = f"{settings.Nordigen.Base}/requisitions" r = requests.get(path, headers=headers) try: - parsed = r.json() - except JSONDecodeError: - self.log.error(f"Error parsing requisitions response: {r.content}") - return False + obj = Requisitions.from_json(r.content) + except ValidationError as err: + self.log.error(f"Validation error: {err}") + return + parsed = obj.to_dict() if "results" in parsed: return parsed["results"] else: self.log.error(f"Results not in requisitions response: {parsed}") + return False def get_accounts(self, requisition): @@ -168,10 +163,11 @@ class Nordigen(util.Base): path = f"{settings.Nordigen.Base}/requisitions/{requisition}/" r = requests.get(path, headers=headers) try: - parsed = r.json() - except JSONDecodeError: - self.log.error(f"Error parsing accounts response: {r.content}") - return False + obj = Agreement.from_json(r.content) + except ValidationError as err: + self.log.error(f"Validation error: {err}") + return + parsed = obj.to_dict() if "accounts" in parsed: return parsed["accounts"] return False @@ -184,14 +180,11 @@ class Nordigen(util.Base): path = f"{settings.Nordigen.Base}/accounts/{account_id}/details/" r = requests.get(path, headers=headers) try: - parsed = r.json() - except JSONDecodeError: - self.log.error(f"Error parsing account response: {r.content}") - return False - # if "accounts" in parsed: - # return parsed["accounts"] - # return False - parsed = parsed["account"] + obj = AccountDetails.from_json(r.content) + except ValidationError as err: + self.log.error(f"Validation error: {err}") + return + parsed = obj.to_dict()["account"] if "bban" in parsed and parsed["currency"] == "GBP": sort_code = parsed["bban"][0:6] account_number = parsed["bban"][6:] @@ -252,3 +245,16 @@ class Nordigen(util.Base): else: to_return[req["institution_id"]] = [account_info] return to_return + + def get_transactions(self, account_id): + """ + Get all transactions for an account. + :param account_id: account to fetch transactions for + :return: list of transactions + :rtype: dict + """ + headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"} + path = f"{settings.Nordigen.Base}/accounts/{account_id}/transactions/" + r = requests.get(path, headers=headers) + obj = TXRoot.from_json(r.content) + return obj.to_dict()["transactions"]["booked"] diff --git a/handler/ux/commands.py b/handler/ux/commands.py index f7936ef..1750f71 100644 --- a/handler/ux/commands.py +++ b/handler/ux/commands.py @@ -541,7 +541,7 @@ class IRCCommands(object): class transactions(object): name = "transactions" authed = True - helptext = "Get a list of transactions. Usage: transactions " + helptext = "Get a list of transactions from TrueLayer. Usage: transactions " @staticmethod def run(cmd, spl, length, authed, msg, agora, tx, ux): @@ -559,6 +559,27 @@ class IRCCommands(object): description = transaction["description"] msg(f"{timestamp} {txid} {ptxid} {txtype} {amount}{currency} {description}") + class ntransactions(object): + name = "ntransactions" + authed = True + helptext = "Get a list of transactions from Nordigen. Usage: ntransactions " + + @staticmethod + def run(cmd, spl, length, authed, msg, agora, tx, ux): + if length == 2: + account_id = spl[1] + transactions = tx.sinks.nordigen.get_transactions(account_id) + for transaction in transactions: + msg(dumps(transaction)) + # txid = transaction["transaction_id"] + # ptxid = transaction["meta"]["provider_transaction_id"] + # txtype = transaction["transaction_type"] + # timestamp = transaction["timestamp"] + # amount = transaction["amount"] + # currency = transaction["currency"] + # description = transaction["description"] + # msg(f"{timestamp} {txid} {ptxid} {txtype} {amount}{currency} {description}") + class mapaccount(object): name = "mapaccount" authed = True