Writing RESTful Web Services in Python with Flask

Writing RESTful Web Services in Python with Flask

REST is the most common way to structure web APIs over HTTP these days. It is based on modeling everything as resources identified by URLs and manipulating the resources using the HTTP verbs GET, POST, PUT and DELETE. Flask is an extensible Python micro-framework for web development. You can develop a REST API using Flask on its own, but, the Flask-RestFUL extension directly supports REST API development by exposing a resource-based approach. In this article I’ll explore the various components of a REST API built on top of Flask-RestFUL via a simple example.

Our example projects is called “Over Achiever” and allows you to get, create, update and delete goals and sub-goals that can be nested arbitrarily.

Each user can have his or her own elaborate tree-like structure of nested goals and subgoals. A goal can be marked as complete by providing an end timestamp. The full source code can be found here.

Models

I model the domain as SQLAlchemy models. This has the nice property of mapping resources directly to rows in a database. It is a very common pattern and makes many tasks trivial. GET, POST, PUT and DELETE REST operations on resource X often are simply SELECT, INSERT, UPDATE and DELETE operation on a single table. Note, that often you would want to operate on memory models or process data before/after storing/retrieving.

Let’s start. There are two concepts we want to model a “user” and a goal”. We’ll put them in a file called models.py

from datetime import datetimefrom sqlalchemy import (Column,                        DateTime,                        ForeignKey,                        Integer,                        String)from sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy.orm import relationshipBase = declarative_base()metadata = Base.metadataclass User(Base):    __tablename__ = 'user'    id = Column(Integer, primary_key=True)    name = Column(String(64), unique=True)    email = Column(String(120), index=True, unique=True)    password = Column(String(128))class Goal(Base):    __tablename__ = 'goal'    id = Column(Integer, primary_key=True)    user_id = Column(ForeignKey('user.id'), nullable=False)    parent_id = Column(ForeignKey('goal.id'))    name = Column(String(64), unique=True)    description = Column(String(512))    start = Column(DateTime, default=datetime.now)    end = Column(DateTime, nullable=True)    user = relationship('User')    parent = relationship(lambda: Goal,                          remote_side=id,                          backref='sub_goals')

The User model is completely trivial and stores user name, role and credentials. The Goal model is more interesting. It stores goal name, description, start and end time and the reference to the user that defined this goal. But, it also has a parent reference to the same goal table. This self-reference of sorts is how the hierarchical nature of goals is accomplished in the relational database. While, in programming language you would typically model it as each goal having a list of sub-goals here it is modeled differently where each goal may have a parent goal. A goal with no parent is a top-level goal.

Resources

Resources are a Flask-Restful concept. Each resource may have a get(), post(), put() and delete() method. Each resource directly support a specific REST endpoint and the methods implement the HTTP verbs for this resource. All the methods are optional.

Here is the Goal Resource class:

class Goal(Resource):  def get(self):   """Get all goals organized by user and in hierarchy"""    q = db.session.query    result = {}    users = q(m.User).all()    for u in users:      result[u.name] = _get_goal_tree(q, u)    return result  def post(self):    parser = RequestParser()    parser.add_argument('user', type=str, required=True)    parser.add_argument('name', type=str, required=True)    parser.add_argument('parent_name', type=str)    parser.add_argument('description', type=str, required=False)    args = parser.parse_args()    # Get a SQL Alchemy query object    q = db.session.query    # Create a new goal    user = q(m.User).filter_by(name=args.user).one()    # Find parent goal by name    if args.parent_name:      parent = q(m.Goal).filter_by(name=args.parent_name).one()    else:      parent = None    goal = m.Goal(user=user,                          parent=parent,                          name=args.name,                          description=args.description)    db.session.add(goal)    db.session.commit()  def put(self):  """Update end time"""    parser = RequestParser()    parser.add_argument('name', type=str, required=True)    args = parser.parse_args()    # Get a SQL Alchemy query object    q = db.session.query    goal = q(m.Goal).filter_by(name=args.name).one()    goal.end = datetime.now()    db.session.commit()

The RequestParser allows you to specify and parse the request parameters and then use them in the implementation of each method. The post(), put() and delete() methods don’t return anything, which if successful will result in a simple 200 HTTP status code on the caller side. If anything goes wrong you can return any HTTP error code via Flask’s abort() method.

The API

The api.py module contains the entry point to the service, initializes several critical components like the configuration and the database and maps routes (HTTP end points) to resources:

import osfrom flask import Flaskfrom flask_restful import Apifrom flask.ext.sqlalchemy import SQLAlchemyfrom resources import User, Goalimport resourcesdef create_app(): app = Flask(__name__) app.config.from_object('over_achiever.config') resources.db = app.db = SQLAlchemy(app) api = Api(app) resource_map = (  (User, '/v1.0/users'),  (Goal, '/v1.0/goals'), ) for resource, route in resource_map:  api.add_resource(resource, route) return appthe_app = create_app()if __name__ == "__main__": print("If you run locally, browse to localhost:5000") host = '0.0.0.0' port = int(os.environ.get("PORT", 5000)) the_app.run(host=host, port=port) 

The create_app() function is the key. It mixes together all the ingredients and return an application objects that is ready to run. Then in the __main__ section the run() method is called, listening on all interfaces (ip address 0.0.0.0) and using port 5000 or the PORT environment variable (needed for Heroku).

Testing

The tests serve two functions:

  1. Make sure the code actually works as intended
  2. Show how to use the API in practice

I use the standard unittest module for implementing the test and the provided Flask test client (via the app.test_client()) to invoke the API through its end points:

class OverAchieverTest(TestCase):    def setUp(self):        self.app = create_app()        self.session = create_mem_db(metadata, self.app.db)        self.test_app = self.app.test_client()        # add users        u1 = m.User(name='user-1',                    email='[email protected]',                    password='123')        u2 = m.User(name='user-2',                    email='[email protected]',                    password='123')        self.user = m.User(name='user-3',                           email='[email protected]',                           password='123')        self.session.add(u1)        self.session.add(u2)        self.session.add(self.user)        # add goals        goals = [None] * 8        goals[0] = m.Goal(user=u1, name='goal-0')        goals[1] = m.Goal(user=u1, name='goal-1')        goals[2] = m.Goal(user=u1, name='goal-2', parent=goals[1])        goals[3] = m.Goal(user=u1, name='goal-3', parent=goals[1])        goals[4] = m.Goal(user=u1, name='goal-4', parent=goals[3])        goals[5] = m.Goal(user=u1, name='goal-5', parent=goals[3])        goals[6] = m.Goal(user=u1, name='goal-6', parent=goals[3])        goals[7] = m.Goal(user=u2, name='goal-7')        for g in goals:            self.session.add(g)        self.session.commit()    def tearDown(self):        pass    def test_get_goals(self):        #q = self.session.query        url = '/v1.0/goals'        response = self.test_app.get(url)        result = json.loads(response.data)        expected = {'user-1':                             {'goal-0': {},                               'goal-1':                                 {'goal-2': {},                                   'goal-3': {                                     'goal-4': {},                                     'goal-5': {},                                     'goal-6': {}}}},                           'user-2':                             {'goal-7': {}},                           'user-3': {}}        self.assertEqual(expected, result)    def test_add_new_goal(self):        q = self.session.query        name = 'new-goal'        # verify the goal doesn't exist yet        self.assertIsNone(q(m.Goal).filter_by(name=name).scalar())        params = dict(user=self.user.name,                      name=name)        url = '/v1.0/goals'        response = self.test_app.post(url, data=params)        self.assertEqual(200, response.status_code)        self.assertIsNotNone(q(m.Goal).filter_by(name=name).scalar())    def test_add_nested_goals(self):        q = self.session.query        name = 'new-goal'        # verify the goal doesn't exist yet        self.assertIsNone(q(m.Goal).filter_by(name=name).scalar())        params = dict(user=self.user.name,                      name=name)        url = '/v1.0/goals'        response = self.test_app.post(url, data=params)        self.assertEqual(200, response.status_code)        parent_goal = q(m.Goal).filter_by(name=name).one()        child_name = 'child'        params = dict(user=self.user.name,                      name=child_name,                      parent_name=parent_goal.name)        response = self.test_app.post(url, data=params)        self.assertEqual(200, response.status_code)        child_goal = q(m.Goal).filter_by(name=child_name).one()        self.assertEqual(child_name, child_goal.name)        self.assertEqual(parent_goal, child_goal.parent)    def test_complete_goal(self):        q = self.session.query        params = dict(user='user-1',                      name='goal-0')        url = '/v1.0/goals'        response = self.test_app.put(url, data=params)        self.assertEqual(200, response.status_code)        goal = q(m.Goal).filter_by(name='goal-0').one()        self.assertIsNotNone(goal.end) 

Conclusion

This is a very minimal example of a REST API, but it contains all the pieces and you can use it as a starting point for more ambitious projects. If you feel comfortable with Python, Flask and SQLAlchemy then more power to you.

devx-admin

devx-admin

Share the Post:
AI Girlfriend Craze

AI Girlfriend Craze Threatens Relationships

The surge in virtual AI girlfriends’ popularity is playing a role in the escalating issue of loneliness among young males, and this could have serious

AIOps Innovations

Senser is Changing AIOps

Senser, an AIOps platform based in Tel Aviv, has introduced its groundbreaking AI-powered observability solution to support developers and operations teams in promptly pinpointing the

Malyasian Networks

Malaysia’s Dual 5G Network Growth

On Wednesday, Malaysia’s Prime Minister Anwar Ibrahim announced the country’s plan to implement a dual 5G network strategy. This move is designed to achieve a

Advanced Drones Race

Pentagon’s Bold Race for Advanced Drones

The Pentagon has recently unveiled its ambitious strategy to acquire thousands of sophisticated drones within the next two years. This decision comes in response to

Important Updates

You Need to See the New Microsoft Updates

Microsoft has recently announced a series of new features and updates across their applications, including Outlook, Microsoft Teams, and SharePoint. These new developments are centered

AI Girlfriend Craze

AI Girlfriend Craze Threatens Relationships

The surge in virtual AI girlfriends’ popularity is playing a role in the escalating issue of loneliness among young males, and this could have serious repercussions for America’s future. A

AIOps Innovations

Senser is Changing AIOps

Senser, an AIOps platform based in Tel Aviv, has introduced its groundbreaking AI-powered observability solution to support developers and operations teams in promptly pinpointing the root causes of service disruptions

Bebop Charging Stations

Check Out The New Bebob Battery Charging Stations

Bebob has introduced new 4- and 8-channel battery charging stations primarily aimed at rental companies, providing a convenient solution for clients with a large quantity of batteries. These wall-mountable and

Malyasian Networks

Malaysia’s Dual 5G Network Growth

On Wednesday, Malaysia’s Prime Minister Anwar Ibrahim announced the country’s plan to implement a dual 5G network strategy. This move is designed to achieve a more equitable incorporation of both

Advanced Drones Race

Pentagon’s Bold Race for Advanced Drones

The Pentagon has recently unveiled its ambitious strategy to acquire thousands of sophisticated drones within the next two years. This decision comes in response to Russia’s rapid utilization of airborne

Important Updates

You Need to See the New Microsoft Updates

Microsoft has recently announced a series of new features and updates across their applications, including Outlook, Microsoft Teams, and SharePoint. These new developments are centered around improving user experience, streamlining

Price Wars

Inside Hyundai and Kia’s Price Wars

South Korean automakers Hyundai and Kia are cutting the prices on a number of their electric vehicles (EVs) in response to growing price competition within the South Korean market. Many

Solar Frenzy Surprises

Solar Subsidy in Germany Causes Frenzy

In a shocking turn of events, the German national KfW bank was forced to discontinue its home solar power subsidy program for charging electric vehicles (EVs) after just one day,

Electric Spare

Electric Cars Ditch Spare Tires for Efficiency

Ira Newlander from West Los Angeles is thinking about trading in his old Ford Explorer for a contemporary hybrid or electric vehicle. However, he has observed that the majority of

Solar Geoengineering Impacts

Unraveling Solar Geoengineering’s Hidden Impacts

As we continue to face the repercussions of climate change, scientists and experts seek innovative ways to mitigate its impacts. Solar geoengineering (SG), a technique involving the distribution of aerosols

Razer Discount

Unbelievable Razer Blade 17 Discount

On September 24, 2023, it was reported that Razer, a popular brand in the premium gaming laptop industry, is offering an exceptional deal on their Razer Blade 17 model. Typically

Innovation Ignition

New Fintech Innovation Ignites Change

The fintech sector continues to attract substantial interest, as demonstrated by a dedicated fintech stage at a recent event featuring panel discussions and informal conversations with industry professionals. The gathering,

Import Easing

Easing Import Rules for Big Tech

India has chosen to ease its proposed restrictions on imports of laptops, tablets, and other IT hardware, allowing manufacturers like Apple Inc., HP Inc., and Dell Technologies Inc. more time

Semiconductor Stock Plummet

Dramatic Downturn in Semiconductor Stocks Looms

Recent events show that the S&P Semiconductors Select Industry Index seems to be experiencing a downturn, which could result in a decline in semiconductor stocks. Known as a key indicator

Anthropic Investment

Amazon’s Bold Anthropic Investment

On Monday, Amazon announced its plan to invest up to $4 billion in the AI firm Anthropic, acquiring a minority stake in the process. This decision demonstrates Amazon’s commitment to

AI Experts Get Hired

Tech Industry Rehiring Wave: AI Experts Wanted

A few months ago, Big Tech companies were downsizing their workforce, but currently, many are considering rehiring some of these employees, especially in popular fields such as artificial intelligence. The

Lagos Migration

Middle-Class Migration: Undermining Democracy?

As the middle class in Lagos, Nigeria, increasingly migrates to private communities, a PhD scholar from a leading technology institute has been investigating the impact of this development on democratic

AI Software Development

ChatGPT is Now Making Video Games

Pietro Schirano’s foray into using ChatGPT, an AI tool for programming, has opened up new vistas in game and software development. As design lead at business finance firm Brex, Schirano

Llama Codebot

Developers! Here’s Your Chatbot

Meta Platforms has recently unveiled Code Llama, a free chatbot designed to aid developers in crafting coding scripts. This large language model (LLM), developed using Meta’s Llama 2 model, serves

Tech Layoffs

Unraveling the Tech Sector’s Historic Job Losses

Throughout 2023, the tech sector has experienced a record-breaking number of job losses, impacting tens of thousands of workers across various companies, including well-established corporations and emerging startups in areas

Chinese 5G Limitation

Germany Considers Limiting Chinese 5G Tech

A recent report has put forth the possibility that Germany’s Federal Ministry of the Interior and Community may consider limiting the use of Chinese 5G technology by local network providers

Modern Warfare

The Barak Tank is Transforming Modern Warfare

The Barak tank is a groundbreaking addition to the Israeli Defense Forces’ arsenal, significantly enhancing their combat capabilities. This AI-powered military vehicle is expected to transform the way modern warfare

AI Cheating Growth

AI Plagiarism Challenges Shake Academic Integrity

As generative AI technologies like ChatGPT become increasingly prevalent among students and raise concerns about widespread cheating, prominent universities have halted their use of AI detection software, such as Turnitin’s