В этой статье вы узнаете, как:
Поднять PostgreSQL и подготовить базу данных.
Настроить тесты с Pytest и внедрить подход TDD.
Создать модели данных и миграции с SQLAlchemy и Alembic.
Реализовать CRUD-операции с помощью Litestar.
Проверить работу API с использованием curl.
Что вас ждёт:
Если в первой части мы заложили фундамент проекта (выбор инструментов, настройка окружения и структура), то здесь мы превратим этот каркас в полноценное API для управления резюме кошек (или людей — как вам ближе). Мы подключим базу данных, добавим тесты, настроим миграции и даже проверим всё в действии. К концу статьи у вас будет рабочее API, которое можно потрогать руками (или лапками 🐾). Полный код доступен на GitHub — ссылка в конце!
Вступление: с чего начнём?
В первой части мы остановились на базовой структуре проекта и настройке зависимостей.
А именно:
Выбрали инструменты — Litestar вместо Fastapi, Granian вместо Uvicorn, KeyDB вместо Redis.
Настроили окружение с помощью uv.
Создали структуру проекта.
Добавили Makefile для удобства работы.
Теперь мы готовы двигаться дальше и строить “стены” нашего приложения. Сегодня мы превратим заготовку в рабочее API с CRUD-операциями, подключим базу через SQLAlchemy, настроим миграции с Alembic и напишем первые тесты с Pytest. Поехали! 🚀
Подготовка PostgreSQL

Если у вас уже есть PostgreSQL, просто создайте новую базу данных. Если нет — давайте поднимем его в Docker. Это быстро и удобно!
🚀 Шаг 1: Создание docker-compose файла
Создаём файл:
nvim docker-compose.yaml
И вставляем туда этот код:
services:
postgres:
image: postgres
container_name: postgres
hostname: postgres
ports:
- 5432:5432
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: ltcats_test_db
volumes:
- "postgres-data:/var/lib/postgesql"
volumes:
postgres-data:
Запускаем контейнер:
docker compose up -d
Проверяем, что всё работает:
docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
Ожидаемый вывод:
CONTAINER ID NAMES IMAGE
STATUS PORTS
2099ccd4d2dd postgres postgres
Up 22 seconds 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp
🧪 Шаг 2: Проверка подключения
Убедимся, что база доступна. Используйте psql:
psql -h localhost -p 5432 -U postgres -d ltcats_test_db
Или, если у вас есть pgcli:
pgcli -h localhost -p 5432 -U postgres -d ltcats_test_db
📦 Шаг 3: Добавляем зависимости
База готова, пора подключить её к проекту. Добавляем библиотеки через uv:
uv add asyncpg litestar-asyncpg
✅ PostgreSQL готов к работе!
Настройка тестов с Pytest

Начинаем с тестов, это наш первый шаг к надёжному коду. Мы будем использовать подход TDD (Test-Driven Development), чтобы сразу проверять, что всё работает как надо.
🧪 Шаг 1: Установка зависимостей
Добавляем нужные пакеты:
uv add pytest pytest-asyncio sqlalchemy advanced-alchemy
📂 Шаг 2: Создание структуры для тестов
Создаём файлы в папке src/tests:
mkdir -p src/tests && touch src/tests/__init__.py src/tests/conftest.py src/tests/test_users.py
Теперь структура проекта выглядит так:
.
├── Makefile
├── README.md
├── docker-compose.yaml
├── pyproject.toml
├── src
│ ├── app.py
│ └── tests
│ ├── __init__.py,
│ ├── conftest.py,
│ └── test_users.py
└── uv.lock
⚙️ Шаг 3: Настройка Pytest
В pyproject.toml добавляем конфигурацию:
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
🛠️ Шаг 4: Фикстуры в conftest.py
Файл conftest.py нужен для:
Хранения общих фикстур, которые используются в тестах. Фикстуры — это функции, которые выполняют настройку и очистку данных для тестов.
Избегания повторяющегося кода: Помогает избегать дублирования кода в тестах. Если есть функции или данные, которые используются в разных тестах, их можно вынести в conftest.py.
Упрощения тестов: Сделать тесты более читаемыми и простыми, вынося общую логику в отдельный файл.
Вот его содержимое:
import asyncio
import pytest_asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from postgres.models.base import Base
# Настройка тестовой БД
TEST_DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost/ltcats_test_db"
# Создаем новый событийный цикл, который будет использоваться для запуска асинхронных тестов.
@pytest_asyncio.fixture(scope="session") # Параметр scope="session" означает, что событийный цикл будет создан один раз на всю сессию тестирования, а не для каждого теста отдельно
def event_loop():
loop = asyncio.get_event_loop()
yield loop
# Фикстура для движка базы данных
@pytest_asyncio.fixture(scope="session")
async def db_engine():
# Создает движок с подключением к тестовой базе данных
engine = create_async_engine(TEST_DATABASE_URL, echo=True) # Параметр echo=True включает вывод SQL-запросов в консоль для отладки.
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all) #Создает все таблицы в базе данных с помощью Base.metadata.create_all.
yield engine # Выдает движок для использования в тестах.
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all) # После завершения тестов удаляет все таблицы (Base.metadata.drop_all)
await engine.dispose() # и освобождает ресурсы движка (engine.dispose()).
# Фикстура для сессии базы данных
@pytest_asyncio.fixture
async def db_session(db_engine) -> AsyncSession:
async with AsyncSession(db_engine) as session: # Создает новую сессию базы данных.
yield session # Выдает сессию для использования в тестах.
await session.rollback() # После завершения теста откатывает все изменения (session.rollback()), чтобы база данных оставалась в исходном состоянии.
✍️ Шаг 5: Пишем первый тест
В test_users.py добавляем тест для создания пользователя
Далее перейдём в test_users.py и так как в нашем приложении будут пользователи, то первым делом мы займёмся ими.
Определим:
Как будет выглядеть структура создания пользователя.
Как будет инициализироваться модель.
Добавим пользователя в базу.
Достанем его из базы и проверим, сохранились ли данные.
Вперёд, напишем первый тест:
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from models.users import UserCreate
from postgres.models.users import User
from msgspec import structs
# Маркируем тест как асинхронный
@pytest.mark.asyncio
async def test_create_user(db_session: AsyncSession):
# Создаем данные для нового пользователя (котика)
user_data = UserCreate(
first_name="Васька", # Имя котика
last_name="Мурзиков", # Фамилия котика
email="vasyamurzikov@whiskers.com", # Почта котика
)
# Создаем объект пользователя из данных
user = User(**structs.asdict(user_data))
# Добавляем пользователя в сессию базы данных
db_session.add(user)
# Сохраняем изменения в базе данных
await db_session.commit()
# Выполняем запрос для получения пользователя по email
result = await db_session.execute(
select(User).where(User.email == "vasyamurzikov@whiskers.com")
)
# Получаем пользователя из результата запроса
fetched_user = result.scalar_one()
# Проверяем, что имя и email пользователя соответствуют ожидаемым значениям
assert fetched_user.first_name == "Васька"
assert fetched_user.email == "vasyamurzikov@whiskers.com"
🚀 Шаг 6: Запускаем тесты
Запускаем командой:
uv run pytest
Или добавляем в Makefile:
test:
uv run pytest
И запускаем через make:
make test
После чего, закономерно узнаем, что наш тест упал:
ERROR src/tests - ModuleNotFoundError: No module named 'postgres'
Тест падает с ошибкой ModuleNotFoundError, значит, мы забыли создать модели. Переходим к следующему шагу!
Модели данных и миграции

Теперь определим модели и настроим миграции, чтобы база данных соответствовала нашему коду.
📂 Шаг 1: Создание файлов моделей
Создаём структуру:
mkdir -p src/models src/postgres/models && \
touch src/models/__init__.py src/postgres/__init__.py src/postgres/models/__init__.py \
src/models/users.py src/postgres/models/users.py
Проверяем, структуру и файлы:
src
├── models
│ ├── __init__.py
│ └── users.py
├── postgres
│ ├── __init__.py
│ └── models
│ ├── __init__.py
│ └── users.py
Переходим к следующему шагу.
Определяем модели с использованием SQLAlchemy:
📌 Примечание: Тут надо сделать небольшую сноску, я планирую использовать в проекте контроллер и репозиторий, поэтому классы буду наследовать от base.UUIDAuditBase и возможно от base.UUIDBase, для этого сценария подходят только они. Я потратил некоторое количество времени, чтобы убедиться в этом, а если хотите пойти другим путём, то в целом можете пользоваться любыми другими.
И ещё при наследовании от base.UUIDBase в таблице автоматически добавится поле id с uuid’ом, а если наследоваться от base.UUIDAuditBase, то помимо id добавятся такие поля, как created_at и updated_at, надо иметь это ввиду.
📋 Шаг 2: Модель User
В src/postgres/models/users.py добавляем:
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from litestar.plugins.sqlalchemy import base
class User(base.UUIDAuditBase):
__tablename__ = "users"
first_name: Mapped[str] = mapped_column(String(50), nullable=False)
last_name: Mapped[str] = mapped_column(String(50), nullable=True)
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
📦 Шаг 3: Добавляем msgspec
Здесь пора добавить в проект msgspec:
uv add msgspec
✍️ Шаг 4: Структура UserCreate
И обновим файл src/models/users.py:
from msgspec import Struct
class UserCreate(Struct, kw_only=True, omit_defaults=True):
first_name: str
last_name: str | None = None
email: str
Эта структура, в последствии, понадобиться для удобства и валидации входных данных.
Параметр kw_only=True в определении структуры (msgspec.Struct), означает, что все поля структуры должны передаваться, как ключевые аргументы, при создании экземпляра класса.
✅ Шаг 5: Проверяем тест
Запускаем make test. Теперь тест должен пройти успешно!
Ожидаемый вывод:
❯ uv run pytest
======================================================================= test session starts =======================================================================
platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0
rootdir: /home/user/uprojectfolder/litestarcatscv
configfile: pyproject.toml
plugins: Faker-35.2.0, anyio-4.8.0, asyncio-0.25.3
asyncio: mode=Mode.STRICT, asyncio_default_fixture_loop_scope=function
collected 1 item
src/tests/test_users.py . [100%]
======================================================================== 1 passed in 0.24s ========================================================================
Настройка миграций с Alembic

Пора синхронизировать наши модели с базой данных через миграции.
🔧 Шаг 1: Установка Alembic
Добавляем в проект alembic:
uv add alembic
⚙️ Шаг 2: Инициализация Alembic
📌 Примечание: В версии SQLAlchemy 1.4 появилась экспериментальная поддержка asyncio, позволяющая использовать большую часть интерфейса в асинхронных приложениях. Alembic в настоящее время не предоставляет асинхронный API напрямую, но может использовать движок SQLAlchemy Async для запуска миграций и автоматической генерации.
Новые конфигурации могут использовать шаблон -t «async» для запуска среды, которую можно использовать с асинхронным DBAPI, например asyncpg, выполнив команду:
Создаём структуру для миграций:
alembic init -t async src/postgres/alembic/
📂 Шаг 3: Переносим конфигурацию
Переносим alembic.ini в папку src/configs:
mkdir -p src/configs && touch src/configs/__init__.py
mv alembic.ini src/configs/
⚙️ Шаг 4: Настраиваем alembic.ini
Обновляем файл:
script_location = src/postgres/alembic
version_locations = src/postgres/alembic/versions
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost/ltcats_test_db
🛠️ Шаг 5: Настраиваем env.py
В src/postgres/alembic/env.py добавляем:
import importlib
import os
from src.postgres.models.base import Base
# Настройка конфигурации
config = context.config
config_file = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'configs', 'alembic.ini'))
fileConfig(config_file)
# Динамически импортируем все модели из src/models/
models_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'models'))
for filename in os.listdir(models_dir):
if filename.endswith('.py') and filename not in ['__init__.py', 'base.py']:
module_name = f"src.postgres.models.{filename[:-3]}"
importlib.import_module(module_name)
#Добавляем Метаданные модели
target_metadata = Base.metadata
🚀 Шаг 6: Создаём первую миграцию
Генерируем миграцию:
alembic -c src/configs/alembic.ini revision -m "create user table" --autogenerate
Ожидаемый вывод:
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'users'
INFO [alembic.autogenerate.compare] Detected added index ''ix_users_email'' on '('email',)'
Generating /home/user/uprojects/litestarcatscv/src/postgres/alembic/versions/9047a811291c_create_user_table.py ... done
А в папке src/postgres/alembic/versions, появиться новый файл миграции со следующим содержимым:
"""create user table
Revision ID: 2d7f1d2c604d
Revises:
Create Date: YYYY-MM-DD
"""
import advanced_alchemy
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '2d7f1d2c604d'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('first_name', sa.String(length=50), nullable=False),
sa.Column('last_name', sa.String(length=50), nullable=True),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True),
sa.Column('created_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
sa.Column('updated_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_users'))
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')
# ### end Alembic commands ###
✅ Шаг 7: Применяем миграцию
Применяем изменения:
alembic -c src/configs/alembic.ini upgrade head
Вывод:
Module -- src.postgres.models.users
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 9047a811291c, create user table
📋 Шаг 8: Обновляем Makefile
Добавляем удобные команды, чтобы было проще работать с alembic:
ALEMBIC_CONFIG = src/configs/alembic.ini
check-alembic:
@command -v alembic >/dev/null 2>&1 || { echo "Alembic is not installed. Run 'make install'."; exit 1; }
revision: check-alembic
alembic -c $(ALEMBIC_CONFIG) revision -m '$(msg)' --autogenerate
upgrade: check-alembic
alembic -c $(ALEMBIC_CONFIG) upgrade head
downgrade: check-alembic
alembic -c $(ALEMBIC_CONFIG) downgrade -1
Теперь миграции запускаются просто:
make revision msg="create tables"
make upgrade
make downgrade
Реализация API с Litestar

Пришло время создать CRUD-эндпоинты для работы с пользователями.
🌐 Шаг 1: Создаём контроллер
Для эндпоинтов будем использовать контроллеры.
Для этого создадим директорию controllers и добавим туда файлы:
mkdir -p src/controllers && touch src/controllers/__init__.py src/controllers/users.py
✍️ Шаг 2: Код контроллера
В src/controllers/users.py:
# Импортируем необходимые модули из Litestar для создания контроллера и обработки HTTP-запросов
from litestar import Controller, get, post, patch, delete
# Импортируем Provide для работы с зависимостями
from litestar.di import Provide
# Импортируем асинхронную сессию SQLAlchemy для работы с базой данных
from sqlalchemy.ext.asyncio import AsyncSession
# Импортируем схемы данных для создания и обновления пользователей
from src.models.users import UserCreate, UserPatch
# Импортируем модель пользователя для работы с базой данных
from src.postgres.models.users import User
# Импортируем утилиты msgspec для преобразования данных
from msgspec import structs, to_builtins
# Импортируем пагинацию из Litestar для вывода списков с ограничением и смещением
from litestar.pagination import OffsetPagination
# Импортируем фильтры и репозиторий из плагина SQLAlchemy для Litestar
from litestar.plugins.sqlalchemy import (
filters,
repository,
)
# Импортируем тип UUID для работы с уникальными идентификаторами
from uuid import UUID
# Определяем класс репозитория для работы с пользователями в базе данных
class UsersRepository(repository.SQLAlchemyAsyncRepository[User]):
"""Репозиторий для работы с моделью пользователей в базе данных."""
model_type = User # Указываем, что репозиторий работает с моделью User
# Функция-зависимость для предоставления экземпляра репозитория
async def provide_users_repo(db_session: AsyncSession) -> UsersRepository:
"""Создает и возвращает экземпляр UsersRepository с переданной сессией базы данных."""
return UsersRepository(session=db_session)
# Определяем контроллер для управления пользователями
class UserController(Controller):
path = "/users" # Базовый путь для всех маршрутов контроллера (например, /users/)
dependencies = {"users_repo": Provide(provide_users_repo)} # Зависимость репозитория пользователей
# Метод для получения списка пользователей с пагинацией
@get(path="/")
async def list_users(
self,
users_repo: UsersRepository, # Репозиторий для доступа к данным
limit_offset: filters.LimitOffset, # Параметры пагинации (ограничение и смещение)
) -> OffsetPagination[User]:
"""Возвращает список пользователей с пагинацией."""
# Получаем список пользователей и их общее количество из репозитория
results, total = await users_repo.list_and_count(limit_offset)
# Возвращаем объект пагинации с результатами
return OffsetPagination[User](
items=results, # Список пользователей
total=total, # Общее количество пользователей
limit=limit_offset.limit, # Лимит записей на странице
offset=limit_offset.offset, # Смещение (сколько записей пропущено)
)
# Метод для создания нового пользователя
@post("/")
async def create_user(
self,
data: UserCreate, # Данные для создания пользователя (схема UserCreate)
users_repo: UsersRepository # Репозиторий для работы с базой
) -> User:
"""Создает нового пользователя в базе данных."""
# Преобразуем данные из схемы в словарь и создаем объект User
user = await users_repo.add(User(**structs.asdict(data)))
# Фиксируем изменения в базе данных
await users_repo.session.commit()
return user # Возвращаем созданного пользователя
# Метод для получения пользователя по его UUID
@get("/{user_id:uuid}")
async def get_user(
self,
user_id: UUID, # Уникальный идентификатор пользователя
users_repo: UsersRepository # Репозиторий для доступа к данным
) -> User:
"""Возвращает данные пользователя по его UUID."""
# Получаем пользователя из репозитория по идентификатору
user = await users_repo.get(user_id)
return user # Возвращаем найденного пользователя
# Метод для частичного обновления пользователя
@patch("/{user_id:uuid}")
async def update_user(
self,
user_id: UUID, # Уникальный идентификатор пользователя
data: UserPatch, # Данные для обновления (схема UserPatch)
users_repo: UsersRepository # Репозиторий для работы с базой
) -> User:
"""Обновляет данные существующего пользователя."""
# Преобразуем данные из схемы в словарь
raw_obj = to_builtins(data)
# Добавляем идентификатор пользователя в данные
raw_obj["id"] = user_id
# Создаем объект User с обновленными данными
user = User(**raw_obj)
# Обновляем пользователя в репозитории
updated_user = await users_repo.update(user)
# Фиксируем изменения в базе данных
await users_repo.session.commit()
return updated_user # Возвращаем обновленного пользователя
# Метод для удаления пользователя
@delete("/{user_id:uuid}")
async def delete_user(
self,
user_id: UUID, # Уникальный идентификатор пользователя
users_repo: UsersRepository # Репозиторий для работы с базой
) -> None:
"""Удаляет пользователя из базы данных по его UUID."""
# Удаляем пользователя из репозитория по идентификатору
await users_repo.delete(user_id)
# Фиксируем изменения в базе данных
await users_repo.session.commit()
⚙️ Шаг 3: Обновляем app.py
В src/app.py:
# Импортируем необходимые модули из Litestar для создания приложения и работы с зависимостями
from litestar import Litestar
from litestar.di import Provide
# Импортируем контроллер пользователей из нашего проекта
from src.controllers.users import UserController
# Импортируем модули для асинхронной работы с базой данных через SQLAlchemy
from litestar.contrib.sqlalchemy.plugins import (
AsyncSessionConfig, # Конфигурация асинхронной сессии
SQLAlchemyAsyncConfig, # Основная конфигурация SQLAlchemy
SQLAlchemyInitPlugin, # Плагин для инициализации SQLAlchemy
)
# Импортируем Parameter для задания параметров запросов
from litestar.params import Parameter
# Импортируем фильтры и базовый класс моделей из плагина SQLAlchemy
from litestar.plugins.sqlalchemy import filters, base
# Импортируем плагин для сериализации моделей SQLAlchemy в JSON
from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin
# Настройка базы данных
# Определяем строку подключения к базе данных PostgreSQL с использованием драйвера asyncpg
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost/ltcats_test_db"
# Функция для предоставления пагинации в запросах
async def provide_limit_offset_pagination(
current_page: int = Parameter(
ge=1, # Значение должно быть больше или равно 1
query="currentPage", # Имя параметра в запросе
default=1, # Значение по умолчанию — первая страница
required=False, # Параметр необязательный
),
page_size: int = Parameter(
query="pageSize", # Имя параметра в запросе
ge=1, # Значение должно быть больше или равно 1
default=10, # По умолчанию 10 элементов на странице
required=False, # Параметр необязательный
),
) -> filters.LimitOffset:
"""
Предоставляет параметры пагинации для запросов к базе данных.
Возвращает объект LimitOffset, который задает лимит и смещение для выборки данных.
- current_page: номер текущей страницы.
- page_size: количество элементов на странице.
"""
return filters.LimitOffset(page_size, page_size * (current_page - 1))
# Настройка асинхронной сессии SQLAlchemy
# expire_on_commit=False предотвращает истечение объектов после коммита транзакции
session_config = AsyncSessionConfig(expire_on_commit=False)
# Конфигурация подключения SQLAlchemy к базе данных
db_config = SQLAlchemyAsyncConfig(
connection_string=DATABASE_URL, # Строка подключения к базе данных
before_send_handler="autocommit", # Автоматический коммит после выполнения запросов
session_config=session_config, # Передаем конфигурацию сессии
)
# Функция инициализации базы данных при запуске приложения
async def on_startup() -> None:
"""
Инициализирует базу данных при старте приложения.
Создает все таблицы, определенные в метаданных моделей (UUIDBase).
"""
async with db_config.get_engine().begin() as conn:
await conn.run_sync(base.UUIDBase.metadata.create_all)
# Инициализация плагина SQLAlchemy для интеграции с Litestar
sqlalchemy_plugin = SQLAlchemyInitPlugin(config=db_config)
# Создание экземпляра приложения Litestar
app = Litestar(
route_handlers=[UserController], # Подключаем контроллер пользователей
on_startup=[on_startup], # Выполняем функцию инициализации при запуске
dependencies={"limit_offset": Provide(provide_limit_offset_pagination)}, # Зависимость пагинации
plugins=[sqlalchemy_plugin, SQLAlchemySerializationPlugin()], # Подключаем плагины
)
📌 Примечание: В комментариях, подробно указано, что за что отвечает. Если в кратце, то здесь мы обозначили что для подключения к БД будем использовать sqlalchemy, с помощью плагина SQLAlchemyInitPlugin, а с помощью другого плагина SQLAlchemySerializationPlugin будем сериализовать модели SQLAlchemy в JSON. А также добавлена в зависимости пагинация. Сможем использовать её , например для получения списка пользователей.
Остальные модели и контроллеры я добавлю по той же схеме. А вы их можете просто скопировать или попробовать добавить самостоятельно.
Запуск и проверка

🎉 Шаг 1: Запускаем приложение
Запускаем:
make run
Ожидаемый вывод:
❯ make run
uv run granian --interface asgi src/app:app
[INFO] Starting granian (main PID: 485148)
[INFO] Listening at: http://127.0.0.1:8000
[INFO] Spawning worker-1 with pid: 485149
[INFO] Started worker-1
[INFO] Started worker-1 runtime-1
🧪 Шаг 2: Проверяем эндпоинты с curl
Создание пользователя (POST /users):
curl -X POST http://localhost:8000/users -H "Content-Type: application/json" -d '{"first_name": "Мурзик", "last_name": "Котов", "email": "murzik@example.com"}'
curl -X POST http://localhost:8000/users -H "Content-Type: application/json" -d '{"first_name": "Барсик", "last_name": "Пушистый", "email": "barsik@example.com"}'
curl -X POST http://localhost:8000/users -H "Content-Type: application/json" -d '{"first_name": "Васька", "last_name": "Лапкин", "email": "vaska@example.com"}'
Ожидаемый ответ:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"first_name": "Мурзик",
"last_name": "Котов",
"email": "murzik@example.com",
"created_at": "2023-10-01T12:00:00Z",
"updated_at": "2023-10-01T12:00:00Z"
}
Получение списка пользователей (GET /users):
curl -X GET "http://localhost:8000/users?limit=2&offset=0"
Ожидаемый ответ:
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"first_name": "Мурзик",
"last_name": "Котов",
"email": "murzik@example.com",
"created_at": "2023-10-01T12:00:00Z",
"updated_at": "2023-10-01T12:00:00Z"
},
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"first_name": "Барсик",
"last_name": "Пушистый",
"email": "barsik@example.com",
"created_at": "2023-10-01T12:05:00Z",
"updated_at": "2023-10-01T12:05:00Z"
}
],
"total": 3,
"limit": 2,
"offset": 0
}
Получение пользователя по UUID (GET /users/{user_id:uuid}):
curl -X GET http://localhost:8000/users/123e4567-e89b-12d3-a456-426614174000
Замените 123e4567-e89b-12d3-a456-426614174000, на реальный UUID пользователя, например, из ответа на создание “Мурзика”.
Ожидаемый ответ:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"first_name": "Мурзик",
"last_name": "Котов",
"email": "murzik@example.com",
"created_at": "2023-10-01T12:00:00Z",
"updated_at": "2023-10-01T12:00:00Z"
}
Обновление пользователя (PATCH /users/{user_id:uuid})
curl -X PATCH http://localhost:8000/users/550e8400-e29b-41d4-a716-446655440000 \
-H "Content-Type: application/json" \
-d '{"first_name": "Мурзик Updated"}'
Ожидаемый ответ:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"first_name": "Мурзик Updated",
"last_name": "Котов",
"email": "murzik@example.com",
"created_at": "2023-10-01T12:00:00Z",
"updated_at": "2023-10-01T12:10:00Z"
}
Удаление пользователя (DELETE /users/{user_id:uuid}):
curl -X DELETE http://localhost:8000/users/123e4567-e89b-12d3-a456-426614174000
Ожидаемый ответ:
Код состояния: 204 No Content
Тело ответа отсутствует.
Итоги

🐾 Поздравляю! База готова. Мы отлично потрудились!
Что мы сделали:
✅ Подготовили PostgreSQL в Docker.
✅ Настроили тесты с Pytest и TDD.
✅ Создали модели данных и миграции с Alembic.
✅ Реализовали CRUD-операции с Litestar.
✅ Проверили работу API через curl.
Дальше будет интереснее. Нарастим мышцы нашему приложению.
Ссылка на GitHub: https://github.com/pulichkin/litestarcats.git
Читать 1-ую часть.