DevGang
Авторизоваться

Создание 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 и их свойств, в таком порядке: idtitleinstructor и 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 включают в себя IntEnumDateList и 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 с очень небольшим количеством шаблонного кода.

Источник:

#Python #GraphQL #FastAPI
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

В подарок 100$ на счет при регистрации

Получить