1558801639
A RESTful API is an application program interface (API) that uses HTTP requests to GET, PUT, POST and DELETE data. In this tutorial, we’ll be learning and creating a RESTful APIs with Flask.
This article’s intention is to provide a easy-to-follow project-based process on how to create a RESTful API using the Flask framework.
A bit of context – I’ve written a bunch of articles on Django-driven RESTful APIs. Though a great resource for Django enthusiasts, not everyone wants to code in Django. Besides, it’s always good to acquaint yourself with other frameworks.
Learning Flask is easier and faster. It’s super easy to setup and get things running. Unlike Django (which is heavier), you’ll never have functionality lying around that you aren’t using.
Typical of all our web apps, we’ll use the TDD approach. It’s really simple. Here’s how we do Test Driven Development:
We’re going to develop an API for a bucketlist. A bucketlist is a list of all the goals you want to achieve, dreams you want to fulfill and life experiences you desire to experience before you die (or hit the bucket). The API shall therefore have the ability to:
Let’s start with configuring our Flask app structure!
First, we’ll create our application directory. On the terminal, create an empty directory called bucketlist with mkdir bucketlist
. Then, Cd
into the directory. Create an isolated virtual environment:
$ virtualenv venv
Install Autoenv globally using pip install autoenv
Here’s why – Autoenv helps us to set commands that will run every time we cd
into our directory. It reads the .env
file and executes for us whatever is in there.
Create a .env
file and add the following:
source env/bin/activate
export FLASK_APP="run.py"
export SECRET="some-very-long-string-of-random-characters-CHANGE-TO-YOUR-LIKING"
export APP_SETTINGS="development"
export DATABASE_URL="postgresql://localhost/flask_api"
The first line activates our virtual environment venv
that we just created. Line 2, 3 and 4 export our FLASK_APP, SECRET, APP_SETTINGS and DATABASE_URL
variables. We’ll integrate these variables as we progress through the development process.
The first line activates our virtual environment venv
that we just created. Line 2, 3 and 4 export our FLASK_APP, SECRET, APP_SETTINGS and DATABASE_URL
variables. We’ll integrate these variables as we progress through the development process.
Run the following to update and refresh your .bashrc:
$ echo "source `which activate.sh`" >> ~/.bashrc
$ source ~/.bashrc
You will see something like this on your terminal
Sometimes autoenv
might not work if you have zsh installed. A good workaround would be to simply source the .env
file and we are set.
$ source .env
Conversely, if you don’t want to automate things for the long run, you don’t have to use autoenv
. A simple export directly from the terminal would do.
$ export FLASK_APP="run.py"
$ export APP_SETTINGS="development"
$ export SECRET="a-long-string-of-random-characters-CHANGE-TO-YOUR-LIKING"
$ export DATABASE_URL="postgresql://localhost/flask_api"
Inside our virtual environment, we’ll create a bunch of files to lay out our app directory stucture. Here’s what it should look like:
├── bucketlist (this is the directory we cd into)
├── app
│ ├── __init__.py
│ └── models.py
├── instance
│ └── __init__.py
├── manage.py
├── requirements.txt
├── run.py
└── test_bucketlist.py
After doing this, install Flask using pip.
(venv)$ pip install flask
Flask needs some sought of configuration to be available before the app starts. Since environments (development, production or testing) require specific settings to be configured, we’ll have to set environment-specific things such as a secret key
, debug mode
and test mode
in our configurations file.
If you haven’t already, create a directory and call it instance. Inside this directory, create a file called config.py and also init.py. Inside our config file, we’ll add the following code:
# /instance/config.py
import os
class Config(object):
"""Parent configuration class."""
DEBUG = False
CSRF_ENABLED = True
SECRET = os.getenv('SECRET')
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL')
class DevelopmentConfig(Config):
"""Configurations for Development."""
DEBUG = True
class TestingConfig(Config):
"""Configurations for Testing, with a separate test database."""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/test_db'
DEBUG = True
class StagingConfig(Config):
"""Configurations for Staging."""
DEBUG = True
class ProductionConfig(Config):
"""Configurations for Production."""
DEBUG = False
TESTING = False
app_config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'staging': StagingConfig,
'production': ProductionConfig,
}
The Config
class contains the general settings that we want all environments to have by default. Other environment classes inherit from it and can be used to set settings that are only unique to them. Additionally, the dictionary app_config
is used to export the 4 environments we’ve specified. It’s convenient to have it so that we can import the config under its name tag in future.
A couple of config variables to note:
True
, allowing us to use the Flask debugger. It also automagically reloads the application when it’s updated. However, it should be set to False
in production.The tools we need for our database to be up and running are:
We might have used a easy-to-setup database such as SQLite. But since we want to learn something new, powerful and awesome, we’ll go with PostgreSQL.
SQLAlchemy is our Object Relational Mapper (ORM). Why should we use an ORM, you ask? An ORM converts the raw SQL data (called querysets) into data we can understand called objects in a process called serialization
and vice versa (deserialization). Instead of painstakingly writing complex raw SQL queries, why not use a tested tool developed just for this purpose?
Let’s install the requirements as follows:
(venv)$ pip install flask-sqlalchemy psycopg2 flask-migrate
Ensure you have installed PostgresSQL in your computer and it’s server is running locally on port 5432
In your terminal, create a Postgres database:
(venv) $ createdb test_db
(venv) $ createdb flask_api
Createdb is a wrapper around the SQL command CREATE DATABASE
. We created
test_db
for our testing environment.flask_api
for development environment.We’ve used two databases so that we do not interfere with the integrity of our main database when running our tests.
It’s time to right some code! Since we are creating an API, we’ll install Flask API extension.
(venv)$ pip install Flask-API
Flask API is an implementation of the same web browsable APIs that Django REST framework provides. It’ll helps us implement our own browsable API.
In our empty app/__init__.py
file, we’ll add the following:
# app/__init__.py
from flask_api import FlaskAPI
from flask_sqlalchemy import SQLAlchemy
# local import
from instance.config import app_config
# initialize sql-alchemy
db = SQLAlchemy()
def create_app(config_name):
app = FlaskAPI(__name__, instance_relative_config=True)
app.config.from_object(app_config[config_name])
app.config.from_pyfile('config.py')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
return app
The create_app
function wraps the creation of a new Flask object, and returns it after it’s loaded up with configuration settings using app.config
and connected to the DB using db.init_app(app)
.
We’ve also disabled track modifications for SQLAlchemy because it’ll be deprecated in future due to it’s significant performance overhead. For debugging enthusiasts, you can set it to True
for now.
Now, we need to define an entry point to start our app. Let’s edit the run.py
file.
import os
from app import create_app
config_name = os.getenv('APP_SETTINGS') # config_name = "development"
app = create_app(config_name)
if __name__ == '__main__':
app.run()
Now we can run the application on our terminal to see if it works:
(venv)$ flask run
We can also run it using python run.py
. We should see something like this:
It’s time to create our bucketlist model. A model is a representation of a table in a database. Add the following inside the empty models.py
file:
# app/models.py
from app import db
class Bucketlist(db.Model):
"""This class represents the bucketlist table."""
__tablename__ = 'bucketlists'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
date_created = db.Column(db.DateTime, default=db.func.current_timestamp())
date_modified = db.Column(
db.DateTime, default=db.func.current_timestamp(),
onupdate=db.func.current_timestamp())
def __init__(self, name):
"""initialize with name."""
self.name = name
def save(self):
db.session.add(self)
db.session.commit()
@staticmethod
def get_all():
return Bucketlist.query.all()
def delete(self):
db.session.delete(self)
db.session.commit()
def __repr__(self):
return "".format(self.name)
Here’s what we’ve done in the models.py
file:
db
connection from the app/__init__.py
.Bucketlist
class that inherits from db.Model
and assigned a table. name bucketlists
(it should always be plural). We’ve therefore created a table to store our bucketlists.id
field contains the primary key, the name
field will store the name of the bucketlist.__repr__
method represents the object instance of the model whenever it is queries.get_all()
method is a static method that’ll be used to get all the bucketlists in a single query.save()
method will be used to add a new bucketlist to the DB.delete()
method will be used to delete an existing bucketlist from the DB.Migrations is a way of propagating changes we make to our models (like adding a field, deleting a model, etc.) into your database schema. Now that we’ve a defined model in place, we need to tell the database to create the relevant schema.
Flask-Migrate uses Alembic to autogenerate migrations for us. It will serve this purpose.
A migration script will conveniently help us make and apply migrations everytime we edit our models. It’s good practice to separate migration tasks and not mix them with the code in our app.
That said, we’ll create a new file called manage.py.
Our directory structure should now look like this:
├── bucketlist
├── app
│ ├── __init__.py
│ └── models.py
├── instance
│ ├── __init__.py
│ └── config.py
├── manage.py
├── requirements.txt
├── run.py
└── test_bucketlist.py
Add the following code to manage.py
:
# manage.py
import os
from flask_script import Manager # class for handling a set of commands
from flask_migrate import Migrate, MigrateCommand
from app import db, create_app
from app import models
app = create_app(config_name=os.getenv('APP_SETTINGS'))
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
The Manager
class keeps track of all the commands and handles how they are called from the command line. The MigrateCommand contains a set of migration commands. We’ve also imported the models so that the script can find the models to be migrated. The manager also adds the migration commands and enforces that they start with db
.
We will run migrations initialization, using the db init
command as follows:
(venv)$ python manage.py db init
You’ll notice a newly created folder called migrations. This holds the setup necessary for running migrations. Inside of “migrations” is a folder called “versions”, which will contain the migration scripts as they are created.
Next, we’ll run the actual migrations using the db migrate
command:
(venv)$ python manage.py db migrate
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'results'
Generating /bucketlist/migrations/versions/63dba2060f71_.py
...done
You’ll also notice that in your versions folder there is a migration file. This file is auto-generated by Alembic based on the model.
Finally, we’ll apply the migrations to the database using the db upgrade
command:
(venv)$ python manage.py db upgrade
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 536e84635828, empty message
Our DB is now updated with our bucketlists
table. If you jump into the psql
prompt, here’s a screenshot on how you can confirm if the table exists:
Inside our tests directory, let’s create tests. Creating tests that fail is the first step of TD.(Failing is good). These tests will help guide us in creating our functionality. It might seem daunting at first to write tests but it’s really easy once you get practicing.
On the parent directory, create a test file called test_bucketlist.py
. This file will contain the following:
# test_bucketlist.py
import unittest
import os
import json
from app import create_app, db
class BucketlistTestCase(unittest.TestCase):
"""This class represents the bucketlist test case"""
def setUp(self):
"""Define test variables and initialize app."""
self.app = create_app(config_name="testing")
self.client = self.app.test_client
self.bucketlist = {'name': 'Go to Borabora for vacation'}
# binds the app to the current context
with self.app.app_context():
# create all tables
db.create_all()
def test_bucketlist_creation(self):
"""Test API can create a bucketlist (POST request)"""
res = self.client().post('/bucketlists/', data=self.bucketlist)
self.assertEqual(res.status_code, 201)
self.assertIn('Go to Borabora', str(res.data))
def test_api_can_get_all_bucketlists(self):
"""Test API can get a bucketlist (GET request)."""
res = self.client().post('/bucketlists/', data=self.bucketlist)
self.assertEqual(res.status_code, 201)
res = self.client().get('/bucketlists/')
self.assertEqual(res.status_code, 200)
self.assertIn('Go to Borabora', str(res.data))
def test_api_can_get_bucketlist_by_id(self):
"""Test API can get a single bucketlist by using it's id."""
rv = self.client().post('/bucketlists/', data=self.bucketlist)
self.assertEqual(rv.status_code, 201)
result_in_json = json.loads(rv.data.decode('utf-8').replace("'", "\""))
result = self.client().get(
'/bucketlists/{}'.format(result_in_json['id']))
self.assertEqual(result.status_code, 200)
self.assertIn('Go to Borabora', str(result.data))
def test_bucketlist_can_be_edited(self):
"""Test API can edit an existing bucketlist. (PUT request)"""
rv = self.client().post(
'/bucketlists/',
data={'name': 'Eat, pray and love'})
self.assertEqual(rv.status_code, 201)
rv = self.client().put(
'/bucketlists/1',
data={
"name": "Dont just eat, but also pray and love :-)"
})
self.assertEqual(rv.status_code, 200)
results = self.client().get('/bucketlists/1')
self.assertIn('Dont just eat', str(results.data))
def test_bucketlist_deletion(self):
"""Test API can delete an existing bucketlist. (DELETE request)."""
rv = self.client().post(
'/bucketlists/',
data={'name': 'Eat, pray and love'})
self.assertEqual(rv.status_code, 201)
res = self.client().delete('/bucketlists/1')
self.assertEqual(res.status_code, 200)
# Test to see if it exists, should return a 404
result = self.client().get('/bucketlists/1')
self.assertEqual(result.status_code, 404)
def tearDown(self):
"""teardown all initialized variables."""
with self.app.app_context():
# drop all tables
db.session.remove()
db.drop_all()
# Make the tests conveniently executable
if __name__ == "__main__":
unittest.main()
A bit of testing explanation. Inside the test_bucketlist_creation(self)
we make a POST request using a test client to the /bucketlists/
url. The return value is obtained and its status code is asserted to be equal to a status code of 201(Created)
. If it’s equal to 201, the test assertion is true, making the test pass. Finally, it checks whether the returned response contains the name of the bucketlist we just created. This is done using self.assertIn(a, b)
If the assertion evaluates to true, the test passes.
Now we’ll run the test as follows:
(venv)$ python test_bucketlist.py
All the tests must fail. Now don’t be scared. This is good because we have no functionality to make the test pass. Now’s the time to create the API functionality that will make our tests pass.
Our API is supposed to handle four HTTP requests
Let’s get this done straight away. Inside our app/__init__.py
file, we’ll edit it as follows:
# app/__init__.py
# existing import remains
from flask import request, jsonify, abort
def create_app(config_name):
from api.models import Bucketlist
#####################
# existing code remains #
#####################
@app.route('/bucketlists/', methods=['POST', 'GET'])
def bucketlists():
if request.method == "POST":
name = str(request.data.get('name', ''))
if name:
bucketlist = Bucketlist(name=name)
bucketlist.save()
response = jsonify({
'id': bucketlist.id,
'name': bucketlist.name,
'date_created': bucketlist.date_created,
'date_modified': bucketlist.date_modified
})
response.status_code = 201
return response
else:
# GET
bucketlists = Bucketlist.get_all()
results = []
for bucketlist in bucketlists:
obj = {
'id': bucketlist.id,
'name': bucketlist.name,
'date_created': bucketlist.date_created,
'date_modified': bucketlist.date_modified
}
results.append(obj)
response = jsonify(results)
response.status_code = 200
return response
return app
We’ve imported
request
for handling our requests.jsonify
to turn the JSON output into a Response object with the application/json mimetype.abort
which will abort a request with an HTTP error code early.We’ve also added an import from api.models import Bucketlist
immediately inside the create_app
method so that we get access to the Bucketlist model while preventing the horror of circular imports. Flask provides an @app.route
decorator on top of the new function def bucketlists()
which enforces us to only accepts GET and POST requests. Our function first checks the type of request it receives. If it’s a POST
, it creates a bucketlist by extracting the name
from the request and saves it using the save()
method we defined in our model. It consequently returns the newly created bucketlist as a JSON object. If it’s a GET
request, it gets all the bucketlists from the bucketlists table and returns a list of bucketlists as JSON objects. If there’s no bucketlist on our table, it will return an empty JSON object {}
.
Now let’s see if our new GET
and POST
functionality makes our tests pass.
Run the tests as follows:
(venv)$ python test_bucketlists.py
2 out of 5 tests should pass. We’ve now handled the GET and POST requests successfully.
At this moment, our API can only create and get all the bucketlists. It cannot get a single bucketlist using its bucketlist ID. Also, it can neither edit a bucketlist nor delete it from the DB. To complete it, we’d want to add these functionalities.
On our app/__init__.py
file, let’s edit as follows:
# app/__init__.py
# existing import remains
def create_app(config_name):
#####################
# existing code remains #
#####################
###################################
# The GET and POST code is here
###################################
@app.route('/bucketlists/', methods=['GET', 'PUT', 'DELETE'])
def bucketlist_manipulation(id, **kwargs):
# retrieve a buckelist using it's ID
bucketlist = Bucketlist.query.filter_by(id=id).first()
if not bucketlist:
# Raise an HTTPException with a 404 not found status code
abort(404)
if request.method == 'DELETE':
bucketlist.delete()
return {
"message": "bucketlist {} deleted successfully".format(bucketlist.id)
}, 200
elif request.method == 'PUT':
name = str(request.data.get('name', ''))
bucketlist.name = name
bucketlist.save()
response = jsonify({
'id': bucketlist.id,
'name': bucketlist.name,
'date_created': bucketlist.date_created,
'date_modified': bucketlist.date_modified
})
response.status_code = 200
return response
else:
# GET
response = jsonify({
'id': bucketlist.id,
'name': bucketlist.name,
'date_created': bucketlist.date_created,
'date_modified': bucketlist.date_modified
})
response.status_code = 200
return response
return app
We’ve now defined a new function def bucketlist_manipulation()
which uses a decorator that enforces it to only handle GET, PUT and DELETE
Http requests. We query the db to filter using an id of the given bucketlist we want to access. If there’s no bucketlist, it aborts
and returns a 404 Not Found
status. The second if-elif-else code blocks handle deleting, updating or getting a bucketlist respectively.
Now, we expect all the tests to pass. Let’s run them and see if all of them actually pass.
(venv)$ python test_bucketlist.py
We should now see all the test passing.
Fire up Postman. Key in [http://localhost:5000/bucketlists/](http://localhost:5000/bucketlists/)
and send a POST request with a name as the payload. We should get a response like this:
We can play around with Curl as well to see it working from our terminal:
We intend to allow bucketlists to be owned by users. For now, anyone can manipulate a bucketlist even if they did not create it. We’ve got to fix this security hole.
How do we keep track of users, you ask? We define a model.
# app/models.py
from app import db
from flask_bcrypt import Bcrypt
class User(db.Model):
"""This class defines the users table """
__tablename__ = 'users'
# Define the columns of the users table, starting with the primary key
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(256), nullable=False, unique=True)
password = db.Column(db.String(256), nullable=False)
bucketlists = db.relationship(
'Bucketlist', order_by='Bucketlist.id', cascade="all, delete-orphan")
def __init__(self, email, password):
"""Initialize the user with an email and a password."""
self.email = email
self.password = Bcrypt().generate_password_hash(password).decode()
def password_is_valid(self, password):
"""
Checks the password against it's hash to validates the user's password
"""
return Bcrypt().check_password_hash(self.password, password)
def save(self):
"""Save a user to the database.
This includes creating a new user and editing one.
"""
db.session.add(self)
db.session.commit()
class Bucketlist(db.Model):
"""This class defines the bucketlist table."""
__tablename__ = 'bucketlists'
# define the columns of the table, starting with its primary key
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
date_created = db.Column(db.DateTime, default=db.func.current_timestamp())
date_modified = db.Column(
db.DateTime, default=db.func.current_timestamp(),
onupdate=db.func.current_timestamp())
created_by = db.Column(db.Integer, db.ForeignKey(User.id))
def __init__(self, name, created_by):
"""Initialize the bucketlist with a name and its creator."""
self.name = name
self.created_by = created_by
def save(self):
"""Save a bucketlist.
This applies for both creating a new bucketlist
and updating an existing onupdate
"""
db.session.add(self)
db.session.commit()
@staticmethod
def get_all(user_id):
"""This method gets all the bucketlists for a given user."""
return Bucketlist.query.filter_by(created_by=user_id)
def delete(self):
"""Deletes a given bucketlist."""
db.session.delete(self)
db.session.commit()
def __repr__(self):
"""Return a representation of a bucketlist instance."""
return "".format(self.name)
Here’s what we’ve done:
One-to-Many
relationship between the two tables. We defined this relationship by adding the db.relationship() function on the User table (parent table)cascade="all, delete-orphan"
will delete all bucketlists when a referenced user is deleted.generate_password_hash(pasword)
. This will make our users password be secure from dictionary and brute force attacks.get_all()
method to get all the bucketlists for a given user.Don’t forget to install Flask-Bcrypt
(venv)$ pip install flask-bcrypt
Migrate the changes we’ve just made to the db
(venv)$ python manage.py db migrate
(venv)$ python manage.py db upgrade
Now we have a user table to keep track of registered users.
Our app will have many tests from now on. It’s best practice to have a test folder that will houses all our tests. We’ll create a folder called tests. Inside this folder, we’ll move our test_bucketlists.py file into it.
Our directory structure should now look like this:
├── bucketlist
├── app
│ ├── __init__.py
│ └── models.py
├── instance
│ ├── __init__.py
│ └── config.py
├── manage.py
├── requirements.txt
├── run.py
├── tests
│ └── test_bucketlist.py
Also, we’ll edit the manage.py
as follows:
import os
import unittest
# class for handling a set of commands
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from app import db, create_app
# initialize the app with all its configurations
app = create_app(config_name=os.getenv('APP_SETTINGS'))
migrate = Migrate(app, db)
# create an instance of class that will handle our commands
manager = Manager(app)
# Define the migration command to always be preceded by the word "db"
# Example usage: python manage.py db init
manager.add_command('db', MigrateCommand)
# define our command for testing called "test"
# Usage: python manage.py test
@manager.command
def test():
"""Runs the unit tests without test coverage."""
tests = unittest.TestLoader().discover('./tests', pattern='test*.py')
result = unittest.TextTestRunner(verbosity=2).run(tests)
if result.wasSuccessful():
return 0
return 1
if __name__ == '__main__':
manager.run()
The decorator on top of test()
allows us to define a command called test
. Inside the function, we load the tests from the tests folder using the TestLoader()
class and then run them with TextTestRunner.run()
. If it’s successful, we exit gracefully with a return 0
.
Let’s test it out on our terminal.
(venv)$ python manage.py test
The tests should fail. This is because we’ve not modified our code to work with the new changes in the model.
From now on, we’ll use this command to run our tests.
Token-based authentication is a security technique that authenticates users who attempt to login to a server using a security token provided by the server. Without the token, a user won’t be granted access to restricted resources. You can find more intricate details about token based authentication here
For us to implement this authentication, we’ll use a Python package called PyJWT.
PyJWT allows us to encode and decode JSON Web Tokens (JWT). That being said, let’s install it:
(venv)$ pip install PyJWT
For our users to authenticate, the access token is going to be placed in the Authorization HTTP header in all our bucketlist requests.
Here’s how the header looks like:
Authorization: "Bearer <The-access-token-is-here>"
We’ll put the word Bearer
before the token and separate them with a space character.
Don’t forget the space in between the Bearer
and the token.
We need to create a way to encode the token before it’s sent to the user. We also need to have a way to decode the token when the user sends it via the Authorization header.
In our model.py we’ll create a function inside our User model
to generate the token and another one to decode it. Let’s add the following code:
# /app/models.py
## previous imports ###
import jwt
from datetime import datetime, timedelta
class User(db.Model):
"""Maps to users table """
__tablename__ = 'users'
###########################################
## Existing code for defining table columns is here ##
###########################################
def __init__(self, email, password):
#### INIT CODE LIES HERE ###################
###########################################
def password_is_valid(self, password):
##### PASSWORD CHECK CODE LIES HERE ####
###########################################
def save(self):
######### CODE FOR SAVING USER LIES HERE ##
############################################
def generate_token(self, user_id):
""" Generates the access token"""
try:
# set up a payload with an expiration time
payload = {
'exp': datetime.utcnow() + timedelta(minutes=5),
'iat': datetime.utcnow(),
'sub': user_id
}
# create the byte string token using the payload and the SECRET key
jwt_string = jwt.encode(
payload,
current_app.config.get('SECRET'),
algorithm='HS256'
)
return jwt_string
except Exception as e:
# return an error in string format if an exception occurs
return str(e)
@staticmethod
def decode_token(token):
"""Decodes the access token from the Authorization header."""
try:
# try to decode the token using our SECRET variable
payload = jwt.decode(token, current_app.config.get('SECRET'))
return payload['sub']
except jwt.ExpiredSignatureError:
# the token is expired, return an error string
return "Expired token. Please login to get a new token"
except jwt.InvalidTokenError:
# the token is invalid, return an error string
return "Invalid token. Please register or login"
The generate_token()
takes in a user ID as an argument, uses jwt
to create a token using the secret key, and makes it time-based by defining its expiration time. The token is valid for 5 minutes as specified in the timedelta. You can set it to your liking.
The decode_token()
takes in a token as an argument and checks whether the token is valid. If it is, it returns the user ID as the payload. It returns an error messsage if the token is expired or invalid.
Don’t forget to import jwt and the datetime above.
Our app is growing bigger. We’ll have to organize it into components. Flask uses the concept of **Blueprints ** to make application components.
Blueprints are simply a set of operations that can be registered on a given app. Think of it as an extension of the app that can address a specific functionality.
We’ll create an authentication blueprint.
This blueprint will focus on handling user registration and logins.
Inside our /app directory create a folder and call it auth.
Our auth folder should contain:
One-to-Many
relationship between the two tables. We defined this relationship by adding the db.relationship() function on the User table (parent table)cascade="all, delete-orphan"
will delete all bucketlists when a referenced user is deleted.generate_password_hash(pasword)
. This will make our users password be secure from dictionary and brute force attacks.get_all()
method to get all the bucketlists for a given user.In our auth/__init__.py
file, initialize a blueprint.
# auth/__init__.py
from flask import Blueprint
# This instance of a Blueprint that represents the authentication blueprint
auth_blueprint = Blueprint('auth', __name__)
from . import views
Then import the blueprint and register it at the bottom of the app/__init__.py
, just before the return app
line.
# app/__init__.py
# imports lie here
def create_app(config_name):
#####################################################
### Existing code for intializing the app with its configurations ###
#####################################################
@app.route('/bucketlists/', methods=['GET', 'PUT', 'DELETE'])
def bucketlist_manipulation(id, **kwargs):
#########################################################
### Existing code for creating, updating and deleting a bucketlist #####
#########################################################
...
# import the authentication blueprint and register it on the app
from .auth import auth_blueprint
app.register_blueprint(auth_blueprint)
return app
Testing should never be an afterthought. It should always come first.
We’re going to add a new test file that will house all our tests for the authentication blueprint.
It’ll test whether our API can handle user registration, user login and access-token generation.
In our tests directory, create a file naming it test_auth.py. Write the following code in it:
# /tests/test_auth.py
import unittest
import json
from app import create_app, db
class AuthTestCase(unittest.TestCase):
"""Test case for the authentication blueprint."""
def setUp(self):
"""Set up test variables."""
self.app = create_app(config_name="testing")
# initialize the test client
self.client = self.app.test_client
# This is the user test json data with a predefined email and password
self.user_data = {
'email': 'test@example.com',
'password': 'test_password'
}
with self.app.app_context():
# create all tables
db.session.close()
db.drop_all()
db.create_all()
def test_registration(self):
"""Test user registration works correcty."""
res = self.client().post('/auth/register', data=self.user_data)
# get the results returned in json format
result = json.loads(res.data.decode())
# assert that the request contains a success message and a 201 status code
self.assertEqual(result['message'], "You registered successfully.")
self.assertEqual(res.status_code, 201)
def test_already_registered_user(self):
"""Test that a user cannot be registered twice."""
res = self.client().post('/auth/register', data=self.user_data)
self.assertEqual(res.status_code, 201)
second_res = self.client().post('/auth/register', data=self.user_data)
self.assertEqual(second_res.status_code, 202)
# get the results returned in json format
result = json.loads(second_res.data.decode())
self.assertEqual(
result['message'], "User already exists. Please login.")
We’ve initialized our test with a test client for making requests to our API and some test data.
The first test function test_registration()
sends a post request to /auth/register
and tests the response it gets. It ensures that the status code is 201, meaning we’ve successfully created a user.
The second test function tests whether the API can only register a user once. Having duplicates in the database is bad for business.
Now let’s run the tests using python manage.py test
. The tests should fail.
----------------------------------------------------------------------
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
The reason our tests fail is simply because we lack the functionality they need to test. Let’s implement something that’ll make these two tests to pass.
Open up the views.py file and add the following code:
# /app/auth/views.py
from . import auth_blueprint
from flask.views import MethodView
from flask import make_response, request, jsonify
from app.models import User
class RegistrationView(MethodView):
"""This class registers a new user."""
def post(self):
"""Handle POST request for this view. Url ---> /auth/register"""
# Query to see if the user already exists
user = User.query.filter_by(email=request.data['email']).first()
if not user:
# There is no user so we'll try to register them
try:
post_data = request.data
# Register the user
email = post_data['email']
password = post_data['password']
user = User(email=email, password=password)
user.save()
response = {
'message': 'You registered successfully. Please log in.'
}
# return a response notifying the user that they registered successfully
return make_response(jsonify(response)), 201
except Exception as e:
# An error occured, therefore return a string message containing the error
response = {
'message': str(e)
}
return make_response(jsonify(response)), 401
else:
# There is an existing user. We don't want to register users twice
# Return a message to the user telling them that they they already exist
response = {
'message': 'User already exists. Please login.'
}
return make_response(jsonify(response)), 202
registration_view = RegistrationView.as_view('register_view')
# Define the rule for the registration url ---> /auth/register
# Then add the rule to the blueprint
auth_blueprint.add_url_rule(
'/auth/register',
view_func=registration_view,
methods=['POST'])
Here’s what we have added:
One-to-Many
relationship between the two tables. We defined this relationship by adding the db.relationship() function on the User table (parent table)cascade="all, delete-orphan"
will delete all bucketlists when a referenced user is deleted.generate_password_hash(pasword)
. This will make our users password be secure from dictionary and brute force attacks.get_all()
method to get all the bucketlists for a given user.Let’s run our tests once more. Only the AuthTestCase tests should pass. The bucketlist tests still fail because we havent modified the
init.py` code.
test_already_registered_user (test_auth.AuthTestCase)
Test that a user cannot be registered twice. ... ok
test_registration (test_auth.AuthTestCase)
Test user registration works correcty. ... ok
Bucketlist failed tests fall here
----------------------------------------------------------------------
(venv)
We’ll test our registration functionality by making a request using Postman.
But before we make the requests, ensure the API is up and running.
(venv) $ python run.py development
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 225-021-817
Now you can make a POST request to localhost:5000/auth/register
. Specify an email and a password of your choice to represent the user we are registering. Click send.
A user will have to login to gain access to our API. Currently, we are lacking this login functionality. Let’s start with some tests. We’ll add two more tests at the bottom of our test_auth.py
as follows:
# tests/test_auth.py
class AuthTestCase(unittest.TestCase):
"""Test case for the authentication blueprint."""
def setUp(self):
#### EXISTING CODE FOR SETUP LIES HERE ####
def test_registration(self):
#### EXISTING TEST CODE LIES HERE ####
def test_already_registered_user(self):
### EXISTING TEST CODE LIES HERE #####
def test_user_login(self):
"""Test registered user can login."""
res = self.client().post('/auth/register', data=self.user_data)
self.assertEqual(res.status_code, 201)
login_res = self.client().post('/auth/login', data=self.user_data)
# get the results in json format
result = json.loads(login_res.data.decode())
# Test that the response contains success message
self.assertEqual(result['message'], "You logged in successfully.")
# Assert that the status code is equal to 200
self.assertEqual(login_res.status_code, 200)
self.assertTrue(result['access_token'])
def test_non_registered_user_login(self):
"""Test non registered users cannot login."""
# define a dictionary to represent an unregistered user
not_a_user = {
'email': 'not_a_user@example.com',
'password': 'nope'
}
# send a POST request to /auth/login with the data above
res = self.client().post('/auth/login', data=not_a_user)
# get the result in json
result = json.loads(res.data.decode())
# assert that this response must contain an error message
# and an error status code 401(Unauthorized)
self.assertEqual(res.status_code, 401)
self.assertEqual(
result['message'], "Invalid email or password, Please try again")
The test_user_login()
function tests whether our API can successfully login a registered user. It also tests for the access token.
The other test function test_non_registered_user_login()
tests whether our API can restrict signing in to only registered users.
Again, we’ll make the tests pass by implementing its functionality. Let’s create the login view.
from . import auth_blueprint
from flask.views import MethodView
from flask import Blueprint, make_response, request, jsonify
from app.models import User
class RegistrationView(MethodView):
"""This class-based view registers a new user."""
#### EXISTING REGISTRATION CODE HERE ####
##########################################
class LoginView(MethodView):
"""This class-based view handles user login and access token generation."""
def post(self):
"""Handle POST request for this view. Url ---> /auth/login"""
try:
# Get the user object using their email (unique to every user)
user = User.query.filter_by(email=request.data['email']).first()
# Try to authenticate the found user using their password
if user and user.password_is_valid(request.data['password']):
# Generate the access token. This will be used as the authorization header
access_token = user.generate_token(user.id)
if access_token:
response = {
'message': 'You logged in successfully.',
'access_token': access_token.decode()
}
return make_response(jsonify(response)), 200
else:
# User does not exist. Therefore, we return an error message
response = {
'message': 'Invalid email or password, Please try again'
}
return make_response(jsonify(response)), 401
except Exception as e:
# Create a response containing an string error message
response = {
'message': str(e)
}
# Return a server error using the HTTP Error Code 500 (Internal Server Error)
return make_response(jsonify(response)), 500
# Define the API resource
registration_view = RegistrationView.as_view('registration_view')
login_view = LoginView.as_view('login_view')
# Define the rule for the registration url ---> /auth/register
# Then add the rule to the blueprint
auth_blueprint.add_url_rule(
'/auth/register',
view_func=registration_view,
methods=['POST'])
# Define the rule for the registration url ---> /auth/login
# Then add the rule to the blueprint
auth_blueprint.add_url_rule(
'/auth/login',
view_func=login_view,
methods=['POST']
)
Here, we’ve defined a class-based view just like we did on the registration section.
It dispatches the POST
request to the post()
method as well. This is to capture the user credentials(email, password) when they login. It checks whether the password given is valid, generates an access token for the user and returns a response containing the token.
We’ve also handled exceptions gracefully so that if one occurs, our API will continue running and won’t crush.
Finally, we defined a url for the login route.
Make a POST request. Input the email and password we specified for the user during registration. Click send. You should get an access token in the JSON response.
If you run the tests, you will notice that the login tests pass, but the bucketlist one still fail. It’s time to refactor this tests.
First, we’ll create two helper functions for registering and signing in our test user.
# tests/test_bucketlist.py
class BucketlistTestCase(unittest.TestCase):
"""This class represents the bucketlist test case"""
def setUp(self):
"""Set up test variables."""
#### SETUP VARIABLES ARE HERE #####
####################################
def register_user(self, email="user@test.com", password="test1234"):
"""This helper method helps register a test user."""
user_data = {
'email': email,
'password': password
}
return self.client().post('/auth/register', data=user_data)
def login_user(self, email="user@test.com", password="test1234"):
"""This helper method helps log in a test user."""
user_data = {
'email': email,
'password': password
}
return self.client().post('/auth/login', data=user_data)
############################################
##### ALL OUR TESTS METHODS LIE HERE #######
# Make the tests conveniently executable
if __name__ == "__main__":
unittest.main()
We do this so that when we want to register or login a test user (which is in all the tests), we don’t have to repeat ourselves. We’ll simply call the function and we are set.
Next, we’ll define a way to get the access token and add it to the Authorization header in all our client requests. Here’s a code snippet of the how were going to do it.
def test_bucketlist_creation(self):
"""Test the API can create a bucketlist (POST request)"""
# register a test user, then log them in
self.register_user():
result = self.login_user()
# obtain the access token
access_token = json.loads(result.data.decode())['access_token']
# ensure the request has an authorization header set with the access token in it
res = self.client().post(
'/bucketlists/',
headers=dict(Authorization="Bearer " + access_token),
data=self.bucketlist)
We can now go ahead and refactor the whole test_bucketlist.py
file. After refactoring all our request, we should have something like this:
import unittest
import os
import json
from app import create_app, db
class BucketlistTestCase(unittest.TestCase):
"""This class represents the bucketlist test case"""
def setUp(self):
"""Define test variables and initialize app."""
self.app = create_app(config_name="testing")
self.client = self.app.test_client
self.bucketlist = {'name': 'Go to Borabora for vacay'}
# binds the app to the current context
with self.app.app_context():
# create all tables
db.session.close()
db.drop_all()
db.create_all()
def register_user(self, email="user@test.com", password="test1234"):
user_data = {
'email': email,
'password': password
}
return self.client().post('/auth/register', data=user_data)
def login_user(self, email="user@test.com", password="test1234"):
user_data = {
'email': email,
'password': password
}
return self.client().post('/auth/login', data=user_data)
def test_bucketlist_creation(self):
"""Test API can create a bucketlist (POST request)"""
self.register_user()
result = self.login_user()
access_token = json.loads(result.data.decode())['access_token']
# create a bucketlist by making a POST request
res = self.client().post(
'/bucketlists/',
headers=dict(Authorization="Bearer " + access_token),
data=self.bucketlist)
self.assertEqual(res.status_code, 201)
self.assertIn('Go to Borabora', str(res.data))
def test_api_can_get_all_bucketlists(self):
"""Test API can get a bucketlist (GET request)."""
self.register_user()
result = self.login_user()
access_token = json.loads(result.data.decode())['access_token']
# create a bucketlist by making a POST request
res = self.client().post(
'/bucketlists/',
headers=dict(Authorization="Bearer " + access_token),
data=self.bucketlist)
self.assertEqual(res.status_code, 201)
# get all the bucketlists that belong to the test user by making a GET request
res = self.client().get(
'/bucketlists/',
headers=dict(Authorization="Bearer " + access_token),
)
self.assertEqual(res.status_code, 200)
self.assertIn('Go to Borabora', str(res.data))
def test_api_can_get_bucketlist_by_id(self):
"""Test API can get a single bucketlist by using it's id."""
self.register_user()
result = self.login_user()
access_token = json.loads(result.data.decode())['access_token']
rv = self.client().post(
'/bucketlists/',
headers=dict(Authorization="Bearer " + access_token),
data=self.bucketlist)
# assert that the bucketlist is created
self.assertEqual(rv.status_code, 201)
# get the response data in json format
results = json.loads(rv.data.decode())
result = self.client().get(
'/bucketlists/{}'.format(results['id']),
headers=dict(Authorization="Bearer " + access_token))
# assert that the bucketlist is actually returned given its ID
self.assertEqual(result.status_code, 200)
self.assertIn('Go to Borabora', str(result.data))
def test_bucketlist_can_be_edited(self):
"""Test API can edit an existing bucketlist. (PUT request)"""
self.register_user()
result = self.login_user()
access_token = json.loads(result.data.decode())['access_token']
# first, we create a bucketlist by making a POST request
rv = self.client().post(
'/bucketlists/',
headers=dict(Authorization="Bearer " + access_token),
data={'name': 'Eat, pray and love'})
self.assertEqual(rv.status_code, 201)
# get the json with the bucketlist
results = json.loads(rv.data.decode())
# then, we edit the created bucketlist by making a PUT request
rv = self.client().put(
'/bucketlists/{}'.format(results['id']),
headers=dict(Authorization="Bearer " + access_token),
data={
"name": "Dont just eat, but also pray and love :-)"
})
self.assertEqual(rv.status_code, 200)
# finally, we get the edited bucketlist to see if it is actually edited.
results = self.client().get(
'/bucketlists/{}'.format(results['id']),
headers=dict(Authorization="Bearer " + access_token))
self.assertIn('Dont just eat', str(results.data))
def test_bucketlist_deletion(self):
"""Test API can delete an existing bucketlist. (DELETE request)."""
self.register_user()
result = self.login_user()
access_token = json.loads(result.data.decode())['access_token']
rv = self.client().post(
'/bucketlists/',
headers=dict(Authorization="Bearer " + access_token),
data={'name': 'Eat, pray and love'})
self.assertEqual(rv.status_code, 201)
# get the bucketlist in json
results = json.loads(rv.data.decode())
# delete the bucketlist we just created
res = self.client().delete(
'/bucketlists/{}'.format(results['id']),
headers=dict(Authorization="Bearer " + access_token),)
self.assertEqual(res.status_code, 200)
# Test to see if it exists, should return a 404
result = self.client().get(
'/bucketlists/1',
headers=dict(Authorization="Bearer " + access_token))
self.assertEqual(result.status_code, 404)
# Make the tests conveniently executable
if __name__ == "__main__":
unittest.main()
We’ll refactor the methods that handle the HTTP requests for bucketlist creation and getting all the bucketlists. Open up /app/init.py file and edit as follows:
# /app/__init__.py
## imports ##
from flask import request, jsonify, abort, make_response
def create_app(config_name):
from models import Bucketlist, User
###########################################
### EXISTING APP CONFIG CODE LIES HERE ###
###########################################
@app.route('/bucketlists/', methods=['POST', 'GET'])
def bucketlists():
# Get the access token from the header
auth_header = request.headers.get('Authorization')
access_token = auth_header.split(" ")[1]
if access_token:
# Attempt to decode the token and get the User ID
user_id = User.decode_token(access_token)
if not isinstance(user_id, str):
# Go ahead and handle the request, the user is authenticated
if request.method == "POST":
name = str(request.data.get('name', ''))
if name:
bucketlist = Bucketlist(name=name, created_by=user_id)
bucketlist.save()
response = jsonify({
'id': bucketlist.id,
'name': bucketlist.name,
'date_created': bucketlist.date_created,
'date_modified': bucketlist.date_modified,
'created_by': user_id
})
return make_response(response), 201
else:
# GET all the bucketlists created by this user
bucketlists = Bucketlist.query.filter_by(created_by=user_id)
results = []
for bucketlist in bucketlists:
obj = {
'id': bucketlist.id,
'name': bucketlist.name,
'date_created': bucketlist.date_created,
'date_modified': bucketlist.date_modified,
'created_by': bucketlist.created_by
}
results.append(obj)
return make_response(jsonify(results)), 200
else:
# user is not legit, so the payload is an error message
message = user_id
response = {
'message': message
}
return make_response(jsonify(response)), 401
We first added two imports: the User
model and the make_response
from Flask.
In the bucketlist function, we check for the authorization header from the request and extract the access token. Then, we decoded the token using User.decode_token(token)
to give us the payload. The payload is expected to be a user ID if the token is valid and not expired. If the token is not valid or expired, the payload will be an error message as a string.
Copy the token and paste it to the header section, creating an Authorization header. Don’t forget to put the word Bearer before the token with a space separating them like this:
Authorization: "Bearer dfg32r22349r40eiwoijr232394029wfopi23r2.2342..."
Make a POST request to localhost:5000/bucketlists/
, specifying the name of the bucketlist. Click send.
Ensure you’ve set the Authorization header just as we did for the POST request.
Make a GET request to localhost:5000/bucketlists/
and retrieve all the bucketlists our user just created.
We’ll refactor the PUT
and DELETE
functionality the same way we tackled the GET
and POST
.
# /app/__init__.py
## imports ##
from flask import request, jsonify, abort, make_response
def create_app(config_name):
from models import Bucketlist, User
############################################################
### Existing code for initializing the app with its configurations lies here ###
############################################################
@app.route('/bucketlists/', methods=['POST', 'GET'])
def bucketlists():
#### CODE FOR GET and POST LIES HERE#####
###############################
@app.route('/bucketlists/', methods=['GET', 'PUT', 'DELETE'])
def bucketlist_manipulation(id, **kwargs):
# get the access token from the authorization header
auth_header = request.headers.get('Authorization')
access_token = auth_header.split(" ")[1]
if access_token:
# Get the user id related to this access token
user_id = User.decode_token(access_token)
if not isinstance(user_id, str):
# If the id is not a string(error), we have a user id
# Get the bucketlist with the id specified from the URL ()
bucketlist = Bucketlist.query.filter_by(id=id).first()
if not bucketlist:
# There is no bucketlist with this ID for this User, so
# Raise an HTTPException with a 404 not found status code
abort(404)
if request.method == "DELETE":
# delete the bucketlist using our delete method
bucketlist.delete()
return {
"message": "bucketlist {} deleted".format(bucketlist.id)
}, 200
elif request.method == 'PUT':
# Obtain the new name of the bucketlist from the request data
name = str(request.data.get('name', ''))
bucketlist.name = name
bucketlist.save()
response = {
'id': bucketlist.id,
'name': bucketlist.name,
'date_created': bucketlist.date_created,
'date_modified': bucketlist.date_modified,
'created_by': bucketlist.created_by
}
return make_response(jsonify(response)), 200
else:
# Handle GET request, sending back the bucketlist to the user
response = {
'id': bucketlist.id,
'name': bucketlist.name,
'date_created': bucketlist.date_created,
'date_modified': bucketlist.date_modified,
'created_by': bucketlist.created_by
}
return make_response(jsonify(response)), 200
else:
# user is not legit, so the payload is an error message
message = user_id
response = {
'message': message
}
# return an error response, telling the user he is Unauthorized
return make_response(jsonify(response)), 401
# import the authentication blueprint and register it on the app
from .auth import auth_blueprint
app.register_blueprint(auth_blueprint)
return app
Running python manage.py test
should now yield passing tests.
test_already_registered_user (test_auth.AuthTestCase)
Test that a user cannot be registered twice. ... ok
test_non_registered_user_login (test_auth.AuthTestCase)
Test non registered users cannot login. ... ok
test_registration (test_auth.AuthTestCase)
Test user registration works correcty. ... ok
test_user_login (test_auth.AuthTestCase)
Test registered user can login. ... ok
test_api_can_get_all_bucketlists (test_bucketlist.BucketlistTestCase)
Test API can get a bucketlist (GET request). ... ok
test_api_can_get_bucketlist_by_id (test_bucketlist.BucketlistTestCase)
Test API can get a single bucketlist by using it's id. ... ok
test_bucketlist_can_be_edited (test_bucketlist.BucketlistTestCase)
Test API can edit an existing bucketlist. (PUT request) ... ok
test_bucketlist_creation (test_bucketlist.BucketlistTestCase)
Test API can create a bucketlist (POST request) ... ok
test_bucketlist_deletion (test_bucketlist.BucketlistTestCase)
Test API can delete an existing bucketlist. (DELETE request). ... ok
----------------------------------------------------------------------
Ran 9 tests in 1.579s
OK
(venv)
Now let’s test to see if it works on Postman.
Fire up the API using python run.py development
Make a GET request for a single bucketlist to localhost:5000/bucketlists/2
Feel free to play around with the PUT and DELETE functionality.
We’ve covered quite a lot on securing our API. We went through defining a user model and integrating users into our API. We also covered token-based authentication and used an authentication blueprint to implement it.
Even though our main focus is to write the code, we should not let testing be an afterthought.
For us to improve on code quality, there has to be tests. Testing is the secret to increasing the agility of your product development. In everything project you do, put TTD first.
#flask #python #rest #api #web-development
1655630160
Install via pip:
$ pip install pytumblr
Install from source:
$ git clone https://github.com/tumblr/pytumblr.git
$ cd pytumblr
$ python setup.py install
A pytumblr.TumblrRestClient
is the object you'll make all of your calls to the Tumblr API through. Creating one is this easy:
client = pytumblr.TumblrRestClient(
'<consumer_key>',
'<consumer_secret>',
'<oauth_token>',
'<oauth_secret>',
)
client.info() # Grabs the current user information
Two easy ways to get your credentials to are:
interactive_console.py
tool (if you already have a consumer key & secret)client.info() # get information about the authenticating user
client.dashboard() # get the dashboard for the authenticating user
client.likes() # get the likes for the authenticating user
client.following() # get the blogs followed by the authenticating user
client.follow('codingjester.tumblr.com') # follow a blog
client.unfollow('codingjester.tumblr.com') # unfollow a blog
client.like(id, reblogkey) # like a post
client.unlike(id, reblogkey) # unlike a post
client.blog_info(blogName) # get information about a blog
client.posts(blogName, **params) # get posts for a blog
client.avatar(blogName) # get the avatar for a blog
client.blog_likes(blogName) # get the likes on a blog
client.followers(blogName) # get the followers of a blog
client.blog_following(blogName) # get the publicly exposed blogs that [blogName] follows
client.queue(blogName) # get the queue for a given blog
client.submission(blogName) # get the submissions for a given blog
Creating posts
PyTumblr lets you create all of the various types that Tumblr supports. When using these types there are a few defaults that are able to be used with any post type.
The default supported types are described below.
We'll show examples throughout of these default examples while showcasing all the specific post types.
Creating a photo post
Creating a photo post supports a bunch of different options plus the described default options * caption - a string, the user supplied caption * link - a string, the "click-through" url for the photo * source - a string, the url for the photo you want to use (use this or the data parameter) * data - a list or string, a list of filepaths or a single file path for multipart file upload
#Creates a photo post using a source URL
client.create_photo(blogName, state="published", tags=["testing", "ok"],
source="https://68.media.tumblr.com/b965fbb2e501610a29d80ffb6fb3e1ad/tumblr_n55vdeTse11rn1906o1_500.jpg")
#Creates a photo post using a local filepath
client.create_photo(blogName, state="queue", tags=["testing", "ok"],
tweet="Woah this is an incredible sweet post [URL]",
data="/Users/johnb/path/to/my/image.jpg")
#Creates a photoset post using several local filepaths
client.create_photo(blogName, state="draft", tags=["jb is cool"], format="markdown",
data=["/Users/johnb/path/to/my/image.jpg", "/Users/johnb/Pictures/kittens.jpg"],
caption="## Mega sweet kittens")
Creating a text post
Creating a text post supports the same options as default and just a two other parameters * title - a string, the optional title for the post. Supports markdown or html * body - a string, the body of the of the post. Supports markdown or html
#Creating a text post
client.create_text(blogName, state="published", slug="testing-text-posts", title="Testing", body="testing1 2 3 4")
Creating a quote post
Creating a quote post supports the same options as default and two other parameter * quote - a string, the full text of the qote. Supports markdown or html * source - a string, the cited source. HTML supported
#Creating a quote post
client.create_quote(blogName, state="queue", quote="I am the Walrus", source="Ringo")
Creating a link post
#Create a link post
client.create_link(blogName, title="I like to search things, you should too.", url="https://duckduckgo.com",
description="Search is pretty cool when a duck does it.")
Creating a chat post
Creating a chat post supports the same options as default and two other parameters * title - a string, the title of the chat post * conversation - a string, the text of the conversation/chat, with diablog labels (no html)
#Create a chat post
chat = """John: Testing can be fun!
Renee: Testing is tedious and so are you.
John: Aw.
"""
client.create_chat(blogName, title="Renee just doesn't understand.", conversation=chat, tags=["renee", "testing"])
Creating an audio post
Creating an audio post allows for all default options and a has 3 other parameters. The only thing to keep in mind while dealing with audio posts is to make sure that you use the external_url parameter or data. You cannot use both at the same time. * caption - a string, the caption for your post * external_url - a string, the url of the site that hosts the audio file * data - a string, the filepath of the audio file you want to upload to Tumblr
#Creating an audio file
client.create_audio(blogName, caption="Rock out.", data="/Users/johnb/Music/my/new/sweet/album.mp3")
#lets use soundcloud!
client.create_audio(blogName, caption="Mega rock out.", external_url="https://soundcloud.com/skrillex/sets/recess")
Creating a video post
Creating a video post allows for all default options and has three other options. Like the other post types, it has some restrictions. You cannot use the embed and data parameters at the same time. * caption - a string, the caption for your post * embed - a string, the HTML embed code for the video * data - a string, the path of the file you want to upload
#Creating an upload from YouTube
client.create_video(blogName, caption="Jon Snow. Mega ridiculous sword.",
embed="http://www.youtube.com/watch?v=40pUYLacrj4")
#Creating a video post from local file
client.create_video(blogName, caption="testing", data="/Users/johnb/testing/ok/blah.mov")
Editing a post
Updating a post requires you knowing what type a post you're updating. You'll be able to supply to the post any of the options given above for updates.
client.edit_post(blogName, id=post_id, type="text", title="Updated")
client.edit_post(blogName, id=post_id, type="photo", data="/Users/johnb/mega/awesome.jpg")
Reblogging a Post
Reblogging a post just requires knowing the post id and the reblog key, which is supplied in the JSON of any post object.
client.reblog(blogName, id=125356, reblog_key="reblog_key")
Deleting a post
Deleting just requires that you own the post and have the post id
client.delete_post(blogName, 123456) # Deletes your post :(
A note on tags: When passing tags, as params, please pass them as a list (not a comma-separated string):
client.create_text(blogName, tags=['hello', 'world'], ...)
Getting notes for a post
In order to get the notes for a post, you need to have the post id and the blog that it is on.
data = client.notes(blogName, id='123456')
The results include a timestamp you can use to make future calls.
data = client.notes(blogName, id='123456', before_timestamp=data["_links"]["next"]["query_params"]["before_timestamp"])
# get posts with a given tag
client.tagged(tag, **params)
This client comes with a nice interactive console to run you through the OAuth process, grab your tokens (and store them for future use).
You'll need pyyaml
installed to run it, but then it's just:
$ python interactive-console.py
and away you go! Tokens are stored in ~/.tumblr
and are also shared by other Tumblr API clients like the Ruby client.
The tests (and coverage reports) are run with nose, like this:
python setup.py test
Author: tumblr
Source Code: https://github.com/tumblr/pytumblr
License: Apache-2.0 license
1594289280
The REST acronym is defined as a “REpresentational State Transfer” and is designed to take advantage of existing HTTP protocols when used for Web APIs. It is very flexible in that it is not tied to resources or methods and has the ability to handle different calls and data formats. Because REST API is not constrained to an XML format like SOAP, it can return multiple other formats depending on what is needed. If a service adheres to this style, it is considered a “RESTful” application. REST allows components to access and manage functions within another application.
REST was initially defined in a dissertation by Roy Fielding’s twenty years ago. He proposed these standards as an alternative to SOAP (The Simple Object Access Protocol is a simple standard for accessing objects and exchanging structured messages within a distributed computing environment). REST (or RESTful) defines the general rules used to regulate the interactions between web apps utilizing the HTTP protocol for CRUD (create, retrieve, update, delete) operations.
An API (or Application Programming Interface) provides a method of interaction between two systems.
A RESTful API (or application program interface) uses HTTP requests to GET, PUT, POST, and DELETE data following the REST standards. This allows two pieces of software to communicate with each other. In essence, REST API is a set of remote calls using standard methods to return data in a specific format.
The systems that interact in this manner can be very different. Each app may use a unique programming language, operating system, database, etc. So, how do we create a system that can easily communicate and understand other apps?? This is where the Rest API is used as an interaction system.
When using a RESTful API, we should determine in advance what resources we want to expose to the outside world. Typically, the RESTful API service is implemented, keeping the following ideas in mind:
The features of the REST API design style state:
For REST to fit this model, we must adhere to the following rules:
#tutorials #api #application #application programming interface #crud #http #json #programming #protocols #representational state transfer #rest #rest api #rest api graphql #rest api json #rest api xml #restful #soap #xml #yaml
1604399880
I’ve been working with Restful APIs for some time now and one thing that I love to do is to talk about APIs.
So, today I will show you how to build an API using the API-First approach and Design First with OpenAPI Specification.
First thing first, if you don’t know what’s an API-First approach means, it would be nice you stop reading this and check the blog post that I wrote to the Farfetchs blog where I explain everything that you need to know to start an API using API-First.
Before you get your hands dirty, let’s prepare the ground and understand the use case that will be developed.
If you desire to reproduce the examples that will be shown here, you will need some of those items below.
To keep easy to understand, let’s use the Todo List App, it is a very common concept beyond the software development community.
#api #rest-api #openai #api-first-development #api-design #apis #restful-apis #restful-api
1652251528
Opencart REST API extensions - V3.x | Rest API Integration : OpenCart APIs is fully integrated with the OpenCart REST API. This is interact with your OpenCart site by sending and receiving data as JSON (JavaScript Object Notation) objects. Using the OpenCart REST API you can register the customers and purchasing the products and it provides data access to the content of OpenCart users like which is publicly accessible via the REST API. This APIs also provide the E-commerce Mobile Apps.
Opencart REST API
OCRESTAPI Module allows the customer purchasing product from the website it just like E-commerce APIs its also available mobile version APIs.
Opencart Rest APIs List
Customer Registration GET APIs.
Customer Registration POST APIs.
Customer Login GET APIs.
Customer Login POST APIs.
Checkout Confirm GET APIs.
Checkout Confirm POST APIs.
If you want to know Opencart REST API Any information, you can contact us at -
Skype: jks0586,
Email: letscmsdev@gmail.com,
Website: www.letscms.com, www.mlmtrees.com
Call/WhatsApp/WeChat: +91–9717478599.
Download : https://www.opencart.com/index.php?route=marketplace/extension/info&extension_id=43174&filter_search=ocrest%20api
View Documentation : https://www.letscms.com/documents/api/opencart-rest-api.html
More Information : https://www.letscms.com/blog/Rest-API-Opencart
VEDIO : https://vimeo.com/682154292
#opencart_api_for_android #Opencart_rest_admin_api #opencart_rest_api #Rest_API_Integration #oc_rest_api #rest_api_ecommerce #rest_api_mobile #rest_api_opencart #rest_api_github #rest_api_documentation #opencart_rest_admin_api #rest_api_for_opencart_mobile_app #opencart_shopping_cart_rest_api #opencart_json_api
1652251629
Unilevel MLM Wordpress Rest API FrontEnd | UMW Rest API Woocommerce Price USA, Philippines : Our API’s handle the Unilevel MLM woo-commerce end user all functionalities like customer login/register. You can request any type of information which is listed below, our API will provide you managed results for your all frontend needs, which will be useful for your applications like Mobile App etc.
Business to Customer REST API for Unilevel MLM Woo-Commerce will empower your Woo-commerce site with the most powerful Unilevel MLM Woo-Commerce REST API, you will be able to get and send data to your marketplace from other mobile apps or websites using HTTP Rest API request.
Our plugin is used JWT authentication for the authorization process.
REST API Unilevel MLM Woo-commerce plugin contains following APIs.
User Login Rest API
User Register Rest API
User Join Rest API
Get User info Rest API
Get Affiliate URL Rest API
Get Downlines list Rest API
Get Bank Details Rest API
Save Bank Details Rest API
Get Genealogy JSON Rest API
Get Total Earning Rest API
Get Current Balance Rest API
Get Payout Details Rest API
Get Payout List Rest API
Get Commissions List Rest API
Withdrawal Request Rest API
Get Withdrawal List Rest API
If you want to know more information and any queries regarding Unilevel MLM Rest API Woocommerce WordPress Plugin, you can contact our experts through
Skype: jks0586,
Mail: letscmsdev@gmail.com,
Website: www.letscms.com, www.mlmtrees.com,
Call/WhatsApp/WeChat: +91-9717478599.
more information : https://www.mlmtrees.com/product/unilevel-mlm-woocommerce-rest-api-addon
Visit Documentation : https://letscms.com/documents/umw_apis/umw-apis-addon-documentation.html
#Unilevel_MLM_WooCommerce_Rest_API's_Addon #umw_mlm_rest_api #rest_api_woocommerce_unilevel #rest_api_in_woocommerce #rest_api_woocommerce #rest_api_woocommerce_documentation #rest_api_woocommerce_php #api_rest_de_woocommerce #woocommerce_rest_api_in_android #woocommerce_rest_api_in_wordpress #Rest_API_Woocommerce_unilevel_mlm #wp_rest_api_woocommerce