Главная страница

курсовая_v1. Курсовая работа Необходимо


Скачать 2.93 Mb.
НазваниеКурсовая работа Необходимо
Дата30.03.2022
Размер2.93 Mb.
Формат файлаpdf
Имя файлакурсовая_v1.pdf
ТипКурсовая
#429949
страница2 из 6
1   2   3   4   5   6
Настраиваем Alembic
Когда схема базы данных описана, необходимо сгенерировать миграции, но для этого сначала нужно настроить Alembic.
Чтобы воспользоваться командой alembic, необходимо выполнить следующие шаги:
1. Установить пакет: pip install alembic
2. Инициализировать Alembic: cd analyzer && alembic init db/alembic.
Эта команда создаст файл конфигурации analyzer/alembic.ini и папку analyzer/db/alembic со следующим содержимым:
• env.py — вызывается каждый раз при запуске Alembic. Подключает в Alembic реестр sqlalchemy.MetaData с описанием желаемого состояния БД и содержит инструкции по запуску миграций.
• script.py.mako — шаблон, на основе которого генерируются миграции.
• versions — папка, в которой Alembic будет искать (и генерировать) миграции.
3. Указать адрес базы данных в файле alembic.ini:
; analyzer/alembic.ini
[alembic] sqlalchemy.url = postgresql://user:hackme@localhost/analyzer
4. Указать описание желаемого состояния базы данных
(реестр sqlalchemy.MetaData), чтобы Alembic мог генерировать миграции автоматически:
# analyzer/db/alembic/env.py
from
analyzer.db
import
schema
target_metadata = schema.metadata
Alembic настроен и им уже можно пользоваться, но в нашем случае такая конфигурация имеет ряд недостатков:
1. Утилита alembic ищет alembic.ini в текущей рабочей директории. Путь к alembic.ini можно указать аргументом командной строки, но это неудобно: хочется иметь возможность вызывать команду из любой папки без дополнительных параметров.
2. Чтобы настроить Alembic на работу с определенной базой данных, требуется менять файл alembic.ini
. Гораздо удобнее было бы указать настройки БД переменной окружения и/или аргументом командной строки, например
--pg-url
3. Название утилиты alembic не очень хорошо коррелирует с названием нашего сервиса (а пользователь фактически может вообще не владеть
Python и ничего не знать об Alembic). Конечному пользователю было бы намного удобнее, если бы все исполняемые команды сервиса имели общий префикс, например analyzer-*
Эти проблемы решаются с помощью небольшой обертки analyzer/db/__main__.py:

Для обработки аргументов командной строки Alembic использует стандартный модуль argparse
. Он позволяет добавить необязательный аргумент
--pg-url со значением по умолчанию из переменной окружения
ANALYZER_PG_URL
Код
import
os
from
alembic.config
import
CommandLine, Config
from
analyzer.utils.pg
import
DEFAULT_PG_URL
def
main
()
: alembic = CommandLine() alembic.parser.add_argument(
'--pg-url'
, default=os.getenv(
'ANALYZER_PG_URL'
, DEFAULT_PG_URL), help=
'Database URL [env var: ANALYZER_PG_URL]'
) options = alembic.parser.parse_args()
# Создаем объект конфигурации Alembic config = Config(file_=options.config, ini_section=options.name, cmd_opts=options)
# Меняем значение sqlalchemy.url из конфига Alembic config.set_main_option(
'sqlalchemy.url'
, options.pg_url)
# Запускаем команду alembic exit(alembic.run_cmd(config, options))
if
__name__ ==
'__main__'
:
main()

Путь до файла alembic.ini можно рассчитывать относительно расположения исполняемого файла, а не текущей рабочей директории пользователя.
Код
import
os
from
alembic.config
import
CommandLine, Config
from
pathlib
import
Path
PROJECT_PATH = Path(__file__).parent.parent.resolve()
def
main
()
: alembic = CommandLine() options = alembic.parser.parse_args()
# Если указан относительный путь (alembic.ini), добавляем в начало
# абсолютный путь до приложения
if
not
os.path.isabs(options.config): options.config = os.path.join(PROJECT_PATH, options.config)
# Создаем объект конфигурации Alembic config = Config(file_=options.config, ini_section=options.name, cmd_opts=options)
# Подменяем путь до папки с alembic на абсолютный (требуется, чтобы alembic
# мог найти env.py, шаблон для генерации миграций и сами миграции)
alembic_location = config.get_main_option(
'script_location'
)
if
not
os.path.isabs(alembic_location): config.set_main_option(
'script_location'
, os.path.join(PROJECT_PATH, alembic_location))
# Запускаем команду alembic exit(alembic.run_cmd(config, options))
if
__name__ ==
'__main__'
: main()
Когда утилита для управления состоянием БД
готова, ее можно зарегистрировать в setup.py как исполняемую команду с понятным конечному пользователю названием, например analyzer-db:
Регистрация исполняемой команды в setup.py
from
setuptools
import
setup setup(..., entry_points={
'console_scripts'
: [
'analyzer-db = analyzer.db.__main__:main'

]
})
После переустановки модуля будет сгенерирован файл env/bin/analyzer-db и команда analyzer-db станет доступной:
$ pip install -e
'.[dev]'
"""
Утилита для управления состоянием базы данных, обертка над alembic.
Можно вызывать из любой директории, а также указать произвольный DSN для базы данных, отличный от указанного в файле alembic.ini.
""" import argparse import logging import os from alembic.config import CommandLine from analyzer.utils.pg import DEFAULT_PG_URL, make_alembic_config def main(): logging.basicConfig(level=logging.DEBUG) alembic = CommandLine() alembic.parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter alembic.parser.add_argument(
'--pg-url', default=os.getenv('ANALYZER_PG_URL', DEFAULT_PG_URL), help='Database URL [env var: ANALYZER_PG_URL]'
) options = alembic.parser.parse_args() if 'cmd' not in options: alembic.parser.error('too few arguments') exit(128) else: config = make_alembic_config(options) exit(alembic.run_cmd(config, options)) if __name__ == '__main__': main()
Генерируем миграции
Чтобы сгенерировать миграции, требуется два состояния: желаемое (которое мы описали объектами SQLAlchemy) и реальное (база данных, в нашем случае пустая).
Я решил, что проще всего поднять Postgres с помощью Docker и для удобства добавил команду make postgres
, запускающую в фоновом режиме контейнер с
PostgreSQL на 5432 порту:
Поднимаем PostgreSQL и генерируем миграцию

$ make postgres
$ analyzer-db revision --message=
"Initial"
--autogenerate
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table
'imports'
INFO [alembic.autogenerate.compare] Detected added table
'citizens'
INFO [alembic.autogenerate.compare] Detected added index
'ix__citizens__town'
on
'['
town
']'
INFO [alembic.autogenerate.compare] Detected added table
'relations'
Generating
/Users/alvassin/Work/backendschool2019/analyzer/db/alembic/versions/d5f704ed4 610_initial.py ...
done
Alembic в целом хорошо справляется с рутинной работой генерации миграций, но я хотел бы обратить внимание на следующее:

Пользовательские типы данных, указанные в создаваемых таблицах, создаются автоматически (в нашем случае — gender
), но код для их удаления в downgrade не генерируется. Если применить, откатить и потом еще раз применить миграцию, это вызовет ошибку, так как указанный тип данных уже существует.
Удаляем тип данных gender в методе downgrade
from
alembic
import
op
from
sqlalchemy
import
Column, Enum
GenderType = Enum(
'female'
,
'male'
, name=
'gender'
)
def
upgrade
()
:
# При создании таблицы тип данных GenderType будет создан автоматически op.create_table(
'citizens'
, ...,
Column(
'gender'
, GenderType, nullable=
False
))
def
downgrade
()
: op.drop_table(
'citizens'
)
# После удаления таблицы тип данных необходимо удалить
GenderType.drop(op.get_bind())

В методе downgrade некоторые действия иногда можно убрать (если мы удаляем таблицу целиком, можно не удалять ее индексы отдельно):
Например
def
downgrade
()
: op.drop_table(
'relations'
)

# Следующим шагом мы удаляем таблицу citizens, индекс будет удален автоматически
# эту строчку можно удалить op.drop_index(op.f(
'ix__citizens__town'
), table_name=
'citizens'
) op.drop_table(
'citizens'
) op.drop_table(
'imports'
)
Когда миграция исправлена и готова, применим ее:
$ analyzer-db upgrade head
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> d5f704ed4610, Initial
Приложение
Прежде чем приступить к созданию обработчиков, необходимо сконфигурировать приложение aiohttp.
Если посмотреть aiohttp quickstart, можно написать приблизительно такой
код
import
logging
from
aiohttp
import
web
def
main
()
:
# Настраиваем логирование logging.basicConfig(level=logging.DEBUG)
# Создаем приложение app = web.Application()
# Регистрируем обработчики app.router.add_route(...)
# Запускаем приложение web.run_app(app)
Этот код вызывает ряд вопросов и имеет ряд недостатков:

Как конфигурировать приложение? Как минимум, необходимо указать хост и порт для подключения клиентов, а также информацию для подключения к базе данных.
Решать эту задачу можно с помощью модуля
ConfigArgParse
: он расширяет стандартный argparse и позволяет использовать для конфигурации аргументы командной строки, переменные окружения
(незаменимые для конфигурации Docker-контейнеров) и даже файлы
конфигурации (а также совмещать эти способы). C помощью
ConfigArgParse также можно валидировать значения параметров конфигурации приложения.
Пример обработки параметров с помощью ConfigArgParse
from
aiohttp
import
web
from
configargparse
import
ArgumentParser, ArgumentDefaultsHelpFormatter
from
analyzer.utils.argparse
import
positive_int parser = ArgumentParser(
# Парсер будет искать переменные окружения с префиксом ANALYZER_,
# например ANALYZER_API_ADDRESS и ANALYZER_API_PORT
auto_env_var_prefix=
'ANALYZER_'
,
# Покажет значения параметров по умолчанию formatter_class=ArgumentDefaultsHelpFormatter
) parser.add_argument(
'--api-address'
, default=
'0.0.0.0'
, help=
'IPv4/IPv6 address API server would listen on'
)
# Разрешает только целые числа больше нуля parser.add_argument(
'--api-port'
, type=positive_int, default=
8081
, help=
'TCP port API server would listen on'
)
def
main
()
:
# Получаем параметры конфигурации, которые можно передать как аргументами
# командной строки, так и переменными окружения args = parser.parse_args()
# Запускаем приложение на указанном порту и адресе app = web.Application() web.run_app(app, host=args.api_address, port=args.api_port)
if
__name__ ==
'__main__'
: main()
Кстати,
ConfigArgParse
, как и argparse
, умеет генерировать подсказку по запуску команды с описанием всех аргументов (необходимо позвать команду с аргументом
-h или
--help
). Это невероятно облегчает жизнь пользователям вашего ПО:

Например
• $ python __main__.py -- help
• usage: __main__.py [-h] [--api-address API_ADDRESS] [--api-port
API_PORT]

• If an arg is specified
in
more than one place,
then
commandline values override environment variables which override defaults.

• optional arguments:
• -h, -- help show this help message and exit
• --api-address API_ADDRESS

• IPv4/IPv6 address API server would listen on
[env var: ANALYZER_API_ADDRESS] (default: 0.0.0.0)
--api-port API_PORT TCP port API server would listen on [env var:
ANALYZER_API_PORT] (default: 8081)

После получения переменные окружения больше не нужны и даже могут представлять опасность — например, они могут случайно «утечь» с отображением информации об ошибке. Злоумышленники в первую очередь будут пытаться получить информацию об окружении, поэтому очистка
переменных окружения считается хорошим тоном.
Можно было бы воспользоваться os.environ.clear()
, но Python позволяет управлять поведением модулей стандартной библиотеки с помощью многочисленных переменных окружения (например, вдруг потребуется включить режим отладки asyncio
?), поэтому разумнее очищать переменные окружения по префиксу приложения, указанного в
ConfigArgParser
Пример
import
os
from
typing
import
Callable
from
configargparse
import
ArgumentParser
from
yarl
import
URL
from
analyzer.api.app
import
create_app
from
analyzer.utils.pg
import
DEFAULT_PG_URL
ENV_VAR_PREFIX =
'ANALYZER_'
parser = ArgumentParser(auto_env_var_prefix=ENV_VAR_PREFIX) parser.add_argument(
'--pg-url'
, type=URL, default=URL(DEFAULT_PG_URL), help=
'URL to use to connect to the database'
)
def
clear_environ
(rule: Callable)
:
"""
Очищает переменные окружения, переменные для очистки определяет переданная функция rule
"""
# Ключи из os.environ копируются в новый tuple, чтобы не менять объект
# os.environ во время итерации
for
name
in
filter(rule, tuple(os.environ)): os.environ.pop(name)
def
main
()
:
# Получаем аргументы args = parser.parse_args()
# Очищаем переменные окружения по префиксу ANALYZER_
clear_environ(
lambda
i: i.startswith(ENV_VAR_PREFIX))
# Запускаем приложение app = create_app(args)

if
__name__ ==
'__main__'
: main()

Запись логов в stderr/файл в основном потоке блокирует цикл
событий.
По умолчанию logging.basicConfig() настраивает запись логов в stderr
Чтобы логирование не мешало эффективной работе асинхронного приложения, необходимо выполнять запись логов в отдельном потоке. Для этого можно воспользоваться готовым методом из модуля aiomisc.
Настраиваем логирование с помощью aiomisc
import
logging
from
aiomisc.log
import
basic_config basic_config(logging.DEBUG, buffered=
True
)

Как масштабировать приложение, если одного процесса станет
недостаточно для обслуживания входящего трафика? Можно сначала аллоцировать сокет, затем с помощью fork создать несколько новых отдельных процессов, и соединения на сокете будут распределяться между ними механизмами ядра (конечно, под Windows это не работает).
Пример
import
os
from
sys
import
argv
import
forklib
from
aiohttp.web
import
Application, run_app
from
aiomisc
import
bind_socket
from
setproctitle
import
setproctitle
def
main
()
: sock = bind_socket(address=
'0.0.0.0'
, port=
8081
, proto_name=
'http'
) setproctitle(
f'[Master]
{os.path.basename(argv[
0
])}
'
)
def
worker
()
: setproctitle(
f'[Worker]
{os.path.basename(argv[
0
])}
'
) app = Application() run_app(app, sock=sock) forklib.fork(os.cpu_count(), worker, auto_restart=
True
)
if
__name__ ==
'__main__'
:
main()

Требуется ли приложению обращаться или аллоцировать какие-либо
ресурсы во время работы? Если нет, по соображениям безопасности все ресурсы (в нашем случае — сокет для подключения клиентов) можно аллоцировать на старте, а затем сменить пользователя на nobody. Он обладает ограниченным набором привиллегий — это здорово усложнит жизнь злоумышленникам.
Пример
import
os
import
pwd
from
aiohttp.web
import
run_app
from
aiomisc
import
bind_socket
from
analyzer.api.app
import
create_app
def
main
()
:
# Аллоцируем сокет sock = bind_socket(address=
'0.0.0.0'
, port=
8085
, proto_name=
'http'
) user = pwd.getpwnam(
'nobody'
) os.setgid(user.pw_gid) os.setuid(user.pw_uid) app = create_app(...) run_app(app, sock=sock)
if
__name__ ==
'__main__'
: main()

В конце концов вынесли создание приложения в отдельную параметризуемую функцию create_app, чтобы можно было легко создавать идентичные приложения для тестирования.
Сериализация данных
Все успешные ответы обработчиков будем возвращать в формате JSON.
Информацию об ошибках клиентам тоже было бы удобно получать в сериализованном виде (например, чтобы увидеть, какие поля не прошли валидацию).
Документация aiohttp предлагает метод json_response
, который принимает объект, сериализует его в JSON и возвращает новый объект aiohttp.web.Response с заголовком
Content-Type: application/json и сериализованными данными внутри.
Как сериализовать данные с помощью json_response
from
aiohttp.web
import
Application, View, run_app
from
aiohttp.web_response
import
json_response

class
SomeView
(View)
:
async
def
get
(self)
:
return
json_response({
'hello'
:
'world'
}) app = Application() app.router.add_route(
'*'
,
'/hello'
, SomeView) run_app(app)
Но существует и другой способ: aiohttp позволяет зарегистрировать произвольный сериализатор для определенного типа данных ответа в реестре aiohttp.PAYLOAD_REGISTRY
. Например, можно указать сериализатор aiohttp.JsonPayload для объектов типа
Mapping
В этом случае обработчику будет достаточно вернуть объект
Response с данными ответа в параметре body
. aiohttp найдет сериализатор, соответствующий типу данных и сериализует ответ.
Помимо того, что сериализация объектов описана в одном месте, этот подход еще и более гибкий — он позволяет реализовывать очень интересные решения
(мы рассмотрим один из вариантов использования в обработчике
GET
/imports/$import_id/citizens
).
1   2   3   4   5   6


написать администратору сайта