├── .circleci └── config.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── __init__.py ├── doubles └── monzo.py ├── examples └── remove_all_transaction_attachments.py ├── make.bat ├── monzo ├── __init__.py ├── auth.py ├── const.py ├── errors.py ├── monzo.py └── utils.py ├── requirements.txt ├── settings.py ├── setup.py └── tests ├── test_api_endpoints.py └── test_api_errors.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/python:3.8.0a3 6 | 7 | working_directory: ~/monzo-python 8 | 9 | steps: 10 | - checkout 11 | - restore_cache: 12 | keys: 13 | - v1-dependencies-{{ checksum "requirements.txt" }} 14 | - v1-dependencies- 15 | - run: 16 | name: install dependencies 17 | command: | 18 | python3 -m venv venv 19 | . venv/bin/activate 20 | pip install -r requirements.txt 21 | - save_cache: 22 | paths: 23 | - ./venv 24 | key: v1-dependencies-{{ checksum "requirements.txt" }} 25 | - run: 26 | name: lint 27 | command: | 28 | . venv/bin/activate 29 | black --py36 --check monzo/ tests 30 | - run: 31 | name: run tests 32 | command: | 33 | . venv/bin/activate 34 | python -m pytest tests/ 35 | 36 | - store_artifacts: 37 | path: test-reports 38 | destination: test-reports 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | # docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # Testing 93 | .pytest_cache/ 94 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Muyiwa Olu-Ogunleye 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This project is no longer under active development 2 | 3 | It's been a while since I've written Python as my primary language, not to mention life and work doesn't allow me to spend enough time on this project. Please feel free to fork and continue the work. As for the `monzo` namespace for `PyPy`, I'll be keeping a hold of it for security reasons. 4 | 5 | [![CircleCI](https://circleci.com/gh/muyiwaolu/monzo-python/tree/dev.svg?style=svg)](https://circleci.com/gh/muyiwaolu/monzo-python/tree/dev) 6 | # monzo-python 7 | 8 | ## Requirements 9 | 10 | * `Python >= 3.5` 11 | 12 | ## Quickstart 13 | Make sure you have Python and pip on your machine 14 | 15 | To install the package run 16 | 17 | `pip install monzo` 18 | 19 | If the above doesn’t work, you may want to run the above command as an admin by prefixing `sudo`. 20 | 21 | ### Example 22 | Open up a Python terminal (or create a Python file) and enter the following: 23 | 24 | ```python 25 | from monzo import Monzo # Import Monzo class 26 | 27 | client = Monzo('access_token_goes_here') # Replace access token with a valid token found at: https://developers.monzo.com/ 28 | account_id = client.get_first_account()['id'] # Get the ID of the first account linked to the access token 29 | balance = client.get_balance(account_id) # Get your balance object 30 | print(balance['balance']) # 100000000000 31 | print(balance['currency']) # GBP 32 | print(balance['spend_today']) # 2000 33 | ``` 34 | 35 | Yup. That easy. To see what more you can do with the `client` variable, take a look at the [tests](https://github.com/muyiwaolu/monzo-python/blob/master/tests/test_api_endpoints.py). 36 | 37 | ### OAuth 38 | 39 | The library also supports OAuth. Read the [wiki entry](https://github.com/muyiwaolu/monzo-python/wiki/OAuth) for more information. 40 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /doubles/monzo.py: -------------------------------------------------------------------------------- 1 | """A replacement for monzo.monzo.Monzo for testing puropses 2 | """ 3 | 4 | class Monzo(object): 5 | """The stubbed out class representation of the Monzo client 6 | """ 7 | 8 | def __init__(self, access_token): 9 | pass 10 | 11 | def whoami(self): 12 | """Gives information about an access token. 13 | (https://monzo.com/docs/#authenticating-requests) 14 | 15 | :rtype: A stubbed dictionary representation of the authentication status. 16 | """ 17 | return { 18 | "authenticated": True, 19 | "client_id": "client_id", 20 | "user_id": "user_id" 21 | } 22 | 23 | def get_accounts(self): 24 | """Get all accounts that belong to a user. (https://monzo.com/docs/#list-accounts) 25 | 26 | :rtype: A stubbed collection of accounts for a user. 27 | 28 | """ 29 | return { 30 | "accounts": [ 31 | { 32 | "id": "acc_00009237aqC8c5umZmrRdh", 33 | "description": "Peter Pan's Account", 34 | "created": "2015-11-13T12:17:42Z" 35 | } 36 | ] 37 | } 38 | 39 | def get_first_account(self): 40 | """Gets the first account for a user. 41 | 42 | :rtype: A stubbed dictionary representation of the authentication status. 43 | 44 | """ 45 | accounts = self.get_accounts() 46 | return accounts['accounts'][0] 47 | 48 | def get_transactions(self, account_id): 49 | """Get all transactions of a given account. (https://monzo.com/docs/#list-transactions) 50 | 51 | :param account_id: The unique identifier for the account which the transactions belong to. 52 | :rtype: A stubbed collection of transaction objects for specific user. 53 | """ 54 | return { 55 | "transactions": [ 56 | { 57 | "account_balance": 13013, 58 | "amount": -510, 59 | "created": "2015-08-22T12:20:18Z", 60 | "currency": "GBP", 61 | "description": "THE DE BEAUVOIR DELI C LONDON GBR", 62 | "id": "tx_00008zIcpb1TB4yeIFXMzx", 63 | "merchant": "merch_00008zIcpbAKe8shBxXUtl", 64 | "metadata": {}, 65 | "notes": "Salmon sandwich 🍞", 66 | "is_load": False, 67 | "settled": "2015-08-23T12:20:18Z", 68 | "category": "eating_out" 69 | }, 70 | { 71 | "account_balance": 12334, 72 | "amount": -679, 73 | "created": "2015-08-23T16:15:03Z", 74 | "currency": "GBP", 75 | "description": "VUE BSL LTD ISLINGTON GBR", 76 | "id": "tx_00008zL2INM3xZ41THuRF3", 77 | "merchant": "merch_00008z6uFVhVBcaZzSQwCX", 78 | "metadata": {}, 79 | "notes": "", 80 | "is_load": False, 81 | "settled": "2015-08-24T16:15:03Z", 82 | "category": "eating_out" 83 | }, 84 | ] 85 | } 86 | 87 | def get_transaction(self, transaction_id): 88 | """Retrieve data for a specific transaction. (https://docs.monzo.com/#retrieve-transaction) 89 | :param transaction_id: The unique identifier for the transaction for which data should be retrieved for. 90 | :rtype: A dictionary containing the data for the specified transaction_id. 91 | """ 92 | return {"transaction": { 93 | "account_balance": 13013, 94 | "amount": -510, 95 | "created": "2015-08-22T12:20:18Z", 96 | "currency": "GBP", 97 | "description": "THE DE BEAUVOIR DELI C LONDON GBR", 98 | "id": "tx_00008zIcpb1TB4yeIFXMzx", 99 | "merchant": { 100 | "address": { 101 | "address": "98 Southgate Road", 102 | "city": "London", 103 | "country": "GB", 104 | "latitude": 51.54151, 105 | "longitude": -0.08482400000002599, 106 | "postcode": "N1 3JD", 107 | "region": "Greater London" 108 | }, 109 | "created": "2015-08-22T12:20:18Z", 110 | "group_id": "grp_00008zIcpbBOaAr7TTP3sv", 111 | "id": "merch_00008zIcpbAKe8shBxXUtl", 112 | "logo": "https://pbs.twimg.com/profile_images/527043602623389696/68_SgUWJ.jpeg", 113 | "emoji": "🍞", 114 | "name": "The De Beauvoir Deli Co.", 115 | "category": "eating_out" 116 | }, 117 | "metadata": {}, 118 | "notes": "Salmon sandwich 🍞", 119 | "is_load": False, 120 | "settled": "2015-08-23T12:20:18Z" 121 | } 122 | } 123 | 124 | def get_balance(self, account_id): 125 | """Gets the balance of a given account. (https://monzo.com/docs/#read-balance) 126 | 127 | :param account_id: The unique identifier for the account which the balance belong to. 128 | :rtype: A stubbed dictionary representation of the current account balance. 129 | 130 | """ 131 | return { 132 | "balance": 5000, 133 | "currency": "GBP", 134 | "spend_today": 0 135 | } 136 | 137 | def get_webhooks(self, account_id): 138 | """Gets the webhooks of a given account. (https://monzo.com/docs/#list-webhooks) 139 | 140 | :param account_id: The unique identifier for the account which the webhooks belong to. 141 | :rtype: A stubbed collection of webhooks that belong to an account. 142 | 143 | """ 144 | return { 145 | "webhooks": [ 146 | { 147 | "account_id": "acc_000091yf79yMwNaZHhHGzp", 148 | "id": "webhook_000091yhhOmrXQaVZ1Irsv", 149 | "url": "http://example.com/callback" 150 | }, 151 | { 152 | "account_id": "acc_000091yf79yMwNaZHhHGzp", 153 | "id": "webhook_000091yhhzvJSxLYGAceC9", 154 | "url": "http://example2.com/anothercallback" 155 | } 156 | ] 157 | } 158 | 159 | def get_first_webhook(self, account_id): 160 | """Gets the first webhook of a given account. 161 | 162 | :param account_id: The unique identifier for the account which the first webhook belong to. 163 | :rtype: A stubbed dictionary representation of the first webhook belonging to an account, if it exists. 164 | """ 165 | webhooks = self.get_webhooks(account_id) 166 | return webhooks['webhooks'][0] 167 | 168 | def delete_webhook(self, webhook_id): 169 | """Deletes the a specified webhook. (https://monzo.com/docs/#deleting-a-webhook) 170 | 171 | :param webhook_id: The unique identifier for the webhook to delete. 172 | :rtype: An empty dictionary 173 | """ 174 | return {} 175 | 176 | def delete_all_webhooks(self, account_id): 177 | """Removes all webhooks associated with the specified account, if it exists. 178 | 179 | :param account_id: The unique identifier for the account which all webhooks will be removed from. 180 | :rtype: None 181 | """ 182 | webhooks = self.get_webhooks(account_id) 183 | for webhook in webhooks['webhooks']: 184 | self.delete_webhook(webhook['id']) 185 | 186 | def register_webhook(self, webhook_url, account_id): 187 | """Registers a webhook. (https://monzo.com/docs/#registering-a-webhook) 188 | 189 | :param webhook_url: The webhook url to register. 190 | :param account_id: The unique identifier for the account to register the webhook. 191 | 192 | :rtype: A stubbed dictionary representing a webhook 193 | """ 194 | return { 195 | "webhook": { 196 | "account_id": "account_id", 197 | "id": "webhook_id", 198 | "url": "http://example.com" 199 | } 200 | } 201 | 202 | def register_attachment(self, transaction_id, file_url, file_type): 203 | """Attaches an image to a transaction. (https://monzo.com/docs/#register-attachment) 204 | 205 | :param transaction_id: The unique identifier for the transaction to register the attachment to. 206 | :param file_url: The url of the file to attach. 207 | :param transaction_id: The type of the file specified in file_url 208 | 209 | :rtype: A stubbed dictionary representation of the attachment that was just registered. 210 | """ 211 | return { 212 | "attachment": { 213 | "id": "attach_00009238aOAIvVqfb9LrZh", 214 | "user_id": "user_00009238aMBIIrS5Rdncq9", 215 | "external_id": "tx_00008zIcpb1TB4yeIFXMzx", 216 | "file_url": "https://s3-eu-west-1.amazonaws.com/mondo-image-uploads/user_00009237hliZellUicKuG1/LcCu4ogv1xW28OCcvOTL-foo.png", 217 | "file_type": "image/png", 218 | "created": "2015-11-12T18:37:02Z" 219 | } 220 | } 221 | 222 | def deregister_attachment(self, attachment_id): 223 | """Removed a previously attached image from a transaction. (https://monzo.com/docs/#deregister-attachment) 224 | 225 | :param transaction_id: The unique identifier for the attachment to deregister. 226 | :rtype: An empty Dictionary, if the deregistration was successful. 227 | """ 228 | return {} 229 | 230 | 231 | def create_feed_item(self, account_id, feed_type, url, params): 232 | """Creates a feed item. (https://monzo.com/docs/#create-feed-item) 233 | 234 | :param account_id: The unique identifier for the account to create the feed item for. 235 | :param feed_type: The type of feed item (currently only `basic` is supported). 236 | :param url: The url to open if a feed item is tapped 237 | :param params: A map of parameters which vary based on type 238 | 239 | :rtype: An empty Dictionary, if the feed item creation was successful. 240 | """ 241 | return {} 242 | 243 | def get_pots(self): 244 | """Get all pots for a user. (https://monzo.com/docs/#list-pots) 245 | 246 | :rtype: A collection of pots for a user. 247 | 248 | """ 249 | return { 250 | "pots": [ 251 | { 252 | "balance": 100, 253 | "deleted": False, 254 | "currency": "GBP", 255 | "id": "pot_1234567890123456789012", 256 | "created": "2017-12-25T21:13:45.045Z", 257 | "updated": "2018-02-11T18:38:56.624Z", 258 | "style": "purple_gradient", 259 | "name": "My Pot" 260 | } 261 | ] 262 | } 263 | 264 | def deposit_into_pot(self, pot_id, account_id, amount_in_pennies): 265 | """Move money from an account into a pot. (https://monzo.com/docs/#deposit-into-a-pot) 266 | 267 | :param pot_id: The unique identifier for the pot to deposit the money to. 268 | :param account_id: The unique identifier for the account to move the money from. 269 | :param amount_in_pennies: The amount of money to move to the pot in pennies. 270 | 271 | :rtype: A dictionary containing information on the pot that was updated. 272 | """ 273 | return { 274 | "balance": 200, 275 | "created": "2018-04-03T21:55:11.037Z", 276 | "currency": "GBP", 277 | "deleted": False, 278 | "id": "pot_1234567890123456789012", 279 | "maximum_balance": -1, 280 | "minimum_balance": -1, 281 | "name": "My awesome pot", 282 | "round_up": False, 283 | "style": "raspberry", 284 | "type": "default", 285 | "updated": "2018-04-04T16:55:11.037Z" 286 | } 287 | 288 | def withdraw_from_pot(self, account_id, pot_id, amount_in_pennies): 289 | """Move money from an account into a pot. (https://monzo.com/docs/#withdraw-from-a-pot) 290 | 291 | :param account_id: The unique identifier for the account to move the money to. 292 | :param pot_id: The unique identifier for the pot to withdraw the money from. 293 | :param amount_in_pennies: The amount of money to move to the pot in pennies. 294 | 295 | :rtype: A dictionary containing information on the pot that was updated. 296 | """ 297 | return { 298 | "balance": 300, 299 | "created": "2018-04-03T21:55:11.037Z", 300 | "currency": "GBP", 301 | "deleted": False, 302 | "id": "pot_1234567890123456789012", 303 | "maximum_balance": -1, 304 | "minimum_balance": -1, 305 | "name": "My awesome pot", 306 | "round_up": False, 307 | "style": "raspberry", 308 | "type": "default", 309 | "updated": "2018-04-04T16:55:11.037Z" 310 | } 311 | 312 | def update_transaction_metadata(self, transaction_id, key, value): 313 | """Update a metadata key value pair for a given transaction. (https://monzo.com/docs/#annotate-transaction) 314 | :param transaction_id: The unique identifier for the transaction for which notes should be updated. 315 | :param key: The key for the element of metadata to be updated. 316 | :param value: The value to be associated with the given key. 317 | :rtype: The updated transaction object. 318 | """ 319 | return { 320 | "transactions": [ 321 | { 322 | "account_balance": 13013, 323 | "amount": -510, 324 | "created": "2015-08-22T12:20:18Z", 325 | "currency": "GBP", 326 | "description": "THE DE BEAUVOIR DELI C LONDON GBR", 327 | "id": "tx_00008zIcpb1TB4yeIFXMzx", 328 | "merchant": "merch_00008zIcpbAKe8shBxXUtl", 329 | "metadata": {key:value}, 330 | "notes": "Salmon sandwich 🍞", 331 | "is_load": False, 332 | "settled": "2015-08-23T12:20:18Z", 333 | "category": "eating_out" 334 | } 335 | ] 336 | } -------------------------------------------------------------------------------- /examples/remove_all_transaction_attachments.py: -------------------------------------------------------------------------------- 1 | from monzo.monzo import Monzo 2 | from settings import get_environment_var 3 | 4 | client = Monzo(get_environment_var('ACCESS_TOKEN')) 5 | account_id = client.get_first_account()['id'] 6 | transactions = client.get_transactions(account_id)['transactions'] 7 | for transaction in transactions: 8 | if transaction['attachments']: 9 | for attachment in transaction['attachments']: 10 | client.deregister_attachment(attachment['id']) 11 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\MonzoPython.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\MonzoPython.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /monzo/__init__.py: -------------------------------------------------------------------------------- 1 | from monzo.monzo import Monzo 2 | from monzo.auth import MonzoOAuth2Client 3 | -------------------------------------------------------------------------------- /monzo/auth.py: -------------------------------------------------------------------------------- 1 | """OAuth2 session client to handle authentication of calls to the Monzo API 2 | 3 | Adapted from the implementation for a Fitbit API Python client by Orcas, Inc. 4 | and used under the Apache2 license: https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | Original code may be found at: https://github.com/orcasgit/python-fitbit 7 | 8 | """ 9 | 10 | from requests.auth import HTTPBasicAuth 11 | from requests_oauthlib import OAuth2Session 12 | from oauthlib.oauth2 import TokenExpiredError 13 | 14 | from monzo.utils import save_token_to_file, load_token_from_file 15 | from monzo.errors import ( 16 | BadRequestError, 17 | UnauthorizedError, 18 | ForbiddenError, 19 | MethodNotAllowedError, 20 | PageNotFoundError, 21 | NotAcceptibleError, 22 | TooManyRequestsError, 23 | InternalServerError, 24 | GatewayTimeoutError, 25 | ) 26 | from monzo.const import ( 27 | CLIENT_ID, 28 | CLIENT_SECRET, 29 | ACCESS_TOKEN, 30 | REFRESH_TOKEN, 31 | EXPIRES_AT, 32 | MONZO_CACHE_FILE, 33 | ) 34 | 35 | 36 | class MonzoOAuth2Client(object): 37 | AUTHORIZE_ENDPOINT = "https://auth.monzo.com" 38 | API_ENDPOINT = "https://api.monzo.com" 39 | API_VERSION = 1 40 | 41 | _authorization_url = AUTHORIZE_ENDPOINT 42 | _request_token_url = "{0}/oauth2/token".format(API_ENDPOINT) 43 | _access_token_url = _request_token_url 44 | _refresh_token_url = _request_token_url 45 | 46 | _localhost = "http://localhost" 47 | 48 | def __init__( 49 | self, 50 | client_id, 51 | client_secret, 52 | access_token=None, 53 | refresh_token=None, 54 | expires_at=None, 55 | refresh_callback=save_token_to_file, 56 | redirect_uri=_localhost, 57 | *args, 58 | **kwargs, 59 | ): 60 | """ 61 | Create a MonzoOAuth2Client object. 62 | Specify the first 7 parameters if you have them to access user data. 63 | Specify just the first 2 parameters to start the setup for user authorization 64 | (These are generated at https://developers.monzo.com/) 65 | 66 | :param client_id: Client id string as given by Monzo Developer website 67 | :param client_secret: Client secret string as given by Monzo Developer website 68 | :param access_token: String token needed to access Monzo API 69 | :param refresh_token: String token used to refresh expired access token. 70 | :param expires_at: Unix time representation of access token expiry 71 | :param refresh_callback: Callback function for when access token is refreshed 72 | :param redirect_uri: URL to which user is redirected to after authentication by Monzo 73 | """ 74 | 75 | self.client_id, self.client_secret = client_id, client_secret 76 | token = {} 77 | if access_token: 78 | token[ACCESS_TOKEN] = access_token 79 | if refresh_token: 80 | token[REFRESH_TOKEN] = refresh_token 81 | if expires_at: 82 | token[EXPIRES_AT] = expires_at 83 | 84 | self.session = OAuth2Session( 85 | client_id, 86 | token_updater=refresh_callback, 87 | token=token, 88 | redirect_uri=redirect_uri, 89 | ) 90 | self.timeout = kwargs.get("timeout", None) 91 | 92 | @classmethod 93 | def from_json(cls, filename=MONZO_CACHE_FILE, refresh_callback=save_token_to_file): 94 | """Loads a MonzoOAuth2Client object from a json representation of the 95 | client credentials and token from a file. 96 | 97 | :param filename: Path to file from which to load information 98 | :param refresh_callback: Callback function for when access token is refreshed 99 | :rtype: MonzoOAuth2Client object as defined in loaded file 100 | """ 101 | token = load_token_from_file(filename) 102 | client_id = token.get(CLIENT_ID) 103 | client_secret = token.get(CLIENT_SECRET) 104 | access_token = token.get(ACCESS_TOKEN) 105 | refresh_token = token.get(REFRESH_TOKEN) 106 | expires_at = token.get(EXPIRES_AT) 107 | 108 | # Check if file contains information for a valid OAuth session 109 | if access_token or all(client_id, client_secret): 110 | return MonzoOAuth2Client( 111 | client_id, 112 | client_secret, 113 | access_token=access_token, 114 | refresh_token=refresh_token, 115 | expires_at=expires_at, 116 | refresh_cb=refresh_callback, 117 | ) 118 | 119 | def make_request(self, url, data=None, method=None, **kwargs): 120 | """ 121 | Builds and makes the OAuth2 Request, catches errors 122 | https://docs.monzo.com/#errors 123 | """ 124 | if self.timeout is not None and "timeout" not in kwargs: 125 | kwargs["timeout"] = self.timeout 126 | 127 | data = data or {} 128 | method = method or ("POST" if data else "GET") 129 | 130 | try: 131 | response = self.validate_response( 132 | self.session.request(method, url, data=data, **kwargs) 133 | ) 134 | 135 | except (UnauthorizedError, TokenExpiredError) as e: 136 | self.refresh_token() 137 | response = self.make_request(url, data=data, method=method, **kwargs) 138 | 139 | return response 140 | 141 | def authorize_token_url(self, redirect_uri=None, **kwargs): 142 | """Step 1: Return the URL the user needs to go to in order to grant us 143 | authorization to look at their data. Then redirect the user to that 144 | URL, open their browser to it, or tell them to copy the URL into their 145 | browser. 146 | 147 | :param redirect_uri: url to which the response will posted. 148 | :rtype: A Tuple consisting of the authentication url and the state token. 149 | """ 150 | if redirect_uri: 151 | self.session.redirect_uri = redirect_uri 152 | 153 | return self.session.authorization_url( 154 | MonzoOAuth2Client._authorization_url, **kwargs 155 | ) 156 | 157 | def fetch_access_token(self, code, redirect_uri=None): 158 | """Step 2: Given the code from Monzo from step 1, call 159 | Monzo again and returns an access token object. Extract the needed 160 | information from that and save it to use in future API calls. 161 | the token is internally saved 162 | 163 | :rtype: A Dictionary representation of the authentication status. 164 | """ 165 | if redirect_uri: 166 | self.session.redirect_uri = redirect_uri 167 | 168 | token = self.session.fetch_token( 169 | MonzoOAuth2Client._access_token_url, 170 | username=self.client_id, 171 | password=self.client_secret, 172 | code=code, 173 | ) 174 | 175 | if self.session.token_updater: 176 | self.session.token_updater(token) 177 | 178 | return token 179 | 180 | def refresh_token(self): 181 | """Step 3: obtains a new access_token from the the refresh token 182 | obtained in step 2. Only do the refresh if there is `token_updater(),` 183 | which saves the token. 184 | 185 | :rtype: A Dictionary representation of the authentication token. 186 | """ 187 | token = self.session.refresh_token( 188 | MonzoOAuth2Client._refresh_token_url, 189 | auth=HTTPBasicAuth(self.client_id, self.client_secret), 190 | ) 191 | 192 | token.update({CLIENT_SECRET: self.client_secret}) 193 | 194 | if self.session.token_updater: 195 | self.session.token_updater(token) 196 | 197 | return token 198 | 199 | def validate_response(self, response): 200 | """Validate the response and raises any appropriate errors. 201 | https://docs.monzo.com/#errors 202 | 203 | :param response: The response to validate 204 | :rtype: A Dictionary representation of the response, if no errors occured. 205 | """ 206 | json_response = response.json() 207 | if response.status_code == 200: 208 | return json_response 209 | if response.status_code == 400: 210 | raise BadRequestError(json_response["message"]) 211 | if response.status_code == 401: 212 | raise UnauthorizedError(json_response["message"]) 213 | if response.status_code == 403: 214 | raise ForbiddenError(json_response["message"]) 215 | if response.status_code == 404: 216 | raise PageNotFoundError(json_response["message"]) 217 | if response.status_code == 405: 218 | raise MethodNotAllowedError(json_response["message"]) 219 | if response.status_code == 406: 220 | raise NotAcceptibleError(json_response["message"]) 221 | if response.status_code == 429: 222 | raise TooManyRequestsError(json_response["message"]) 223 | if response.status_code == 500: 224 | raise InternalServerError(json_response["message"]) 225 | if response.status_code == 504: 226 | raise GatewayTimeoutError(json_response["message"]) 227 | -------------------------------------------------------------------------------- /monzo/const.py: -------------------------------------------------------------------------------- 1 | CLIENT_ID = "client_id" 2 | CLIENT_SECRET = "client_secret" 3 | ACCESS_TOKEN = "access_token" 4 | REFRESH_TOKEN = "refresh_token" 5 | EXPIRES_AT = "expires_at" 6 | 7 | MONZO_CACHE_FILE = "monzo.json" 8 | -------------------------------------------------------------------------------- /monzo/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | The module that represents each possible Monzo API error; documented at: 3 | https://monzo.co.uk/docs/#errors as individual Python errors. 4 | """ 5 | 6 | 7 | class BadRequestError(Exception): 8 | """An error to be raised when a request has missing arguments or is malformed.""" 9 | 10 | 11 | class UnauthorizedError(Exception): 12 | """An error to be raised when a request is not authenticated.""" 13 | 14 | 15 | class ForbiddenError(Exception): 16 | """An error to be raised when a request has insufficient permissions.""" 17 | 18 | 19 | class MethodNotAllowedError(Exception): 20 | """An error to be raised when a request is using an incorrect HTTP verb.""" 21 | 22 | 23 | class PageNotFoundError(Exception): 24 | """An error to be raised when a request requests an endpoint that doesn't exist.""" 25 | 26 | 27 | class NotAcceptibleError(Exception): 28 | """An error to be raised when an application does not accept the content 29 | format returned according to the Accept headers sent in the request.""" 30 | 31 | 32 | class TooManyRequestsError(Exception): 33 | """An error to be raised when an application is exceeding its rate limit. 34 | Back off, buddy. :p""" 35 | 36 | 37 | class InternalServerError(Exception): 38 | """An error with Monzo's servers.""" 39 | 40 | 41 | class GatewayTimeoutError(Exception): 42 | """A timeout has occured on Monzo's servers.""" 43 | -------------------------------------------------------------------------------- /monzo/monzo.py: -------------------------------------------------------------------------------- 1 | """A wrapper around the official Monzo API endpoints. 2 | 3 | This module contains the class `Monzo` which represents a wrapper around 4 | HTTP calls to Monzo's API endpoints. 5 | """ 6 | 7 | from monzo.auth import MonzoOAuth2Client 8 | from datetime import datetime 9 | from functools import partial 10 | 11 | import string 12 | import random 13 | 14 | 15 | class Monzo(object): 16 | """The class representation of Monzo's API endpoints. 17 | 18 | Please note that functions without a reference to the official Monzo API 19 | docs page are convenience functions which are created - based on the official 20 | API functions - to make life easier for developers. 21 | 22 | e.g. `get_first_account` calls `get_account` and returns the first `account` 23 | object, if it exists. 24 | 25 | :param access_token: The access token to authorise API calls. 26 | """ 27 | 28 | API_URL = ( 29 | "https://api.monzo.com/" 30 | ) #: (str): A representation of the current Monzo api url. 31 | 32 | def __init__(self, access_token): 33 | """Starts an OAuth session with just an access token 34 | This will fail once the token expires, 35 | for a longer-lived session use Monzo.from_oauth_session() 36 | 37 | :param access_token: A valid access token from https://developers.monzo.com/ 38 | """ 39 | self.oauth_session = MonzoOAuth2Client(None, None, access_token=access_token) 40 | 41 | @classmethod 42 | def from_oauth_session(cls, oauth): 43 | """Inserts an existing MonzoOAuth2Client into this Monzo object 44 | 45 | :param oauth: The MonzoOAuth2Client to be used by the newly created Monzo object. 46 | :rtype: A new Monzo object 47 | """ 48 | new_monzo = cls(None) 49 | new_monzo.oauth_session = oauth 50 | return new_monzo 51 | 52 | def whoami(self): 53 | """Gives information about an access token. (https://monzo.com/docs/#authenticating-requests) 54 | 55 | :rtype: A Dictionary representation of the authentication status. 56 | """ 57 | url = "{0}/ping/whoami".format(self.API_URL) 58 | response = self.oauth_session.make_request(url) 59 | return response 60 | 61 | def get_accounts(self): 62 | """Get all accounts that belong to a user. (https://monzo.com/docs/#list-accounts) 63 | 64 | :rtype: A Collection of accounts for a user. 65 | 66 | """ 67 | url = "{0}/accounts".format(self.API_URL) 68 | response = self.oauth_session.make_request(url) 69 | return response 70 | 71 | def get_first_account(self): 72 | """Gets the first account for a user. 73 | 74 | :rtype: A Dictionary representation of the first account belonging to a user, if it exists. 75 | 76 | """ 77 | accounts = self.get_accounts() 78 | if len(accounts["accounts"]) <= 0: 79 | raise LookupError("There are no accounts associated with this user.") 80 | return accounts["accounts"][0] 81 | 82 | def get_transactions(self, account_id, before=None, since=None, limit=None): 83 | """Get all transactions of a given account. (https://monzo.com/docs/#list-transactions) 84 | 85 | :param account_id: The unique identifier for the account which the transactions belong to. 86 | :param before: A datetime representing the time of the earliest transaction to return (Can't take transaction id as input) 87 | :param since: A datetime representing the time of the earliest transaction to return. (Can also take a transaction id) 88 | :param limit: The maximum number of transactions to return (Max = 100) 89 | :rtype: A collection of transaction objects for specific user. 90 | """ 91 | if isinstance(before, datetime): 92 | before = before.isoformat() + "Z" 93 | if isinstance(since, datetime): 94 | since = since.isoformat() + "Z" 95 | url = "{0}/transactions".format(self.API_URL) 96 | params = { 97 | "expand[]": "merchant", 98 | "account_id": account_id, 99 | "before": before, 100 | "since": since, 101 | "limit": limit, 102 | } 103 | response = self.oauth_session.make_request(url, params=params) 104 | if any([before, since, limit]): 105 | last_transaction_id = response["transactions"][-1]["id"] 106 | next_page = partial( 107 | self.get_transactions, 108 | account_id, 109 | before=before, 110 | since=last_transaction_id, 111 | limit=limit, 112 | ) 113 | response.update({"next_page": next_page}) 114 | 115 | return response 116 | 117 | def get_transaction(self, transaction_id): 118 | """Retrieve data for a specific transaction. (https://docs.monzo.com/#retrieve-transaction) 119 | :param transaction_id: The unique identifier for the transaction for which data should be retrieved for. 120 | :rtype: A dictionary containing the data for the specified transaction_id. 121 | """ 122 | url = "{0}/transactions/{1}".format(self.API_URL, transaction_id) 123 | response = self.oauth_session.make_request(url) 124 | return response 125 | 126 | def get_balance(self, account_id): 127 | """Gets the balance of a given account. (https://monzo.com/docs/#read-balance) 128 | 129 | :param account_id: The unique identifier for the account which the balance belong to. 130 | :rtype: Dictionary representation of the current account balance. 131 | 132 | """ 133 | url = "{0}/balance".format(self.API_URL) 134 | params = {"account_id": account_id} 135 | response = self.oauth_session.make_request(url, params=params) 136 | return response 137 | 138 | def get_webhooks(self, account_id): 139 | """Gets the webhooks of a given account. (https://monzo.com/docs/#list-webhooks) 140 | 141 | :param account_id: The unique identifier for the account which the webhooks belong to. 142 | :rtype: A collection of webhooks that belong to an account. 143 | 144 | """ 145 | url = "{0}/webhooks".format(self.API_URL) 146 | params = {"account_id": account_id} 147 | response = self.oauth_session.make_request(url, params=params) 148 | return response 149 | 150 | def get_first_webhook(self, account_id): 151 | """Gets the first webhook of a given account. 152 | 153 | :param account_id: The unique identifier for the account which the first webhook belong to. 154 | :rtype: A Dictionary representation of the first webhook belonging to an account, if it exists. 155 | """ 156 | webhooks = self.get_webhooks(account_id) 157 | if len(webhooks["webhooks"]) <= 0: 158 | raise LookupError("There are no webhooks associated with the account.") 159 | return webhooks["webhooks"][0] 160 | 161 | def delete_webhook(self, webhook_id): 162 | """Deletes the a specified webhook. (https://monzo.com/docs/#deleting-a-webhook) 163 | 164 | :param webhook_id: The unique identifier for the webhook to delete. 165 | :rtype: An empty Dictionary, if the deletion was successful. 166 | """ 167 | url = "{0}/webhooks/{1}".format(self.API_URL, webhook_id) 168 | response = self.oauth_session.make_request(url, method="DELETE") 169 | return response 170 | 171 | def delete_all_webhooks(self, account_id): 172 | """Removes all webhooks associated with the specified account, if it exists. 173 | 174 | :param account_id: The unique identifier for the account which all webhooks will be removed from. 175 | :rtype: None 176 | """ 177 | webhooks = self.get_webhooks(account_id) 178 | for webhook in webhooks["webhooks"]: 179 | self.delete_webhook(webhook["id"]) 180 | 181 | def register_webhook(self, webhook_url, account_id): 182 | """Registers a webhook. (https://monzo.com/docs/#registering-a-webhook) 183 | 184 | :param webhook_url: The webhook url to register. 185 | :param account_id: The unique identifier for the account to register the webhook. 186 | 187 | :rtype: Registers a webhook to an account. 188 | """ 189 | url = "{0}/webhooks".format(self.API_URL) 190 | data = {"account_id": account_id, "url": webhook_url} 191 | response = self.oauth_session.make_request(url, data=data) 192 | return response 193 | 194 | def register_attachment(self, transaction_id, file_url, file_type): 195 | """Attaches an image to a transaction. (https://monzo.com/docs/#register-attachment) 196 | 197 | :param transaction_id: The unique identifier for the transaction to register the attachment to. 198 | :param file_url: The url of the file to attach. 199 | :param transaction_id: The type of the file specified in file_url 200 | 201 | :rtype: Dictionary representation of the attachment that was just registered. 202 | """ 203 | url = "{0}/attachment/register".format(self.API_URL) 204 | data = { 205 | "external_id": transaction_id, 206 | "file_url": file_url, 207 | "file_type": file_type, 208 | } 209 | response = self.oauth_session.make_request(url, data=data) 210 | return response 211 | 212 | def deregister_attachment(self, attachment_id): 213 | """Removed a previously attached image from a transaction. (https://monzo.com/docs/#deregister-attachment) 214 | 215 | :param transaction_id: The unique identifier for the attachment to deregister. 216 | :rtype: An empty Dictionary, if the deregistration was successful. 217 | """ 218 | url = "{0}/attachment/deregister".format(self.API_URL) 219 | data = {"id": attachment_id} 220 | response = self.oauth_session.make_request(url, data=data) 221 | return response 222 | 223 | def create_feed_item(self, account_id, feed_type, url, params): 224 | """Creates a feed item. (https://monzo.com/docs/#create-feed-item) 225 | 226 | :param account_id: The unique identifier for the account to create the feed item for. 227 | :param feed_type: The type of feed item (currently only `basic` is supported). 228 | :param url: The url to open if a feed item is tapped 229 | :param params: A map of parameters which vary based on type 230 | 231 | :rtype: An empty Dictionary, if the feed item creation was successful. 232 | """ 233 | url = "{0}/feed".format(self.API_URL) 234 | data = { 235 | "account_id": account_id, 236 | "type": feed_type, 237 | "url": url, 238 | "params[title]": params.get("title"), 239 | "params[image_url]": params.get("image_url"), 240 | "params[body]": params.get("body"), 241 | "params[background_color]": params.get("background_color"), 242 | "params[body_color]": params.get("body_color"), 243 | "params[title_color]": params.get("title_color"), 244 | } 245 | response = self.request.post(url, data=data) 246 | return response 247 | 248 | def get_pots(self): 249 | """Get all pots for a user. (https://monzo.com/docs/#list-pots) 250 | 251 | :rtype: A collection of pots for a user. 252 | 253 | """ 254 | url = "{0}/pots".format(self.API_URL) 255 | response = self.oauth_session.make_request(url) 256 | return response 257 | 258 | def deposit_into_pot(self, pot_id, account_id, amount_in_pennies): 259 | """Move money from an account into a pot. (https://monzo.com/docs/#deposit-into-a-pot) 260 | 261 | :param pot_id: The unique identifier for the pot to deposit the money to. 262 | :param account_id: The unique identifier for the account to move the money from. 263 | :param amount_in_pennies: The amount of money to move to the pot in pennies. 264 | 265 | :rtype: A dictionary containing information on the pot that was updated. 266 | """ 267 | url = "{0}/pots/{1}/deposit".format(self.API_URL, pot_id) 268 | unique_string = "".join(random.choice(string.ascii_letters) for i in range(15)) 269 | data = { 270 | "source_account_id": account_id, 271 | "amount": amount_in_pennies, 272 | "dedupe_id": unique_string, 273 | } 274 | 275 | response = self.oauth_session.make_request(url, data=data, method="PUT") 276 | return response 277 | 278 | def withdraw_from_pot(self, account_id, pot_id, amount_in_pennies): 279 | """Move money from an account into a pot. (https://monzo.com/docs/#withdraw-from-a-pot) 280 | 281 | :param account_id: The unique identifier for the account to move the money to. 282 | :param pot_id: The unique identifier for the pot to withdraw the money from. 283 | :param amount_in_pennies: The amount of money to move to the pot in pennies. 284 | 285 | :rtype: A dictionary containing information on the pot that was updated. 286 | """ 287 | url = "{0}/pots/{1}/withdraw".format(self.API_URL, pot_id) 288 | unique_string = "".join(random.choice(string.ascii_letters) for i in range(15)) 289 | data = { 290 | "destination_account_id": account_id, 291 | "amount": amount_in_pennies, 292 | "dedupe_id": unique_string, 293 | } 294 | 295 | response = self.oauth_session.make_request(url, data=data, method="PUT") 296 | return response 297 | 298 | def update_transaction_metadata(self, transaction_id, key, value): 299 | """Update a metadata key value pair for a given transaction. (https://monzo.com/docs/#annotate-transaction) 300 | :param transaction_id: The unique identifier for the transaction for which notes should be updated. 301 | :param key: The key for the element of metadata to be updated. 302 | :param value: The value to be associated with the given key. 303 | :rtype: The updated transaction object. 304 | """ 305 | url = "{0}/transactions/{1}".format(self.API_URL, transaction_id) 306 | data = {"metadata[" + key + "]": value} 307 | response = self.oauth_session.make_request(url, data=data, method="PATCH") 308 | return response 309 | 310 | def update_transaction_notes(self, transaction_id, notes): 311 | """Update notes for a given transaction. (https://monzo.com/docs/#annotate-transaction) 312 | :param transaction_id: The unique identifier for the transaction for which notes should be updated. 313 | :param notes: The new notes to be attached to the transaction. 314 | :rtype: The updated transaction object. 315 | """ 316 | return self.update_transaction_metadata(transaction_id, "notes", notes) 317 | -------------------------------------------------------------------------------- /monzo/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from monzo.const import MONZO_CACHE_FILE 3 | 4 | 5 | def save_token_to_file(token, filename=MONZO_CACHE_FILE): 6 | """Saves a token dictionary to a json file""" 7 | with open(filename, "w") as fp: 8 | json.dump(token, fp, sort_keys=True, indent=4) 9 | 10 | 11 | def load_token_from_file(filename=MONZO_CACHE_FILE): 12 | """Loads a json file and returns a dictionary of its contents""" 13 | with open(filename, "r") as fp: 14 | data = json.load(fp) 15 | return data 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black>=19.3b0 2 | pytest>=5.0.1 3 | python-dotenv>=0.10.3 4 | requests>=2.22.0 5 | requests-oauthlib>=1.2.0 6 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | from os.path import join, dirname 2 | from dotenv import load_dotenv 3 | import os 4 | 5 | dotenv_path = join(dirname(__file__), '.env') 6 | load_dotenv(dotenv_path) 7 | 8 | def get_environment_var(key): 9 | """ 10 | Returns the value of a given environment variable key or None. 11 | 12 | If the key doesn't exist in the operating system environment variables, it 13 | looks for key in a '.env' file (via the dotenv library). 14 | 15 | Arguments: 16 | key -- the environment variable key 17 | 18 | Returns: 19 | The corresponding value of the key, or None if it doesn't exist. 20 | """ 21 | return os.environ.get(key) 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='monzo', 4 | version='0.10.0', 5 | description='A python SDK for interacting with the Monzo API.', 6 | url='https://github.com/muyiwaolu/monzo-python', 7 | author='Muyiwa Olu', 8 | author_email='muyiolu94@gmail.com', 9 | license='MIT', 10 | packages=['monzo'], 11 | install_requires=[ 12 | 'requests==2.20.0', 13 | 'requests-oauthlib==1.0.0', 14 | 'python-dotenv==0.5.1' 15 | ], 16 | ) 17 | -------------------------------------------------------------------------------- /tests/test_api_endpoints.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from doubles.monzo import Monzo 3 | 4 | 5 | class TestApiEndpoints: 6 | @pytest.fixture 7 | def client(self): 8 | return Monzo("stubbed") 9 | 10 | def test_whoami(self, client): 11 | whoami = client.whoami() 12 | assert whoami["authenticated"] 13 | 14 | def test_get_accounts(self, client): 15 | accounts = client.get_accounts() 16 | assert accounts["accounts"] is not None 17 | 18 | def test_get_transactions(self, client): 19 | account_id = client.get_first_account()["id"] 20 | transactions = client.get_transactions(account_id) 21 | assert transactions["transactions"] is not None 22 | 23 | def test_get_transaction(self, client): 24 | account_id = client.get_first_account()["id"] 25 | first_transaction_id = client.get_transactions(account_id)["transactions"][0][ 26 | "id" 27 | ] 28 | transaction_data = client.get_transaction(first_transaction_id) 29 | assert transaction_data["transaction"] is not None 30 | 31 | def test_get_balance(self, client): 32 | account_id = client.get_first_account()["id"] 33 | balance = client.get_balance(account_id) 34 | assert balance["balance"] is not None 35 | 36 | def test_get_pots(self, client): 37 | pots = client.get_pots()["pots"] 38 | assert pots is not None 39 | 40 | def test_deposit_into_pot(self, client): 41 | pot_id = client.get_pots()["pots"][0]["id"] 42 | account_id = client.get_first_account()["id"] 43 | pot_info = client.deposit_into_pot(pot_id, account_id, 1000) 44 | assert pot_info is not None 45 | 46 | def test_withdraw_from_pot(self, client): 47 | pot_id = client.get_pots()["pots"][0]["id"] 48 | account_id = client.get_first_account()["id"] 49 | pot_info = client.withdraw_from_pot(account_id, pot_id, 1000) 50 | assert pot_info is not None 51 | 52 | def test_get_webhooks(self, client): 53 | account_id = client.get_first_account()["id"] 54 | webhooks = client.get_webhooks(account_id) 55 | assert webhooks["webhooks"] is not None 56 | 57 | def test_delete_webhook(self, client): 58 | account_id = client.get_first_account()["id"] 59 | client.register_webhook( 60 | webhook_url="https://google.co.uk", account_id=account_id 61 | ) 62 | webhook_id = client.get_first_webhook(account_id)["id"] 63 | assert client.delete_webhook(webhook_id) == {} 64 | 65 | def test_register_webhook(self, client): 66 | account_id = client.get_first_account()["id"] 67 | client.register_webhook( 68 | webhook_url="https://google.co.uk", account_id=account_id 69 | ) 70 | webhooks = client.get_webhooks(account_id) 71 | assert len(webhooks) > 0 72 | 73 | def test_register_and_remove_attachment(self, client): 74 | account_id = client.get_first_account()["id"] 75 | transactions = client.get_transactions(account_id) 76 | first_transaction_id = transactions["transactions"][0]["id"] 77 | image_attachment = client.register_attachment( 78 | transaction_id=first_transaction_id, 79 | file_url="does not matter", 80 | file_type="does not matter", 81 | ) 82 | attachment_id = image_attachment["attachment"]["id"] 83 | assert attachment_id is not None 84 | deregistered_attachment_response = client.deregister_attachment(attachment_id) 85 | assert deregistered_attachment_response is not None 86 | 87 | def test_create_feed_item(self, client): 88 | account_id = client.get_first_account()["id"] 89 | params = { 90 | "title": "does not matter", 91 | "body": "does not matter", 92 | "image_url": "does not matter", 93 | } 94 | feed_item = client.create_feed_item( 95 | account_id=account_id, feed_type="blah", url="blah", params=params 96 | ) 97 | assert feed_item is not None 98 | 99 | def test_update_transaction_metadata(self, client): 100 | account_id = client.get_first_account()["id"] 101 | transactions = client.get_transactions(account_id) 102 | first_transaction_id = transactions["transactions"][0]["id"] 103 | updated_transaction = client.update_transaction_metadata( 104 | transaction_id=first_transaction_id, key="keyvalue", value="does not matter" 105 | ) 106 | assert updated_transaction is not None 107 | -------------------------------------------------------------------------------- /tests/test_api_errors.py: -------------------------------------------------------------------------------- 1 | from monzo.monzo import Monzo 2 | from monzo.errors import BadRequestError 3 | import pytest 4 | 5 | 6 | class TestApiErrors: 7 | @pytest.fixture 8 | def unauthorized_client(self): 9 | return Monzo("gibberish") 10 | 11 | def test_whoami(self, unauthorized_client): 12 | with pytest.raises(BadRequestError): 13 | unauthorized_client.whoami() 14 | --------------------------------------------------------------------------------