diff --git a/starling b/starling index 65c865b..59aeefe 100755 --- a/starling +++ b/starling @@ -11,12 +11,13 @@ import sys import types base_url = 'https://api.starlingbank.com/api/v2' +uid_re = re.compile(r'^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[\da-f]{4}-[\da-f]{12}$') #-----------------------------------------------------------------------------# class Question: - def __init__(self, name, description, type, values): + def __init__(self, name, description, type, values=None): self.name = name self.description = description self.type = type @@ -38,8 +39,10 @@ class Question: return f'exactly {self.values} digits' if self.type == 'enum': return ', '.join(self.values) + if self.type == 'uid': + return 'UID' else: - raise Exception(f'Unknown value type {self.type} for "{self.description}"') + raise Exception(f'Unknown value type "{self.type}" for "{self.description}"') def valid_answer(self): if self.type == 'string': @@ -50,8 +53,10 @@ class Question: return size == self.values and re.match('^\d+$', self.answer) if self.type == 'enum': return self.answer in self.values + if self.type == 'uid': + return uid_re.match(self.answer) != None else: - raise Exception(f'Unknown value type {self.type} for "{self.description}"') + raise Exception(f'Unknown value type "{self.type}" for "{self.description}"') class Form: @@ -90,6 +95,14 @@ class StarlingClient: Question('account_description', 'Account description', 'string', (1, 255,)), ) + new_account_form = Form( + 'new account', + Question('payee_uid', 'Payee UID', 'uid'), + Question('sort_code', 'Sort code', 'digits', 6), + Question('account_number', 'Account number', 'digits', 8), + Question('account_description', 'Account description', 'string', (1, 255,)), + ) + def __init__(self): self.tokens = self.read_tokens('main', 'payments') @@ -106,8 +119,7 @@ class StarlingClient: 'Authorization': f'Bearer {self.tokens[token]}', } response = requests.get(url, headers=headers) - response.raise_for_status() - return json.loads(response.text, object_hook=lambda obj: types.SimpleNamespace(**obj)) + return self.parse_response(response) def put(self, path, data, token='main'): url = f'{base_url}/{path}' @@ -115,7 +127,15 @@ class StarlingClient: 'Authorization': f'Bearer {self.tokens[token]}', } response = requests.put(url, json=data, headers=headers) - response.raise_for_status() + return self.parse_response(response) + + def parse_response(self, response): + if response.status_code != requests.codes.ok: + print(f'ERROR: got HTTP status code {response.status_code}') + print(f'Request: {response.request}') + print(f'Response:') + print(response.text) + sys.exit(1) return json.loads(response.text, object_hook=lambda obj: types.SimpleNamespace(**obj)) ### Low-level API wrappers ### @@ -137,6 +157,9 @@ class StarlingClient: return self.get('payees') return self.put('payees', data) + def payees_account(self, payee_uid, data): + return self.put(f'payees/{payee_uid}/account', data) + ### Mid-level methods to munge the data from the low-level calls ### def formatted_balance(self, accountUid): @@ -194,7 +217,7 @@ class StarlingClient: else: print(f'There are {count} payees for this account holder:') for payee in payees: - sys.stdout.write(f' {payee.payeeName} ({payee.payeeType}) - ') + sys.stdout.write(f' {payee.payeeUid}: {payee.payeeName} ({payee.payeeType}) - ') count = len(payee.accounts) if count == 0: print('no accounts.') @@ -206,9 +229,12 @@ class StarlingClient: for account in payee.accounts: sort_code = f'{account.bankIdentifier[0:2]}-{account.bankIdentifier[2:4]}-{account.bankIdentifier[4:6]}' account_number = account.accountIdentifier - print(f' {account.payeeAccountUid}: {sort_code} {account_number} {account.description}') + details = f'{account.payeeAccountUid}: {sort_code} {account_number} {account.description}' + if account.defaultAccount: + details += ' (default)' + print(f' {details}') - def add_payee(self): + def payee_add(self): details = self.new_payee_form.complete() data = { 'payeeName': details['payee_name'], @@ -231,6 +257,33 @@ class StarlingClient: print(f'Failed to create payee: {response}') sys.exit(1) + def account_add(self): + details = self.new_account_form.complete() + data = { + 'description': details['account_description'], + 'defaultAccount': False, + 'countryCode': 'GB', + 'accountIdentifier': details['account_number'], + 'bankIdentifier': details['sort_code'], + 'bankIdentifierType': 'SORT_CODE', + } + response = self.payees_account(details['payee_uid'], data) + if response.success: + print(f'Successfully added account with UID {response.payeeAccountUid}') + else: + print(f'Failed to create account: {response}') + sys.exit(1) + + def pay(self, payee_uid, amount, ref): + primary = None + for account in self.accounts().accounts: + if account.accountType == 'PRIMARY': + primary = account + break + else: + sys.exit('ERROR: No PRIMARY account found') + print(primary) + #-----------------------------------------------------------------------------# parser = argparse.ArgumentParser( @@ -238,15 +291,13 @@ parser = argparse.ArgumentParser( description='Interacts with Starling Bank\'s API to manage bank accounts.', formatter_class=argparse.RawDescriptionHelpFormatter, epilog='''recognised actions and args: - - show customer and account details - payees - list the customer's payees - add_payee - adds a new payee + - show account holder and details + payees - list the customer's payees + payee add - adds a new payee + payee del - deletes an existing payee + account add - adds a new account to a payee + pay - pay an existing payee ''') - -parser.add_argument('-t', '--test', action='store_true', default=False, - help='use test servers rather than live servers') -parser.add_argument('-v', '--verbose', action='store_true', default=False, - help='print XML requests and responses') parser.add_argument('action', help='which action to perform', nargs='*') args = parser.parse_args() @@ -256,17 +307,52 @@ action_args = args.action if len(action_args) == 0: client.default_action() sys.exit(0) - action = action_args.pop(0) count = len(action_args) if action == 'payees': if count > 0: parser.error(f'Too many arguments for "{action}" action') client.list_payees() -elif action == 'add_payee': - if count > 0: +elif action == 'payee': + if count < 1: + parser.error(f'Too few arguments for "{action}" action') + subaction = action_args.pop(0) + if subaction == 'add': + if count > 1: + parser.error(f'Too many arguments for "{action} {subaction}" action') + client.payee_add() + elif subaction == 'del': + if count < 2: + parser.error(f'Too few arguments for "{action} {subaction}" action') + if count > 2: + parser.error(f'Too many arguments for "{action} {subaction}" action') + payee_uid = action_args.pop(0) + client.payee_del(payee_uid) + else: + parser.error(f'Unknown "{action} {subaction}" action') +elif action == 'account': + if count < 1: + parser.error(f'Too few arguments for "{action}" action') + subaction = action_args.pop(0) + if subaction == 'add': + if count > 1: + parser.error(f'Too many arguments for "{action} {subaction}" action') + client.account_add() + elif subaction == 'del': + if count < 2: + parser.error(f'Too few arguments for "{action} {subaction}" action') + if count > 2: + parser.error(f'Too many arguments for "{action} {subaction}" action') + account_uid = action_args.pop(0) + client.account_del(account_uid) + else: + parser.error(f'Unknown "{action} {subaction}" action') +elif action == 'pay': + if count < 3: + parser.error(f'Too few arguments for "{action}" action') + if count > 3: parser.error(f'Too many arguments for "{action}" action') - client.add_payee() + client.pay(*action_args) else: parser.error(f'Unknown action "{action}"')