Interacting With Web Services – Real Python


In this section, you’ll look at three popular frameworks for building REST APIs in Python. Each framework has pros and cons, so you’ll have to evaluate which works best for your needs. To this end, in the next sections, you’ll look at a REST API in each framework. All the examples will be for a similar API that manages a collection of countries.

The fields name, capital, and area store data about a specific country somewhere in the world.

Most of the time, data sent from a REST API comes from a database. Connecting to a database is beyond the scope of this tutorial. For the examples below, you’ll store your data in a Python list. The exception to this is the Django REST framework example, which runs off the SQLite database that Django creates.

To keep things consistent, you’ll use countries as your main endpoint for all three frameworks. You’ll also use JSON as your data format for all three frameworks.

Now that you’ve got the background for the API, you can move on to the next section, where you’ll look at the REST API in Flask.

Flask

Flask is a Python microframework used to build web applications and REST APIs. Flask provides a solid backbone for your applications while leaving many design choices up to you. Flask’s main job is to handle HTTP requests and route them to the appropriate function in the application.

Below is an example Flask application for the REST API:

# app.py
from flask import Flask, request, jsonify

app = Flask(__name__)

countries = [
    {"id": 1, "name": "Thailand", "capital": "Bangkok", "area": 513120},
    {"id": 2, "name": "Australia", "capital": "Canberra", "area": 7617930},
    {"id": 3, "name": "Egypt", "capital": "Cairo", "area": 1010408},
]

def _find_next_id():
    return max(country["id"] for country in countries) + 1

@app.get("/countries")
def get_countries():
    return jsonify(countries)

@app.post("/countries")
def add_country():
    if request.is_json:
        country = request.get_json()
        country["id"] = _find_next_id()
        countries.append(country)
        return country, 201
    return {"error": "Request must be JSON"}, 415

This application defines the API endpoint /countries to manage the list of countries. It handles two different kinds of requests:

  1. GET /countries returns the list of countries.
  2. POST /countries adds a new country to the list.

You can try out this application by installing flask with pip:

$ python -m pip install flask

Once flask is installed, save the code in a file called app.py. To run this Flask application, you first need to set an environment variable called FLASK_APP to app.py. This tells Flask which file contains your application.

Run the following command inside the folder that contains app.py:

$ export FLASK_APP=app.py

This sets FLASK_APP to app.py in the current shell. Optionally, you can set FLASK_ENV to development, which puts Flask in debug mode:

$ export FLASK_ENV=development

Besides providing helpful error messages, debug mode will trigger a reload of the application after all code changes. Without debug mode, you’d have to restart the server after every change.

With all the environment variables ready, you can now start the Flask development server by calling flask run:

$ flask run
* Serving Flask app "app.py" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

This starts up a server running the application. Open up your browser and go to http://127.0.0.1:5000/countries, and you’ll see the following response:

[
    {
        "area": 513120,
        "capital": "Bangkok",
        "id": 1,
        "name": "Thailand"
    },
    {
        "area": 7617930,
        "capital": "Canberra",
        "id": 2,
        "name": "Australia"
    },
    {
        "area": 1010408,
        "capital": "Cairo",
        "id": 3,
        "name": "Egypt"
    }
]

This JSON response contains the three countries defined at the start of app.py. Take a look at the following code to see how this works:

@app.get("/countries")
def get_countries():
    return jsonify(countries)

This code uses @app.get(), a Flask route decorator, to connect GET requests to a function in the application. When you access /countries, Flask calls the decorated function to handle the HTTP request and return a response.

In the code above, get_countries() takes countries, which is a Python list, and converts it to JSON with jsonify(). This JSON is returned in the response.

Now take a look at add_country(). This function handles POST requests to /countries and allows you to add a new country to the list. It uses the Flask request object to get information about the current HTTP request:

@app.post("/countries")
def add_country():
    if request.is_json:
        country = request.get_json()
        country["id"] = _find_next_id()
        countries.append(country)
        return country, 201
    return {"error": "Request must be JSON"}, 415

This function performs the following operations:

  1. Using request.is_json to check that the request is JSON
  2. Creating a new country instance with request.get_json()
  3. Finding the next id and setting it on the country
  4. Appending the new country to countries
  5. Returning the country in the response along with a 201 Created status code
  6. Returning an error message and 415 Unsupported Media Type status code if the request wasn’t JSON

add_country() also calls _find_next_id() to determine the id for the new country:

def _find_next_id():
    return max(country["id"] for country in countries) + 1

This helper function uses a generator expression to select all the country IDs and then calls max() on them to get the largest value. It increments this value by 1 to get the next ID to use.

You can try out this endpoint in the shell using the command-line tool curl, which allows you to send HTTP requests from the command line. Here, you’ll add a new country to the list of countries:

$ curl -i http://127.0.0.1:5000/countries 
-X POST 
-H 'Content-Type: application/json' 
-d '{"name":"Germany", "capital": "Berlin", "area": 357022}'

HTTP/1.0 201 CREATED
Content-Type: application/json
...

{
    "area": 357022,
    "capital": "Berlin",
    "id": 4,
    "name": "Germany"
}

This curl command has some options that are helpful to know:

  • -X sets the HTTP method for the request.
  • -H adds an HTTP header to the request.
  • -d defines the request data.

With these options set, curl sends JSON data in a POST request with the Content-Type header set to application/json. The REST API returns 201 CREATED along with the JSON for the new country you added.

You can use curl to send a GET request to /countries to confirm that the new country was added. If you don’t use -X in your curl command, then it sends a GET request by default:

$ curl -i http://127.0.0.1:5000/countries

HTTP/1.0 200 OK
Content-Type: application/json
...

[
    {
        "area": 513120,
        "capital": "Bangkok",
        "id": 1,
        "name": "Thailand"
    },
    {
        "area": 7617930,
        "capital": "Canberra",
        "id": 2,
        "name": "Australia"
    },
    {
        "area": 1010408,
        "capital": "Cairo",
        "id": 3,
        "name": "Egypt"
    },
    {
        "area": 357022,
        "capital": "Berlin",
        "id": 4,
        "name": "Germany"
    }
]

This returns the full list of countries in the system, with the newest country at the bottom.

This is just a sampling of what Flask can do. This application could be expanded to include endpoints for all the other HTTP methods. Flask also has a large ecosystem of extensions that provide additional functionality for REST APIs, such as database integrations, authentication, and background processing.

Django REST Framework

Another popular option for building REST APIs is Django REST framework. Django REST framework is a Django plugin that adds REST API functionality on top of an existing Django project.

To use Django REST framework, you need a Django project to work with. If you already have one, then you can apply the patterns in the section to your project. Otherwise, follow along and you’ll build a Django project and add in Django REST framework.

First, install Django and djangorestframework with pip:

$ python -m pip install Django djangorestframework

This installs Django and djangorestframework. You can now use the django-admin tool to create a new Django project. Run the following command to start your project:

$ django-admin startproject countryapi

This command creates a new folder in your current directory called countryapi. Inside this folder are all the files you need to run your Django project. Next, you’re going to create a new Django application inside your project. Django breaks up the functionality of a project into applications. Each application manages a distinct part of the project.

To create the application, change directories to countryapi and run the following command:

$ python manage.py startapp countries

This creates a new countries folder inside your project. Inside this folder are the base files for this application.

Now that you’ve created an application to work with, you need to tell Django about it. Alongside the countries folder that you just created is another folder called countryapi. This folder contains configurations and settings for your project.

Open up the settings.py file that’s inside the countryapi folder. Add the following lines to INSTALLED_APPS to tell Django about the countries application and Django REST framework:

# countryapi/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "countries",
]

You’ve added a line for the countries application and rest_framework.

You may be wondering why you need to add rest_framework to the applications list. You need to add it because Django REST framework is just another Django application. Django plugins are Django applications that are packaged up and distributed and that anyone can use.

The next step is to create a Django model to define the fields of your data. Inside of the countries application, update models.py with the following code:

# countries/models.py
from django.db import models

class Country(models.Model):
    name = models.CharField(max_length=100)
    capital = models.CharField(max_length=100)
    area = models.IntegerField(help_text="(in square kilometers)")

This code defines a Country model. Django will use this model to create the database table and columns for the country data.

Run the following commands to have Django update the database based on this model:

$ python manage.py makemigrations
Migrations for 'countries':
  countries/migrations/0001_initial.py
    - Create model Country

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, countries, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  ...

These commands use Django migrations to create a new table in the database.

This table starts empty, but it would be nice to have some initial data so you can test Django REST framework. To do this, you’re going to use a Django fixture to load some data in the database.

Copy and save the following JSON data into a file called countries.json and save it inside the countries directory:

[
    {
        "model": "countries.country",
        "pk": 1,
        "fields": {
            "name": "Thailand",
            "capital": "Bangkok",
            "area": 513120
        }
    },
    {
        "model": "countries.country",
        "pk": 2,
        "fields": {
            "name": "Australia",
            "capital": "Canberra",
            "area": 7617930
        }
    },
    {
        "model": "countries.country",
        "pk": 3,
        "fields": {
            "name": "Egypt",
            "capital": "Cairo",
            "area": 1010408
        }
    }
]

This JSON contains database entries for three countries. Call the following command to load this data in the database:

$ python manage.py loaddata countries.json
Installed 3 object(s) from 1 fixture(s)

This adds three rows to the database.

With that, your Django application is all set up and populated with some data. You can now start adding Django REST framework to the project.

Django REST framework takes an existing Django model and converts it to JSON for a REST API. It does this with model serializers. A model serializer tells Django REST framework how to convert a model instance into JSON and what data to include.

You’ll create your serializer for the Country model from above. Start by creating a file called serializers.py inside of the countries application. Once you’ve done that, add the following code to serializers.py:

# countries/serializers.py
from rest_framework import serializers
from .models import Country

class CountrySerializer(serializers.ModelSerializer):
    class Meta:
        model = Country
        fields = ["id", "name", "capital", "area"]

This serializer, CountrySerializer, subclasses serializers.ModelSerializer to automatically generate JSON content based on the model fields of Country. Unless specified, a ModelSerializer subclass will include all fields from the Django model in the JSON. You can modify this behavior by setting fields to a list of data you wish to include.

Just like Django, Django REST framework uses views to query data from the database to display to the user. Instead of writing REST API views from scratch, you can subclass Django REST framework’s ModelViewSet class, which has default views for common REST API operations.

Here’s a list of the actions that ModelViewSet provides and their equivalent HTTP methods:

HTTP method Action Description
GET .list() Get a list of countries.
GET .retrieve() Get a single country.
POST .create() Create a new country.
PUT .update() Update a country.
PATCH .partial_update() Partially update a country.
DELETE .destroy() Delete a country.

As you can see, these actions map to the standard HTTP methods you’d expect in a REST API. You can override these actions in your subclass or add additional actions based on the requirements of your API.

Below is the code for a ModelViewSet subclass called CountryViewSet. This class will generate the views needed to manage Country data. Add the following code to views.py inside the countries application:

# countries/views.py
from rest_framework import viewsets

from .models import Country
from .serializers import CountrySerializer

class CountryViewSet(viewsets.ModelViewSet):
    serializer_class = CountrySerializer
    queryset = Country.objects.all()

In this class, serializer_class is set to CountrySerializer and queryset is set to Country.objects.all(). This tells Django REST framework which serializer to use and how to query the database for this specific set of views.

Once the views are created, they need to be mapped to the appropriate URLs or endpoints. To do this, Django REST framework provides a DefaultRouter that will automatically generate URLs for a ModelViewSet.

Create a urls.py file in the countries application and add the following code to the file:

# countries/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter

from .views import CountryViewSet

router = DefaultRouter()
router.register(r"countries", CountryViewSet)

urlpatterns = [
    path("", include(router.urls))
]

This code creates a DefaultRouter and registers CountryViewSet under the countries URL. This will place all the URLs for CountryViewSet under /countries/.

Finally, you need to update the project’s base urls.py file to include all the countries URLs in the project. Update the urls.py file inside of the countryapi folder with the following code:

# countryapi/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("countries.urls")),
]

This puts all the URLs under /countries/. Now you’re ready to try out your Django-backed REST API. Run the following command in the root countryapi directory to start the Django development server:

$ python manage.py runserver

The development server is now running. Go ahead and send a GET request to /countries/ to get a list of all the countries in your Django project:

$ curl -i http://127.0.0.1:8000/countries/ -w 'n'

HTTP/1.1 200 OK
...

[
    {
        "id": 1,
        "name":"Thailand",
        "capital":"Bangkok",
        "area":513120
    },
    {
        "id": 2,
        "name":"Australia",
        "capital":"Canberra",
        "area":7617930
    },
    {
        "id": 3,
        "name":"Egypt",
        "capital":"Cairo",
        "area":1010408
    }
]

Django REST framework sends back a JSON response with the three countries you added earlier. The response above is formatted for readability, so your response will look different.

The DefaultRouter you created in countries/urls.py provides URLs for requests to all the standard API endpoints:

  • GET /countries/
  • GET /countries/<country_id>/
  • POST /countries/
  • PUT /countries/<country_id>/
  • PATCH /countries/<country_id>/
  • DELETE /countries/<country_id>/

You can try out a few more endpoints below. Send a POST request to /countries/ to a create a new Country in your Django project:

$ curl -i http://127.0.0.1:8000/countries/ 
-X POST 
-H 'Content-Type: application/json' 
-d '{"name":"Germany", "capital": "Berlin", "area": 357022}' 
-w 'n'

HTTP/1.1 201 Created
...

{
    "id":4,
    "name":"Germany",
    "capital":"Berlin",
    "area":357022
}

This creates a new Country with the JSON you sent in the request. Django REST framework returns a 201 Created status code and the new Country.

You can view an existing Country by sending a request to GET /countries/<country_id>/ with an existing id. Run the following command to get the first Country:

$ curl -i http://127.0.0.1:8000/countries/1/ -w 'n'

HTTP/1.1 200 OK
...

{
    "id":1,
    "name":"Thailand",
    "capital":"Bangkok",
    "area":513120
}

The response contains the information for the first Country. These examples only covered GET and POST requests. Feel free to try out PUT, PATCH, and DELETE requests on your own to see how you can fully manage your model from the REST API.

As you’ve seen, Django REST framework is a great option for building REST APIs, especially if you have an existing Django project and you want to add an API.

FastAPI

FastAPI is a Python web framework that’s optimized for building APIs. It uses Python type hints and has built-in support for async operations. FastAPI is built on top of Starlette and Pydantic and is very performant.

Below is an example of the REST API built with FastAPI:

# app.py
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

def _find_next_id():
    return max(country.country_id for country in countries) + 1

class Country(BaseModel):
    country_id: int = Field(default_factory=_find_next_id, alias="id")
    name: str
    capital: str
    area: int

countries = [
    Country(id=1, name="Thailand", capital="Bangkok", area=513120),
    Country(id=2, name="Australia", capital="Canberra", area=7617930),
    Country(id=3, name="Egypt", capital="Cairo", area=1010408),
]

@app.get("/countries")
async def get_countries():
    return countries

@app.post("/countries", status_code=201)
async def add_country(country: Country):
    countries.append(country)
    return country

This application uses the features of FastAPI to build a REST API for the same country data you’ve seen in the other examples.

You can try this application by installing fastapi with pip:

$ python -m pip install fastapi

You’ll also need to install uvicorn[standard], a server that can run FastAPI applications:

$ python -m pip install uvicorn[standard]

If you’ve installed both fastapi and uvicorn, then save the code above in a file called app.py. Run the following command to start up a development server:

$ uvicorn app:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

The server is now running. Open up a browser and go to http://127.0.0.1:8000/countries. You’ll see FastAPI respond with this:

[
    {
        "id": 1,
        "name":"Thailand",
        "capital":"Bangkok",
        "area":513120
    },
    {
        "id": 2,
        "name":"Australia",
        "capital":"Canberra",
        "area":7617930
    },
    {
        "id": 3,
        "name":"Egypt",
        "capital":"Cairo",
        "area":1010408
    }
]

FastAPI responds with a JSON array containing a list of countries. You can also add a new country by sending a POST request to /countries:

$ curl -i http://127.0.0.1:8000/countries 
-X POST 
-H 'Content-Type: application/json' 
-d '{"name":"Germany", "capital": "Berlin", "area": 357022}' 
-w 'n'

HTTP/1.1 201 Created
content-type: application/json
...

{"id":4,"name":"Germany","capital":"Berlin","area": 357022}

You added a new country. You can confirm this with GET /countries:

$ curl -i http://127.0.0.1:8000/countries -w 'n'

HTTP/1.1 200 OK
content-type: application/json
...

[
    {
        "id":1,
        "name":"Thailand",
        "capital":"Bangkok",
        "area":513120,
    },
    {
        "id":2,
        "name":"Australia",
        "capital":"Canberra",
        "area":7617930
    },
    {
        "id":3,
        "name":"Egypt",
        "capital":"Cairo",
        "area":1010408
    },
    {
        "id":4,
        "name": "Germany",
        "capital": "Berlin",
        "area": 357022
    }
]

FastAPI returns a JSON list including the new country you just added.

You’ll notice that the FastAPI application looks similar to the Flask application. Like Flask, FastAPI has a focused feature set. It doesn’t try to handle all aspects of web application development. It’s designed to build APIs with modern Python features.

If you look near the top of app.py, then you’ll see a class called Country that extends BaseModel. The Country class describes the structure of the data in the REST API:

class Country(BaseModel):
    country_id: int = Field(default_factory=_find_next_id, alias="id")
    name: str
    capital: str
    area: int

This is an example of a Pydantic model. Pydantic models provide some helpful features in FastAPI. They use Python type annotations to enforce the data type for each field in the class. This allows FastAPI to automatically generate JSON, with the correct data types, for API endpoints. It also allows FastAPI to validate incoming JSON.

It’s helpful to highlight the first line as there’s a lot going on there:

country_id: int = Field(default_factory=_find_next_id, alias="id")

In this line, you see country_id, which stores an integer for the ID of the Country. It uses the Field function from Pydantic to modify the behavior of country_id. In this example, you’re passing Field the keyword arguments default_factory and alias.

The first argument, default_factory, is set to _find_next_id(). This argument specifies a function to run whenever a new Country is created. The return value will be assigned to country_id.

The second argument, alias, is set to id. This tells FastAPI to output the key "id" instead of "country_id" in the JSON:

{
    "id":1,
    "name":"Thailand",
    "capital":"Bangkok",
    "area":513120,
},

This alias also means you can use id when you create a new Country. You can see this in the countries list:

countries = [
    Country(id=1, name="Thailand", capital="Bangkok", area=513120),
    Country(id=2, name="Australia", capital="Canberra", area=7617930),
    Country(id=3, name="Egypt", capital="Cairo", area=1010408),
]

This list contains three instances of Country for the initial countries in the API. Pydantic models provide some great features and allow FastAPI to easily process JSON data.

Now take a look at the two API functions in this application. The first, get_countries(), returns a list of countries for GET requests to /countries:

@app.get("/countries")
async def get_countries():
    return countries

FastAPI will automatically create JSON based on the fields in the Pydantic model and set the right JSON data type from the Python type hints.

The Pydantic model also provides a benefit when you make a POST request to /countries. You can see in the second API function below that the parameter country has a Country annotation:

@app.post("/countries", status_code=201)
async def add_country(country: Country):
    countries.append(country)
    return country

This type annotation tells FastAPI to validate the incoming JSON against Country. If it doesn’t match, then FastAPI will return an error. You can try this out by making a request with JSON that doesn’t match the Pydantic model:

$ curl -i http://127.0.0.1:8000/countries 
-X POST 
-H 'Content-Type: application/json' 
-d '{"name":"Germany", "capital": "Berlin"}' 
-w 'n'

HTTP/1.1 422 Unprocessable Entity
content-type: application/json
...

{
    "detail": [
        {
            "loc":["body","area"],
            "msg":"field required",
            "type":"value_error.missing"
        }
    ]
}

The JSON in this request was missing a value for area, so FastAPI returned a response with the status code 422 Unprocessable Entity as well as details about the error. This validation is made possible by the Pydantic model.

This example only scratches the surface of what FastAPI can do. With its high performance and modern features like async functions and automatic documentation, FastAPI is worth considering for your next REST API.





Source link

Latest articles

Related articles

Leave a reply

Please enter your comment!
Please enter your name here