commit 3944821d4bcfc842ca007acb70957811e1f0baff Author: Mark Sheppard Date: Mon Jul 10 23:19:40 2023 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bf0ea0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +keys/** +tokens/** diff --git a/starling b/starling new file mode 100755 index 0000000..9173c34 --- /dev/null +++ b/starling @@ -0,0 +1,145 @@ +#!/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}"') + +#-----------------------------------------------------------------------------#