Skip to content

hemanta212/flask-rest-api

Repository files navigation

Flask REST API

This is a simple implementation of flask api with JWT(JSON WEB TOKENS). You get a token by logging in and send this token in headers for accessing every other routes.

Table of contents

Implementation Features

  • Rolling out custom flask decorators
  • Token based auth with jwt
  • Custom application registering and separate client tokens
  • Http Basic username:pass authentication
  • Flask blueprinting for modularization
  • Flask database migration.
  • Supports postgres, sql, sqlite etc dbs with sqlalchemy
  • Returning diffrent http status codes
  • Some common short flask tricks
    • Defining same routes with diffrent methods to reduce if/else nests
    • Returning python string, dicts autogenerates a json response (no need jsonify)
    • Calling .app_context().push() on flask app instance. Helps a ton in intrepreter to play with db and stuff without initializing

Usage

This api has user and post tables in db. Every routes relating to ‘user’ and ‘post’ requires a token in the header which can be obtained by logging in throgh login route.

This assumes you have completed the configuration, created admin user and have its credentials. See configuration section.

NOTE: Any illegal or failed requests on any route will generate non-200 status code and return a json dict with one “message” key stating the reason

Example:

{"message": "no user found"}

Login and get a token

ENDPOINT: “login” [GET REQ] Payload: auth(username, pass)

import requests
from requests.auth import HTTPBasicAuth

URL = "https://apiurl.com"
# fill username and password with your acc info
auth = HTTPBasicAuth("username", "password")

login_response = requests.get(URL+"/login", auth=auth)
login_response.status_code # confirm that its 200

token = login_response.json()["token"]

headers = {
    "x-access-token": token
}

RESPONSE: 200

{"token": "soasloiwurpoewiurpowierupwoeirf"}

For accessing every other route, you need to specify this token in the header as value of “x-access-token”.

NOTE: This token has no time limit and will never expire, see why. For making expirable tokens have a look at here.

Creating a user

ENDPOINT: “user” [POST REQ] REQ: admin token

Creating a user requires admin account”s token. You send a post request to the “user” endpoint.

The payload should have username and password.

headers = {"x-access-token": token}
data = {"username": "some usename", "password": "my password"}

create_user = requests.post(URL+"/user", data=data, headers=headers

Response: 200

{
    "message": "User created successfully"
}

Viewing user info

ENDPOINT: “user” [GET REQ] REQ: admin token

Sending the request gets you all users info

headers = {"x-access-token": token}
requests.get(URL+"/user", headers=headers)

RESPONSE: 200

{"users": [
    {"admin": true, "id": 1,
     "password": "sha256$Sot2tcp9$671301dae8s45ad6f2fe0f583f8e60bfc90b24f045fcb791c4483711ca9c6d09",
     "public_id": "e9572ee6-4b5e-45e4-a840-58a33b04b8a7",
     "username": "my username"}
   ]
}

Viewing Single User

ENDPOINT: “user/public_id” [GET REQ] REQ: admin token

You can get public id of user by sending GET req to “user” endpoint: see above

requests.get(URL+"/user/public_id", headers=headers)

RESPONSE: 200

{"user":
 {
    "admin": false,
     "id": 2,
     "password": "sha256$f8ulwnAv$8af6f5590e8af54c8d2171cc9afc568727a8a763e8c875855f8b7d27f5dfcccd",
     "public_id": "1f190b06-263s-42aa-86e9-460d0aff93d9",
     "username": "my username"
 }
}

Promoting a user

ENDPOINT: “user/public_id” [PUT REQ] REQ: admin token

headers = {"x-access-token": token}
requests.put(URL+"/user/public_id", headers=headers)

RESPONSE: 200

{"message": "The user has been promoted!"}

Deleting a user

ENDPOINT: “user/public_id” [DELETE REQ] REQ: admin token

headers = {"x-access-token": token}
requests.delete(URL+"/user/public_id", headers=headers)

RESPONSE: 200

{"message": "The user has been deleted!"}

Creating a post

ENDPOINT: “template” [POST REQ]

The payload should have title and url and optionally description.

headers = {"x-access-token": token}
data = {"title": "some title",
         "url": "http:/test.com",
         "description": "some desc",
         }
requests.put(URL+"/user/public_id", headers=headers)

RESPONSE: 200

{"message": "Post created"}

Viewing post

ENDPOINT: “template” [GET REQ]

headers = {"x-access-token": token}
requests.get(URL+"/template", headers=headers)

RESPONSE: 200

{"templates": [
    {"description": "Done",
     "id": 27,
     "posted": "Mon, 12 Oct 2020 04:51:27 GMT",
     "title": "Test thing",
     "url": "https://i.imgur.com/yYGxFJX.jpeg",
     "username": "somerandomusername",
     "posted": true},

    {"description": null,
     "id": 27,
     "posted": "Mon, 12 Oct 2020 04:51:27 GMT",
     "title": "Test thing",
     "url": "https://i.imgur.com/yYGxFJX.jpeg",
     "username": null,
     "posted": false},   ]
}

Note: Sometimes user_id, description can be null.

View filtered post

ENDPOINT: “/” [GET REQ]

The api provides a way to get approved post (with approved propery set to true + current user’s own post) with a single api call.

requests.get(URL+"/", headers=headers)

RESPONSE: 200

{"templates": [
    {"description": "Done",
     "id": 27,
     "posted": "Mon, 12 Oct 2020 04:51:27 GMT",
     "title": "Test thing",
     "url": "https://i.imgur.com/yYGxFJX.jpeg",
     "username": "somerandomusername",
     "posted": true}
   ]
}

Note: Sometimes user_id, description can be null too.

Viewing Single Post

ENDPOINT: “template/template_id” [GET REQ]

You can get template id of post by sending GET req to “template” endpoint: see above

requests.get(URL+"/template/template_id", headers=headers)

RESPONSE: 200

{"template":
 {"description": "Done",
     "id": 27,
     "posted": "Mon, 12 Oct 2020 04:51:27 GMT",
     "title": "Test thing",
     "url": "https://i.imgur.com/yYGxFJX.jpeg",
     "user_id": "alskjdf_dfkdjf"
 }
}

Note: Sometimes user_id, description can be null too.

Updating a post

ENDPOINT: “template/template_id” [PUT REQ]

Updating a post is same as creating it.

headers = {"x-access-token": token}
data = {"title": "some title",
         "url": "http:/test.com",
         "description": "some desc",
         }
requests.put(URL+"/template/template_id", data=data, headers=headers)

RESPONSE: 200

{"message": "Post Updated"}

Deleting a post

ENDPOINT: “template/template_id” [DELETE REQ]

headers = {"x-access-token": token}
requests.delete(URL+"/template/template_id", headers=headers)

RESPONSE: 200

{"message": "The post has been deleted"}

Configuration

All the configs are set in the meme_api/__init__.py file.

Installing dependencies

  • With Pip
    $ python3 -m venv .venv
    $ .venv/bin/python -m pip install -r requirements.txt
        
  • With Poetry
    $ poetry install
        

Database config

The config SQLALCHEMY_DATABASE_URI is made from different env vars parts like HOST_NAME, HOST_PASS etc You need to set those variables Or you can just use sqlite db.

A minimal ‘.env’ config looks like

export SECRET_KEY='mysecretkey'
export SQLALCHEMY_DATABASE_URI='sqlite:///site.db'
export FLASK_APP=run.py

This same config along with example config for hosted sql (eg MYSQL) server is available in .env_eg file. Just rename, edit and source this file.

#+ .env_eg file +#
export SECRET_KEY='mysecretkey'
export SQLALCHEMY_DATABASE_URI='sqlite:///site.db'
export FLASK_APP=run.py

# For a hosted mysql/postgres server
# Note: if SQLALCHEMY_DATABASE_URI env var is present these env vars will be ignored & WONT BE USED
export DB_USERNAME='username of database'
export DB_PASS='password of database'
export DB_HOST='host address url of database'
export DB_NAME='name of db and tablename eg. mysqldb$posts'

Database initialization and migration

Before initializing the database. Create a migrations folder for you db and delete the existing one

$ rm -rf ./migrations
$ python -m flask db init # makes migrations folder

Run migrate to create the tables required by the models

$ python -m flask db migrate
$ python -m flask db upgrade

Once you make any changes to models you need to migrate & upgrade the database as shown above

Setting up migration for existing database

In case you already have a database initialized(ie db schema created) through different option and want to integrate flask-migrate in it.

First: Initialize the migrations folder Note: delete existing migrations folder

$ python -m flask db init

Create another empty database table and point the database env variables to this empty table (in case of sqlite just change the ‘site.db’ name to ‘site2.db’)

$ python -m flask db migrate

Now again point to your original database column in environment vars (for sqlite just change ‘site2.db’ back to ‘site.db’)

$ python -m flask db stamp head
$ python -m flask db migrate # you should see 'no change in schema detected' message

You are all set. From now, if you make any changes to models you need to migrate & upgrade the database as shown below

$ python -m flask db migrate
$ python -m flask db upgrade

Creating an admin user

Only admin users are allowed to create new accounts through api. Thus a admin user has to be manually created (or you could remove that if statement and create user acc through that route)

import uuid

from werkzeug.security import generate_password_hash

from run import app
from meme_api import db
from meme_api.models import User

app.app_context().push()

hashed_pass = generate_password_hash('secretpassword', method='sha256')

admin = User(username='admin',
             password=hashed_pass,
             admin=True,
             public_id=str(uuid.uuid4()) )

db.session.add(admin)
db.session.commit()

Application registration tokens

The token generated by the api never expires. For preventing leaked tokens to be misued and also limit the database connections, the prod branch of this repo implements a application based registering.

A random uuid is generated and manually put into the meme_api/apps.py file. This id can now be used in headers for requesting every route.

#+ apps.py file +
registered = {
    'someapp': 'generated random uuid',
    'cli': 'another uuid for another app',
}
headers = {
    'x-application-token': 'uuid token for application',
    'x-access-token': 'user login token',
}

Every routes including login now requires above ‘x-application-token’ header for the request to be successful.

Loosening other routes

With application based authentication in place, the routes for creating new user, getting all users etc can be loosened to not require an admin token.

Useful tools

There are many good tools to leverage understanding of how api’s and http requests work.

  • CLI tools for testing, debugging API endpoints.

Httpbin.org:

  • An dedicated website which provides post, delete, put etc endpoints in httpbin.org/post, /delete respectivly. Returns all the headers and data info it got in nice json format.
    • Great partner tool with httpie

Postman (and similar others)

  • Exploring, testing endpoints with diffrent kinds of requests in a friendly UI. Helps creating a test suite.

Inner details

What requests” BasicHTTPAuth does

import requests
from requests.auth import HTTPBasicAuth

URL = "https://httpbin.org"
auth = HTTPBasicAuth("username", "password")

login_response = requests.post(URL+"/post", auth=auth)

print(login_response.json())

Response

{"args": {},
 "data": "",
 "files": {},
 "form": {},
 "headers": {"Accept": "*/*",
             "Accept-Encoding": "gzip, deflate",
             "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
             "Content-Length": "0",
             "Host": "httpbin.org",
             "User-Agent": "python-requests/2.24.0",
             "X-Amzn-Trace-Id": "Root=1-5f8aee35-211905107cfea23a2ad3b865"},
 "json": null,
 "origin": "35.229.170.146",
 "url": "https://httpbin.org/post"}

What we are interested in is the Authorization header. Basically the requests transformed the username and password to base64 encoded string and passed the header.

header = {
    "Authorization": "Basic " + Base64encoded(username + ":" + password)
}

So instead of passing auth arg we can also create this authorization header ourself and should get the same result

Implementing own auth header

import requests
import base64

URL = "httpbin.org/post"
token = base64.b64encode(bytes("username:pass", "utf-8"))
headers  = {"Authorization": f"Basic {token.decode()}"}
response = requests.get(URL, headers=headers)

print(response.json())
{"args": {},
 "data": "",
 "files": {},
 "form": {},
 "headers": {"Accept": "*/*",
             "Accept-Encoding": "gzip, deflate",
             "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
             "Content-Length": "0",
             "Host": "httpbin.org",
             "User-Agent": "python-requests/2.24.0",
             "X-Amzn-Trace-Id": "Root=1-5f8af1bb-716f15011a1b61770e118a7f"},
 "json": null,
 "origin": "35.229.170.146",
 "url": "https://httpbin.org/post"}

Diffrent ways to get request data in flask

Ref: stackoverflow page

  • request.data : used for fallback data storage mostly empty
  • request.args: the key/value pairs in the URL query string
  • request.form: the key/value pairs in the body, from a HTML post form, or JavaScript request that isn’t JSON encoded
  • request.files: the files in the body, which Flask keeps separate from form. HTML forms must use enctype=multipart/form-data or files will not be uploaded.
  • request.values: combined args and form, preferring args if keys overlap
  • request.json: parsed JSON data. The request must have the application/json content type, or use request.get_json(force=True) to ignore the content type.

TODOS

[ ] Add Tests

[ ] Add Logging

About

Implementation of simple flask json REST API.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published