Overview
RESTful API stands for Representational State Transfer Application Programming Interface
Tech Stack
- Python 2.7.x (with pip)
- virtualenv (
sudo easy_install virtualenv
) - Postgres
Clone the project, and cd
to the folder cd flask-restful-swagger
:
git clone <todo: add .git>
-
Create a virtual env for Python
virtualenv venv
-
Activate the virtual env
source venv/bin/activate
-
Install dependencies
pip install –r requirements.txt
-
After Postgres is set up, initialize db migrations for schema management, and make a migration
python manage.py db init python manage.py db migrate python manage.py db upgrade
-
To start the app
python run.py
After setup, the main command line development workflow consists of:
- Activating the virtualenv
- Running the application
- Making any database migrations when there are changes in the models
Activate virtualenv
source venv/bin/activate
Deactivate virtualenv
deactivate
Run the app
python run.py
Migrate the database (do both)
python manage.py db migrate
python manage.py db upgrade
Swagger UI URL
http://localhost:5000/index.html
Access Tokens currently set to expire in 7 days from generation time.
Refresh Tokens currently do not expire.
When the database is deleted, the migrations
folder needs to be deleted and then
What is the __init__.py
file for?
A comparison of json outputs on stackoverflow.
A REST API can have parameters in at least two ways:
- As part of the URL-path (i.e.
/api/resource/parametervalue
) - As a query argument (i.e.
/api/resource?parameter=value
)
Two resource classes are needed per endpoint. One resource class will handle endpoints that do not have url path arguments, the other will.
api.add_resource(Pets, '/v1/pets')
api.add_resource(Pets_method, '/v1/pets/<pet_arg>')
Pets
is a class found in pets_resource.py
, Pets_method
is a class also found in pets_resource.py
, and Pet
is a class found in models.py
for the database model/table. It is important to have unique class names.
Simple Class
class SimpleClass(Resource):
"""This is the SimpleClass class"""
def get(self):
"""GET method"""
# Do something
return 200
def post(self):
"""POST method"""
# Do something
return 201
GET (all) method
- If a user needs a token to access this endpoint, add
method_decorators = [authorized]
. To read more about decorators, click here for official python docs, here for flask docs, and here for flask-restful docs. - Create the GET method
- Query the database for
all()
- To create nested JSON, write a for loop that loops through every key + value pair
output
automatically gets jsonified
class Pets(Resource):
method_decorators = [authorized]
def get(self):
"""Pets' GET method
Queries for all pets
Returns:
401 -- Object not found or Unauthorized
200 -- pets successfully queried
"""
pets = Pet.query.all()
for pet in pets:
output = {"name": pet.name,
"age": pet.age,
"sex": pet.sex,
"breed": pet.breed
}
return output, 200
POST method
- If there are URL parameters as a query argument, get them with
request.args.get()
- If there is a JSON object, use
request.get_json(force=True)
- JSON object = python dictionary
pet = Pet()
creates an instance of Pet class. aka, creates a new row in the tablepets_owned
db.session.add(pet)
adds the row into the database, however,db.session.commit()
is what completes the database transaction.user.add_pet(pet)
calls a User class methodadd_pet()
and populates the association table between user and pet.
class Pets(Resource):
method_decorators = [authorized]
def post(self):
"""Pets' POST method
Adds a new Pet Model from JSON.
Returns:
200 -- JSON object of pet
401 -- pet already exists
500 -- Error
"""
json_data = request.get_json(force=True)
name = json_data['name']
username = json_data['username']
password = json_data['password']
user = Users.query.filter_by(username=username).first()
pet = Pet.query.filter_by(name=name).first()
if pet is not None:
return {"Error": "Unauthorized to query or could not be found"}, 401
else:
pet = Pet()
pet.name = name
pet.age = age
pet.sex = sex
pet.breed = breed
db.session.add(pet)
user.add_pet(pet)
db.session.commit()
output = {"name": pet.name,
"age": pet.age,
"sex": pet.sex,
"breed": pet.breed
}
return output, 200
return {"Error": "Data store not configured or unreachable"}, 500
-
Get parameters as part of the URL path by accepting an argument in the method.
-
In order the get a URL path, a parser is needed.
In
views.py
:parser = reqparse.RequestParser() parser.add_argument('pet_arg', type=str, required=True, help='pet_argument')
GET (one) method
class Pets_method(Resource):
method_decorators = [authorized]
def get(self, pet_arg):
"""Pets' GET method
Queries for a single pet
Parameters:
pet_arg
Returns:
401 -- Unauthorized to query or could not be found
404 -- Object not found
500 -- Data store not configured or unreachable
"""
if pet is not None:
pet = Pet.query.filter_by(
name=pet_arg).first_or_404()
if pet is None:
return {"Error": "Object not found"}, 404
else:
output = {"name": pet.name,
"age": pet.age,
"sex": pet.sex,
"breed": pet.breed
}
return output, 200
return {"Error": "Data store not configured or unreachable"}, 500
PATCH is similar to POST except you have to query it first. Using first_or_404()
returns 404 if it can't find the parameter passed in the URL path or the first object.
PATCH method
class Pets_method(Resource):
method_decorators = [authorized]
def patch(self, pet_arg):
"""Pets_method's PATCH method
Updates a single pet
Returns:
401 -- Unauthorized to query or could not be found
404 -- Object not found
500 -- Data store not configured or unreachable
"""
json_data = request.get_json(force=True)
if json_data:
json_data = request.get_json(force=True)
name = json_data['name']
username = json_data['username']
password = json_data['password']
user = Users.query.filter_by(username=username).first_or_404()
pet = Pet.query.filter_by(name=name).first()
pet.name = name
pet.age = age
pet.sex = sex
pet.breed = breed
db.session.add(pet)
user.add_pet(pet)
db.session.commit()
output = {"name": pet.name,
"age": pet.age,
"sex": pet.sex,
"breed": pet.breed
}
return output, 200
return {"Error": "Data store not configured or unreachable"}, 500
DELETE is similar to GET (one) except you have to call db.session.delete()
to delete an object and db.session.commit()
to complete the transaction.
DELETE method
class Pets_method(Resource):
method_decorators = [authorized]
def delete(self, pet_args):
"""Pets_method's DELETE method
Deletes a single pet
Returns:
204 -- Deletes pet
401 -- Unauthorized to query or could not be found
404 -- Object not found
500 -- Data store not configured or unreachable
"""
if pet_args:
pet = Pet.query.filter_by(
name=pet_arg).first()
if pet:
db.session.delete(pet)
db.session.commit()
return {"Success": "Successfully removed pet"}, 204
if pet is None:
return {"Error": "Object not found"}, 404
return {"Error": "Data store not configured or unreachable"}, 500
SQLAlchemy is a powerful Python-based ORM. Flask-SQLAlchemy is a flask wrapper for it.
SQLAlchemy cheat sheet from a codementor here.
Creating a table and db model
SQL:
CREATE TABLE tokens (
token_id SERIAL PRIMARY KEY,
grant_type VARCHAR(80),
access_token VARCHAR(255),
refresh_token VARCHAR(255),
expires_at DATETIME,
token_type VARCHAR(80)
)
SQLAlchemy:
class Token(db.Model):
__tablename__ = 'token'
id = db.Column(db.Integer, primary_key=True)
grant_type = db.Column(db.String(80))
access_token = db.Column(db.String(255))
refresh_token = db.Column(db.String(255))
expires_at = db.Column(db.DateTime)
token_type = db.Column(db.String(80))
Creating a class in SQLAlchemy= creating a table in SQL. SQLAlchemy allows relationship and foreign key declarations
Official docs here, simple explanation of relationship types here with graphics
Information about backref
here, and here
Information about back_populates
here
1:1 (using back_populates creates a bi-directional 1:1 relationship)
class Users(db.Model):
...
...
token = db.relationship('Token', back_populates='users')
class Token(db.Model):
...
...
users = db.relationship('Users', back_populates='token')
1:many
class Users(db.Model):
...
...
groups = db.relationship('Group', backref='group', lazy='dynamic')
class Group(db.Model):
__tablename__ = 'group'
id = db.Column(db.Integer, primary_key=True)
group_name = db.Column(db.String(100))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
many:many
cat_toe_to_leg_association_table = db.Table('cat_toe_to_leg_association_table', db.Column('cat_toe_id', db.Integer, db.ForeignKey('cat_toe.id')),db.Column('cat_leg_id', db.Integer, db.ForeignKey('cat_leg.id')))
class CatToe(db.Model):
__tablename__ = 'cat_toe'
id = db.Column(db.Integer, primary_key=True)
class CatLeg(db.Model):
__tablename__ = 'cat_leg'
id = db.Column(db.Integer, primary_key=True)
toes_to_legs = db.relationship(
'CatToe', secondary=cat_toe_to_leg_association_table, backref="cat_leg", lazy='dynamic')
Many to many relationships adds association tables to link the two together. It is possible to have bi-directional relationships and query into another table entirely using the defined many to many relationship. For example, you can query a single library book and be able to query all of the genres in one single statement because of the bi-directional relationship.
group_association_table = db.Table('group_association_table',
db.Column('group_id', db.Integer,
db.ForeignKey('group.id')),
db.Column('user_id', db.Integer,
db.ForeignKey('users.id'))
)
Official docs here
Create a new row in the db by calling the class name. Since everything in our Token
table is optional we can skip several columns and leave it null
. (Example in shell)
>>> token = Token(grant_type='admin', access_token='123', refresh_token='abc', token_type='string')
>>> token.access_token
123
>>> db.session.add(token)
>>> db.session.commit()
The the db.session.add()
and db.session.commit()
are required when making additions or changes to the database with POST and PATCH methods.
Official docs and all methods here
Query every single token by calling all()
.
SQL:
SELECT * FROM token
SQLAlchemy:
Token.query.all()
Filtering
There are two ways to filter: filter
and filter_by
. filter
vs filter_by
SQL:
SELECT * FROM token WHERE grant_type = "admin"
SQLAlchemy with filter_by
:
Token.query.filter_by(grant_type='admin').all()
SQLAlchemy with filter
:
Token.query.filter(Token.token_type == 'string').all()
SQL:
SELECT * FROM token WHERE token_type = "string" AND grant_type = "admin"
SQLAlchemy:
Token.query.filter(Token.token_type == 'string', Token.grant_type == 'admin').all()
Fetching Records
Official docs here
all()
- Get all records
first()
- Get first record or None
first_or_404()
- Get first record or return 404
one()
- Get first record, error if 0 or if > 1
Documentation for Join here
Endpoint testing
Paw — paid, mac only
Postman — free, mac, windows, linux, chrome extension
Prettify code
PEP 8 styling (extension available on Atom and Sublime)
JSON: Sublime — HTML, CSS, JS Prettify (JSON included) Atom — Pretty JSON
Database
Postico — free, mac only Alternatives
What are the limitations of the free trial?
- At most 5 connection favorites
- Only a single window per connection
- Table filters are disabled
- There is no time limit — use the trial as long as you want!)
Method 1: curl
Method 2: requests Library
The requests library is capable of support GET, PUT, POST, and DELETE methods
-
Start up a venv
-
Install requests library
pip install requests
Simple Example in Shell
>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass')) >>> r.status_code 200 >>> r.headers['content-type'] 'application/json; charset=utf8' >>> r.encoding 'utf-8' >>> r.text u'{"type":"User"...' >>> r.json() {u'private_gists': 419, u'total_private_repos': 77, ...}
Method 3: Paw or Postman
-
-
A tool that allows the user to create multiple isolated Python environments on one machine. (e.g. system uses v2.6, one app uses v3.5 and another v2.7.)
-
Keeps different project environments isolated and contained
-
Note: Make sure if you create a
.gitignore
file by using the commandtouch .gitignore
, and adding venv and secret (for your secret key(s)) to avoid checking in your virtualenv and secret key(s) into the repo by adding these two lines to.gitignore
.*venv* *secret*
-
For installation instructions and setting up virtualenv: http://flask.pocoo.org/docs/0.12/installation/#installation
-
-
Create a new virtual environment for your copy of the Highlands API. Most of the time, the python that comes with this is 2.7. You can designate what version of Python you want to develop in and more information about virtual environments using this.
-
Activate your virtual environment.
source venv/bin/activate
-
Perform a git clone of the boilerplate/base here.
git clone <todo: insert .git here>
-
cd
into the directory and findrequirements.txt
and install the requirements for this project into your virtual environment using the line below. For a guide on installingpip
, click here.pip install -r requirements.txt
-
Make sure that the current working directory is on your
PYTHONPATH
export PYTHONPATH=.:$PYTHONPATH
-
Start the app
python run.py
Cheatsheet here
Flask-Testing: unit testing utilities for Flask
unittest: unit testing framework
unittest2: unit testing framework
nose: extends unittest
doctest: writes tests in docstrings of a function
Example of TDD of a flask API with nose, flask-restful, flask-sqlalchemy
test client: pass a WSGI application (and response wrapper) to app for testing
Selenium: browser automation, "end to end" testing Alternatives to Selenium
Flask's own documentation on testing
coverage: measure code coverage
Types of Software Testing Credit Buzzle
HTTP Protocol Definitions Credit w3