Создание GraphQL сервера с FastAPI
FastAPI - это высокопроизводительный фреймворк для создания веб-API с помощью Python. Его простой и интуитивно понятный характер позволяет легко и быстро разрабатывать надежные веб-API, используя очень мало шаблонного кода. В этой статье мы расскажем о FastAPI и о том, как настроить с его помощью сервер GraphQL.
Из официальной документации, создания веб - приложений с FastAPI сокращает около 40 процентов индуцированных ошибок разработчика, и это стало возможным благодаря использованию объявлений типа Python 3.6. Благодаря всем его функциям, включая автоматическое создание интерактивной документации API, создание веб-приложений с помощью Python никогда не было таким простым.
Настройка нашего приложения
Прежде чем мы начнем, давайте подтвердим, что у нас установлен Python 3.6+, выполнив следующую команду в терминале:
python --version
Если это вернет ошибку, кликните здесь, чтобы загрузить и установить Python на свой локальный компьютер. Установка Python 3.4+ по умолчанию поставляется с pip, однако нам понадобится Python 3.6+, чтобы иметь возможность запускать FastAPI. Pip - это предпочтительный менеджер пакетов Python, который мы будем использовать для установки поддерживающих модулей для нашего приложения FasAPI.
Установив Python и pip, давайте добавим FastAPI на нашу машину, выполнив следующую команду в терминале:
pip install fastapi
Нам также понадобится Uvicorn, сервер ASGI (интерфейс асинхронного серверного шлюза) для обслуживания нашего приложения. Давайте запустим следующую команду в терминале, чтобы установить его:
pip install uvicorn
После этого мы можем продолжить и создать новый каталог для нашего приложения. Назовем это fastapi-graphql
. Внутри нашего нового каталога мы создадим новый файл с именем main.py
. Это будет индексный файл для нашего сервера.
Основы GraphQL: запросы и схема
Запросы GraphQL
В GraphQL запросы используются для получения данных, как и запросы GET в архитектуре REST API. Однако с запросами GraphQL у нас есть выбор запрашивать именно то, что мы хотим. Например, предположим, что у нас есть API для образовательных курсов. Запрос к нашему API будет выглядеть так:
{
getCourse {
id
title
instructor
publishDate
}
}
Когда мы посылаем этот запрос, мы должны получить ответ с курсами от нашего API и их свойств, в таком порядке: id
, title
, instructor
и publishDate
:
{
"data": {
"getCourse": [
{
"id": "1",
"title": "Python variables explained",
"instructor": "Tracy Williams",
"publishDate": "12th May 2020"
},
{
"id": "2",
"title": "How to use functions in Python",
"instructor": "Jane Black",
"publishDate": "9th April 2018"
}
]
}
}
При желании мы можем попросить наш API вернуть список курсов, но на этот раз только со свойством title
:
{
getCourse {
title
}
}
Мы должны получить такой ответ:
{
"data": {
"getCourse": [
{
"title": "Python variables explained"
},
{
"title": "How to use functions in Python"
}
]
}
}
Эта гибкость - одна из вещей, которые делают приложения, созданные с помощью GraphQL, очень расширяемыми, и это стало возможным благодаря объявлению типа в GraphQL.
Схема GraphQL
Схема описывает нашу службу GraphQL, какие данные он содержит, и формат этих данных. Из нашего запроса мы увидели, что можем указать, какие данные будут нам отправлены и как мы хотим, чтобы эти данные представлялись. Это связано с тем, что наш GraphQL API уже содержит схему для всех данных, и эта схема включает все доступные поля, подполя и их тип данных.
Чтобы продемонстрировать это, мы создадим файл schemas.py
в нашем корневом каталоге, который мы будем использовать для размещения всех полей данных. Давайте начнем с типа курса. Он должен содержать всю информацию для конкретного курса, и из приведенного выше примера запроса курс включает поля id, title, instructor и publish_date:
from graphene import String, ObjectType
class CourseType(ObjectType):
id = String(required=True)
title = String(required=True)
instructor = String(required=True)
publish_date = String()
В нашем файле schemas.py
мы начали с импорта типов String
и ObjectType
из graphene
. Graphene - это библиотека Python для построения схем и типов GraphQL. Давайте запустим следующую команду в терминале, чтобы установить его:
pip install graphene
После того, как мы успешно импортировали String
и ObjectType
из graphene
, мы перешли к определению класса CourseType
с импортированными ObjectType
в скобках. Мы объявим почти все наши типы GraphQL как типы объектов.
Следующее, что мы сделали, это создали разные поля для нашего CourseType
, и мы использовали тип String
для каждого поля. Некоторые другие типы из graphene
включают в себя Int
, Enum
, Date
, List
и Boolean
.
Обратите внимание, что мы также добавили аргумент required
в тип String
для id
, title
и instructor
. Это означает, что мы не сможем добавить курс в нашу службу GraphQL без включения этих полей, хотя мы все равно можем исключить любое из них при выполнении запросов.
Настройка временной базы данных
Теперь, когда у нас есть схема, нам также понадобится место для хранения и извлечения данных нашего курса. для этой демонстрации мы будем использовать базу данных JSON. Однако FastAPI поддерживает как реляционные, так и нереляционные базы данных, такие как PostgreSQL, MySQL, MongoDB, ElasticSearch и т.д.
Давайте создадим файл courses.json
в нашем корневом каталоге и вставим в него следующий блок кода:
[
{
"id": "1",
"title": "Python variables explained",
"instructor": "Tracy Williams",
"publish_date": "12th May 2020"
},
{
"id": "2",
"title": "How to use functions in Python",
"instructor": "Jane Black",
"publish_date": "9th April 2018"
},
{
"id": "3",
"title": "Asynchronous Python",
"instructor": "Matthew Rivers",
"publish_date": "10th July 2020"
},
{
"id": "4",
"title": "Build a REST API",
"instructor": "Babatunde Mayowa",
"publish_date": "3rd March 2016"
}
]
Мы сможем использовать наш GraphQL API для изменения и извлечения данных из этого файла.
Создание наших резолверов запросов
Резолверы - это то, что наша служба GraphQL будет использовать для взаимодействия с нашей схемой и источником данных. Чтобы создать преобразователь запросов для получения курсов, мы начнем с импорта fastapi
в наш файл main.py
:
from fastapi import FastAPI
Затем давайте импортируем соответствующие типы, которые мы будем использовать в нашем преобразователе запросов, как мы это сделали в файле schemas.py
:
from graphene import ObjectType, List, String, Schema
Обратите внимание, что мы добавили два новых типа: List
который мы будем использовать в качестве оболочки для наших CourseType
; и Schema
, который мы будем использовать для выполнения операции.
Мы также импортируем AsyncioExecutor
из graphql.execution.executors.asyncio
, что позволит нам выполнять асинхронные вызовы в нашем сервисе GraphQL; GraphQLApp
из starlette.graphql
; и CourseType
из нашего сервиса файл schemas.py
:
from graphql.execution.executors.asyncio import AsyncioExecutor
from starlette.graphql import GraphQLApp
from schemas import CourseType
Наконец, импортируем встроенный пакет json
для работы с нашим файлом courses.json
:
import json
Наш последний блок импорта должен выглядеть так:
from fastapi import FastAPI
from graphene import ObjectType, List, String, Schema
from graphql.execution.executors.asyncio import AsyncioExecutor
from starlette.graphql import GraphQLApp
from schemas import CourseType
import json
Затем давайте создадим наш запрос, добавив в наш файл main.py
следующий блок кода:
class Query(ObjectType):
course_list = None
get_course = List(CourseType)
async def resolve_get_course(self, info):
with open("./courses.json") as courses:
course_list = json.load(courses)
return course_list
В приведенном выше блоке мы начали с создания класса Query
на основе типа объекта, а затем инициализировали переменную course_list
. Здесь мы будем хранить данные курса.
Поскольку мы будем возвращать различные объекты курса в списке, мы использовали тип List
, который мы импортировали из graphene
, чтобы обернуть наш CourseType
, а затем присвоили его переменной get_course
. Это будет имя нашего запроса.
Важно отметить, что при выполнении запроса в клиенте GraphQL нам необходимо указать имя в верблюжьем регистре, то есть getCourse
вместо get_course
.
Затем мы создали метод resolver
в строке 4 для запроса get_course
. Имя метода resolver
должно начинаться с префикса resolve
, за которым следует имя запроса — в данном случае get_course
— и затем разделяться символом подчеркивания.
Метод resolver
также ожидает два позиционных аргумента, которые мы включили в определение метода. В строках 5-7 мы загружаем данные из нашего файла courses.json
, назначаем его course_list
, а затем возвращаем переменную.
Запуск нашего сервера GraphQL на базе FastAPI
Теперь, когда мы создали наш запрос GraphQL, давайте инициализируем FastAPI и назначим нашу службу GraphQL маршруту индекса.
app = FastAPI()
app.add_route("/", GraphQLApp(
schema=Schema(query=Query),
executor_class=AsyncioExecutor)
)
Затем давайте запустим следующую команду на нашем терминале, чтобы запустить наше приложение FastAPI:
uvicorn main:app --reload
Мы должны получить сообщение об успехе, подобное этому:
Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Started reloader process
Started server process
Waiting for application startup.
Application startup complete.
Мы можем продолжить и протестировать наш сервер GraphQL, перейдя в http://127.0.0.1:8000
. Мы должны увидеть страницу, которая выглядит так:
Давайте вставим следующий запрос на левую панель и выполним вызов нашего API, нажав кнопку запуска:
{
getCourse {
id
title
instructor
publishDate
}
}
Мы должны получить такой ответ:
Получение только одного курса
Давайте посмотрим, как сделать запрос, чтобы получить данные только для одного курса. Мы добавим параметр id
в наш запрос, чтобы наш сервер GraphQL знал, что нам нужен только курс, который соответствует id
. Чтобы это сработало, давайте заменим наш класс Query
приведенным ниже кодом:
class Query(ObjectType):
course_list = None
get_course = Field(List(CourseType), id=String())
async def resolve_get_course(self, info, id=None):
with open("./courses.json") as courses:
course_list = json.load(courses)
if (id):
for course in course_list:
if course['id'] == id: return [course]
return course_list
В строке 3 нашего нового класса запросов мы заключили значение get_course
в тип поля, который позволяет нам добавлять наши параметры запроса. Здесь мы добавили идентификатор param
типа String
. Мы также включили параметр id в метод resolve_get_course()
и дали ему значение по умолчанию None
, чтобы сделать его необязательным.
В строках 7-9 мы добавили условие, которое возвращает только тот курс, который соответствует идентификатору, если он указан. Нам также нужно будет добавить тип поля к нашему импорту графена, прежде чем мы двинемся дальше:
from graphene import ObjectType, List, String, Schema, Field
Теперь мы можем пойти дальше и получить только один курс, который соответствует идентификатору со следующим запросом:
{
getCourse(id: "2") {
id
title
instructor
publishDate
}
}
Мы должны получить это в качестве нашего ответа:
{
"data": {
"getCourse": [
{
"id": "2",
"title": "How to use functions in Python",
"instructor": "Jane Black",
"publishDate": "9th April 2018"
}
]
}
}
Мутации GraphQL
Мы видели, как настроить наш сервер GraphQL с FastAPI и получать данные с него. Теперь давайте посмотрим, как мы можем использовать мутации GraphQL для добавления новых курсов в наше хранилище данных или обновления существующих курсов. Начнем с добавления типа Mutation
к нашему импорту graphene
:
from graphene import ObjectType, List, String, Schema, Field, Mutation
Теперь мы можем создать класс, названный CreateCourse
по типу Mutation
:
class CreateCourse(Mutation):
course = Field(CourseType)
class Arguments:
id = String(required=True)
title = String(required=True)
instructor = String(required=True)
В нашем классе CreateCourse
мы начали с создания переменной для нашего курса, которую мы обернули в класс Field
. Это то, что мы вернем пользователю после успешного создания курса.
Затем мы перешли к созданию класса для наших аргументов мутации. Наши аргументы здесь id
, title
и instructor
. Нам нужно будет предоставить эту информацию при внесении нашей мутации CreateCourse
.
Далее нам понадобится метод создания курсов mutate
. Здесь мы будем использовать аргументы, предоставленные пользователем, для создания нового курса в нашем хранилище данных. В этом случае мы изменим наш файл courses.json
. Однако в продакшен приложении вам, вероятно, потребуется база данных, и, как упоминалось ранее, FastAPI поддерживает как реляционные, так и нереляционные базы данных.
Создадим наш метод mutate
. Обратите внимание, что он должен быть назван mutate
, так как это то, что ожидает наша служба GraphQL:
class CreateCourse(Mutation):
...
async def mutate(self, info, id, title, instructor):
with open("./courses.json", "r+") as courses:
course_list = json.load(courses)
course_list.append({"id": id, "title": title, "instructor": instructor})
courses.seek(0)
json.dump(course_list, courses, indent=2)
return CreateCourse(course=course_list[-1])
В операторе return нашего метода mutate
мы вызвали класс CreateCourse
и в скобках присвоили вновь созданный курс переменной course
, которую мы объявили ранее. Это то, что наш GraphQL API вернет пользователю в ответ на запрос на изменение.
Теперь, когда у нас есть наш класс CreateCourse
, давайте создадим новый класс из ObjectType
с названием Mutation
. Здесь мы будем хранить все наши мутации:
class Mutation(ObjectType):
create_course = CreateCourse.Field()
Сделав это, мы можем добавить наш класс Mutation
в Schema
в вызове функции app.add_route()
:
app.add_route("/", GraphQLApp(
schema=Schema(query=Query, mutation=Mutation),
executor_class=AsyncioExecutor)
)
Теперь мы можем проверить это, выполнив следующий запрос в нашем клиенте GraphQL:
mutation {
createCourse(
id: "11"
title: "Python Lists"
instructor: "Jane Melody"
) {
course {
id
title
instructor
}
}
}
И мы должны получить такой ответ:
{
"data": {
"createCourse": {
"course": {
"id": "11",
"title": "Python Lists",
"instructor": "Jane Melody"
}
}
}
}
Обработка ошибок запроса
Давайте посмотрим, как мы можем обрабатывать ошибки в нашем приложении, добавляя проверку уже существующих идентификаторов. Если пользователь пытается создать курс с идентификатором, который уже существует в нашем хранилище данных, наш сервер GraphQL должен ответить сообщением об ошибке: Course with provided id already exists!
Непосредственно перед добавлением нового курса в наше хранилище данных в функции CreateCourse
mutate вставим следующий код:
for course in course_list:
if course['id'] == id:
raise Exception('Course with provided id already exists!')
Выше мы прошлись по нашему хранилищу данных course_list
и проверили, есть ли существующий курс с тем же идентификатором, что и у входящего курса. Если у нас есть совпадение, то должно быть сделано исключение.
В зависимости от базы данных и ORM, которые мы выбираем для нашего приложения, процесс проверки наличия ранее существовавшего значения может отличаться. Однако при возникновении исключения GraphQL всегда возвращает ошибку. С новым изменением в нашем коде CreateCourse
, вот как должен выглядеть наш класс мутации:
class CreateCourse(Mutation):
course = Field(CourseType)
class Arguments:
id = String(required=True)
title = String(required=True)
instructor = String(required=True)
publish_date = String()
async def mutate(self, info, id, title, instructor):
with open("./courses.json", "r+") as courses:
course_list = json.load(courses)
for course in course_list:
if course['id'] == id:
raise Exception('Course with provided id already exists!')
course_list.append({"id": id, "title": title, "instructor": instructor})
courses.seek(0)
json.dump(course_list, courses, indent=2)
return CreateCourse(course=course_list[-1])
Теперь мы можем проверить это, попытавшись создать новый курс с уже существующим id
. Давайте запустим точную мутацию из последнего запроса CreateCourse
в нашем клиенте GraphQL:
mutation {
createCourse(
id: "11"
title: "Python Lists"
instructor: "Jane Melody"
) {
course {
id
title
instructor
}
}
}
В качестве ответа мы должны получить следующее:
{
"data": {
"createCourse": null
},
"errors": [
{
"message": "Course with provided id already exists!",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createCourse"
]
}
]
}
Вывод
В этой статье мы узнали основы FastAPI и то, как мы можем использовать его для настройки сервера GraphQL. Сочетание этих двух технологий дает действительно захватывающий опыт веб-разработки.
С GraphQL мы можем относительно легко писать сложные запросы, давая веб-клиентам возможность запрашивать именно то, что они хотят. А с FastAPI мы можем создавать надежные, высокопроизводительные серверы GraphQL с очень небольшим количеством шаблонного кода.