From 6ac6be061a63bd0a611868fb056f487f61598afd Mon Sep 17 00:00:00 2001 From: realpython-bot Date: Wed, 16 Sep 2026 15:04:52 +0000 Subject: [PATCH] Add materials for Python and REST APIs: Interacting With Web Services Create the api-integration-in-python folder, which the tutorial never had. It holds the requests examples from the "Consuming APIs" section as runnable scripts and the Flask, Django REST framework, and FastAPI countries APIs from the "Tools of the Trade" section. Co-Authored-By: Claude Opus 5 --- api-integration-in-python/README.md | 75 ++++++++++ .../consuming-apis/delete_request.py | 12 ++ .../consuming-apis/get_request.py | 14 ++ .../consuming-apis/patch_request.py | 13 ++ .../consuming-apis/post_request.py | 23 +++ .../consuming-apis/put_request.py | 16 +++ .../django-api/countries/__init__.py | 0 .../django-api/countries/admin.py | 3 + .../django-api/countries/apps.py | 5 + .../countries/fixtures/countries.json | 29 ++++ .../countries/migrations/0001_initial.py | 23 +++ .../countries/migrations/__init__.py | 0 .../django-api/countries/models.py | 7 + .../django-api/countries/serializers.py | 9 ++ .../django-api/countries/tests.py | 3 + .../django-api/countries/urls.py | 9 ++ .../django-api/countries/views.py | 9 ++ .../django-api/countryapi/__init__.py | 0 .../django-api/countryapi/asgi.py | 16 +++ .../django-api/countryapi/settings.py | 131 ++++++++++++++++++ .../django-api/countryapi/urls.py | 7 + .../django-api/countryapi/wsgi.py | 16 +++ .../django-api/manage.py | 23 +++ api-integration-in-python/fastapi-api/app.py | 33 +++++ api-integration-in-python/flask-api/app.py | 28 ++++ api-integration-in-python/requirements.txt | 6 + 26 files changed, 510 insertions(+) create mode 100644 api-integration-in-python/README.md create mode 100644 api-integration-in-python/consuming-apis/delete_request.py create mode 100644 api-integration-in-python/consuming-apis/get_request.py create mode 100644 api-integration-in-python/consuming-apis/patch_request.py create mode 100644 api-integration-in-python/consuming-apis/post_request.py create mode 100644 api-integration-in-python/consuming-apis/put_request.py create mode 100644 api-integration-in-python/django-api/countries/__init__.py create mode 100644 api-integration-in-python/django-api/countries/admin.py create mode 100644 api-integration-in-python/django-api/countries/apps.py create mode 100644 api-integration-in-python/django-api/countries/fixtures/countries.json create mode 100644 api-integration-in-python/django-api/countries/migrations/0001_initial.py create mode 100644 api-integration-in-python/django-api/countries/migrations/__init__.py create mode 100644 api-integration-in-python/django-api/countries/models.py create mode 100644 api-integration-in-python/django-api/countries/serializers.py create mode 100644 api-integration-in-python/django-api/countries/tests.py create mode 100644 api-integration-in-python/django-api/countries/urls.py create mode 100644 api-integration-in-python/django-api/countries/views.py create mode 100644 api-integration-in-python/django-api/countryapi/__init__.py create mode 100644 api-integration-in-python/django-api/countryapi/asgi.py create mode 100644 api-integration-in-python/django-api/countryapi/settings.py create mode 100644 api-integration-in-python/django-api/countryapi/urls.py create mode 100644 api-integration-in-python/django-api/countryapi/wsgi.py create mode 100755 api-integration-in-python/django-api/manage.py create mode 100644 api-integration-in-python/fastapi-api/app.py create mode 100644 api-integration-in-python/flask-api/app.py create mode 100644 api-integration-in-python/requirements.txt diff --git a/api-integration-in-python/README.md b/api-integration-in-python/README.md new file mode 100644 index 0000000000..216ed0b129 --- /dev/null +++ b/api-integration-in-python/README.md @@ -0,0 +1,75 @@ +# Python and REST APIs: Interacting With Web Services + +This folder provides the code examples for the Real Python tutorial [Python and REST APIs: Interacting With Web Services](https://realpython.com/api-integration-in-python/). + +The examples are grouped into one subfolder per section of the tutorial, because the Flask and FastAPI examples both use a file called `app.py`, and because the tutorial itself advises you to keep each example in its own folder: + +- `consuming-apis/`: the `requests` examples from **REST and Python: Consuming APIs**. The tutorial shows these in the REPL, so here they're runnable scripts that `print()` the results, one script per HTTP method section. +- `flask-api/`: the Flask countries API from **Tools of the Trade → Flask**. +- `django-api/`: the `countryapi` Django project with Django REST framework from **Tools of the Trade → Django REST Framework**. +- `fastapi-api/`: the FastAPI countries API from **Tools of the Trade → FastAPI**. + +## Setup + +Create and activate a virtual environment: + +```console +$ python -m venv venv +$ source venv/bin/activate +``` + +Install the pinned dependencies: + +```console +(venv) $ python -m pip install -r requirements.txt +``` + +The single `requirements.txt` covers all four examples. If you'd rather isolate them, then create one virtual environment per subfolder and install only the packages that example needs. + +## Consuming APIs With `requests` + +Each script sends one kind of request to [JSONPlaceholder](https://jsonplaceholder.typicode.com/) and prints the response, so you need an internet connection to run them: + +```console +(venv) $ cd consuming-apis/ +(venv) $ python get_request.py +{'userId': 1, 'id': 1, 'title': 'delectus aut autem', 'completed': False} +200 +application/json; charset=utf-8 +``` + +The other scripts are `post_request.py`, `put_request.py`, `patch_request.py`, and `delete_request.py`. + +## Flask + +```console +(venv) $ cd flask-api/ +(venv) $ export FLASK_APP=app.py +(venv) $ export FLASK_DEBUG=1 +(venv) $ flask run +``` + +Then request the endpoint at `http://127.0.0.1:5000/countries`. + +## Django REST Framework + +The `django-api/` folder is the `countryapi` project that the tutorial creates with `django-admin startproject countryapi` and `python manage.py startapp countries`. Set up its database and load the fixture before you start the server: + +```console +(venv) $ cd django-api/ +(venv) $ python manage.py migrate +(venv) $ python manage.py loaddata countries.json +Installed 3 object(s) from 1 fixture(s) +(venv) $ python manage.py runserver +``` + +Then request the endpoint at `http://127.0.0.1:8000/countries/`. + +## FastAPI + +```console +(venv) $ cd fastapi-api/ +(venv) $ uvicorn app:app --reload +``` + +Then request the endpoint at `http://127.0.0.1:8000/countries`. diff --git a/api-integration-in-python/consuming-apis/delete_request.py b/api-integration-in-python/consuming-apis/delete_request.py new file mode 100644 index 0000000000..b2bfd22581 --- /dev/null +++ b/api-integration-in-python/consuming-apis/delete_request.py @@ -0,0 +1,12 @@ +"""Send a DELETE request to JSONPlaceholder to remove a to-do. + +From the "DELETE" section of the tutorial. +""" + +import requests + +api_url = "https://jsonplaceholder.typicode.com/todos/10" +response = requests.delete(api_url) +print(response.json()) + +print(response.status_code) diff --git a/api-integration-in-python/consuming-apis/get_request.py b/api-integration-in-python/consuming-apis/get_request.py new file mode 100644 index 0000000000..cc808a7002 --- /dev/null +++ b/api-integration-in-python/consuming-apis/get_request.py @@ -0,0 +1,14 @@ +"""Send a GET request to JSONPlaceholder. + +From the "GET" section of the tutorial. +""" + +import requests + +api_url = "https://jsonplaceholder.typicode.com/todos/1" +response = requests.get(api_url) +print(response.json()) + +# Beyond the JSON data, you can inspect the response itself. +print(response.status_code) +print(response.headers["Content-Type"]) diff --git a/api-integration-in-python/consuming-apis/patch_request.py b/api-integration-in-python/consuming-apis/patch_request.py new file mode 100644 index 0000000000..8c3791ab37 --- /dev/null +++ b/api-integration-in-python/consuming-apis/patch_request.py @@ -0,0 +1,13 @@ +"""Send a PATCH request to JSONPlaceholder to modify one field. + +From the "PATCH" section of the tutorial. +""" + +import requests + +api_url = "https://jsonplaceholder.typicode.com/todos/10" +todo = {"title": "Mow lawn"} +response = requests.patch(api_url, json=todo) +print(response.json()) + +print(response.status_code) diff --git a/api-integration-in-python/consuming-apis/post_request.py b/api-integration-in-python/consuming-apis/post_request.py new file mode 100644 index 0000000000..9ebce7a2ca --- /dev/null +++ b/api-integration-in-python/consuming-apis/post_request.py @@ -0,0 +1,23 @@ +"""Send a POST request to JSONPlaceholder to create a new to-do. + +From the "POST" section of the tutorial. +""" + +import json + +import requests + +api_url = "https://jsonplaceholder.typicode.com/todos" +todo = {"userId": 1, "title": "Buy milk", "completed": False} +response = requests.post(api_url, json=todo) +print(response.json()) + +print(response.status_code) + +# An equivalent version that serializes the JSON and sets the +# Content-Type header manually instead of using the json argument. +headers = {"Content-Type": "application/json"} +response = requests.post(api_url, data=json.dumps(todo), headers=headers) +print(response.json()) + +print(response.status_code) diff --git a/api-integration-in-python/consuming-apis/put_request.py b/api-integration-in-python/consuming-apis/put_request.py new file mode 100644 index 0000000000..c1825ba67e --- /dev/null +++ b/api-integration-in-python/consuming-apis/put_request.py @@ -0,0 +1,16 @@ +"""Send a PUT request to JSONPlaceholder to replace an existing to-do. + +From the "PUT" section of the tutorial. +""" + +import requests + +api_url = "https://jsonplaceholder.typicode.com/todos/10" +response = requests.get(api_url) +print(response.json()) + +todo = {"userId": 1, "title": "Wash car", "completed": True} +response = requests.put(api_url, json=todo) +print(response.json()) + +print(response.status_code) diff --git a/api-integration-in-python/django-api/countries/__init__.py b/api-integration-in-python/django-api/countries/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api-integration-in-python/django-api/countries/admin.py b/api-integration-in-python/django-api/countries/admin.py new file mode 100644 index 0000000000..4fd549025a --- /dev/null +++ b/api-integration-in-python/django-api/countries/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin # noqa: F401 + +# Register your models here. diff --git a/api-integration-in-python/django-api/countries/apps.py b/api-integration-in-python/django-api/countries/apps.py new file mode 100644 index 0000000000..d2a8fde084 --- /dev/null +++ b/api-integration-in-python/django-api/countries/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CountriesConfig(AppConfig): + name = "countries" diff --git a/api-integration-in-python/django-api/countries/fixtures/countries.json b/api-integration-in-python/django-api/countries/fixtures/countries.json new file mode 100644 index 0000000000..28918ed19e --- /dev/null +++ b/api-integration-in-python/django-api/countries/fixtures/countries.json @@ -0,0 +1,29 @@ +[ + { + "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 + } + } +] diff --git a/api-integration-in-python/django-api/countries/migrations/0001_initial.py b/api-integration-in-python/django-api/countries/migrations/0001_initial.py new file mode 100644 index 0000000000..f4cd1ac871 --- /dev/null +++ b/api-integration-in-python/django-api/countries/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# Generated by Django 6.1 on 2026-09-16 15:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Country', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('capital', models.CharField(max_length=100)), + ('area', models.IntegerField(help_text='(in square kilometers)')), + ], + ), + ] diff --git a/api-integration-in-python/django-api/countries/migrations/__init__.py b/api-integration-in-python/django-api/countries/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api-integration-in-python/django-api/countries/models.py b/api-integration-in-python/django-api/countries/models.py new file mode 100644 index 0000000000..2419fa098d --- /dev/null +++ b/api-integration-in-python/django-api/countries/models.py @@ -0,0 +1,7 @@ +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)") diff --git a/api-integration-in-python/django-api/countries/serializers.py b/api-integration-in-python/django-api/countries/serializers.py new file mode 100644 index 0000000000..e08c04364d --- /dev/null +++ b/api-integration-in-python/django-api/countries/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from .models import Country + + +class CountrySerializer(serializers.ModelSerializer): + class Meta: + model = Country + fields = ["id", "name", "capital", "area"] diff --git a/api-integration-in-python/django-api/countries/tests.py b/api-integration-in-python/django-api/countries/tests.py new file mode 100644 index 0000000000..e55d689097 --- /dev/null +++ b/api-integration-in-python/django-api/countries/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase # noqa: F401 + +# Create your tests here. diff --git a/api-integration-in-python/django-api/countries/urls.py b/api-integration-in-python/django-api/countries/urls.py new file mode 100644 index 0000000000..b590c8151f --- /dev/null +++ b/api-integration-in-python/django-api/countries/urls.py @@ -0,0 +1,9 @@ +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))] diff --git a/api-integration-in-python/django-api/countries/views.py b/api-integration-in-python/django-api/countries/views.py new file mode 100644 index 0000000000..541fcae2eb --- /dev/null +++ b/api-integration-in-python/django-api/countries/views.py @@ -0,0 +1,9 @@ +from rest_framework import viewsets + +from .models import Country +from .serializers import CountrySerializer + + +class CountryViewSet(viewsets.ModelViewSet): + serializer_class = CountrySerializer + queryset = Country.objects.all() diff --git a/api-integration-in-python/django-api/countryapi/__init__.py b/api-integration-in-python/django-api/countryapi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api-integration-in-python/django-api/countryapi/asgi.py b/api-integration-in-python/django-api/countryapi/asgi.py new file mode 100644 index 0000000000..28ff44a5ef --- /dev/null +++ b/api-integration-in-python/django-api/countryapi/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for countryapi project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "countryapi.settings") + +application = get_asgi_application() diff --git a/api-integration-in-python/django-api/countryapi/settings.py b/api-integration-in-python/django-api/countryapi/settings.py new file mode 100644 index 0000000000..e1832f848b --- /dev/null +++ b/api-integration-in-python/django-api/countryapi/settings.py @@ -0,0 +1,131 @@ +""" +Django settings for countryapi project. + +Generated by 'django-admin startproject' using Django 6.1. + +For more information on this file, see +https://docs.djangoproject.com/en/6.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/6.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = ( + "django-insecure-cl)!rffwsl_fs)bjbug%l%91hwm&8l)^th2^stgpay*+j9#@f+" +) + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "countries", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "countryapi.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "countryapi.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/6.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/6.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.1/howto/static-files/ + +STATIC_URL = "static/" + + +# Email +# https://docs.djangoproject.com/en/6.1/topics/email/#topic-email-configuration + +MAILERS = { + "default": { + "BACKEND": "django.core.mail.backends.console.EmailBackend", + }, +} diff --git a/api-integration-in-python/django-api/countryapi/urls.py b/api-integration-in-python/django-api/countryapi/urls.py new file mode 100644 index 0000000000..0d1011dac1 --- /dev/null +++ b/api-integration-in-python/django-api/countryapi/urls.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path("admin/", admin.site.urls), + path("", include("countries.urls")), +] diff --git a/api-integration-in-python/django-api/countryapi/wsgi.py b/api-integration-in-python/django-api/countryapi/wsgi.py new file mode 100644 index 0000000000..4fe52b05ee --- /dev/null +++ b/api-integration-in-python/django-api/countryapi/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for countryapi project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "countryapi.settings") + +application = get_wsgi_application() diff --git a/api-integration-in-python/django-api/manage.py b/api-integration-in-python/django-api/manage.py new file mode 100755 index 0000000000..72aca1e1e0 --- /dev/null +++ b/api-integration-in-python/django-api/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "countryapi.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/api-integration-in-python/fastapi-api/app.py b/api-integration-in-python/fastapi-api/app.py new file mode 100644 index 0000000000..e2886531a6 --- /dev/null +++ b/api-integration-in-python/fastapi-api/app.py @@ -0,0 +1,33 @@ +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 diff --git a/api-integration-in-python/flask-api/app.py b/api-integration-in-python/flask-api/app.py new file mode 100644 index 0000000000..272fd13bdb --- /dev/null +++ b/api-integration-in-python/flask-api/app.py @@ -0,0 +1,28 @@ +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 diff --git a/api-integration-in-python/requirements.txt b/api-integration-in-python/requirements.txt new file mode 100644 index 0000000000..2de3669c63 --- /dev/null +++ b/api-integration-in-python/requirements.txt @@ -0,0 +1,6 @@ +Django==6.1 +Flask==3.1.3 +djangorestframework==3.18.0 +fastapi==0.141.1 +requests==2.34.2 +uvicorn==0.52.4