├── .editorconfig ├── .gitignore ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── bootstrap.sh └── cashman ├── __init__.py ├── index.py └── model ├── __init__.py ├── expense.py ├── income.py ├── transaction.py └── transaction_type.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.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 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 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 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | *.iml 104 | .idea/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Using lightweight alpine image 2 | FROM python:3.10-alpine 3 | 4 | # Installing packages 5 | RUN apk update 6 | RUN pip install --no-cache-dir pipenv 7 | 8 | # Defining working directory and adding source code 9 | WORKDIR /usr/src/app 10 | COPY Pipfile Pipfile.lock bootstrap.sh ./ 11 | COPY cashman ./cashman 12 | 13 | # Install API dependencies 14 | RUN pipenv install --system --deploy 15 | 16 | # Start app 17 | EXPOSE 5000 18 | ENTRYPOINT ["/usr/src/app/bootstrap.sh"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Auth0 Blog Samples 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [packages] 9 | 10 | flask = "*" 11 | marshmallow = "*" 12 | 13 | 14 | [dev-packages] 15 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7a1183e8437754c1b3283422b097e82b8cc4a38536f9bd2b280fa9de7e391327" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.python.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "click": { 18 | "hashes": [ 19 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 20 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 21 | ], 22 | "markers": "python_version >= '3.7'", 23 | "version": "==8.1.3" 24 | }, 25 | "flask": { 26 | "hashes": [ 27 | "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b", 28 | "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526" 29 | ], 30 | "index": "pypi", 31 | "version": "==2.2.2" 32 | }, 33 | "importlib-metadata": { 34 | "hashes": [ 35 | "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670", 36 | "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23" 37 | ], 38 | "markers": "python_version < '3.10'", 39 | "version": "==4.12.0" 40 | }, 41 | "itsdangerous": { 42 | "hashes": [ 43 | "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", 44 | "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" 45 | ], 46 | "markers": "python_version >= '3.7'", 47 | "version": "==2.1.2" 48 | }, 49 | "jinja2": { 50 | "hashes": [ 51 | "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", 52 | "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" 53 | ], 54 | "markers": "python_version >= '3.7'", 55 | "version": "==3.1.2" 56 | }, 57 | "markupsafe": { 58 | "hashes": [ 59 | "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", 60 | "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", 61 | "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", 62 | "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", 63 | "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", 64 | "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", 65 | "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", 66 | "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", 67 | "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", 68 | "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", 69 | "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", 70 | "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", 71 | "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", 72 | "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", 73 | "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", 74 | "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", 75 | "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", 76 | "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", 77 | "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", 78 | "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", 79 | "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", 80 | "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", 81 | "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", 82 | "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", 83 | "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", 84 | "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", 85 | "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", 86 | "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", 87 | "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", 88 | "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", 89 | "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", 90 | "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", 91 | "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", 92 | "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", 93 | "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", 94 | "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", 95 | "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", 96 | "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", 97 | "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", 98 | "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" 99 | ], 100 | "markers": "python_version >= '3.7'", 101 | "version": "==2.1.1" 102 | }, 103 | "marshmallow": { 104 | "hashes": [ 105 | "sha256:1172ce82765bf26c24a3f9299ed6dbeeca4d213f638eaa39a37772656d7ce408", 106 | "sha256:48e2d88d4ab431ad5a17c25556d9da529ea6e966876f2a38d274082e270287f0" 107 | ], 108 | "index": "pypi", 109 | "version": "==3.17.1" 110 | }, 111 | "packaging": { 112 | "hashes": [ 113 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", 114 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" 115 | ], 116 | "markers": "python_version >= '3.6'", 117 | "version": "==21.3" 118 | }, 119 | "pyparsing": { 120 | "hashes": [ 121 | "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", 122 | "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" 123 | ], 124 | "markers": "python_full_version >= '3.6.8'", 125 | "version": "==3.0.9" 126 | }, 127 | "werkzeug": { 128 | "hashes": [ 129 | "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", 130 | "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5" 131 | ], 132 | "markers": "python_version >= '3.7'", 133 | "version": "==2.2.2" 134 | }, 135 | "zipp": { 136 | "hashes": [ 137 | "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2", 138 | "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009" 139 | ], 140 | "markers": "python_version >= '3.7'", 141 | "version": "==3.8.1" 142 | } 143 | }, 144 | "develop": {} 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Developing RESTful APIs with Python and Flask 2 | 3 | This repository contains code created throughout the 4 | ["Developing RESTful APIs with Python and Flask"](https://auth0.com/blog/developing-restful-apis-with-python-and-flask) article. 5 | 6 | Check it out for more info! 7 | 8 | ### Running the API 9 | 10 | To run this application, you will need Python 3+ and [Pipenv](https://pipenv.readthedocs.io/en/latest/) installed locally. If you have then, you can issue the following commands: 11 | 12 | ```bash 13 | # from the flask-restful-apis directory 14 | pipenv install 15 | ./bootstrap.sh 16 | ``` 17 | 18 | Then you can issue requests to your API. For example, with `curl`, you can issue requests like that: 19 | 20 | ```bash 21 | # inserting a new income 22 | curl -X POST -H "Content-Type: application/json" -d '{ 23 | "amount": 300.0, 24 | "description": "loan payment" 25 | }' http://localhost:5000/incomes 26 | 27 | # listing all incomes 28 | curl http://localhost:5000/incomes 29 | ``` 30 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export FLASK_APP=./cashman/index.py 3 | pipenv run flask --debug run -h 0.0.0.0 4 | -------------------------------------------------------------------------------- /cashman/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/flask-restful-apis/cd6170cedffe5a8ef3998035fffc4550f50b975b/cashman/__init__.py -------------------------------------------------------------------------------- /cashman/index.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request 2 | 3 | from cashman.model.expense import Expense, ExpenseSchema 4 | from cashman.model.income import Income, IncomeSchema 5 | from cashman.model.transaction_type import TransactionType 6 | 7 | app = Flask(__name__) 8 | 9 | transactions = [ 10 | Income('Salary', 5000), 11 | Income('Dividends', 200), 12 | Expense('pizza', 50), 13 | Expense('Rock Concert', 100) 14 | ] 15 | 16 | 17 | @app.route('/incomes') 18 | def get_incomes(): 19 | schema = IncomeSchema(many=True) 20 | incomes = schema.dump( 21 | filter(lambda t: t.type == TransactionType.INCOME, transactions) 22 | ) 23 | return jsonify(incomes) 24 | 25 | 26 | @app.route('/incomes', methods=['POST']) 27 | def add_income(): 28 | income = IncomeSchema().load(request.get_json()) 29 | transactions.append(income) 30 | return "", 204 31 | 32 | 33 | @app.route('/expenses') 34 | def get_expenses(): 35 | schema = ExpenseSchema(many=True) 36 | expenses = schema.dump( 37 | filter(lambda t: t.type == TransactionType.EXPENSE, transactions) 38 | ) 39 | return jsonify(expenses) 40 | 41 | 42 | @app.route('/expenses', methods=['POST']) 43 | def add_expense(): 44 | expense = ExpenseSchema().load(request.get_json()) 45 | transactions.append(expense) 46 | return "", 204 47 | 48 | 49 | if __name__ == "__main__": 50 | app.run() 51 | -------------------------------------------------------------------------------- /cashman/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/flask-restful-apis/cd6170cedffe5a8ef3998035fffc4550f50b975b/cashman/model/__init__.py -------------------------------------------------------------------------------- /cashman/model/expense.py: -------------------------------------------------------------------------------- 1 | from marshmallow import post_load 2 | 3 | from .transaction import Transaction, TransactionSchema 4 | from .transaction_type import TransactionType 5 | 6 | 7 | class Expense(Transaction): 8 | def __init__(self, description, amount): 9 | super(Expense, self).__init__(description, -abs(amount), TransactionType.EXPENSE) 10 | 11 | def __repr__(self): 12 | return ''.format(self=self) 13 | 14 | 15 | class ExpenseSchema(TransactionSchema): 16 | @post_load 17 | def make_expense(self, data, **kwargs): 18 | return Expense(**data) 19 | -------------------------------------------------------------------------------- /cashman/model/income.py: -------------------------------------------------------------------------------- 1 | from marshmallow import post_load 2 | 3 | from .transaction import Transaction, TransactionSchema 4 | from .transaction_type import TransactionType 5 | 6 | 7 | class Income(Transaction): 8 | def __init__(self, description, amount): 9 | super(Income, self).__init__(description, amount, TransactionType.INCOME) 10 | 11 | def __repr__(self): 12 | return ''.format(self=self) 13 | 14 | 15 | class IncomeSchema(TransactionSchema): 16 | @post_load 17 | def make_income(self, data, **kwargs): 18 | return Income(**data) 19 | -------------------------------------------------------------------------------- /cashman/model/transaction.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | from marshmallow import Schema, fields 4 | 5 | 6 | class Transaction(object): 7 | def __init__(self, description, amount, type): 8 | self.description = description 9 | self.amount = amount 10 | self.created_at = dt.datetime.now() 11 | self.type = type 12 | 13 | def __repr__(self): 14 | return ''.format(self=self) 15 | 16 | 17 | class TransactionSchema(Schema): 18 | description = fields.Str() 19 | amount = fields.Number() 20 | created_at = fields.Date() 21 | type = fields.Str() 22 | -------------------------------------------------------------------------------- /cashman/model/transaction_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class TransactionType(Enum): 5 | INCOME = "INCOME" 6 | EXPENSE = "EXPENSE" 7 | --------------------------------------------------------------------------------