Реализация API¶
Примечание
Этот материал находится в состоянии наполнения информацией.
Общая информация об API¶
Для нашего проекта исползуется API на базе GraphQL. В частности его реализация для Python Graphene. Настоятельно рекомендую ознакомится с документацией к Graphene. Graphene позволяет автоматически преобразовывать модели Django в модели GraphQL, для этого мы будем использовать Graphene-Django.
Соглашение о реализации API¶
Во избежание путаницы в коде рекомендую придерживаться следующих правил при написании API для приложения:
- Для всех компонентов API в приложении создаётся python-модуль api.
- Схемы Graphene должны быть оформлены в виде отдельного python-файла
[[schema.py](http://schema.py)]([schema.py](http://schema.py)). - Мутации Graphene должны быть оформлены в виде отдельного python-файла
[[mutations.py](http://mutations.py)]([mutations.py](http://mutations.py)). - Если в процессе программирования API возникла необходимость выделить какие-либо функции, миксины или др. программный код в виде отдельного файла (при этом этот код не используется нигде, кроме API), то в директории api можно создать нужное количество python-файлов или даже модулей (например:
api/[[common.py](http://common.py)]([common.py](http://common.py))). Их имена не регламентируются. - Желательно для всех элементов (узлов, объектов или их полей) задавать описание. Например, для поля
name = graphene.String(description='Имя'), для объекта (class) можно использовать стандарнтый метод__doc__(он же опрееляется ещё как „““ Этот класс реализует нечто… „““)
Пример реализации API для приложения «Академия»¶
Для начала рассмотрим состав приложения «Академия»:
akademio/
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
└── tests.py
Согласно соглашению, создадим модуль для API следующего содержания:
akademio/api
├── __init__.py
├── mutations.py
└── schema.py
Реализация схемы¶
Схема - это представления данных для API, т.е. здесь мы должны описать отдаваемые по запросу данные. А данные у нас представлены в виде моделей, их можно посмотреть на gitlab.com.
Для начала в файле [[schema.py](http://schema.py)]([schema.py](http://schema.py)) импортируем всё, что нам может понадобиться при разработке схемы:
import graphene # сам Graphene
from graphene_django import DjangoObjectType # Класс описания модели Django для Graphene
from siriuso.api.mixins import SiriusoAuthNode, SiriusoObjectId # Миксины для описания типов Graphene
from graphene_permissions.permissions import AllowAny # Класс доступа к типу Graphene
from django.utils.translation import ugettext_lazy as _ # Функция перевода внутренних сообщений Django
from siriuso.api.filters import SiriusoFilterConnectionField # Коннектор для получения данных
from siriuso.api.types import SiriusoLingvo # Объект Graphene для представления мультиязычного поля
from ..models import * # модели приложения
Нужно учитывать тот факт, что Graphene сам определит все поля в описываемых моделях (в том числе связанные модели), и, если ему изветны типы этих полей, то он сделает из доступными для вывода данных. Поэтому все связанные с текущей моделью другие модели должны быть тоже определены в Graphene. Исходы из вышеописанного реализуем схему для моделей приложения.
# Реализация для модели AkademioPagxoTipo
class AkademioPagxoTipoNode(SiriusoAuthNode, DjangoObjectType):
"""
Тип страницы академии
"""
permission_classes = (AllowAny,)
json_filter_fields = {
'nomo__enhavo': ['contains', 'icontains'],
'kodo': ['icontains',]
}
nomo = graphene.Field(SiriusoLingvo, description=_('Наименование типа страницы'))
class Meta:
model = AkademioPagxoTipo
filter_fields = {
'uuid': ['exact'],
'kodo': ['exact', 'icontains', 'istartswith'],
}
interfaces = (graphene.relay.Node,)
Рассмотрим класс AkademioPagxoTipoNode, описывающий для API нашу модель типов страниц подробнее. Начнём с имени - имя по сути повторяет имя модели Django, но с суффиксом Node, это сделано для удобства, чтобы можно было быстро найти класс, описывающий ту или иную модель. Заметьте, что имя класса не должно совпадать с именем класса модели Django! Класс наследуется от 2 других классов: SiriusoAuthNode и DjangoObjectType.
SiriusoAuthNode - это миксин, который добавляет проверку прав через Django graphene permissions, доступ к объектам этой модели определяется как раз в переменной permission_classes. В ней можно перечислить несколько уровней доступа в виде кортежа (tuple). Т.е. получаем permission_classes = (AllowAny,). Вообще планируется уйти от использования Django graphene permissions, но пока всё же рекомендую применять его при создании схем.
DjangoObjectType - это класс, который позволяет преобразовать модель Django в узлы схемы, и при всех наследованиях класса узла он должен указываться последним в списке родителей.
Далее идет переменная json_filter_fields, её описание вы не найдете в документации по графену, т.к. она испольщуется только для полей с многоязычным контентом. В качестве значения должна содержать словарь с именами полей модели Django и список (list или tuple) функций, используемых для поиска значений по полю. текущее значение указывает коннетору SiriusoFilterConnectionField, что при задании поисковой строки по модели, значения нужно искать в поле nomo и kodo. Поскольку поле nomo является JSON, то может содержать текстовые ключи со значаниеями, поэтому и указано nomo__enhavo, т.е. нужно смотреть именно на наличие ключа enhavo и искать сопадения в нём.
Переменная nomo по сути переопределяет поле модели nomo. Это сделано для того, чтобы преобразовать значение JSON поля модели и извлечь только нужный контент на требуемом языке. Таким образо должны быть переопределены все мультиязычные поля моделей Django
И вот мы дошли до метаклассаclass Meta, который содержит все необходимые данные для преобразования в узел схемы. А именно:
model - содержит класс модели Django, обязательная переменная; filter_fields - принимет в виде значения списко, кортеж (list, tuple) или словарь (dict). В случае со списочными значениями - в виде стро указываются имена полей модели Django, по которым будет осуществляться фитрация (поное совпадение) при запросе клиента (на языке GraphQL). В случае словаря в качестве ключей (индексов) словаря указываются поля модели Django, а в качетве значения список (list, tuple) функций фильтраци по полю. Например, текущее значение говорит, что можно фильтровать записи узла схемы по полю kodo строго по значению (т.е. при полном сопадении его значения с учетом регистра!), а так же по полю nomo применяя строгое сопадение значения с учетом регистра или использовать функции icontains (содержит значение не зависимо от регистра), istartswith (начинается со значения не зависимо от регистра). interfaces - обязательная переменная (list, tuple), которая будет всегда принимать значение (graphene.relay.Node,), т.к. мы используем relay Graphene в API. Если мы разработаем собственный интерфейс, то нужно будет его указать здесь вместо/вместе с graphene.relay.Node
Ну вот, теперь можно и модель AkademioPagxoNode описать в схеме:
class AkademioPagxoNode(SiriusoAuthNode, SiriusoObjectId, DjangoObjectType):
"""
Страница академии
"""
permission_classes = (AllowAny,)
json_filter_fields = {
'teksto__enhavo': ['contains', 'icontains'],
}
teksto = graphene.Field(SiriusoLingvo, description=_('Текст страницы академии'))
class Meta:
model = AkademioPagxo
filter_fields = {
'uuid': ['exact'],
'kodo': ['exact', 'icontains', 'istartswith'],
'forigo': ['exact'],
'arkivo': ['exact'],
'publikigo': ['exact'],
}
interfaces = (graphene.relay.Node,)
Вот тут появился новый миксин SiriusoObjectId при определении класса узла схемы. Этот миксин определяет поле objId для моделей, где есть поле id (сквозной номер). У нас в моделя в качестве первичного ключа используется UUID, поэтому id используется там, где нужна сквозная нумерация записей в БД. В Graphene поле id зарезервировано под внутренний уникальный идентификатор, так вот чтобы не конфликтовать с этим, мы используем objId. Просто не забывайте в список родителей класса узла схемы указывать SiriusoObjectId, остальное миксин сделает за Вас.
Отлично, мы описали модели приложения «Академия»!
Но это ещё не всё: нам надо сказать Graphene, что у нас есть новые узлы, которые мы бы хотели сделать доступными для запросов через API.
Чтобы это сделать нужно в самом конце [[schema.py](http://schema.py)]([schema.py](http://schema.py)) добавить новый класс, являющийся наследником ObjectType, в которм подключить при помощи коннектора SiriusoFilterConnectionField описанные нами узлы. Это совсем просто:
class AkademioQuery(graphene.ObjectType):
akademioj_pagxoj_tipoj = SiriusoFilterConnectionField(AkademioPagxoTipoNode,
description=_('Выводит все доступные типы страниц Академии'))
akademioj_pagxoj = SiriusoFilterConnectionField(AkademioPagxoNode,
description=_('Выводит все доступные страницы Академии'))
Теперь идём в файл siriuso/api/[[schema.py](http://schema.py)]([schema.py](http://schema.py)), импортируем вышеописанный класс AkademioQuery и прописываем его в списке наследуемых класс GlobalQuery:
. . .
from akademio.api.schema import AkademioQuery
from muroj.api.schema import MuroQuery
from muroj.api.mutations import Mutations as MurojMutations
from taskoj.api.schema import Query as TaskojQuery
import graphene
class GlobalQuery(UzantoQuery, StatQueries, KomunumoQuery, ForumoQuery, InformiloQuery, MuroQuery,
TaskojQuery, AkademioQuery, graphene.ObjectType):
pass
. . .
Теперь наши узлы доступны для запроса через API. Можем попробовать запросить через удобный интерфейс Graphene, который расположен на нашем локальном сервере по адресу [localhost:8000/api/v1.1]([localhost:8000/api/v1.1](http://localhost:8000/api/v1.1/))
Запрос списка всех типов страниц Академии будет выглядеть вот так:
query {
akademiojPagxojTipoj {
edges {
node {
uuid
kodo
kreaDato
nomo {
enhavo
}
}
}
}
}
А в ответ мы получим:
{
"data": {
"akademiojPagxojTipoj": {
"edges": []
}
}
}
Это значит, что наша схема заработала, но у нас нет ни одной записи в модели AkademioPagxo. Надо создать для теста несколько, но не через БД же это делать, в конце концов…
Реализация мутаций¶
Мутации - это механизм изменения записей через Graphene. Данный механизм работает только через метод POST протокола HTTP. Реализация изменения на самом деле ещё проще, чем реализация схемы, при этом мутация может менять записи сразу нескольких моделей Django
Начнём с импорта нужных модулей:
from django.utils import timezone
from django.db import transaction # создание и изменения будем делать в блокирующейтранзакции
from django.utils.translation import ugettext_lazy as _
import graphene
from siriuso.utils import set_enhavo
from .schema import AkademioPagxoTipoNode # Будем возращать узел типа страницы в качетсве результата
from ..models import *
Теперь можно написать и код муации по созданию и изменению (в одном флаконе) записи типа страницы:
class RedaktuAkademioPagxoTipo(graphene.Mutation):
status = graphene.Boolean()
message = graphene.String()
pagxo_tipo = graphene.Field(AkademioPagxoTipoNode, description=_('Созданная/изменённая запись типа страницы Академии'))
class Arguments:
uuid = graphene.UUID(description=_('UUID записи'))
kodo = graphene.String(description=_('Код типа'))
nomo = graphene.String(description=_('Название типа'))
forigo = graphene.Boolean(description=_('Признак удаления записи'))
arkivo = graphene.Boolean(description=_('Признак архивной записи'))
publikigo = graphene.Boolean(description=_('Признак опубликованной записи'))
[@staticmethod](profile/staticmethod)
def mutate(root, info, **kwargs):
status = False
message = None
tipo = None
uzanto = info.context.user
if uzanto.is_authenticated:
# Доступ только для администратора
if uzanto.is_admin or uzanto.is_superuser:
with transaction.atomic():
# Создаём новую запись
if not kwargs.get('uuid', False):
# Проверяем наличие всех полей
if not kwargs.get('kodo', False):
message = '{} "{}"'.format(_('Не заполнено обязательное поле'), 'kodo')
# Проверяем наличие записи с таким кодом
try:
AkademioPagxoTipo.objects.get(kodo=kwargs.get('kodo'), forigo=False)
message = _('Запись с таим кодом уже существует')
except AkademioPagxoTipo.DoesNotExist:
pass
if kwargs.get('forigo', False) and not message:
message = '{} "{}"'.format(_('При создании записи не допустимо указание поля'), 'forigo')
if kwargs.get('arkivo', False) and not message:
message = '{} "{}"'.format(_('При создании записи не допустимо указание поля'), 'arkivo')
if not (kwargs.get('nomo', False) or message):
message = '{} "{}"'.format(_('Не заполнено обязательное поле'), 'nomo')
if not message:
tipo = AkademioPagxoTipo.objects.create(
kodo=kwargs.get('kodo'),
speciala=False,
forigo=False,
arkivo=False,
publikigo=kwargs.get('publikigo', False),
publikiga_dato=timezone.now()
)
set_enhavo(tipo.nomo, kwargs.get('nomo'), 'ru_RU')
tipo.save()
status = True
message = _('Запись создана')
else:
# Изменяем запись
if not (kwargs.get('kodo', False) or kwargs.get('nomo', False)
or kwargs.get('forigo', False) or kwargs.get('arkivo', False)
or kwargs.get('publikigo', False)):
message = _('Не задано ни одно поле для изменения')
if not message:
# Ищем запись для изменения
try:
tipo = AkademioPagxoTipo.objects.get(uuid=kwargs.get('uuid'), forigo=False)
tipo.kodo = kwargs.get('kodo', tipo.kodo)
tipo.forigo = kwargs.get('forigo', tipo.forigo)
tipo.foriga_dato = timezone.now() if kwargs.get('forigo', False) else None
tipo.arkivo = kwargs.get('arkivo', tipo.arkivo)
tipo.arkiva_dato = timezone.now() if kwargs.get('arkivo', False) else None
tipo.publikigo = kwargs.get('publikigo', tipo.publikigo)
tipo.publikiga_dato = timezone.now() if kwargs.get('publikigo', False) else None
if kwargs.get('nomo', False):
set_enhavo(tipo.nomo, kwargs.get('nomo'), 'ru_RU')
tipo.save()
status = True
message = _('Запись успешно изменена')
except AkademioPagxoTipo.DoesNotExist:
message = _('Запись не найдена')
else:
message = _('Недостаточно прав')
else:
message = _('Требуется авторизация')
return RedaktuAkademioPagxoTipo(status=status, message=message, pagxo_tipo=tipo)
Класс мутации должен быть наследником Mutation, в качестве возвращаемых полей служат переменные класса status, message и pagxo_tipo. Причем pagxo_tipo - это сама созданная или изменённая запись, т.е. она является AkademioPagxoTipoNode, объявление происходит через graphene.Field: pagxo_tipo = graphene.Field(AkademioPagxoTipoNode). Но для создания и изменения нам нужны аргументы, которые будут использоваться в процесе мутации, они объявляются в метаклассе class Arguments. В данном случае у нас 6 аргументов:
class Arguments:
uuid = graphene.UUID(description=_('UUID записи'))
kodo = graphene.String(description=_('Код типа'))
nomo = graphene.String(description=_('Название типа'))
forigo = graphene.Boolean(description=_('Признак удаления записи'))
arkivo = graphene.Boolean(description=_('Признак архивной записи'))
publikigo = graphene.Boolean(description=_('Признак опубликованной записи'))
Graphene автоматически проверяет типы передаваемых аргументов, нам остается только проверить допустимость значений. Можно объявить аргумент обязательным (required=True) для указания: uuid = graphene.UUID(required=True, description=_(„UUID записи“)).
Сама процедура мутации происходит в статическом методе mutate, первый аргумент функции root (обычно не используется), второй - info - содержит стандартный контекст запроса Django (info.context = request). Если в мутации присутствуют обязательные аргументы, то они должны быть объявлены в параметрах функциях полсе первых двух, необязательные параментры передаются в виде словаря поименованных аргументов **kwargs.
Метод mutation обязательно должен возвращать в качетсве результата объект своего же класса, т.е. в нашем случае будет возвращён объект класса RedaktuAkademioPagxoTipo с заданными в результате мутации переменными.
Теперь, так же как и в случае со схемой, надо объявить Graphene что у нас есть новые мутации, которые мы хотим сделать доступными через API. Для этого точно так же создаём класс-наследник от ObjectType:
class AkademioMutations(graphene.ObjectType):
redaktu_akademio_pagxo_tipo = RedaktuAkademioPagxoTipo.Field(
description=_('''Создаёт или редактирует типы страниц Академии''')
)
А потом импортируем его в siriuso/api/schema.py и вставляем в список родителей класса GlobalMutations.
Всё, теперь наша мутация доступна через API. Однако для её использования нужно быть авторизованным пользователем, да ещё и справами администратора или суперпользователям Django
Для проверки можно выполнить запрос по созданию нового типа страницы:
mutation {
redaktuAkademioPagxoTipo(
kodo: "test", nomo: "Тестовый тип страницы") {
status
message
pagxoTipo {
uuid
kodo
nomo {
enhavo
}
}
}
}
В ответ должно быть сделующее:
{
"data": {
"redaktuAkademioPagxoTipo": {
"status": true,
"message": "Запись создана",
"pagxoTipo": {
"uuid": "69e1944b-8171-4a92-8d45-b0a6ee8699c1",
"kodo": "test",
"nomo": {
"enhavo": "Тест"
}
}
}
}
}
Для изменения созданной записи, нашей мутации нужно передать аргумент uuid (в примере это 69e1944b-8171-4a92-8d45-b0a6ee8699c1), а так же один из изменяемых параметров. Например поменяем название (nomo) типа страницы:
mutation {
redaktuAkademioPagxoTipo(uuid: "69e1944b-8171-4a92-8d45-b0a6ee8699c1",
nomo: "Супер тест!") {
status
message
pagxoTipo {
uuid
kodo
nomo {
enhavo
}
}
}
}
Ответ:
"data": {
"redaktuAkademioPagxoTipo": {
"status": true,
"message": "Запись успешно изменена",
"pagxoTipo": {
"uuid": "69e1944b-8171-4a92-8d45-b0a6ee8699c1",
"kodo": "test",
"nomo": {
"enhavo": "Супер тест!"
}
}
}
}
}
Вот теперь можно попробовать выполнить запрос типов страниц Академии через API , который упомянут в коце раздела 3.1, в ответ мы должны получить список созданных типов страниц. Вот что получилось у меня после моих экспериментов:
{
"data": {
"akademiojPagxojTipoj": {
"edges": [
{
"node": {
"uuid": "db6b8fed-265f-4df7-aa0b-9ad0116f37be",
"kodo": "test",
"kreaDato": "2019-05-13T12:28:57.350014+00:00",
"nomo": {
"enhavo": ""
}
}
},
{
"node": {
"uuid": "83880f16-dbe2-460b-a97f-be1e06cc67dc",
"kodo": "test2",
"kreaDato": "2019-05-13T12:30:09.379637+00:00",
"nomo": {
"enhavo": "Тест2"
}
}
},
{
"node": {
"uuid": "69e1944b-8171-4a92-8d45-b0a6ee8699c1",
"kodo": "test3",
"kreaDato": "2019-05-13T13:14:27.431441+00:00",
"nomo": {
"enhavo": "Супер тест!"
}
}
}
]
}
}
}
В завершение рекомендую изучить документацию Graphene.
Разрешение собственных полей¶
В узлы схемы можно добавлять собственные поля, которые не содержаться в моделях Django. Для этого нужно в классе узла описать статический метод для разрешения данного поля. Допустим, на нужно поле example, которое содержит 3 собственных поля: id, piи name. Вначале нужно описать самому объект типа Example:
class Example(graphene.ObjectType):
id = graphene.Int()
pi = graphene.Float()
name = graphene.String()
Теперь объявляем поле в нашем классе-узле AkademioPagxoNode:
class AkademioPagxoNode(SiriusoAuthNode, SiriusoObjectId, DjangoObjectType):
"""
Страница академии
"""
permission_classes = (AllowAny,)
json_filter_fields = {
'teksto__enhavo': ['contains', 'icontains'],
}
teksto = graphene.Field(SiriusoLingvo, description=_('Текст страницы академии'))
example = graphene.Node(Example)
class Meta:
model = AkademioPagxo
filter_fields = {
'uuid': ['exact'],
'kodo': ['exact', 'icontains', 'istartswith'],
'forigo': ['exact'],
'arkivo': ['exact'],
'publikigo': ['exact'],
}
interfaces = (graphene.relay.Node,)
[@staticmethod](profile/staticmethod)
def resolve_example(root, info, **kwargs):
return Example(id=100, pi=3.14, name='Example')
Как раз в статическом методе resolve_example определяется и возвращается значение нужного поля. Замечу, что определив метод разрешения поля, можно переопределить значение любого поля, даже поля модели Django. Статический метод разрешения значения поля всегда определяется как def resolve_{field_name}(root, info, **kwargs), где {field_name} - имя поля, root - инстанс текущего узла (в случае с AkademioPagxoNode это будет объект типа AkademioPagxo Django модели), kwargs - дополительные аргументы, передаваемые через API.
Таим образом можно объявлять и разрешать любые поля:
узел модели Django:
uzanto = graphene.Node(UzantoNode)
[@staticmethod](profile/staticmethod)
def resolve_uzanto(root, info, **kwargs):
try:
return Uzanto.objects.get(id=1)
except Uzanto.DoesNotExist:
return None
список узлов модели Django (подключем через коннектор SiriusoFilterConnectionField):
uzantoj = SiriusoFilterConnectionField(UzantoNode)
[@staticmethod](profile/staticmethod)
def resolve_uzanto(root, info, **kwargs):
return Uzanto.objects.filter(id__in=[1,13,5004])
простые скалярные типы:
pi = graphene.Float()
[@staticmethod](profile/staticmethod)
def resolve_pi(root, info, **kwargs):
return 3.1415926
объекты Graphene (ObjectType):
example = graphene.Node(Example)
[@staticmethod](profile/staticmethod)
def resolve_example(root, info, **kwargs):
return Example(id=100, pi=3.14, name='Example')