Added "account add" action
This commit is contained in:
128
starling
128
starling
@@ -11,12 +11,13 @@ import sys
|
|||||||
import types
|
import types
|
||||||
|
|
||||||
base_url = 'https://api.starlingbank.com/api/v2'
|
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:
|
class Question:
|
||||||
|
|
||||||
def __init__(self, name, description, type, values):
|
def __init__(self, name, description, type, values=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.type = type
|
self.type = type
|
||||||
@@ -38,8 +39,10 @@ class Question:
|
|||||||
return f'exactly {self.values} digits'
|
return f'exactly {self.values} digits'
|
||||||
if self.type == 'enum':
|
if self.type == 'enum':
|
||||||
return ', '.join(self.values)
|
return ', '.join(self.values)
|
||||||
|
if self.type == 'uid':
|
||||||
|
return 'UID'
|
||||||
else:
|
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):
|
def valid_answer(self):
|
||||||
if self.type == 'string':
|
if self.type == 'string':
|
||||||
@@ -50,8 +53,10 @@ class Question:
|
|||||||
return size == self.values and re.match('^\d+$', self.answer)
|
return size == self.values and re.match('^\d+$', self.answer)
|
||||||
if self.type == 'enum':
|
if self.type == 'enum':
|
||||||
return self.answer in self.values
|
return self.answer in self.values
|
||||||
|
if self.type == 'uid':
|
||||||
|
return uid_re.match(self.answer) != None
|
||||||
else:
|
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:
|
class Form:
|
||||||
|
|
||||||
@@ -90,6 +95,14 @@ class StarlingClient:
|
|||||||
Question('account_description', 'Account description', 'string', (1, 255,)),
|
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):
|
def __init__(self):
|
||||||
self.tokens = self.read_tokens('main', 'payments')
|
self.tokens = self.read_tokens('main', 'payments')
|
||||||
|
|
||||||
@@ -106,8 +119,7 @@ class StarlingClient:
|
|||||||
'Authorization': f'Bearer {self.tokens[token]}',
|
'Authorization': f'Bearer {self.tokens[token]}',
|
||||||
}
|
}
|
||||||
response = requests.get(url, headers=headers)
|
response = requests.get(url, headers=headers)
|
||||||
response.raise_for_status()
|
return self.parse_response(response)
|
||||||
return json.loads(response.text, object_hook=lambda obj: types.SimpleNamespace(**obj))
|
|
||||||
|
|
||||||
def put(self, path, data, token='main'):
|
def put(self, path, data, token='main'):
|
||||||
url = f'{base_url}/{path}'
|
url = f'{base_url}/{path}'
|
||||||
@@ -115,7 +127,15 @@ class StarlingClient:
|
|||||||
'Authorization': f'Bearer {self.tokens[token]}',
|
'Authorization': f'Bearer {self.tokens[token]}',
|
||||||
}
|
}
|
||||||
response = requests.put(url, json=data, headers=headers)
|
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))
|
return json.loads(response.text, object_hook=lambda obj: types.SimpleNamespace(**obj))
|
||||||
|
|
||||||
### Low-level API wrappers ###
|
### Low-level API wrappers ###
|
||||||
@@ -137,6 +157,9 @@ class StarlingClient:
|
|||||||
return self.get('payees')
|
return self.get('payees')
|
||||||
return self.put('payees', data)
|
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 ###
|
### Mid-level methods to munge the data from the low-level calls ###
|
||||||
|
|
||||||
def formatted_balance(self, accountUid):
|
def formatted_balance(self, accountUid):
|
||||||
@@ -194,7 +217,7 @@ class StarlingClient:
|
|||||||
else:
|
else:
|
||||||
print(f'There are {count} payees for this account holder:')
|
print(f'There are {count} payees for this account holder:')
|
||||||
for payee in payees:
|
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)
|
count = len(payee.accounts)
|
||||||
if count == 0:
|
if count == 0:
|
||||||
print('no accounts.')
|
print('no accounts.')
|
||||||
@@ -206,9 +229,12 @@ class StarlingClient:
|
|||||||
for account in payee.accounts:
|
for account in payee.accounts:
|
||||||
sort_code = f'{account.bankIdentifier[0:2]}-{account.bankIdentifier[2:4]}-{account.bankIdentifier[4:6]}'
|
sort_code = f'{account.bankIdentifier[0:2]}-{account.bankIdentifier[2:4]}-{account.bankIdentifier[4:6]}'
|
||||||
account_number = account.accountIdentifier
|
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()
|
details = self.new_payee_form.complete()
|
||||||
data = {
|
data = {
|
||||||
'payeeName': details['payee_name'],
|
'payeeName': details['payee_name'],
|
||||||
@@ -231,6 +257,33 @@ class StarlingClient:
|
|||||||
print(f'Failed to create payee: {response}')
|
print(f'Failed to create payee: {response}')
|
||||||
sys.exit(1)
|
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(
|
parser = argparse.ArgumentParser(
|
||||||
@@ -238,15 +291,13 @@ parser = argparse.ArgumentParser(
|
|||||||
description='Interacts with Starling Bank\'s API to manage bank accounts.',
|
description='Interacts with Starling Bank\'s API to manage bank accounts.',
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog='''recognised actions and args:
|
epilog='''recognised actions and args:
|
||||||
<none> - show customer and account details
|
<none> - show account holder and details
|
||||||
payees - list the customer's payees
|
payees - list the customer's payees
|
||||||
add_payee - adds a new payee
|
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='*')
|
parser.add_argument('action', help='which action to perform', nargs='*')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -256,17 +307,52 @@ action_args = args.action
|
|||||||
if len(action_args) == 0:
|
if len(action_args) == 0:
|
||||||
client.default_action()
|
client.default_action()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
action = action_args.pop(0)
|
action = action_args.pop(0)
|
||||||
count = len(action_args)
|
count = len(action_args)
|
||||||
if action == 'payees':
|
if action == 'payees':
|
||||||
if count > 0:
|
if count > 0:
|
||||||
parser.error(f'Too many arguments for "{action}" action')
|
parser.error(f'Too many arguments for "{action}" action')
|
||||||
client.list_payees()
|
client.list_payees()
|
||||||
elif action == 'add_payee':
|
elif action == 'payee':
|
||||||
if count > 0:
|
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')
|
parser.error(f'Too many arguments for "{action}" action')
|
||||||
client.add_payee()
|
client.pay(*action_args)
|
||||||
else:
|
else:
|
||||||
parser.error(f'Unknown action "{action}"')
|
parser.error(f'Unknown action "{action}"')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user