Пишем свой первый API c помощью FastAPI
hard

Пишем свой первый API c помощью FastAPI

Создаём код, который возвращает данные с сервера

API — программный интерфейс приложения. Это способ общения одних программ с другими по разным интернет-протоколам. Например, с помощью API можно сделать запрос к сервису погоды, получить оттуда данные и использовать их в своём проекте.

Чтобы другие разработчики могли научить свои приложения запрашивать что-то у вашего сервиса, нужно рассказать о правилах запросов в документации. Сегодня мы напишем свой небольшой API и создадим к нему документацию.

Если раньше вы не программировали на Python, почитайте статьи из нашего мастрида для начинающих, например:

Как установить Python на компьютер и начать на нём писать

Старт в Python для новичков: с чего начать

Что такое API с технической точки зрения

Разработкой API занимаются бэкенд-разработчики. Фактически это просто дополнение к программе, которое умеет получать запросы и отправлять в ответ что-то своё (и что может быть полезно другим разработчикам). И вот это что-то полезное с помощью API можно добавить в приложения и сервисы.

Как правило, все программные интерфейсы обмениваются данными в JSON-формате. Это пары ключ-значение, разделённые двоеточием, например:

{
  "firstname": "Игорь",
  "lastname": "Росляков",
  "city": "Новосибирск",
  "age": 37,
  "bonus": 10000,
  "prev": [
    "Кеды",
    "Лыжи",
    "Зимняя куртка"
  ]
}

Со стороны пользователя работа с API делится на три части.

Знакомство с документацией. Документация может лежать в репозитории GitHub, а может быть вынесена на отдельный сайт и быть красиво оформленной, как у финансового API marketstack:

Выбор инструмента для работы. Чтобы попробовать возможности API, можно воспользоваться специальными приложениями для тестирования, например Restfox или плагин Thunder для VSCode. 

Один из самых популярных инструментов на сегодня — Postman. Некоторые API сразу дают специальную ссылку, чтобы их можно было протестировать в этом сервисе:

Подключение API. Это делается в коде, на том же языке, на котором пишут сервер. Какого-то одного правильного способа нет — даже для отправки запросов можно использовать несколько технических решений.

Иногда в документации можно увидеть сниппеты кода — шаблоны с запросами. Такой код можно скопировать и вставить в свою программу:

Что будем делать сегодня

Сегодня мы напишем небольшой API для понимания, как это выглядит со стороны разработчиков программного интерфейса.

Есть несколько вариантов научить программу отвечать на запросы, мы будем использовать стиль REST (REpresentational State Transfer). В REST используется четыре основных вида запросов, их мы и напишем.

Что будем использовать

Для проекта нам понадобятся:

  • Python-фреймворк FastAPI;
  • сервер Uvicorn;
  • автоматически генерируемая документация для тестирования.

Фреймворк FastAPI позволит заменить несколько десятков строк кода одной и написать методы, которые будут возвращать пользователям нужную нам информацию. Также FastAPI сам создаёт документацию сразу в двух сервисах, где отражает проделанную в коде работу.

У FastAPI есть ещё одно преимущество: он основан на библиотеке для проверки данных pydantic. Можно прямо указать типы значений, и если пользователь попытается прислать нам неправильный запрос, FastAPI сам выдаст подходящую информацию об ошибке. Например, скажет, что запрашивать информацию о продукте в магазине нужно по названию, а не по номеру артикула.

Сервер Uvicorn позволит всему этому работать по HTTP. Мы возьмём этот сервер, потому что он совместим с FastAPI, но это хороший вариант по нескольким причинам:

  • Uvicorn специально разработан для высокоскоростной работы.
  • При этом он простой и минималистичный по своей архитектуре.
  • Кроме HTTP может работать по WebSocket, а значит, с ним можно написать WebSocket API.
  • Этот сервер асинхронный, а значит, может работать с несколькими задачами одновременно.

Устанавливаем FastAPI и Uvicorn

Сначала устанавливаем FastAPI и Uvicorn в систему командами:

pip install fastapi pip install uvicorn

Сделать это можно в командной строке внутри папки будущего проекта…

…либо в среде разработчика во вкладке терминала:

Теперь в Python-скрипте первыми сроками нужно добавить фреймворк и создать объект API:

# импортируем фреймворк и тип значений для дополнительного описания
from fastapi import FastAPI, Query

# создаём объект приложения FastAPI
app = FastAPI()

Импорт параметра Query даст возможность добавить дополнительные пояснения в документацию. Например, что номер запрашиваемой записи должен быть в промежутке от 10 до 20.

Создаём GET-запрос

Чтобы API начал отвечать на запросы пользователя, нужно добавить эндпойнты — окончания запросов, которые выглядят как одно или несколько разделённых косой чертой слов в дополнение к основному адресу. В запросе http://numbersapi.com/number/type таким эндпойнтом будет number/type.

Для создания нужно указать один из HTTP-методов и написать сам эндпойнт. Это делается так: 

  1. Ставим знак @.
  2. Пишем имя созданного FastAPI-приложения.
  3. Ставим через точку метод.
  4. В скобках указываем эндпойнт в кавычках.

Сразу под этой строкой пишем функцию, которая будет реагировать в ответ на этот запрос. Получается, что объявление метода — это декоратор, функция-обёртка, которая выполняется до и после основной обработки запроса.

Так будет выглядеть наш первый метод обработки GET-запроса:

# создаём корневой эндпойнт, на который будут отправлять GET-запрос
@app.get('/')
# пишем асинхронную обработку запроса
async def home():
   # возвращаем информацию
   return {'Hello!': 'You just made a GET-request to the CODE-API'}

По умолчанию почти все API используют английский язык (чтобы разработчикам из всех стран было проще понимать, что там происходит), поэтому мы тоже будем его использовать. 

👉 Обратите внимание, что мы использовали асинхронный синтаксис async def. В нашем случае это не даёт серьёзных преимуществ, потому что мы сразу возвращаем ответ. Но может быть так, что для ответа понадобится сделать запрос в базу данных или на другой сайт. Тогда API отправит этот запрос, а на время ожидания ответа перейдёт к запросу от другого пользователя. В этом польза асинхронного программирования: выполнение программы не блокируется на операциях, которые выполняются на внешних ресурсах.

Теперь запускаем сервер командой в терминале или командной строке:

uvicorn api_on_fastapi:app --reload

Вместо api_on_fastapi нужно подставить имя Python-файла, в котором вы работаете.

После этого в консоли должны появиться сообщения об успешном запуске сервера:

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [10440] using WatchFiles
INFO:     Started server process [10447]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Команда --reload говорит серверу постоянно обновляться, чтобы реагировать на изменения в коде. Сейчас мы можем открыть страницу в браузере по адресу http://127.0.0.1:8000 и получить результат выполнения функции-эндпойнта:

Чтобы посмотреть документацию к нашему API, в браузере нужно открыть страницу http://127.0.0.1:8000/docs. Там появился первый метод: тип, адрес, название, описание успешного ответа.

Мы можем проверить работу запроса прямо в документации. Для этого нужно нажать на кнопки Try it out и Execute:

Создадим ещё один GET-эндпойнт. Он будет таким же, за исключением названия и возвращаемых данных:

# создаём ещё один GET-эндпойнт
@app.get('/new_data')
# пишем асинхронную обработку запроса
async def new_data():
   # возвращаем информацию
   return {'Data': 'New_data'}

Проверим, что изменилось в документации:

Чтобы посмотреть, как можно доставать отдельные данные из API-ответов по значению, создадим словарь. Принцип работы будет тот же, что при работе с JSON-форматом, а создавать проще. Для примера пойдёт.

Создадим переменную-словарь, в которой будут храниться данные о книгах в магазине:

bookshelf = {
       1: {
           'book': 'Ulyss',
           'price': 4.75,
           'author': 'James Joyce'
       },

       2: {
           'book': 'Three Men in a Boat (To Say Nothing of the Dog)',
           'price': 3.99,
           'author': 'Jerome K. Jerome'
       }
   }

Чтобы другие разработчики могли запрашивать в этой переменной-словаре данные о книгах по номеру, мы напишем такой метод:

# создаём ещё один GET-эндпойнт с проверкой типа и получением значения по ключу:
@app.get('/get-book/{book_id}')
# пишем асинхронную обработку запроса и указываем
# тип значения, который ждём от пользователя: целое число
async def get_book(book_id: int):
   # возвращаем информацию о книге с выбранным номером
   return bookshelf[book_id]

Для этого вида запроса мы указали, что при обращении к нашему API нужен запрос с эндпойнтом get-book/{book_id}, причём при объявлении функции записали тип значения — целое число.

В нашем словаре пока всего две книги, поэтому запросы можно сделать с эндпойнтами get-book/1 и get-book/2. Если попробовать запросить номер 3, сервер покажет ошибку Internal Error. А если запросить книгу не по номеру, а по названию, FastAPI напомнит, что нужен другой тип значения.

Проверяем:

Создаём POST-запрос

У нас есть книжный шкаф с двумя книгами, сейчас мы попробуем добавить туда третью. Для создания записи понадобятся ещё два встроенных модуля. 

Анализ и проверки данных в FastAPI работают с помощью библиотеки pydantic. Нам она нужна для создания моделей данных, которые будем добавлять в словарь.

Ещё подключим тип значений Optional. С ним можно сделать часть значений необязательными.

Сначала всё импортируем:

# импортируем тип значений Optional
from typing import Optional
# импортируем базовый класс объектов
from pydantic import BaseModel

Чтобы API понимал, какие данные он должен ждать от пользователя, нужно создать модель-класс. Это будет шаблон, который следует заполнить. Выглядеть будет так:

# создаём класс-модель для добавления в наш общий словарь
class BookInfo(BaseModel):
   # переменная book будет строкой
   book: str
   # переменная price будет вещественным числом
   price: float
   # переменная author будет опциональной, строкового типа и по умолчанию равной None
   author: Optional[str] = None

Теперь при отправлении запроса нужно заполнить как минимум два значения: book и price, а author — по желанию. 

При написании POST-запроса мы добавляем в функцию номер книги и аргумент new_book с типом нашего класса BookInfo. Теперь нужно указать, что в переменную bookshelf нужно добавить словарь с теми же параметрами.

Перед добавлением записи введём проверку, чтобы случайно не добавить книгу с уже существующим номером:

# создаём POST-эндпойнт:
@app.post('/create-book/{book_id}')
# пишем асинхронную обработку запроса и указываем тип значений, который
# ждём от пользователя: целое число и словарь по шаблону BookInfo
async def create_book(book_id: int, new_book: BookInfo):
   # проверяем, что в словаре bookshelf ещё нет книги с таким номером
   if book_id in bookshelf:
       # выводим сообщение об ошибке, которое FastAPI трансформирует в JSON
       return {'Error': 'Book already exists'}

   # если книги с таким номером ещё нет, создаём её под этим номером
   # и с теми же параметрами, которые указали в классе BookInfo
   bookshelf[book_id] = new_book
   # возвращаем данные о новой внесённой книге
   return bookshelf[book_id]

Результатом должно быть сообщение в формате JSON о новой книге.

Теперь попробуем добавить книгу в документации и найти её методом get-book:

Создаём PUT-запрос

Чтобы иметь возможность обновить любой параметр на выбор, нам понадобится ещё один класс-шаблон. Все его параметры будут необязательными, поэтому мы можем указать любой и отправить запрос на сервер.

Класс-модель для обновления информации будет такой:

# создаём класс-модель специально для обновлений книг
class UpdateBook(BaseModel):
   # переменная book будет строкой
   book: Optional[str] = None
   # переменная price будет вещественным числом
   price: Optional[float] = None
   # переменная author будет опциональной, строкового типа и по умолчанию равной None
   author: Optional[str] = None

Метод будет похож на POST. Мы проверим, есть ли запрошенный номер книги в словаре, и если нет, покажем сообщение об ошибке:

# создаём PUT-эндпойнт:
@app.put('/update-book/{book_id}')
# пишем асинхронную обработку запроса и указываем тип значений, который
# ждём от пользователя: целое число и словарь по шаблону UpdateBook
async def update_book(book_id: int, upd_book: UpdateBook):
   # проверяем, что в словаре bookshelf есть книга с таким номером
   if book_id not in bookshelf:
       # если такой книги нет, выводим сообщение об ошибке
       return {'Error': 'Book ID does not exists'}

Если номер есть, обновим параметры из PUT-запроса. Для этого нам нужно проверить, не равен ли каждый параметр None:

   # если номер книги есть в словаре bookshelf, то обновляем её параметры
   # обновляем только те параметры, которые передали в метод PUT
   if upd_book.book != None:
       bookshelf[book_id].book = upd_book.book
   if upd_book.price != None:
       bookshelf[book_id].price = upd_book.price
   if upd_book.author != None:
       bookshelf[book_id].author = upd_book.author

В конце возвращаем обновлённую информацию:

   # возвращаем параметры изменённой книги
   return bookshelf[book_id]

Попробуем добавить новую книгу и изменить один из её параметров:

Этот метод не сработает, если мы попробуем обновить данные у первых двух книг, потому что мы явно определили их в коде. API не может изменить наш скрипт.

Как работать с такими статичными данными, рассмотрим в другой раз.

Создаём DELETE-запрос

Поработаем с удалением записей. Для удаления нам понадобится только номер книги. Попробуем добавить к типу значений параметр Query и описание. В документации напишем, что значение должно быть больше нуля. Вот как будет выглядеть такое описание типа:

book_id: int = Query(..., description='The book ID must be greater than zero')

Удаляться могут только существующие записи, поэтому добавим проверку. Если номера книги в словаре bookshelf нет, выводим ошибку.

# создаём DELETE-эндпойнт:
@app.delete('/delete-book')
# пишем асинхронную обработку запроса и указываем тип значений, который
# ждём от пользователя: целое число. Добавляем описание
def delete_book(book_id: int = Query(..., description='The book ID must be greater than zero')):
   # если книги с таким номером нет, выводим сообщение об ошибке
   if book_id not in bookshelf:
       return {'Error': 'Book ID does not exists'}

Если книга есть в словаре, удаляем запись о ней и выводим соответствующее сообщение:

   # если номер книги есть, удаляем о ней всю информацию, включая номер
   del bookshelf[book_id]
   return {'Done': 'The book successfully deleted'}

Попробуем создать книгу, удалить её и получить информацию о ней методом GET по номеру книги:

Что сделаем в следующий раз

Мы написали простой REST API, и над ним ещё можно работать и работать:

  • добавить новые методы;
  • отправлять запросы на разные IO-системы, чтобы получить реальную пользу от асинхронных функций;
  • написать подробное описание к каждому;
  • сделать несколько вариантов реализации;
  • подключить базу данных;
  • разложить на веб-хостинге.

В следующий раз прокачаем наш REST API или напишем новый — на WebSocket или GraphQL.

# импортируем фреймворк
from fastapi import FastAPI, Query
# импортируем тип значений Optional
from typing import Optional
# импортируем базовый класс объектов
from pydantic import BaseModel

# создаём объект приложения FastAPI
app = FastAPI()


# создаём класс-модель для добавления в наш общий словарь
class BookInfo(BaseModel):
   # переменная book будет строкой
   book: str
   # переменная price будет вещественным числом
   price: float
   # переменная author будет опциональной, строкового типа и по умолчанию равной None
   author: Optional[str] = None


# создаём класс-модель специально для обновлений книг
class UpdateBook(BaseModel):
   # переменная book будет строкой
   book: Optional[str] = None
   # переменная price будет вещественным числом
   price: Optional[float] = None
   # переменная author будет опциональной, строкового типа и по умолчанию равной None
   author: Optional[str] = None


# создаём словарь, где будем хранить всю информацию
bookshelf = {
       # информация по номеру будет выглядеть как набор пар
       # ключ-значение с названием книги, ценой и автором
       1: {
           # информацию внутри словаря можно будет найти по ключу, например 'book'
           'book': 'Ulyss',
           'price': 4.75,
           'author': 'James Joyce'
       },

       2: {
           'book': 'Three Men in a Boat (To Say Nothing of the Dog',
           'price': 3.99,
           'author': 'Jerome K. Jerome'
       }
   }


# создаём корневой эндпойнт, на который отправляется GET-запрос
@app.get('/')
# пишем асинхронную обработку запроса
async def home():
   # возвращаем информацию
   return {'Hello!': 'You just made a GET-request to the CODE-API'}


# создаём ещё один GET-эндпойнт
@app.get('/new-data')
# пишем асинхронную обработку запроса
async def new_data():
   # возвращаем информацию
   return {'Data': 'New_data'}


# создаём GET-эндпойнт с проверкой типа и получением значения по ключу:
@app.get('/get-book/{book_id}')
# пишем асинхронную обработку запроса и указываем тип
# значения, который ждём от пользователя: целое число
async def get_book(book_id: int):
   # возвращаем информацию о книге с выбранным номером
   return bookshelf[book_id]


# создаём POST-эндпойнт:
@app.post('/create-book/{book_id}')
# пишем асинхронную обработку запроса и указываем тип значений, который
# ждём от пользователя: целое число и словарь по шаблону BookInfo
async def create_book(book_id: int, new_book: BookInfo):
   # проверяем, что в словаре bookshelf ещё нет книги с таким номером
   if book_id in bookshelf:
       # выводим сообщение об ошибке, которое FastAPI трансформирует в JSON
       return {'Error': 'Book already exists'}
   # если книги с таким номером ещё нет, создаём её под этим номером
   # и с теми же параметрами, которые указали в классе BookInfo
   bookshelf[book_id] = new_book
   # возвращаем данные о новой внесённой книге
   return bookshelf[book_id]


# создаём PUT-эндпойнт:
@app.put('/update-book/{book_id}')
# пишем асинхронную обработку запроса и указываем тип значений, который
# ждём от пользователя: целое число и словарь по шаблону UpdateBook
async def update_book(book_id: int, upd_book: UpdateBook):
   # проверяем, что в словаре bookshelf есть книга с таким номером
   if book_id not in bookshelf:
       # если книги с таким номером нет, выводим сообщение об ошибке
       return {'Error': 'Book ID does not exists'}

   # если номер книги есть в словаре bookshelf, то обновляем её параметры
   # обновляем только те параметры, которые передали в метод PUT
   if upd_book.book != None:
       bookshelf[book_id].book = upd_book.book

   if upd_book.price != None:
       bookshelf[book_id].price = upd_book.price

   if upd_book.author != None:
       bookshelf[book_id].author = upd_book.author

   # возвращаем параметры изменённой книги
   return bookshelf[book_id]


# создаём DELETE-эндпойнт:
@app.delete('/delete-book')
# пишем асинхронную обработку запроса и указываем тип значений, который
# ждём от пользователя: целое число. Добавляем описание
def delete_book(book_id: int = Query(..., description='The book ID must be greater than zero')):
   # если книги с таким номером нет, выводим сообщение об ошибке
   if book_id not in bookshelf:
       return {'Error': 'Book ID does not exists'}
   # если номер книги есть, удаляем о ней всю информацию, включая номер
   del bookshelf[book_id]
   return {'Done': 'The book successfully deleted'}

Обложка:

Алексей Сухов

Корректор:

Елена Грицун

Вёрстка:

Кирилл Климентьев

Соцсети:

Юлия Зубарева

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Вам может быть интересно
hard
[anycomment]
Exit mobile version