├── .gitignore ├── .travis.yml ├── README.md ├── mondo └── __init__.py ├── setup.py └── tests ├── __init__.py └── test_client.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.ipynb 2 | *.json 3 | *.pyc 4 | *.egg-info 5 | .eggs 6 | build 7 | dist 8 | env 9 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | script: 6 | - python setup.py nosetests -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mondo simple python SDK 2 | 3 | A simple python SDK for dealing with the Mondo API 4 | It deals with tokens and token refreshing behind the scenes 5 | 6 | See the docs https://getmondo.co.uk/docs for details. 7 | 8 | 1. Download the repository 9 | 2. Use the SDK for fun, profit, and global domination 10 | 11 | ## How do I load the SDK 12 | 13 | All of the methods in the class can accept different account details as parameters, 14 | should you open up your app / service to other people, but starting with your own account and 15 | defaults makes it easy to play with. 16 | 17 | You can also specify the default account details to use in the instantiation of MondoClient. 18 | ``` 19 | account = MondoClient('johnny@apple.com', 'p4ssw0rd', 'oauthclient_xxxx', 'client_secret') 20 | ``` 21 | If that goes well and you don't get any errors... 22 | 23 | ## What can you do? 24 | 25 | * account.get_transactions() 26 | * account.get_transaction() 27 | * account.authenticate() 28 | * account.get_accounts() 29 | * account.get_primary_accountID() 30 | * account.create_feed_item() 31 | * account.register_webhook() 32 | * account.deliver_token() 33 | * account.token_refresh() 34 | 35 | 36 | deliver_token() returns a token for handrolling requests and refreshes it when necessary 37 | 38 | The methods above all return JSON rather than request objects (the previous version) 39 | 40 | Look at the code and https://getmondo.co.uk/docs for information on what parameters to use 41 | to refer to other accounts etc. 42 | 43 | ## What about tokens and refreshing them? 44 | It's all taken care of. 45 | If a token expires the code will use the refresh call to get a new one 46 | 47 | ## Example starter code 48 | ``` 49 | from mondo import MondoClient 50 | account = MondoClient('johnny@apple.com', 'p4ssw0rd', 'oauthclient_xxxx', 'client_secret') 51 | 52 | print(account.get_accounts()) 53 | first_account_id = account.get_accounts()[0]['id'] 54 | 55 | trx = account.get_transactions(account.get_primary_accountID(), limit=10) 56 | print(trx) 57 | 58 | singleID = trx[1]['id'] 59 | single = account.get_transaction(singleID) 60 | print(single) 61 | 62 | 63 | account.register_webhook(url='http://mydomain.com/transactions') 64 | 65 | webhooks = account.list_webhooks() 66 | first_webhook = webhooks[0]['id'] 67 | 68 | account.delete_webhook(first_webhook) 69 | ``` 70 | 71 | Evolved from original code by Tito Miguel Costa ( https://bitbucket.org/titomiguelcosta/mondo ) 72 | 73 | -------------------------------------------------------------------------------- /mondo/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import datetime 3 | 4 | 5 | API_ERRORS = { 6 | 400: "400: Bad Req. Your request has missing parameters or is malformed", 7 | 401: "401: Unauthorized. Your request is not authenticated.", 8 | 403: "403: Forbidden. Your request is authenticated \ 9 | but has insufficient permissions.", 10 | 405: "405: Method Not Allowed. You are using the incorrect HTTP verb. \ 11 | Double check whether it should be POST/GET/DELETE/etc.", 12 | 404: "404: Page Not Found. The endpoint requested does not exist.", 13 | 406: "406: Not Acceptable. Your application does not accept the content \ 14 | format returned according to the Accept headers sent in the request.", 15 | 429: "429: Too Many Requests. Your application is exceeding its rate limit. \ 16 | Back off, buddy.", 17 | 500: "500: Internal Server Error. Something is wrong on our end. Whoopsie", 18 | 504: "504 - Gateway Timeout Something has timed out on our end. Whoopsie" 19 | } 20 | 21 | 22 | class MondoClient(object): 23 | def __init__(self, username, password, client, secret, 24 | url="https://api.getmondo.co.uk"): 25 | """Create a client connection and get an access token.""" 26 | self.username = username 27 | self.password = password 28 | self.client = client 29 | self.secret = secret 30 | self.url = url 31 | 32 | # grab an access token as it validates the credentials 33 | 34 | try: 35 | token_response = self.get_token() 36 | self.token = token_response['access_token'] 37 | self.refresh_token = token_response['refresh_token'] 38 | 39 | # set the token expiry time based on time + token expiry in seconds 40 | # This is used in deliver_token() later 41 | now = datetime.datetime.now() 42 | delta = datetime.timedelta(seconds=token_response['expires_in']) 43 | self.token_expires = now + delta 44 | 45 | except TypeError: 46 | raise Exception(token_response) 47 | 48 | def get_token(self, client_id=None, client_secret=None, 49 | username=None, password=None): 50 | """Acquire an access token.""" 51 | if client_id is None: 52 | client_id = self.client 53 | if client_secret is None: 54 | client_secret = self.secret 55 | if username is None: 56 | username = self.username 57 | if password is None: 58 | password = self.password 59 | 60 | payload = {'grant_type': 'password', 61 | 'client_id': client_id, 62 | 'client_secret': client_secret, 63 | 'username': username, 64 | 'password': password} 65 | r = requests.post(self.url + '/oauth2/token', payload) 66 | 67 | if r.status_code == 200: 68 | response = r.json() 69 | self.token = response['access_token'] 70 | self.refresh_token = response['refresh_token'] 71 | return response 72 | else: 73 | return API_ERRORS[r.status_code] 74 | 75 | def token_refresh(self, client_id=None, client_secret=None, 76 | refresh_token=None): 77 | """Refresh a previously acquired token.""" 78 | if client_id is None: 79 | client_id = self.client 80 | if client_secret is None: 81 | client_secret = self.secret 82 | if refresh_token is None: 83 | refresh_token = self.refresh_token 84 | 85 | payload = {'grant_type': 'refresh_token', 86 | 'client_id': client_id, 87 | 'client_secret': client_secret, 88 | 'refresh_token': refresh_token} 89 | r = requests.post(self.url + '/oauth2/token', payload) 90 | 91 | if r.status_code == 200: 92 | response = r.json() 93 | self.token = response['access_token'] 94 | self.refresh_token = response['refresh_token'] 95 | return response 96 | else: 97 | return API_ERRORS[r.status_code] 98 | 99 | def get_transaction(self, transaction_id, access_token=None, merchant=True): 100 | """Get details about a transaction.""" 101 | if access_token is None: 102 | access_token = self.deliver_token() 103 | 104 | headers = {'Authorization': 'Bearer ' + access_token} 105 | params = {} 106 | 107 | if merchant: 108 | params['expand[]'] = 'merchant' 109 | 110 | r = requests.get(self.url + '/transactions/' + transaction_id, 111 | params=params, headers=headers) 112 | 113 | if r.status_code == 200: 114 | return r.json()['transaction'] 115 | else: 116 | return API_ERRORS[r.status_code] 117 | 118 | def get_transactions(self, account_id=None, limit=100, since=None, 119 | before=None, access_token=None, merchant=False): 120 | """List transactions. Defaults to the primary account.""" 121 | if account_id is None: 122 | account_id = self.get_primary_accountID() 123 | 124 | if access_token is None: 125 | access_token = self.deliver_token() 126 | 127 | headers = {'Authorization': 'Bearer ' + access_token} 128 | params = {'limit': limit, "account_id": account_id} 129 | 130 | if merchant: 131 | params['expand[]'] = 'merchant' 132 | 133 | if since is not None: 134 | params['since'] = since 135 | if before is not None: 136 | params['before'] = before 137 | 138 | r = requests.get(self.url + '/transactions', 139 | params=params, headers=headers) 140 | 141 | if r.status_code == 200: 142 | return r.json()['transactions'] 143 | else: 144 | return API_ERRORS[r.status_code] 145 | 146 | def iter_transactions(self, account_id, limit=100, since=None, 147 | before=None, access_token=None, merchant=False): 148 | """Iterate through all transactions matching the pagination criteria. 149 | 150 | Args: 151 | account_id: The ID of the account whose transactions we want. 152 | limit: The number of transactions per page request. If the page of 153 | results we get back is full, we try asking for more. 154 | since: A timestamp or object ID denoting the earliest transaction. 155 | Timestamp limits are inclusive; object IDs are exclusive. 156 | before: A timestamp all transactions must have been created before. 157 | access_token: An access token override. 158 | merchant: Whether to expand the merchant information. 159 | 160 | Yields: 161 | Individual transaction dicts, as per 162 | https://getmondo.co.uk/docs/#transactions. 163 | 164 | Raises: 165 | RuntimeError: when get_transactions returns a string. 166 | """ 167 | while True: 168 | trans = self.get_transactions(account_id=account_id, limit=limit, 169 | since=since, before=before, 170 | access_token=access_token, 171 | merchant=merchant) 172 | 173 | # TODO: Raise an exception in get_transactions and allow it to 174 | # bubble up, so that we don't have to check return types. Use that 175 | # opportunity to create specific exceptions. 176 | if isinstance(trans, str): 177 | raise RuntimeError(trans) 178 | 179 | for t in trans: 180 | yield t 181 | 182 | if len(trans) < limit: 183 | break 184 | 185 | # Move our cursor forward so that next page of results begins 186 | # after the last transaction we received here. 187 | since = t['id'] 188 | 189 | def authenticate(self, access_token=None, client_id=None, user_id=None): 190 | """Authenticate user.""" 191 | if access_token is None: 192 | access_token = self.deliver_token() 193 | if client_id is None: 194 | client_id = self.client 195 | if user_id is None: 196 | user_id = self.username 197 | 198 | headers = {'Authorization': 'Bearer ' + str(access_token)} 199 | r = requests.get(self.url + '/ping/whoami', headers=headers) 200 | 201 | if r.status_code == 200: 202 | return r.json() 203 | else: 204 | return API_ERRORS[r.status_code] 205 | 206 | def get_accounts(self, access_token=None): 207 | """Detailed information about customer's accounts.""" 208 | if access_token is None: 209 | access_token = self.deliver_token() 210 | 211 | headers = {'Authorization': 'Bearer ' + access_token} 212 | 213 | r = requests.get(self.url + '/accounts', headers=headers) 214 | 215 | if r.status_code == 200: 216 | return r.json()['accounts'] 217 | else: 218 | return API_ERRORS[r.status_code] 219 | 220 | def get_primary_accountID(self, access_token=None): 221 | """Get ID from the first account listed against an access token.""" 222 | if access_token is None: 223 | access_token = self.deliver_token() 224 | 225 | headers = {'Authorization': 'Bearer ' + access_token} 226 | 227 | r = requests.get(self.url + '/accounts', headers=headers) 228 | 229 | if r.status_code == 200: 230 | return r.json()['accounts'][0]['id'] 231 | else: 232 | return API_ERRORS[r.status_code] 233 | 234 | def create_feed_item(self, title, image_url, background_color='#FCF1EE', 235 | body_color='#FCF1EE', title_color='#333', 236 | body='', account_id=None, access_token=None): 237 | """Publish a new feed entry.""" 238 | if access_token is None: 239 | access_token = self.deliver_token() 240 | if account_id is None: 241 | account_id = self.get_primary_accountID() 242 | 243 | headers = {'Authorization': 'Bearer ' + access_token} 244 | 245 | payload = { 246 | "account_id": account_id, 247 | "type": "basic", 248 | "params[title]": title, 249 | "params[image_url]": image_url, 250 | "params[background_color]": background_color, 251 | "params[body_color]": body_color, 252 | "params[title_color]": title_color, 253 | "params[body]": body 254 | } 255 | 256 | r = requests.post(self.url + '/feed', data=payload, headers=headers) 257 | 258 | if r.status_code == 200: 259 | return r.json() 260 | else: 261 | return API_ERRORS[r.status_code] 262 | 263 | def register_webhook(self, url, account_id=None, access_token=None): 264 | """Register a webhook 265 | instance.register_webhook(account_id, url, [access_token]) 266 | """ 267 | if access_token is None: 268 | access_token = self.deliver_token() 269 | if account_id is None: 270 | account_id = self.get_primary_accountID() 271 | 272 | headers = {'Authorization': 'Bearer ' + access_token} 273 | payload = {"account_id": account_id, "url": url} 274 | 275 | r = requests.post(self.url + '/webhooks', data=payload, headers=headers) 276 | 277 | if r.status_code == 200: 278 | return r.json() 279 | else: 280 | return API_ERRORS[r.status_code] 281 | 282 | def list_webhooks(self, account_id=None, access_token=None): 283 | """List webhooks registered against an account 284 | instance.list_webhooks([account_id], [access_token]) 285 | """ 286 | if account_id is None: 287 | account_id = self.get_primary_accountID() 288 | if access_token is None: 289 | access_token = self.deliver_token() 290 | 291 | headers = {'Authorization': 'Bearer ' + access_token} 292 | params = {'account_id': account_id} 293 | 294 | r = requests.get(self.url + '/webhooks', params=params, headers=headers) 295 | 296 | if r.status_code == 200: 297 | return r.json()['webhooks'] 298 | else: 299 | return API_ERRORS[r.status_code] 300 | 301 | def delete_webhook(self, webhook_id, access_token=None): 302 | """Delete a webhook 303 | instance.delete_webhook(webhook_id, [access_token]) 304 | """ 305 | if access_token is None: 306 | access_token = self.deliver_token() 307 | 308 | headers = {'Authorization': 'Bearer ' + access_token} 309 | 310 | r = requests.delete(self.url + '/webhooks/' + webhook_id, headers=headers) 311 | 312 | if r.status_code == 200: 313 | return r.json() 314 | else: 315 | return API_ERRORS[r.status_code] 316 | 317 | def get_balance(self, account_id=None, access_token=None): 318 | """Detailed information about customer's accounts.""" 319 | if access_token is None: 320 | access_token = self.deliver_token() 321 | 322 | if account_id is None: 323 | account_id = self.get_primary_accountID() 324 | 325 | headers = {'Authorization': 'Bearer ' + access_token} 326 | params = {"account_id": account_id} 327 | 328 | r = requests.get(self.url + '/balance', headers=headers, params=params) 329 | 330 | if r.status_code == 200: 331 | return r.json() 332 | else: 333 | return API_ERRORS[r.status_code] 334 | 335 | def deliver_token(self): 336 | if datetime.datetime.now() > self.token_expires: 337 | self.token_refresh() 338 | return self.token 339 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | VERSION = '0.0.1' 5 | 6 | 7 | tests_requires = [ 8 | 'nose>=1.3.4', 9 | 'responses>=0.5.1' 10 | ] 11 | 12 | install_requires = [ 13 | 'requests>=2.4.3', 14 | ] 15 | 16 | setup( 17 | name="mondo", 18 | version=VERSION, 19 | description="Mondo Banking API Client", 20 | author=', '.join(( 21 | 'Tito Miguel Costa', 22 | 'Simon Vans-Colina ', 23 | )), 24 | url="https://github.com/simonvc/mondo-python", 25 | packages=["mondo"], 26 | tests_require=tests_requires, 27 | install_requires=install_requires, 28 | license="MIT", 29 | ) 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonvc/mondo-python/7342e054a8e7137b9a8dbff4e477d28e089db177/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | from collections import namedtuple 4 | from contextlib import contextmanager 5 | 6 | import responses 7 | from nose.tools import raises 8 | 9 | from mondo import MondoClient 10 | 11 | Response = namedtuple('Response', ['method', 'url', 'body', 'status', 'content_type']) 12 | 13 | 14 | ACCOUNT_ID = 'account_id' 15 | 16 | 17 | MONDO_RESPONSES = { 18 | 'failed_oauth2': Response( 19 | responses.POST, 20 | 'https://api.getmondo.co.uk/oauth2/token', 21 | json.dumps({ 22 | 'error': 'not found', 23 | }), 24 | 404, 25 | 'application/json' 26 | ), 27 | 'success_oauth2': Response( 28 | responses.POST, 29 | 'https://api.getmondo.co.uk/oauth2/token', 30 | json.dumps({ 31 | 'access_token': 'access_token_here', 32 | 'refresh_token': 'refresh_token_here', 33 | 'expires_in': 10 34 | }), 35 | 200, 36 | 'application/json' 37 | ) 38 | } 39 | 40 | 41 | @contextmanager 42 | def load_response(name): 43 | responses.add(**MONDO_RESPONSES.get(name)._asdict()) 44 | yield 45 | 46 | 47 | @responses.activate 48 | @raises(Exception) 49 | def test_invalid_oauth_raises_exception(): 50 | with load_response('failed_oauth2'): 51 | MondoClient('', '', '', '') 52 | 53 | 54 | @responses.activate 55 | def test_valid_oauth_has_token_with_a_expires_time(): 56 | with load_response('success_oauth2'): 57 | client = MondoClient('', '', '', '') 58 | assert client.token == 'access_token_here' 59 | assert client.refresh_token == 'refresh_token_here' 60 | assert isinstance(client.token_expires, datetime.datetime) 61 | --------------------------------------------------------------------------------