-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
method of the class maps to the corresponding HTTP method when dispatching | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.' | ||
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: | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
flask | ||
flask-sqlalchemy |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |