Skip to content

Release: 2.6

Choose a tag to compare

@AlanLatte AlanLatte released this 08 Sep 00:38
· 2 commits to main since this release
2a3fe80

Метрики

Удалил Tempo + Loki

Зачастую, когда логов в stdout становится слишком много, trace_id не успевает собираться из-за чего были кейсы, когда docker контейнер отваливался и становился «зомби». Его не возможно было удалить, убить, остановить. Временно удалил до лучших времен

Core функции

Single Postgres Pool

Долгожданная фича для ускорения репозиториев.
Ранее коннектор работал по принципу создания пула коннекторов для каждого контекстного запроса. Это было отказоустойчиво, но не эффективно, т.к. объект коннектора к базе данных являлся фабрикой.
Грубо говоря, на каждый http запрос, приложение заново подключалось к базе данных. Плюсы асинхронности тут полностью пропадали и терялась пропускная IO bound способность API к базе данных. В качестве контр-мер был использован DI, а именно один из его провайдеров Resource.
Был создан класс BaseAsyncResource который позволит имплементировать кастомные ресурсы.

Как пользоваться?

В случае, когда нам необходимо создать просто коннектор, который закроется с завершением программы, мы можем просто

from app.pkg.connectors import Connectors

class Container(containers.DeclarativeContainer):
    connector: Connectors = providers.Container(Connectors)

@inject
async def closed_pool(
    psql: Pool = Provide[Container.connector.postgresql.connector],
):
    async with acquire_connection(pool=psql) as cur:
    await cur.execute("SELECT '1'")
    print(await cur.fetchone())

В случае, когда нужно, что бы коннектор открывался только в пределах исполнения функции (а-ля декоратор), нужно использовать Closing

from app.pkg.connectors import Connectors

class Container(containers.DeclarativeContainer):
    connector: Connectors = providers.Container(Connectors)

@inject
async def closed_pool(
    psql: Pool = Closing[Provide[Container.connector.postgresql.connector]],
):
    async with acquire_connection(pool=psql) as cur:
    await cur.execute("SELECT '1'")
    print(await cur.fetchone())

Добавил поддержку min max открытых подключений к бд

в .env добавил две переменные:

MIN_CONNECTION = 
MAX_CONNECTION =

Немного бенчмарков

Так было до
image

Так после
image

Добавил валидацию значений на уровне базы данных.

База должна непременно валидировать данные которые она получает.
В случае с API, строится система формата:

Fastapi -> Router -> Service -> Reposiotry

Где на каждом этапе, часть данных фильтруется/отсеивается. Но в случае, когда к БД можно подключиться и подредачить какие-то данные, валидации не пройдут. Поэтому важно использовать весь функционал валидации postgresql

Поддержка JST

В метод migrate (app.pkg.models.base.model:BaseModel) добавил поддержку JST

JST - легкий модуль который позволяет по описанной JSON схеме формировать случайные данные.

Разбил settings на подгруппы

После 6 месяцев использования шаблона, столкнулся с проблемой: Настроек в сервисе стало СЛИШКОМ много для их постоянной поддержки.

Из-за чего, я принял решение, разбить базовый класс настроек и сгруппировать его по частям на логические части.

Если раньше, все настройки хранились одним полотном в объекте, то сейчас они разбиты:

class Postgresql(_Settings):
    """Postgresql settings."""

    #: str: Postgresql host.
    HOST: str = "localhost"
    #: PositiveInt: positive int (x > 0) port of postgresql.
    PORT: PositiveInt = 5432
    #: str: Postgresql user.
    USER: str = "postgres"
    #: SecretStr: Postgresql password.
    PASSWORD: SecretStr = "postgres"
    #: str: Postgresql database name.
    DATABASE_NAME: str = "postgres"

...

class Settings(_Settings):
    """Server settings.

    Formed from `.env` or `.env.dev` if server running with parameter `dev`.
    """

    #: Postgresql: Postgresql settings.
    POSTGRES: Postgresql

    #: Redis: Redis settings.
    REDIS: Redis

    #: Centrifugo: Centrifugo settings.
    CENTRIFUGO: Centrifugo

    #: RabbitMQ: RabbitMQ settings.
    RABBITMQ: RabbitMQ

!ВАЖНО!
Что бы каждый объект настроек был дочерним классом класса app.pkg.settings.settings:_Settings.


При такой поддержке, все атрибуты разбиты по своим объектам и не мешают друг другу существовать, так же, в .env файле (либо в виртуальном окружении / docker secrets / etc.) произошло логическое разделение благодаря магическому и волшебному аргументу в pydantic: env_nested_delimiter. (читай тут)

Я засунул его в базовый класс настроек: _Settings.Config.
Т.е. теперь, для формирования какой-то настройки достаточно разделить их с помощью __ (см.коммит):

# . API
API__INSTANCE_APP_NAME="template-api"

# .. Server
API__PORT=5000

# .. Logger
API__LOGGER__LEVEL=debug
API__LOGGER__FILE_PATH=./src/logs

В случае, если нам нужно использовать настройку, но она не служит логикой для сервиса, не нужно указывать разделитель. Я не использую данные о директории монтирования данных, но хочу понимать к какому сервису они относятся. Поэтому я задаю название: POSTGRES_DATA_VOLUME (!) тут одно нижнее подчеркивание


Задел на будущее

В директорию app.pkg.models.base.exceptions создал директорию association - которая будет отвечать за формирование ассоциаций с psycopg2 кодами ошибок постгри (смотри полный список ошибок тут). (Что бы вынести сюда все ассоциации из app.internal.repository.postgresql.handlers.handle_exaptions::handle_exaptions)

Документация

Описал документацию для бОльшей части функций.

К примеру, декоратор @collect_response и @handle_exception описал более детально. Добавил в основные, сложные функции описанную документацию с блоками кода. Задокументировал app.internal.repository.repository:Repository

Примеры с кодом

В некоторых местах начал явно указывать >>> для подчеркивания кода на питоне

Переделал 30% документации под google spec docstrings.

Читай более подробно тут: ссылка

Тесты

Добавил поддержку маркера repeat

Добавил поддержку pytest-repeat = "^0.9.1" в группу tests для цикличного прогона тестов.
Да, можно использовать маркер parametrize, но он требует явного указания аргумента в функцию. Что делать сильно не хочется (учитывая, что это просто репитер)

Модуль работает по принципу повторения тестов при установки маркера repeat(X), где X - кол-во повторений

@pytest.mark.repeat(3)
async def test_some_function():
    assert random.randint(0,5) > -1

После указания маркера, в итоговом варианте отчета, тесты будут под нумерацией маркера

test_some_function [0-2] .
test_some_function [1-2] .
test_some_function [2-2] .

Переписал fixture для формирования шаблонных ответов

Появился модуль для более масштабируемого использования моделей.
Работа в сервисах (при изменении бизнес-логики) замыкаю на процессе изменения модели. Т.е.:

Когда меняется модель данных подающихся на вход сервису, тесты не должны быть сильно связанны с этими изменениями. Самое важное - что сама БЛ отрабатывает

Поэтому, был создан модуль tests.fixtures.models.controller:create_model LN: [12-0]
Который отвечает за логику создания модели и заполнения ее данными.
Эта фикстура принимает на вход 2 аргумента:

  • model - Объект целевой модели (НЕ ИНСТАНС)
  • **kwargs - Не обязательные аргументы для явного заполнения в модель (к примеру, когда нужен элемент детерминированности некоторых атрибутов)

Пример использования:

  1. Без использования аргументов с детерминированным исходом:
async def test_correct(create_model, user_repository: UserRepository):
    cmd = await create_model(models.CreateUserCommand)
    user = await user_repository.create(cmd=cmd)
    assert user.username == cmd.username
  1. С использованием атрибутов для детерминированного исхода:
async def test_correct(create_model, user_repository: UserRepository):
    cmd = await create_model(
        models.CreateUserCommand,
        username="test@example.ru"
    )
    user = await user_repository.create(cmd=cmd)
    assert user.username == cmd.username
    assert cmd.username == "test@example.ru"

Важно помнить, что функция использует СЛУЧАЙНЫЕ данные для записи в модель.

Ниже указал примерно как и с какими типами он работает.

Родительский тип Использует метод
int random.randint
float random.random
bool random.randint(0,1)
string lorem
list[type] iterator + тип
obj obj

Напоминаю, что к примеру: SecretString - дочерний класс str

Безопасность.

Обновил зависимости

pycrypto ^=2.6.1 [35015] -|- CVE-2013-7459 -+- Удалил

Heap-based buffer overflow in the ALGnew function in block_templace.c in Python Cryptography Toolkit (aka pycrypto) 2.6.1 allows remote attackers to execute arbitrary code as demonstrated by a crafted iv parameter to cryptmsg.py.

starlette >=0.13.5,<0.27.0 -|- CVE-2023-29159 или тык -+- Обновил

Starlette 0.27.0 fixes a vulnerability: Path traversal vulnerability in StaticFiles.

starlette < 0.25.0 -|- CVE-2023-30798 -+- Обновил

The MultipartParser usage in Encode's Starlette python framework before versions 0.25.0 allows an unauthenticated and remote attacker to specify any number of form fields or files which can cause excessive memory usage resulting in denial of service of the HTTP service.

Добавил проверку на уязвимости с помощью SonarQube

Теперь после каждого коммита и пуша в реп, запускается удалённая процедура сканирования репозитория/pull request на уязвимости. Смотри отчёт тут

Ci/Cd

Удалил тестирование

Сборка github actions с тестами оказалась не стабильной. Из-за чего постоянно откисал сервак. Удалил до лучших времен (см.коммит)