Это финальная самостоятельная работа курса. Вы реализуете REST API для платформы управления событиями — от проектирования схемы до работающего приложения.
В отличие от Модуля 2 где вы работали с raw sqlite3, здесь используется полный стек который вы освоили в Модулях 3 и 4: FastAPI + SQLAlchemy ORM + Alembic + Pydantic.
Работа разбита на шаги в том же порядке что и финальный проект (Уроки 26-28). Если на каком-то шаге возникли вопросы — обращайтесь к соответствующему уроку. Каждый шаг указывает на нужный источник.
Платформа для организации и посещения мероприятий. Организаторы создают события (митапы, концерты, вебинары), участники на них регистрируются. События можно тегировать и фильтровать.
(Урок 26 — Проектирование)
users — пользователи
Хранит и организаторов и участников — роль определяется контекстом использования.
| Столбец | Тип | Ограничения |
|---|---|---|
id |
INTEGER | PK, AUTOINCREMENT |
username |
STRING(50) | NOT NULL, UNIQUE |
email |
STRING(200) | NOT NULL, UNIQUE |
created_at |
STRING(10) | NOT NULL |
tags — теги
| Столбец | Тип | Ограничения |
|---|---|---|
id |
INTEGER | PK, AUTOINCREMENT |
name |
STRING(50) | NOT NULL, UNIQUE |
events — события
| Столбец | Тип | Ограничения |
|---|---|---|
id |
INTEGER | PK, AUTOINCREMENT |
title |
STRING(200) | NOT NULL |
description |
TEXT | nullable |
event_date |
STRING(10) | NOT NULL |
status |
STRING(20) | NOT NULL, DEFAULT 'planned' |
organizer_id |
INTEGER | NOT NULL, FK → users.id |
Допустимые значения status: 'planned', 'ongoing', 'finished', 'cancelled'
registrations — записи на событие (ассоциативная таблица с данными)
| Столбец | Тип | Ограничения |
|---|---|---|
id |
INTEGER | PK, AUTOINCREMENT |
event_id |
INTEGER | NOT NULL, FK → events.id |
user_id |
INTEGER | NOT NULL, FK → users.id |
registered_at |
STRING(10) | NOT NULL |
status |
STRING(20) | NOT NULL, DEFAULT 'confirmed' |
Допустимые значения status: 'confirmed', 'cancelled'
Комбинация (event_id, user_id) должна быть уникальной — один пользователь не может зарегистрироваться на одно событие дважды. Реализуйте через UniqueConstraint.
Подсказка: запрет повторной регистрации
Один пользователь не должен иметь возможность зарегистрироваться на одно и то же событие несколько раз.
Например:
(event_id=1, user_id=5)
может существовать в таблице только один раз.
При этом должны быть допустимы записи:
(event_id=1, user_id=5)
(event_id=2, user_id=5)
(event_id=1, user_id=7)
То есть уникальным должен быть не отдельный столбец, а комбинация двух столбцов: event_id и user_id.
Для создания такого ограничения в SQLAlchemy используется UniqueConstraint, который добавляется через специальный атрибут модели __table_args__.
Пример использования:
__table_args__ = (
UniqueConstraint(
'event_id',
'user_id'
),
)После добавления такого ограничения база данных не позволит создать две одинаковые записи с одной и той же парой значений (event_id, user_id).
Проверьте работу ограничения: попробуйте зарегистрировать одного и того же пользователя на одно событие дважды.
event_tags — связь событий и тегов (многие-ко-многим)
Таблица ассоциации без дополнительных данных. Составной первичный ключ (event_id, tag_id).
Нарисуйте схему связей на бумаге или в любом инструменте. Обозначьте:
- Типы связей (один-ко-многим, многие-ко-многим)
- Внешние ключи
- Каскадное удаление (что должно удаляться при удалении события или пользователя)
Создайте следующую структуру папок и файлов:
event_manager/
├── main.py
├── database.py
├── exceptions.py
├── constants.py
├── models.py
├── repositories/
│ ├── __init__.py
│ ├── users.py
│ ├── tags.py
│ ├── events.py
│ └── registrations.py
├── schemas/
│ ├── __init__.py
│ ├── users.py
│ ├── tags.py
│ ├── events.py
│ └── registrations.py
└── routers/
├── __init__.py
├── users.py
├── tags.py
├── events.py
└── registrations.py
Alembic инициализируете отдельно на Шаге 5.
(Урок 19 — Обработка ошибок, Урок 26)
Вынесите допустимые значения статусов в константы:
EVENT_STATUSES = ['planned', 'ongoing', 'finished', 'cancelled']
REGISTRATION_STATUSES = ['confirmed', 'cancelled']Эти константы будут использоваться и в схемах (валидация), и в роутерах (проверка входных данных).
Реализуйте иерархию исключений:
AppException(Exception)— базовый класс сstatus_codeиmessageNotFoundError(AppException)— 404, сообщение:'{resource} с id={id} не найден'AlreadyExistsError(AppException)— 400, сообщение о дублированииBusinessLogicError(AppException)— 400, для нарушений бизнес-правил (например: нельзя зарегистрироваться на отменённое событие)
(Урок 24 — SQLAlchemy ORM, Урок 26)
Реализуйте все пять сущностей как SQLAlchemy ORM-модели наследуя от DeclarativeBase.
Требования:
event_tags— объявить какTable(не класс), так как дополнительных данных нетRegistration— объявить как класс-модель (есть поляregistered_atиstatus)- Все
relationshipдолжны иметьback_populates - Каскадное удаление через
cascade='all, delete-orphan'для:User.events(удаляем пользователя → удаляются его события)User.registrations(удаляем пользователя → удаляются его регистрации)Event.registrations(удаляем событие → удаляются все его регистрации)
ondelete='CASCADE'вForeignKeyдля таблицыevent_tagsUniqueConstraint('event_id', 'user_id')в моделиRegistration__repr__для каждой модели
(Урок 25 — Миграции)
- Инициализируйте Alembic:
alembic init alembic - Настройте
alembic.ini: укажитеsqlite:///events.db - Настройте
alembic/env.py: импортируйтеBaseизmodels.py, установитеtarget_metadata = Base.metadata - Сгенерируйте первую миграцию:
alembic revision --autogenerate -m "create initial tables" - Проверьте сгенерированный файл — должны быть все пять таблиц в правильном порядке
- Примените:
alembic upgrade head - Проверьте через Python что таблицы созданы
(Урок 18 — Подключение SQLite к FastAPI, Урок 26)
Реализуйте:
- Константу
DATABASE_URL = 'sqlite:///events.db' engineсconnect_args={'check_same_thread': False}get_db()— генератор сyield session,commit()при успехе,rollback()при ошибке- Фабрики зависимостей для каждого репозитория:
get_user_repo,get_tag_repo,get_event_repo,get_registration_repo
Используйте ленивые импорты репозиториев внутри фабрик чтобы избежать циклических зависимостей.
(Урок 17 — Pydantic-схемы)
UserCreate— поля:username(1-50 символов),email(минимум 3 символа),created_atUserResponse— поля:id,username,email,created_at
TagCreate— поле:name(1-50 символов)TagResponse— поля:id,name
EventCreate— поля:title(1-200 символов),description(необязательное),event_date,status(с валидатором черезfield_validatorчто значение входит вEVENT_STATUSES),organizer_id(больше 0),tag_ids(список id тегов, по умолчанию пустой)EventStatusUpdate— одно полеstatusс валидаторомEventResponse— поля:id,title,description,event_date,status,organizer_id,organizer_username(из relationship),tags(список строк — имена тегов)EventShortResponse— какEventResponseно безdescription(для списков)
RegistrationCreate— поля:user_id(больше 0)RegistrationStatusUpdate— полеstatusс валидаторомRegistrationResponse— поля:id,event_id,user_id,registered_at,status,username(из relationship черезregistration.user.username)
Не забудьте __init__.py с реэкспортом всех схем.
(Урок 14 — Паттерн Repository, Урок 24 — SQLAlchemy ORM)
Методы:
get_all() -> listget_by_id(user_id: int)create(username, email, created_at)— обработатьIntegrityError, вернутьNoneпри дублеdelete(user_id: int) -> bool
Методы:
get_all() -> listget_by_id(tag_id: int)get_by_ids(tag_ids: list) -> list— для привязки тегов к событиюcreate(name: str)— обработатьIntegrityErrordelete(tag_id: int) -> bool
Методы:
-
get_all(organizer_id=None, tag_name=None, status=None) -> list— с динамической фильтрациейРеализуйте динамический WHERE через список условий и параметров (паттерн из Урока 12 и Урока 20). Все три фильтра необязательны.
Используйте жадную загрузку:
joinedloadдляEvent.organizer(многие-к-одному)selectinloadдляEvent.tags(многие-ко-многим)
-
get_by_id(event_id: int)— с жадной загрузкойorganizerиtags -
create(title, description, event_date, status, organizer_id, created_at, tags: list)— принимает список объектовTag -
update_status(event_id: int, new_status: str) -> bool -
delete(event_id: int) -> bool
Методы:
get_by_event(event_id: int) -> list— с загрузкойuserget_by_event_and_user(event_id: int, user_id: int)— для проверки дублированияcreate(event_id, user_id, registered_at, status='confirmed')update_status(registration_id: int, new_status: str) -> bool
(Урок 16 — FastAPI, Урок 19 — Обработка ошибок)
| Метод | URL | Действие | Код |
|---|---|---|---|
| GET | / |
Все пользователи | 200 |
| GET | /{user_id} |
Один пользователь | 200 / 404 |
| POST | / |
Создать пользователя | 201 / 400 |
| DELETE | /{user_id} |
Удалить пользователя | 204 / 404 |
| Метод | URL | Действие | Код |
|---|---|---|---|
| GET | / |
Все теги | 200 |
| POST | / |
Создать тег | 201 / 400 |
| DELETE | /{tag_id} |
Удалить тег | 204 / 404 |
| Метод | URL | Действие | Код |
|---|---|---|---|
| GET | / |
Список событий (с фильтрами) | 200 |
| GET | /{event_id} |
Одно событие | 200 / 404 |
| POST | / |
Создать событие | 201 |
| PUT | /{event_id}/status |
Обновить статус | 200 / 404 / 400 |
| DELETE | /{event_id} |
Удалить событие | 204 / 404 |
Query-параметры для GET /:
organizer_id: int— необязательныйtag: str— необязательный, фильтр по имени тегаstatus: str— необязательный
| Метод | URL | Действие | Код |
|---|---|---|---|
| GET | /{event_id}/registrations |
Все записи на событие | 200 / 404 |
| POST | /{event_id}/registrations |
Зарегистрироваться на событие | 201 / 404 / 400 |
| PUT | /{event_id}/registrations/{reg_id}/status |
Обновить статус записи | 200 / 404 / 400 |
Бизнес-правила для регистрации (используйте BusinessLogicError):
- Нельзя зарегистрироваться на событие со статусом
'finished'или'cancelled' - Нельзя зарегистрировать одного пользователя дважды на одно событие
Важно: роутер registrations подключается в main.py с префиксом /events — тогда пути /events/{event_id}/registrations складываются правильно.
Подсказка: проверка бизнес-правил при регистрации
Перед созданием новой регистрации необходимо выполнить несколько дополнительных проверок.
Правило 1. Нельзя зарегистрироваться на завершённое или отменённое событие
Если событие имеет статус finished или cancelled создание новой регистрации должно быть запрещено.
Пример:
Событие:
id=10
status='cancelled'
Попытка регистрации:
user_id=5
Результат:
Ошибка 400
Правило 2. Нельзя зарегистрировать одного пользователя дважды
Перед созданием регистрации необходимо проверить, существует ли уже запись с такой комбинацией:
(event_id, user_id)
Если такая запись найдена, создание новой регистрации должно быть запрещено.
Пример:
(event_id=10, user_id=5)
уже существует в таблице.
Повторная попытка создать такую же регистрацию должна завершаться ошибкой.
Рекомендуемый порядок действий
Перед вызовом метода создания регистрации:
-
Получить событие по
event_id. -
Если событие не найдено — вернуть ошибку
404. -
Проверить статус события.
-
Если статус равен:
finishedcancelled
выбросить
BusinessLogicError. -
Проверить, существует ли уже регистрация данного пользователя на это событие.
-
Если регистрация существует — выбросить
BusinessLogicError. -
Если все проверки пройдены успешно — создать регистрацию.
Для ошибок бизнес-логики используйте исключение BusinessLogicError с понятным сообщением для пользователя.
Подумайте, в каком месте приложения лучше выполнять эти проверки: в роутере или в репозитории.
Подсказка: вложенные маршруты и префикс /events
Регистрация всегда относится к конкретному событию, поэтому маршруты регистрации удобно делать вложенными.
Например:
/events/1/registrations
/events/5/registrations
/events/10/registrations
Из URL сразу видно, для какого события выполняется действие.
При описании эндпоинтов в роутере registrations.py не обязательно повторять весь путь целиком. Внутри роутера можно описывать только часть маршрута:
@router.get('/{event_id}/registrations')Затем в main.py роутер подключается с префиксом:
app.include_router(
registrations.router,
prefix='/events'
)После объединения префикса и маршрута получится итоговый путь:
/events/{event_id}/registrations
Например:
GET /events/1/registrations
POST /events/1/registrations
(Урок 19 — Структура проекта, Урок 28)
Реализуйте:
FastAPI(title='Event Manager API', version='1.0')- Три обработчика исключений:
AppException,HTTPException,Exception @app.on_event('startup')— логирование старта (таблицы создаются через Alembic, не черезcreate_all)- Подключение всех четырёх роутеров с правильными префиксами и тегами
- Эндпоинт
GET /health
Запустите приложение и пройдите по сценариям:
alembic upgrade head
uvicorn main:app --reloadСценарии для проверки в /docs:
- Создать двух пользователей
- Создать три тега (
митап,онлайн,бесплатно) - Создать событие с двумя тегами и
organizer_id=1 - Попробовать создать событие с тем же
organizer_id— должно работать (события не уникальны) GET /events?tag=митап— должно вернуть событиеGET /events?organizer_id=1— фильтрация по организаторуPOST /events/1/registrations {"user_id": 2}— зарегистрировать второго пользователяPOST /events/1/registrations {"user_id": 2}повторно — получить 400 (дубль)PUT /events/1/status {"status": "cancelled"}— отменить событиеPOST /events/1/registrations {"user_id": 1}— получить 400 (событие отменено)DELETE /events/1— удалить событие, убедиться что регистрации удалились каскадноGET /events/1— получить 404GET /health—{"status": "ok"}
Обязательно:
- Все эндпоинты из таблиц реализованы и работают
- Схема БД соответствует заданию (все таблицы, связи, ограничения)
- ORM-модели используют
relationshipсback_populates - Миграции через Alembic (не
create_allвstartup) - Все запросы параметризованы — никаких f-строк в SQL
response_modelуказан для всех GET и POST эндпоинтов- Единый формат ошибок через обработчики в
main.py - Оба бизнес-правила для регистрации проверяются
- Динамическая фильтрация событий работает корректно
Хорошо:
- Жадная загрузка (
joinedload/selectinload) для связанных объектов field_validatorдля статусов в Pydantic-схемахcascade='all, delete-orphan'настроен корректноBusinessLogicError— отдельный класс исключения__init__.pyс реэкспортом в каждой папкеconstants.pyиспользуется и в схемах и в репозиториях
Папка event_manager/ со всеми файлами. Приложение должно запускаться командой:
alembic upgrade head
uvicorn main:app --reloadбез дополнительных настроек.