#!/usr/bin/env -S python3 -ttuI # -*- python -*- #-----------------------------------------------------------------------------# import argparse import io import json import requests import sys import types base_url = 'https://api.starlingbank.com/api/v2' #-----------------------------------------------------------------------------# class StarlingClient: def __init__(self): self.tokens = self.read_tokens('main', 'payments') def read_tokens(self, *names): tokens = {} for name in names: with io.open(f'tokens/{name}.txt') as fh: tokens[name] = fh.read().rstrip() return tokens def get(self, path, token='main'): url = f'{base_url}/{path}' headers = { '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)) ### Low-level API wrappers ### def account_holder(self): return self.get('account-holder') def account_holder_individual(self): return self.get('account-holder/individual') def accounts(self): return self.get('accounts') def balance(self, accountUid): return self.get(f'accounts/{accountUid}/balance') def payees(self): return self.get('payees') ### Mid-level methods to munge the data from the low-level calls ### def formatted_balance(self, accountUid): balance = self.balance(accountUid).effectiveBalance if balance.currency == 'GBP': symbol = '£' elif balance.currency == 'EUR': symbol = '€' else: sys.exit(f'ERROR: Unsupported currency {balance.currency}') major_units = int(balance.minorUnits / 100) minor_units = balance.minorUnits % 100 return f'{symbol}{major_units}.{minor_units:02d}' ### High-level methods which correspond to actions ### def default_action(self): holder = self.account_holder() holder_type = holder.accountHolderType if holder_type == 'INDIVIDUAL': details = self.account_holder_individual() else: sys.exit(f'ERROR: Unsupported account holder type {holder_type}') print('Customer details:') print(f' Name: {details.firstName} {details.lastName}') print(f' Email: <{details.email}>') print(f' Phone: {details.phone}') accounts = self.accounts().accounts count = len(accounts) if count == 0: print('This customer has no bank accounts.') else: if count == 1: print('This customer has one bank account:') else: print(f'This customer has {count} bank accounts:') for account in accounts: balance = self.formatted_balance(account.accountUid) print(f' {account.name} ({account.accountType}): {balance}') def list_payees(self): payees = self.payees().payees count = len(payees) if count == 0: print('There are no payees for this customer.') else: if count == 1: print('There is one payee for this customer:') else: print(f'There are {count} payees for this customer:') for payee in payees: print(f' {payee.payeeName} ({payee.payeeType})') #-----------------------------------------------------------------------------# parser = argparse.ArgumentParser( usage='%(prog)s [options] [action [arg ...]]', 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 contacts [] - get/set contacts for domain expire - allow domain to auto-expire expiry - list domains with expiry info To show the current lock state use "%(prog)s show status". ''') 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() client = StarlingClient() action_args = args.action if len(action_args) == 0: client.default_action() else: 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() else: parser.error(f'Unknown action "{action}"') #-----------------------------------------------------------------------------#