Главная страница
Навигация по странице:

  • Почему нужно начать с setup.py

  • Как указать версии зависимостей

  • База данных Проектируем схему

  • Но у этого способа есть ряд недостатков

  • Описываем схему в SQLAlchemy

  • Создаем реестр MetaData и передаем в него шаблоны именования analyzer/db/schema.pyfrom sqlalchemy import

  • Описываем схему базы данных объектами SQLAlchemy analyzer/db/schema.pyfrom enum import Enum, unique from sqlalchemy import

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


    Скачать 2.93 Mb.
    НазваниеКурсовая работа Необходимо
    Дата30.03.2022
    Размер2.93 Mb.
    Формат файлаpdf
    Имя файлакурсовая_v1.pdf
    ТипКурсовая
    #429949
    страница1 из 6
      1   2   3   4   5   6


    Курсовая работа
    Необходимо: разработать REST API-сервис посредством языка Python, протестировать, упаковать его в Docker-контейнер, а также развернуть с помощью Ansible.
    Примечание: Реализовать REST API-сервис можно по-разному, с помощью
    различных инструментов. Описанное решение не единственно верное, поэтому вы можете
    выбрать инструменты для реализации исходя из своего личного опыта и предпочтений.
    Рис. 1 – структура проекта
    Представим, что интернет-магазин подарков планирует запустить акцию в разных регионах. Чтобы стратегия продаж была эффективной, необходим анализ рынка. У магазина есть поставщик, регулярно присылающий (например, на почту) выгрузки данных с информацией о жителях.
    Давайте разработаем REST API-сервис на Python, который будет анализировать предоставленные данные и выявлять спрос на подарки у жителей разных возрастных групп в разных городах по месяцам.
    В сервисе реализуем следующие обработчики:
    • POST /imports
    Добавляет новую выгрузку с данными;
    • GET /imports/$import_id/citizens
    Возвращает жителей указанной выгрузки;

    • PATCH /imports/$import_id/citizens/$citizen_id
    Изменяет информацию о жителе (и его родственниках) в указанной выгрузке;
    • GET /imports/$import_id/citizens/birthdays
    Вычисляет число подарков, которое приобретет каждый житель выгрузки своим родственникам (первого порядка), сгруппированное по месяцам;
    • GET /imports/$import_id/towns/stat/percentile/age
    Вычисляет 50-й, 75-й и 99-й перцентили возрастов (полных лет) жителей по городам в указанной выборке.
    Итак, необходимо написать сервис на Python, используя фреймворки, библиотеки и
    СУБД.
    Для реализации предлагается выбрать СУБД
    PostgreSQL
    (или другую, по вашему усмотрению)
    , зарекомендовавшую себя как надежное решение c отличной документацией на русском языке
    , сильным русским сообществом (всегда можно найти ответ на вопрос на русском языке). Реляционная модель достаточно универсальна и хорошо понятна многим разработчикам. Хотя то же самое можно было сделать на любой NoSQL СУБД.
    Основная задача сервиса — передача данных по сети между БД и клиентами — не предполагает большой нагрузки на процессор, но требует возможности обрабатывать несколько запросов в один момент времени. Асинхронный подход позволяет эффективно обслуживать нескольких клиентов в рамках одного процесса ОС (в отличие, например, от используемой во Flask/Django pre-fork-модели, которая создает несколько процессов для обработки запросов от пользователей, каждый из них потребляет память, но простаивает большую часть времени). Поэтому в качестве библиотеки для написания сервиса предлагается выбрать асинхронный aiohttp
    Рис 2. – Схема обработки запросов
    SQLAlchemy позволяет декомпозировать сложные запросы на части, переиспользовать их, генерировать запросы с динамическим набором полей (например,
    PATCH-обработчик позволяет частичное обновление жителя с произвольными полями) и сосредоточиться непосредственно на бизнес-логике. С выполнением этих запросов и передачей данных быстрее всех справится драйвер asyncpg
    , а подружить их поможет asyncpgsa
    Один из инструментов для управления состоянием БД и работы с миграциями

    Alembic
    Логику валидации можно лаконично описать схемами
    Marshmallow
    (включая проверки на родственные связи). С помощью модуля aiohttp-spec связать aiohttp-
    обработчики и схемы для валидации данных, а бонусом сгенерировать документацию в формате
    Swagger и отобразить ее в графическом интерфейсе
    Для написания тестов используйте pytest. Для отладки и профилирования можно использовать отладчик PyCharm.
    На любом компьютере с
    Docker
    (и даже на разных ОС) можно запускать упакованное приложение без необходимости настраивать окружение для запуска и легко устанавливать/обновлять/удалять приложение на сервере.
    Для деплоя воспользуйтесь
    Ansible
    . Он позволяет декларативно описывать желаемое состояние сервера и его сервисов, работает по ssh и не требует специального софта.
    Разработка
    Задайте Python-пакету название analyzer и используйте следующую структуру:
    Рис. 3 – структура пакета
    В файле analyzer/__init__.py разместите общую информацию о пакете: описание (
    docstring
    ), версию, лицензию, контакты разработчиков.
    Затем ее можно посмотреть встроенной командой help
    $ python
    >>> import analyzer
    >>> help(analyzer)
    Пример:

    Пакет имеет две входных точки — REST API-сервис
    (
    analyzer/api/__main__.py
    ) и утилита управления состоянием БД
    (
    analyzer/db/__main__.py
    ). Файлы называются
    __main__.py неспроста — во- первых, такое название привлекает внимание, по нему понятно, что файл является входной точкой.
    Во-вторых, благодаря этому подходу к входным точкам можно обращаться с помощью команды python -m
    :
    Почему нужно начать с setup.py?
    Забегая вперед, подумаем, как можно распространять приложение: оно может быть упаковано в zip- (а также wheel/egg-) архив, rpm-пакет, pkg-файл для macOS и установлено на удаленный компьютер, в виртуальную машину, MacBook или Docker-контейнер.
    Главная цель файла setup.py
    — описать пакет с приложением для distutils
    /
    setuptools
    В файле необходимо указать общую информацию о пакете (название, версию, автора и т. д.), но также в нем можно указать требуемые для работы модули, «экстра»-зависимости (например для тестирования), точки входа
    (например, исполняемые команды) и требования к интерпретатору.
    Плагины setuptools позволяют собирать из описанного пакета артефакт.
    Есть встроенные плагины: zip, egg, rpm, macOS pkg. Остальные плагины распространяются через PyPI: wheel
    , xar
    , pex
    В сухом остатке, описав один файл, мы получаем огромные возможности.
    Именно поэтому разработку нового проекта нужно начинать с setup.py.
    В функции setup() зависимые модули указываются списком:

    Но необходимо описать зависимости в отдельных файлах requirements.txt и requirements.dev.txt
    , содержимое которых используется в setup.py
    . Это кажется более гибким, плюс тут есть секрет: впоследствии это позволит собирать Docker-образ быстрее. Зависимости будут ставиться отдельным шагом до установки самого приложения, а при пересборке
    Docker-контейнера попадать в кеш.
    Чтобы setup.py смог прочитать зависимости из файлов requirements.txt и requirements.dev.txt
    , написана функция:
    Стоит отметить, что setuptools при сборке source distribution по умолчанию включает в сборку только файлы
    .py
    ,
    .c
    ,
    .cpp и
    .h
    . Чтобы файлы с зависимостями requirements.txt и requirements.dev.txt попали в пакет, их необходимо явно указать в файле
    MANIFEST.in
    setup.py целиком
    import
    os
    from
    importlib.machinery
    import
    SourceFileLoader
    from
    pkg_resources
    import
    parse_requirements
    from
    setuptools
    import
    find_packages, setup module_name =
    'analyzer'
    # Возможно, модуль еще не установлен (или установлена другая версия), поэтому
    # необходимо загружать __init__.py с помощью machinery.
    module = SourceFileLoader( module_name, os.path.join(module_name,
    '__init__.py'
    )
    ).load_module()
    def
    load_requirements
    (fname: str)
    -> list: requirements = []
    with
    open(fname,
    'r'
    )
    as
    fp:
    for
    req
    in
    parse_requirements(fp.read()): extras =
    '[{}]'
    .format(
    ','
    .join(req.extras))
    if
    req.extras
    else
    ''
    requirements.append(
    '{}{}{}'
    .format(req.name, extras, req.specifier)
    )
    return
    requirements setup( name=module_name, version=module.__version__, author=module.__author__, author_email=module.__email__, license=module.__license__, description=module.__doc__, long_description=open(
    'README.rst'
    ).read(), url=
    'https://github.com/alvassin/backendschool2019'
    , platforms=
    'all'
    , classifiers=[

    'Intended Audience :: Developers'
    ,
    'Natural Language :: Russian'
    ,
    'Operating System :: MacOS'
    ,
    'Operating System :: POSIX'
    ,
    'Programming Language :: Python'
    ,
    'Programming Language :: Python :: 3'
    ,
    'Programming Language :: Python :: 3.8'
    ,
    'Programming Language :: Python :: Implementation :: CPython'
    ], python_requires=
    '>=3.8'
    , packages=find_packages(exclude=[
    'tests'
    ]), install_requires=load_requirements(
    'requirements.txt'
    ), extras_require={
    'dev'
    : load_requirements(
    'requirements.dev.txt'
    )}, entry_points={
    'console_scripts'
    : [
    # f-strings в setup.py не используются из-за соображений
    # совместимости.
    # Несмотря на то, что этот пакет требует Python 3.8, технически
    # source distribution для него может собираться с помощью более
    # ранних версий Python. Не стоит лишать пользователей этой
    # возможности.
    '{0}-api = {0}.api.__main__:main'
    .format(module_name),
    '{0}-db = {0}.db.__main__:main'
    .format(module_name)
    ]
    }, include_package_data=
    True
    )
    Установить проект в режиме разработки можно следующей командой (в editable- режиме Python не установит пакет целиком в папку site-packages
    , а только создаст ссылки, поэтому любые изменения, вносимые в файлы пакета, будут видны сразу):
    # Установить пакет с обычными и extra-зависимостями "dev"
    pip install -e
    '.[dev]'
    # Установить пакет только с обычными зависимостями pip install -e .
    Как указать версии зависимостей?
    Здорово, когда разработчики активно занимаются своими пакетами — в них активнее исправляются ошибки, появляется новая функциональность и можно быстрее получить обратную связь. Но иногда изменения в зависимых библиотеках не имеют обратной совместимости и могут привести к ошибкам в вашем приложении, если не подумать об этом заранее.
    Для каждого зависимого пакета можно указать определенную версию, например aiohttp==3.6.2
    . Тогда приложение будет гарантированно собираться именно с теми версиями зависимых библиотек, с которыми оно было протестировано. Но у этого подхода есть и недостаток — если разработчики исправят критичный баг в зависимом пакете, не влияющий на обратную совместимость, в приложение это исправление не попадет.
    Существует подход к версионированию
    Semantic Versioning
    , который предлагает представлять версию в формате
    MAJOR.MINOR.PATCH
    :

    • MAJOR
    — увеличивается при добавлении обратно несовместимых изменений;
    • MINOR
    — увеличивается при добавлении новой функциональности с поддержкой обратной совместимости;
    • PATCH
    — увеличивается при добавлении исправлений багов с поддержкой обратной совместимости.
    Если зависимый пакет следует этому подходу (о чем авторы обычно сообщают в файлах README или CHANGELOG), то достаточно зафиксировать значения
    MAJOR
    ,
    MINOR
    и ограничить минимальное значение для PATCH-версии:
    >=
    MAJOR.MINOR.PATCH, == MAJOR.MINOR.*
    Такое требование можно реализовать с помощью оператора

    =
    Например, aiohttp=3.6.2
    позволит PIP установить для aiohttp версию 3.6.3, но не 3.7.
    Если указать интервал версий зависимостей, это даст еще одно преимущество
    — не будет конфликтов версий между зависимыми библиотеками.
    Если вы разрабатываете библиотеку, которая требует другой пакет- зависимость, то разрешите для него не одну определенную версию, а интервал.
    Тогда потребителям вашей библиотеки будет намного легче ее использовать
    (вдруг их приложение требует этот же пакет-зависимость, но уже другой версии).
    Semantic Versioning — лишь соглашение между авторами и потребителями пакетов. Оно не гарантирует, что авторы пишут код без багов и не могут допустить ошибку в новой версии своего пакета.

    База данных
    Проектируем схему
    В описании обработчика POST /imports приведен пример выгрузки с информацией о жителях:
    Первой мыслью может возникнуть хранить всю информацию о жителе в одной таблице citizens, где родственные связи были бы представлены полем relatives в виде списка целых чисел.
    Но у этого способа есть ряд недостатков
    1. В обработчике
    GET /imports/$import_id/citizens/birthdays для получения месяцев, на которые приходятся дни рождения родственников, потребуется выполнить слияние таблицы citizens с самой собой. Для этого будет необходимо развернуть список с идентификаторами родственников relatives с помощью фунции
    UNNEST
    Такой запрос будет выполняться сравнительно медленно, и обработчик не
    уложится в 10-секундный таймаут:
    SELECT
    relations.citizen_id, relations.relative_id,
    date_part(
    'month'
    , relatives.birth_date)
    as
    relative_birth_month
    FROM
    (
    SELECT
    citizens.import_id, citizens.citizen_id,
    UNNEST
    (citizens.relatives)
    as
    relative_id
    FROM
    citizens
    WHERE
    import_id =
    1
    )
    as
    relations
    INNER
    JOIN
    citizens
    as
    relatives
    ON
    relations.import_id = relatives.import_id
    AND
    relations.relative_id = relatives.citizen_id
    2. В таком подходе целостность данных в поле
    relatives
    не
    обеспечивается PostgreSQL, а контролируется приложением: технически в список relatives можно добавить любое целое число, в том числе идентификатор несуществующего жителя. Ошибка в коде или человеческий фактор (редактирование записей напрямую в БД администратором) обязательно рано или поздно приведут к несогласованному состоянию данных.
    Далее, нужно привести все требуемые для работы данные к третьей нормальной форме
    , и тогда получается следующая структура:
    1. Таблица imports состоит из автоматически инкрементируемого столбца import_id
    . Он нужен для создания проверки по внешнему ключу в таблице citizens
    2. В таблице citizens хранятся скалярные данные о жителе (все поля за исключением информации о родственных связях).
    В качестве первичного ключа используется пара (
    import_id
    , citizen_id
    ), гарантирующая уникальность жителей citizen_id в рамках import_id
    Внешний ключ citizens.import_id -> imports.import_id гарантирует, что поле citizens.import_id будет содержать только существующие выгрузки.

    3. Таблица relations содержит информацию о родственных связях.
    Одна родственная связь представлена двумя записями (от жителя к родственнику и обратно): эта избыточность позволяет использовать более простое условие при слиянии таблиц citizens и relations и получать информацию более эффективно.
    Первичный ключ состоит из столбцов (
    import_id
    , citizen_id
    , relative_id
    ) и гарантирует, что в рамках одной выгрузки import_id у жителя citizen_id будут родственники c уникальными relative_id
    Также в таблице используются два составных внешних ключа:
    (relations.import_id, relations.citizen_id) ->
    (citizens.import_id, citizens.citizen_id)
    и
    (relations.import_id, relations.relative_id) -> (citizens.import_id, citizens.citizen_id)
    , гарантирующие, что в таблице будут указаны существующие житель citizen_id и родственник relative_id из одной выгрузки.
    Такая структура обеспечивает целостность данных средствами PostgreSQL, позволяет эффективно получать жителей с родственниками из базы данных, но подвержена состоянию гонки во время обновления информации о жителях конкурентными запросами (подробнее рассмотрим при реализации обработчика
    PATCH).
    Описываем схему в SQLAlchemy
    Для создания запросов с помощью SQLAlchemy необходимо описать схему базы данных с помощью специальных объектов: таблицы описываются с помощью sqlalchemy.Table и привязываются к реестру sqlalchemy.MetaData, который хранит всю метаинформацию о базе данных. К слову, реестр
    MetaData способен не только хранить описанную в
    Python метаинформацию, но и представлять реальное состояние базы данных в виде объектов SQLAlchemy.
    Эта возможность в том числе позволяет Alembic сравнивать состояния и генерировать код миграций автоматически.
    Кстати, у каждой базы данных своя схема именования constraints по умолчанию.
    Чтобы вы не тратили время на именование новых constraints или на воспоминания/поиски того, как назван constraint, который вы собираетесь удалить,
    SQLAlchemy предлагает использовать шаблоны именования naming conventions
    Их можно определить в реестре
    MetaData.
    Создаем реестр MetaData и передаем в него шаблоны именования
    # analyzer/db/schema.py
    from
    sqlalchemy
    import
    MetaData convention = {
    'all_column_names'
    :
    lambda
    constraint, table:
    '_'
    .join([ column.name
    for
    column
    in
    constraint.columns.values()
    ]),
    # Именование индексов 'ix'
    :
    'ix__%(table_name)s__%(all_column_names)s'
    ,
    # Именование уникальных индексов

    'uq'
    :
    'uq__%(table_name)s__%(all_column_names)s'
    ,
    # Именование CHECK-constraint-ов 'ck'
    :
    'ck__%(table_name)s__%(constraint_name)s'
    ,
    # Именование внешних ключей 'fk'
    :
    'fk__%(table_name)s__%(all_column_names)s__%(referred_table_name)s'
    ,
    # Именование первичных ключей 'pk'
    :
    'pk__%(table_name)s'
    } metadata = MetaData(naming_convention=convention)
    Если указать шаблоны именования, Alembic воспользуется ими во время автоматической генерации миграций и будет называть все constraints в соответствии с ними. В дальнейшем cозданный реестр
    MetaData потребуется для описания таблиц:
    Описываем схему базы данных объектами SQLAlchemy
    # analyzer/db/schema.py
    from
    enum
    import
    Enum, unique
    from
    sqlalchemy
    import
    (
    Column, Date, Enum
    as
    PgEnum, ForeignKey, ForeignKeyConstraint, Integer,
    String, Table
    )
    @unique
    class
    Gender
    (Enum)
    : female =
    'female'
    male =
    'male'
    imports_table = Table(
    'imports'
    , metadata,
    Column(
    'import_id'
    , Integer, primary_key=
    True
    )
    ) citizens_table = Table(
    'citizens'
    , metadata,
    Column(
    'import_id'
    , Integer, ForeignKey(
    'imports.import_id'
    ), primary_key=
    True
    ),
    Column(
    'citizen_id'
    , Integer, primary_key=
    True
    ),
    Column(
    'town'
    , String, nullable=
    False
    , index=
    True
    ),
    Column(
    'street'
    , String, nullable=
    False
    ),
    Column(
    'building'
    , String, nullable=
    False
    ),
    Column(
    'apartment'
    , Integer, nullable=
    False
    ),
    Column(
    'name'
    , String, nullable=
    False
    ),
    Column(
    'birth_date'
    , Date, nullable=
    False
    ),
    Column(
    'gender'
    , PgEnum(Gender, name=
    'gender'
    ), nullable=
    False
    ),
    ) relations_table = Table(
    'relations'
    , metadata,
    Column(
    'import_id'
    , Integer, primary_key=
    True
    ),

    Column(
    'citizen_id'
    , Integer, primary_key=
    True
    ),
    Column(
    'relative_id'
    , Integer, primary_key=
    True
    ),
    ForeignKeyConstraint(
    (
    'import_id'
    ,
    'citizen_id'
    ),
    (
    'citizens.import_id'
    ,
    'citizens.citizen_id'
    )
    ),
    ForeignKeyConstraint(
    (
    'import_id'
    ,
    'relative_id'
    ),
    (
    'citizens.import_id'
    ,
    'citizens.citizen_id'
    )
    ),
    )
      1   2   3   4   5   6


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