Как ацтеки создавали календари, а мы — API

Всем привет!
Статья на 15-20 минут осознанного чтения. Будет интересна начинающим и, надеюсь, продолжающим увлекательное путешествие людям во вселенной python. Будет рассмотрен один из великого множества подходов к написанию web приложения с API + автоматизированной документацией на OpenAPI (в недалеком прошлом Swagger).
Гипотеза цикличности истории натолкнула на мысль о том, что современные технологии концептуально далеко не новое изобретение. Древние ацтеки верили в некую магическую силу ритуалов, которая заставляла мир как-то существовать и усердно это документировали в своих календарях. Точно также, в нашем мире, все вращается вокруг веб технологий и API различных сервисов пока они не потеряли актуальность... Когда-нибудь это конечно произойдет, но концепция документации чего-либо никогда не перестанет быть актуальной.
Дисклеймер: Решения приведенные в статье не являются руководством, и не претендуют на единственно правильные. При этом являются рабочими и, возможно, будут полезны в вашем проекте. Конструктивная критика категорически приветствуется!
Первое что нам нужно сделать, как и в любом более менее серьезном проекте на языке python — это создать и активировать виртуальную среду в папке проекта.
pipenv shell
Важно в полной мере осознавать смысл виртуального окружения, но в рамках данной статьи подробное описание будет избыточным.
Следующий важный шаг который нам предстоить сделать — это определиться со структурой проекта. Существует много разных подходов к организации, в этом вопросе нет заранее сформированных стандартов. Тут важно придерживаться глобальной цели: «Удобство поддержания и масштабирования кода в будущем».
Вот примерная начальная структура нашего проекта:
/ |
корень проекта |
env/ |
директория для хранения настроек приложения |
src/ |
директория в которой будет хранится основной код приложения |
web_plugin/ |
директория для api |
api_extension.py |
Файл в корне проекта в котором будет описана архитектура api |
точка входа выполнении пакета как программы |
Самое главное мы сделали, осталось всего лишь написать наше приложение.
Начнем с установки необходимых пакетов в виртуальную среду, которые позволят создать полноценное и готовое к масштабированию веб приложение.
pipenv install flask
pipenv install flask_cors
pipenv install flask_restx
pipenv install pydantic
pipenv install waitress
Теперь распределим файлы с исполняемым кодом по соответствующим директориям.
Создаем пространство имен в директории web_plugin. Опыт реального проектирования веб приложений говорит о том что таких пространств может быть большое количество и с целью упрощения их поддержания в будущем желательно заранее разнести функциональные части кода в отдельные файлы.
Добавляем следующие директории и файлы:
web_plugin/api_package |
тут будут храниться пространства имен api |
web_plugin/api_package/default_ns/ns.py |
файл в котором опишем эндпоинты |
web_plugin/api_package/default_ns/urls.py |
файл для хранения адресов api. В каждом пространстве имён будет свой файл. |
web_plugin/api_package/default_ns/ns_schemas.py |
файл содержит модели запросов/ответов методов api. В каждом пространстве имён будет свой файл. |
web_plugin/api_package/default_ns/params_schemas.py |
файл содержит модели параметров запросов. В каждом пространстве имён будет свой файл. |
/api_extension.py |
файл в котором осуществляется подключение api к web серверу |
Хорошая практика — выносить эндпоинты в отдельный файл как константы. Такой код легко читается и поддерживается.
Файл web_plugin/api_package/default_ns/urls.py содержит следующий код:
"""URLs in default namespace"""
GET_HELLO_URL = "/hello"
Следующее что нам будет важно для организации самодокументируемого api — это реализация моделей запросов и ответов. Далее для простоты демонстрации подхода приведен простейший пример, но в дальнейшем масштабирование приложения потребует более красивого решения.
Файл web_plugin/api_package/default_ns/ns_schemas.py содержит следующий код:
"""Default namespace openApi schemas"""
from flask_restx import fields
HELLO_RETURN_SCHEMA = {
"msg": fields.String(),
"err": fields.Boolean(),
}
Данный код позволит не только отображать на странице документации api модель возвращаемую методом как показано на картинке, но фактически валидировать ответ сервера. Другими словами если формат данных не будет соответствовать заданной схеме вы получите в ответе http код 500. Для объемных схем такой подход может тормозить разработку и отладку. Нужно отметить поиск решения проблемы валидации данных и форм во фреймворке Flask тема отдельной статьи.

Помимо моделей ответов сервера имеются еще одна группа элементов требующих валидации — это параметры запросов. Наиболее универсальный подход для этой задачи предоставляет библиотека pydantic.
Файл web_plugin/api_package/default_ns/params_schemas.py содержит следующий код:
"""Schemas for requests params"""
from pydantic import BaseModel, Field
class HelloParams(BaseModel):
"""Hello request params"""
name: str | None = Field(pattern=r"^[^а-яА-Я]*$")
Приведенный пример кода позволяет валидировать поле по определенному шаблону. В данном конкретном случае нам важно получить либо пустую строку либо строку без кириллицы. Имея такой инструмент разработчик может значительно повысить стабильность приложения отсекая некорректные данные на этапе их получения.
Переходим к последнему этапу — формирование пространства имен.
На что хотелось бы обратить внимание — это в первую очередь читаемость кода. Ввиду специфики формирования страницы документации требуется большое количество декораторов, объемных форм, иногда сложная логика обработки входных параметров. Но не так все плохо как кажется. С приобретением навыков оптимизации код становится значительно более лаконичным. В приведенном ниже примере код далек от оптимального. Его задача наглядно продемонстрировать функциональность.
Второе — ВСЕГДА обрабатывать ошибки которые могут дестабилизировать ваше приложение. Недопустимо, что-бы пользователь(или другой разработчик) получал http код 500 на какой либо запрос. Тут на помощь приходят различные валидаторы (как было упомянуто выше pydantic) и ваши собственные модули обработчики ошибок.
Файл web_plugin/api_package/default_ns/ns.py содержит следующий код:
from flask import request
from flask_cors import cross_origin
from flask_restx import Namespace, Resource
from pydantic import ValidationError
from web_plugin.api_package.default_ns.ns_schemas import HELLO_RETURN_SCHEMA
from web_plugin.api_package.default_ns.params_schemas import HelloParams
from web_plugin.api_package.default_ns.urls import GET_HELLO_URL
default_ns = Namespace("default", description="default namespace example")
@default_ns.route(GET_HELLO_URL)
@default_ns.doc(
params={
"name": {"description": "send name to say 'hello'", "type": "str"},
}
)
class Hello(Resource):
"""HTTP method described in self doc interface swagger"""
@cross_origin()
@default_ns.doc("Returns hello greetings")
@default_ns.marshal_with(
default_ns.model("hello_return_schema", HELLO_RETURN_SCHEMA)
)
def get(self) -> tuple:
"""GET request returns greetings to user"""
return_msg = "Hello world!"
try:
# Validate request params
request_params = HelloParams(name=request.args.get("name"))
if request_params.name:
return_msg = "Hello %s!" % (request_params.name.strip())
return_body = {"msg": return_msg, "err": None}
return return_body, 200
except ValidationError as err:
print(err)
return_body = {"msg": "incorrect characters were received",
"err": True}
return return_body, 400
Приведенный код демонстрирует организацию пространства имен api с одним единственным методом типа GET принимающий один параметр в виде строки в которой не должно быть символов кириллицы. В случае получения корректных данных сервер вернет ответ согласно схеме и http код 200.
В случае некорректных данных http код 400 и текст ошибки. При необходимости StackTrace может быть сохранен в лог файл, вывод в консоль не обязателен.
Осталось еще пара штрихов перед тем как мы сможем насладиться полученным результатом. Подключим api к веб серверу — добавим в файл api_extension.py следующие строки:
"""Init api self doc service"""
from flask_restx import Api
from web_plugin.api_package.default_ns.ns import default_ns
open_api = Api(
version="1.0",
title="web app template API",
description="swagger doc service",
doc="/swagger",
)
open_api.add_namespace(default_ns, path="/api/v1")
И опишем непосредственно сам веб сервер в файле main.py:
from flask import Flask
from flask_cors import CORS
from api_extension import open_api
app = Flask(__name__)
open_api.init_app(app)
app.run("127.0.0.1", debug=True, port=5555)
Тут надо отметить, что приведенный код годится для тестирования и отладки API в продакшене такой подход не годится.
Запустим наше приложение командой в терминале предварительно активировав виртуальную среду:
python main.py
Теперь, чтобы протестировать, достаточно зайти в браузере на наш openAPI сервер по адресу http://127.0.0.1:5555/swagger
Откроется страница со списком пространств имен API.

Эта страница позволяет отправлять запросы к веб серверу и получать ответы. В идеале вся необходимая документация для API вашего приложения должна умещаться на данной странице.
Таким образом за относительно короткое время при наличии соответствующих навыков и опыта мы можем собрать шаблон для быстрого масштабирования приложения с web сервером и api. Конечно многие темы остались за кадром, например авторизация, хранение конфигурационных файлов, оптимизация кода для реальной эксплуатации, поставка приложения конечному пользователю и т.д. Они весьма обширны и заслуживают отдельных статей.
P.S. Статьи по теме, которые могут быть полезными [1, 2, 3].
Ссылка на GitHub
dyadyaSerezha
В заключителтном слове у понимается масштабирование, но не увидел тут его. Или пропустил?
stnslvtrfnv Автор
Спасибо за замечание! Обязательно будет продолжение.