Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Flask example and tests #2

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions examples/flask/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Flask

Flask is a lightweight WSGI web application framework, and it's also a good option
for writing RESTful APIs.


## Installation

```
$ pip install -r requirements.txt
$ flask run
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
```

See the [top level README](https://github.com/pallets-eco/flask-api-examples#readme)
for the environment setup and how to play with the API.


## Introduction

Flask provides some route shortcuts for web APIs:

- `app.get()`
- `app.post()`
- `app.put()`
- `app.patch()`
- `app.delete()`

So the following usage:

```python
@app.route('/pets', methods=['POST'])
def create_pet():
pass
```

Can be simplified to:

```python
@app.post('/pets')
def create_pet():
pass
```

With `flask.views.MethodView`, you can also organize the APIs with class. Each
greyli marked this conversation as resolved.
Show resolved Hide resolved
method of the class maps to the corresponding HTTP method when dispatching
greyli marked this conversation as resolved.
Show resolved Hide resolved
the request to this class:

```python
from flask.views import MethodView


class PetResource(MethodView):

def get(self):
pass

def patch(self):
pass

def put(self):
pass

def delete(self):
pass


app.add_url_rule('/pets/<int:pet_id>', view_func=PetResource.as_view('pet'))
```

See more details in [Method Based Dispatching](https://flask.palletsprojects.com/views/#method-based-dispatching).


## Resources

- Documentation: https://flask.palletsprojects.com/
- Source code: https://github.com/pallets/flask/
- Chat: https://discord.gg/pallets
115 changes: 115 additions & 0 deletions examples/flask/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from flask import Flask, request
from werkzeug.exceptions import HTTPException
from flask_sqlalchemy import SQLAlchemy

PET_CATEGORIES = ['cat', 'dog']
PET_REQUIRED_FIELD_ERROR = 'The name and category field is required.'
PET_NAME_LENGTH_ERROR = 'The length of the name must be between 0 and 10.'
PET_CATEGORY_ERROR = 'The category must be one of: dog, cat.'
greyli marked this conversation as resolved.
Show resolved Hide resolved
PET_ID_ERROR = 'Pet not found.'

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)


class PetModel(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(10))
category = db.Column(db.String(10))


@app.before_first_request
def init_database():
"""Create the table and add some fake data."""
pets = [
{'name': 'Kitty', 'category': 'cat'},
{'name': 'Coco', 'category': 'dog'},
{'name': 'Flash', 'category': 'cat'}
]
db.create_all()
for pet_data in pets:
pet = PetModel(**pet_data)
db.session.add(pet)
db.session.commit()


@app.errorhandler(HTTPException)
def handle_http_errors(error):
return {'message': error.name}, error.code


def pet_schema(pet):
return {
'id': pet.id,
'name': pet.name,
'category': pet.category
}


@app.get('/')
def say_hello():
return {'message': 'Hello!'}


@app.get('/pets/<int:pet_id>')
def get_pet(pet_id):
pet = PetModel.query.get(pet_id)
if pet is None:
return {'message': PET_ID_ERROR}, 404
return pet_schema(pet)


@app.get('/pets')
def get_pets():
pets = PetModel.query.all()
return {'pets': [pet_schema(pet) for pet in pets]}


@app.post('/pets')
def create_pet():
data = request.json
if 'name' not in data or 'category' not in data:
return {'message': PET_REQUIRED_FIELD_ERROR}, 400
if len(data['name']) > 10:
greyli marked this conversation as resolved.
Show resolved Hide resolved
return {'message': PET_NAME_LENGTH_ERROR}, 400
if data['category'] not in PET_CATEGORIES:
return {'message': PET_CATEGORY_ERROR}, 400

pet = PetModel(name=data['name'], category=data['category'])
db.session.add(pet)
db.session.commit()
return pet_schema(pet), 201


@app.patch('/pets/<int:pet_id>')
def update_pet(pet_id):
pet = PetModel.query.get(pet_id)
if pet is None:
return {'message': PET_ID_ERROR}, 404

data = request.json
if 'name' in data:
if len(data['name']) > 10:
return {'message': PET_NAME_LENGTH_ERROR}, 400
else:
pet.name = data['name']
if 'category' in data:
if data['category'] not in PET_CATEGORIES:
return {'message': PET_CATEGORY_ERROR}, 400
else:
pet.category = data['category']
db.session.commit()
return pet_schema(pet)


@app.delete('/pets/<int:pet_id>')
def delete_pet(pet_id):
pet = PetModel.query.get(pet_id)
if pet is None:
return {'message': PET_ID_ERROR}, 404
db.session.delete(pet)
db.session.commit()
return '', 204
2 changes: 2 additions & 0 deletions examples/flask/requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
flask
flask-sqlalchemy
26 changes: 26 additions & 0 deletions examples/flask/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
#
# pip-compile requirements.in
#
click==8.0.1
# via flask
flask==2.0.1
# via
# -r requirements.in
# flask-sqlalchemy
flask-sqlalchemy==2.5.1
# via -r requirements.in
greenlet==1.1.0
# via sqlalchemy
itsdangerous==2.0.1
# via flask
jinja2==3.0.1
# via flask
markupsafe==2.0.1
# via jinja2
sqlalchemy==1.4.20
# via flask-sqlalchemy
werkzeug==2.0.1
# via flask
122 changes: 122 additions & 0 deletions test_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from importlib import reload
import sys

import pytest

examples = [
'flask',
]


@pytest.fixture
def client(request):
app_path = f'examples/{request.param}'
sys.path.insert(0, app_path)
import app
app = reload(app)
_app = app.app
_app.testing = True
sys.path.remove(app_path)
return _app.test_client()


@pytest.mark.parametrize('client', examples, indirect=True)
def test_say_hello(client):
rv = client.get('/')
assert rv.status_code == 200
assert rv.json
assert rv.json['message'] == 'Hello!'


@pytest.mark.parametrize('client', examples, indirect=True)
def test_get_pet(client):
rv = client.get('/pets/1')
assert rv.status_code == 200
assert rv.json
assert rv.json['name'] == 'Kitty'
assert rv.json['category'] == 'cat'

rv = client.get('/pets/13')
assert rv.status_code == 404
assert rv.json


@pytest.mark.parametrize('client', examples, indirect=True)
def test_get_pets(client):
rv = client.get('/pets')
assert rv.status_code == 200
assert rv.json
assert len(rv.json['pets']) == 3
assert rv.json['pets'][0]['name'] == 'Kitty'
assert rv.json['pets'][0]['category'] == 'cat'


@pytest.mark.parametrize('client', examples, indirect=True)
def test_create_pet(client):
rv = client.post('/pets', json={
'name': 'Grey',
'category': 'cat'
})
assert rv.status_code == 201
assert rv.json
assert rv.json['name'] == 'Grey'
assert rv.json['category'] == 'cat'


@pytest.mark.parametrize('client', examples, indirect=True)
@pytest.mark.parametrize('data', [
{'name': 'Grey', 'category': 'human'},
{'name': 'Fyodor Mikhailovich Dostoevsky', 'category': 'cat'},
{'category': 'cat'},
{'name': 'Grey'}
])
def test_create_pet_with_bad_data(client, data):
rv = client.post('/pets', json=data)
assert rv.status_code == 400
assert rv.json


@pytest.mark.parametrize('client', examples, indirect=True)
def test_update_pet(client):
new_data = {
'name': 'Ghost',
'category': 'dog'
}

rv = client.patch('/pets/1', json=new_data)
assert rv.status_code == 200
assert rv.json

rv = client.get('/pets/1')
assert rv.status_code == 200
assert rv.json['name'] == new_data['name']
assert rv.json['category'] == new_data['category']

rv = client.patch('/pets/13', json=new_data)
assert rv.status_code == 404
assert rv.json


@pytest.mark.parametrize('client', examples, indirect=True)
@pytest.mark.parametrize('data', [
{'name': 'Fyodor Mikhailovich Dostoevsky'},
{'category': 'human'}
])
def test_update_pet_with_bad_data(client, data):
rv = client.patch('/pets/1', json=data)
assert rv.status_code == 400
assert rv.json


@pytest.mark.parametrize('client', examples, indirect=True)
def test_delete_pet(client):
rv = client.delete('/pets/1')
assert rv.status_code == 204

rv = client.get('/pets/1')
assert rv.status_code == 404
assert rv.json

rv = client.delete('/pets/13')
assert rv.status_code == 404
assert rv.json