Added "account add" action

This commit is contained in:
2023-09-08 22:12:10 +00:00
parent b51df093dc
commit 4736a0eea6

128
starling
View File

@@ -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:
<none> - show customer and account details
payees - list the customer's payees
add_payee - adds a new payee
<none> - show account holder and details
payees - list the customer's payees
payee add - adds a new payee
payee del <payee_uid> - deletes an existing payee
account add <payee_uid> - adds a new account to a payee
pay <account_uid> <amount> <ref> - 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}"')