HTTP APIs
Workshop

Tips for navigating the slides:
  • Press O or Escape for overview mode.
  • Visit this link for a nice printable version
  • Press the copy icon on the upper right of code blocks to copy the code

Welcome!

Classroom "rules":

  • I am here for you!
  • Every question is important
  • Help each other

Introductions

Tell us about yourself:

  • Name
  • Pronouns
  • Location
  • Programming/Web experience
  • What interests you about Python HTTP APIs?
  • What's your favorite flavor of ice cream / popsicle? 🍦

Today's topics

Bit (the raccoon) lecturing
  • (Review) HTTP 101
  • What are HTTP APIs?
    • πŸ‘©πŸΎβ€πŸ’» Exercise: Play with APIs
  • Building HTTP APIs
    • Flask
    • FastAPI
    • πŸ‘©πŸΌβ€πŸ’» Exercise: Build an API
  • Hosting HTTP APIs on Azure
    • πŸ‘©πŸ»β€πŸ’» Exercise: Deploy an API

GitHub Codespaces

Codespaces is an online development environment.

Open a GitHub repo in Codespaces by clicking Code button, selecting Codespaces tab, and clicking Create codespace on main.

Then wait patiently... ☺️

Screenshot of Codespace tab

60 hours of free usage each month. πŸ”— Tips for optimizing quotas

Exercise requirements

Option 1: Online development with Codespaces

Option 2: Local development with VS Code

Option 3: Local development

HTTP 101

HTTP (HyperText Transfer Protocol)

A client sends an HTTP request:


                GET /index.html HTTP/1.1
                Host: www.example.com
                

A server sends back an HTTP response:


                HTTP/1.1 200 OK
                Content-Type: text/html; charset=UTF-8
                Content-Length: 208
                <!DOCTYPE html>
                <html>
                    <head>
                        <title>Example Domain</title>
                    </head>
                    <body>
                        <h1>Example Domain</h1>
                        <p>This domain is to be used for illustrative examples in documents.</p>
                    </body>
                </html>
                

HTTP Status Codes

The most commonly used codes:

Code Meaning
200 OK
301 Moved Permanently
404 Not Found
500 Server Error

See more codes at HTTPcats.com or Wikipedia: HTTP status codes.

HTTP Request Methods

  • GET: retrieve data from a server.
  • POST: send data to a server.
  • PUT: send data to a server, replacing existing data.
  • DELETE: delete data from a server.

HTTP APIs

API (Application Programming Interface)

A way for one program to talk to another program.

Example:


                from weather import forecast

                todays_forecast = forecast(94702)
                

The weather module's API includes a forecast function that takes a zip code and returns a forecast.

HTTP API

Any API that uses HTTP as its communication protocol.

A client sends an HTTP request:


                GET /weather?zip=94530 HTTP/1.1
                Host: api.example.com
                

The server sends back an HTTP response:


                HTTP/1.1 200 OK
                Content-Type: text/json; charset=UTF-8
                Content-Length: 30
                {"temperature": 70, "wind": 5}
                

HTTP API response formats

  • JSON
    
                        {"temperature": 70, "wind": 5}
                        
  • XML (including RSS/ATOM)
    
                        <weather-response>
                          <temperature>70</temperature>
                          <wind>5</wind>
                        </weather-response>
                        
  • Image
    Weather icon for 70 degrees

HTTP APIs: GET vs. POST

GET: retrieve data from a server.
Often used with query parameters.


                GET /weather?zip=94530 HTTP/1.1
                Host: api.example.com
                

POST: send data to a server.
Data is often in JSON or form-encoded.


                POST /scores HTTP/1.1
                Host: api.example.com
                player=pamela&score=50
                

Processing APIs in Python

Use urllib, requests, or urllib3 to make HTTP requests.


                import urllib3

                resp = urllib3.request("GET",
                    "https://api.zippopotam.us/us/94530")
                result = resp.json()
                

Exercise: Play with APIs

Try the APIs below, customizing the URLs as suggested:

  • Zippopotamus: Find out your latitude/longitude.
  • Sunrise/Sunset: Try it for your latitude/longitude.
  • Reddit: Try it with the Python subreddit or your favorite subreddit.

You can either try them in the browser or try parsing them in Python and printing the results.

Popular APIs

Some examples:

πŸ”‘ Most of the popular APIs require you to sign up for a key so that they can track your usage and limit calls based on your payment level.

Internal APIs

An API can also be setup solely for use by the company that made it.

Many websites have a separate codebase for their frontend and backend, and all the communication happens over internal APIs.

Internal APIs should still be documented and easy to use.

Building an HTTP API
...in Python!

Bit (the raccoon) in front of a computer and Python logo

A simple API in Flask


                import json
                import random

                from flask import Flask, request

                app = Flask(__name__)

                @app.route('/v1/generate_name')
                def generate_name():
                    random_name = random.choice(["Minnie", "Margaret", "Myrtle"])
                    result = {"name": random_name}
                    return json.dumps(result)
                

πŸ‘€ Demo: https://flask-simple-api-67uuyfbgx7tlq-function-app.azurewebsites.net/v1/generate_name

πŸ‘©πŸΌβ€πŸ’» Code: https://github.com/pamelafox/simple-flask-api-azure-function/blob/main/api/flask_app.py

A parameterized API in Flask


                import json
                import random
                from flask import Flask, request

                app = Flask(__name__)

                @app.route('/v2/generate_name')
                def generate_name():
                    starts_with = request.args.get("starts_with")
                    names = ["Minnie", "Margaret", "Myrtle", "Noa", "Nadia"]
                    if starts_with:
                        names = [n for n in names if n.lower().startswith(starts_with)]
                    random_name = random.choice(names)
                    result = {"name": random_name}
                    return json.dumps(result)
                

πŸ‘€ Demo: https://flask-simple-api-67uuyfbgx7tlq-function-app.azurewebsites.net/v2/generate_name

πŸ‘©πŸΌβ€πŸ’» Code: https://github.com/pamelafox/simple-flask-api-azure-function/blob/main/api2/flask_app.py

What's that API missing?

  • Error checking
    • Parameter types
    • Required vs. optional parameters
  • Documentation

FastAPI to the rescue!

FastAPI is a Python framework designed specifically for building HTTP APIs.

A simple API in FastAPI


                import random
                import fastapi

                app = fastapi.FastAPI()

                @app.get("/generate_name")
                async def generate_name():
                    names = ["Minnie", "Margaret", "Myrtle", "Noa", "Nadia"]
                    random_name = random.choice(names)
                    return {"name": random_name}
                

πŸ‘©πŸΌβ€πŸ’» Want to follow along? Starter repo:
https://github.com/pamelafox/pyday-fastapi-starter

Running FastAPI locally

1. Put code in api/main.py

2. Install requirements


                pip install fastapi
                pip install "uvicorn[standard]"
                

3. Run the server


                uvicorn api.main:app --reload --port=8000
                

4. Try the API and docs
http://127.0.0.1:8000/generate_name
http://127.0.0.1:8000/docs

Adding query parameters


                import random
                import fastapi

                app = fastapi.FastAPI()

                @app.get("/generate_name")
                async def generate_name(max_len: int = None):
                    names = ["Minnie", "Margaret", "Myrtle", "Noa", "Nadia"]
                    if max_len:
                        names = [n for n in names if len(n) <= max_len]
                    random_name = random.choice(names)
                    return {"name": random_name}
                

FastAPI also supports passing parameters in the path, cookies, headers, or body.

Returning errors in FastAPI


                import random
                import fastapi

                app = fastapi.FastAPI()

                @app.get("/generate_name")
                async def generate_name(max_len:int = None):
                    names = ["Minnie", "Margaret", "Myrtle", "Noa", "Nadia"]
                    if max_len:
                        names = [n for n in names if len(n) <= max_len]
                    if len(names) == 0:
                        raise fastapi.HTTPException(status_code=404, detail="No name found")
                    random_name = random.choice(names)
                    return {"name": random_name}
                

Exercise: Make an API

Starting from this repo:
github.com/pamelafox/simple-fastapi-azure-function

  1. Follow the readme steps to get the FastAPI app running.
  2. Add more names to the list.
  3. Add a new API parameter, like ends_with, includes, or length.
  4. Add a new route to generate something else, like pet names.

πŸ™‹πŸΌβ€β™€οΈπŸ™‹πŸΎβ€β™€οΈπŸ™‹πŸ½β€β™€οΈ Let us know if you need any help! πŸ™‹πŸ»β€β™€οΈπŸ™‹πŸ½β€β™‚οΈπŸ™‹πŸΏβ€β™€οΈ

Productionizing
FastAPI apps

Bit (the raccoon) in the clouds next to Azure logo

Gunicorn

Gunicorn is a production-level server that can run multiple worker processes.

Add gunicorn to requirements.txt:


                fastapi
                uvicorn[standard]
                gunicorn
                

Use gunicorn to run FastAPI app using Uvicorn worker:


                python3 -m gunicorn api.main:app --workers 4 \
                   --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
                

Configuring gunicorn

Gunicorn can be configured with a gunicorn.conf.py file to adjust worker count based on CPU cores.


                import multiprocessing

                max_requests = 1000
                max_requests_jitter = 50
                log_file = "-"
                bind = "0.0.0.0:8000"
                worker_class = "uvicorn.workers.UvicornWorker"
                workers = (multiprocessing.cpu_count() * 2) + 1
                

The run command can be simplified to:


                python3 -m gunicorn main:app
                

Hosting an HTTP API
...on Azure!

Bit (the raccoon) in the clouds next to Azure logo

Hosting considerations

  • How much traffic do you expect?
  • How variable will the traffic be?
  • Do you need scale-to-zero?
  • What's your budget?
  • Is it public facing?
  • How will you manage API use?

Azure hosting options

Cloud Azure
Environment Containers PaaS
Azure Kubernetes Service Container Management Azure App Service Serverless
Azure Container Apps Azure Functions

For FastAPI, App Service or Functions are good choices.

Hosting a FastAPI on Azure Functions

FastAPI API architecture diagram: Azure Functions, App Service Plan, Storage account, App Insights, Log Analytics Workspace

Deploying a FastAPI to Azure Functions

Using the Azure Dev CLI:

What's the hosted API missing?

  • Subscription keys
  • Quotas/Rate limiting
  • Caching
  • CORS handling
  • IP blocking

Adding API Management Policy

An Azure API Management Policy provides all the additional features of a public API.

FastAPI API architecture diagram: Azure Functions, Storage account, API Management policy

Adding Azure CDN

Azure CDN provides a global network of servers to cache your API responses.

FastAPI API architecture diagram: Functions, CDN, Monitoring

πŸ‘€ Demo: staticmaps-rk5lctcdqzvbs-cdn-endpoint.azureedge.net

πŸ‘©πŸΌβ€πŸ’» Code: github.com/pamelafox/staticmaps-function

Exercise: Deploy an API

Starting from this repo (or your fork):
github.com/pamelafox/simple-fastapi-azure-function

  1. Sign up for a free Azure account and create a subscription.
  2. Either open the project in Codespaces or follow these installation steps for the Azure Developer CLI.
  3. Run azd up. If prompted, login to your Azure account.
  4. If it deploys successfully, share the endpoint URL with your classmates. If not, let us know about any bugs. πŸͺ²
  5. Once you've verified the app is working, run azd down to un-deploy the app (so that you don't waste cloud resources unnecessarily).

πŸ™‹πŸΌβ€β™€οΈπŸ™‹πŸΎβ€β™€οΈπŸ™‹πŸ½β€β™€οΈ Let us know if you need any help! πŸ™‹πŸ»β€β™€οΈπŸ™‹πŸ½β€β™‚οΈπŸ™‹πŸΏβ€β™€οΈ

Demo: Icon writer API


                def main(req: func.HttpRequest) -> func.HttpResponse:
                    text = get_param(req, 'text')
                    size = get_param(req, 'size', 80)
                    bgcolor = get_param(req, 'bgcolor', 'black')
                    fontcolor = get_param(req, 'fontcolor', 'white')
                    if text:
                        img = write_icon(text, size=size, bgcolor=bgcolor, fontcolor=fontcolor)
                        img_byte_arr = io.BytesIO()
                        img.save(img_byte_arr, format='PNG')
                        img_byte_arr = img_byte_arr.getvalue()
                        return func.HttpResponse(img_byte_arr, mimetype='image/png')
                    else:
                        return func.HttpResponse(
                            "Text must be specified",
                            status_code=400
                    )
                

πŸ‘€ Demo: pamelafox.github.io/icon-writer-website/

πŸ‘©πŸΌβ€πŸ’» Code: github.com/pamelafox/icon-writer-function/blob/main/IconWriter/__init__.py

More on APIs

Raccoons with laptops

Demo: Regression Model API

A parameterized API based on a sklearn model.


                model = joblib.load(f"{os.path.dirname(os.path.realpath(__file__))}/model.pkl")

                @app.get("/model_predict")
                async def model_predict(
                    years_code: int,
                    years_code_pro: int,
                    ed_level: categories.EdLevel,
                    main_branch: categories.MainBranch,
                    country: categories.Country):
                    X_new = numpy.array([[years_code, years_code_pro, ed_level.value, main_branch.value, country.value]])
                    result = model.predict(X_new)
                    return {"prediction": result[0]}
                

πŸ‘€ Demo: salary-model2-sibqf23ha7ib2-function-app.azurewebsites.net/docs

πŸ‘©πŸΌβ€πŸ’» Code: https://github.com/pamelafox/regression-model-azure-demo/blob/main/function/model_predict/__init__.py

POST APIs with FastAPI

If your API needs to support the creation of data, then requests should go over HTTP POST instead.

How POST requests are specified in FastAPI:


                from typing import Union

                from fastapi import FastAPI
                from pydantic import BaseModel

                class Item(BaseModel):
                    name: str
                    description: Union[str, None] = None
                    price: float
                    tax: Union[float, None] = None

                app = FastAPI()

                @app.post("/items/")
                async def create_item(item: Item):
                    return item
                

Learn more in FastAPI docs

APIs with Django REST

If your site uses a DB and the Django framework, the Django REST framework can be used to create CRUD APIs.


                from django.urls import path, include
                from django.contrib.auth.models import User
                from rest_framework import routers, serializers, viewsets

                class UserSerializer(serializers.HyperlinkedModelSerializer):
                    class Meta:
                        model = User
                        fields = ['url', 'username', 'email', 'is_staff']

                class UserViewSet(viewsets.ModelViewSet):
                    queryset = User.objects.all()
                    serializer_class = UserSerializer

                router = routers.DefaultRouter()
                router.register(r'users', UserViewSet)

                urlpatterns = [
                    path('', include(router.urls)),
                    path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
                ]
                

Any questions?

A bunch of raccoon students with computers