Skip to content

Latest commit

 

History

History
407 lines (271 loc) · 21.7 KB

File metadata and controls

407 lines (271 loc) · 21.7 KB

Урок 28. Финальный проект — Финализация

Остановитесь и посмотрите назад

Прежде чем что-то улучшать — осознайте что сделано.

За этот курс вы прошли путь от SELECT * FROM products в SQLiteStudio до работающего REST API с ORM, миграциями, Pydantic-схемами и Dependency Injection. Это не "hello world" — это архитектурно правильный бэкенд-проект.

Этот урок устроен иначе чем предыдущие. Здесь нет длинного списка нового кода. Здесь три части:

  1. Рефакторинг — небольшие улучшения которые делают проект чище
  2. Честный разбор — что сделано "учебно" и почему
  3. Карта дальнейшего пути — что вы уже умеете и куда идти дальше

Часть 1. Рефакторинг

Убираем дублирование _serialize_* функций

В Уроке 27 в каждом роутере есть функции _serialize_post, _serialize_comment. Это признак что Pydantic-схемы не полностью берут на себя сериализацию. Правильное решение — использовать model_validate чтобы Pydantic сам собирал объект из ORM-модели.

Для этого добавим в схемы настройку model_config:

# schemas/posts.py — улучшенная версия
from pydantic import BaseModel, Field
from typing import List
from pydantic import ConfigDict


class PostResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    # from_attributes=True — говорит Pydantic читать данные
    # из атрибутов объекта, а не только из словаря.
    # Это позволяет передавать ORM-объект напрямую в схему.

    id:              int
    title:           str
    content:         str
    status:          str
    created_at:      str
    author_id:       int

Однако для полей author_username и tags (которые приходят через relationship) всё равно нужна ручная сборка — ORM-объект не знает что Pydantic ждёт плоский список строк вместо объектов Tag. Поэтому в финальном проекте мы оставляем _serialize_* как есть — это честное учебное решение, а не ошибка.

В продакшне эту задачу решают по-разному: через кастомные @validator, через специальные поля computed_field, или через явное преобразование в репозитории. Единого "правильного" способа нет — есть компромиссы.

Выносим константы

Допустимые статусы поста разбросаны по коду. Выносим в одно место:

# constants.py
POST_STATUSES = ['draft', 'published']
# schemas/posts.py — добавить валидацию статуса
from constants import POST_STATUSES
from pydantic import field_validator

class PostCreate(BaseModel):
    ...
    status: str = Field(default='published')

    @field_validator('status')
    @classmethod
    def validate_status(cls, v):
        if v not in POST_STATUSES:
            raise ValueError(f'Статус должен быть одним из: {POST_STATUSES}')
        return v

Теперь невалидный статус даёт 422 автоматически — без проверок в роутере.

Проверка что всё работает

Запустите приложение и пройдите по каждому сценарию из Урока 27:

alembic upgrade head
uvicorn main:app --reload

Чеклист проверки:

□ POST /users — создать пользователя
□ POST /users повторно с тем же email — получить 400
□ POST /tags — создать несколько тегов
□ POST /posts с tag_ids — создать пост
□ GET /posts — получить список
□ GET /posts?tag=python — фильтрация работает
□ GET /posts?author_id=1 — фильтрация по автору
□ GET /posts/1 — полный пост с тегами и автором
□ GET /posts/9999 — получить 404 в правильном формате
□ POST /posts/1/comments — создать комментарий
□ GET /posts/1/comments — получить комментарии
□ DELETE /posts/1 — удалить пост (комментарии каскадно удалятся)
□ GET /health — проверка работоспособности
□ Открыть /docs — убедиться что документация актуальна

Часть 2. Что сделано "учебно"

Это самая важная часть урока.

Наш Blog API — рабочий прототип. Он делает всё что должен делать: принимает запросы, валидирует данные, работает с базой через ORM, возвращает правильные коды статусов. Если запустить его прямо сейчас — он будет работать.

Но между "рабочим прототипом" и "продакшн-приложением" есть несколько шагов. Ни один из них не перечёркивает то что вы сделали — они просто добавляются сверху на уже правильный фундамент.

Что упрощено и как это делается "по-настоящему"


1. Аутентификация и авторизация

Как у нас: author_id передаётся в теле запроса — любой может написать пост от имени любого пользователя.

Как в продакшне: пользователь логинится, получает JWT-токен, передаёт его в заголовке Authorization: Bearer <token>. Сервер проверяет токен и извлекает user_id автоматически — клиент его не передаёт.

# Как это выглядит с JWT в FastAPI:
@router.post('/')
def create_post(
    data: PostCreate,
    current_user: User = Depends(get_current_user)  # из токена
):
    # current_user.id — уже известен, клиент не передаёт
    ...

Что изучить: python-jose или python-jwt для работы с токенами, passlib для хэширования паролей, OAuth2 в FastAPI.


2. Права доступа

Как у нас: любой может удалить любой пост или комментарий.

Как в продакшне: удалять пост может только его автор (или администратор). Это называется авторизация — проверка прав конкретного действия.

# Проверка прав перед удалением:
if post.author_id != current_user.id and not current_user.is_admin:
    raise HTTPException(status_code=403, detail='Нет прав')

Что изучить: RBAC (Role-Based Access Control), разрешения в Django REST Framework.


3. Пагинация

Как у нас: GET /posts возвращает все посты — при 10 000 постов это проблема.

Как в продакшне: курсорная или offset-пагинация с метаданными.

{
    "items": [...],
    "total": 10000,
    "page": 1,
    "pages": 500,
    "has_next": true
}

Что изучить: fastapi-pagination, курсорная пагинация для больших объёмов.


4. Валидация email

Как у нас: email: str — принимаем любую строку.

Как в продакшне: проверка формата через EmailStr из pydantic[email].

from pydantic import EmailStr

class UserCreate(BaseModel):
    email: EmailStr   # автоматически проверяет формат

Что изучить: pip install pydantic[email], дополнительные валидаторы Pydantic.


5. Хранение дат

Как у нас: created_at: str — клиент передаёт дату, мы доверяем формату.

Как в продакшне: дата создания устанавливается сервером автоматически, клиент не может её подделать. Для SQLAlchemy — Column(DateTime, default=func.now()).

# В модели:
from sqlalchemy import DateTime, func
created_at = Column(DateTime, server_default=func.now(), nullable=False)

6. Тестирование

Как у нас: ручная проверка через /docs.

Как в продакшне: автоматические тесты которые запускаются при каждом изменении кода.

# Пример теста FastAPI:
from fastapi.testclient import TestClient

def test_create_user():
    response = client.post('/users', json={'username': 'test', ...})
    assert response.status_code == 201
    assert response.json()['username'] == 'test'

Что изучить: pytest, httpx, pytest-asyncio, тестирование с базой в памяти sqlite:///:memory:.


7. Переменные окружения

Как у нас: DATABASE_URL = 'sqlite:///blog.db' прямо в коде.

Как в продакшне: секреты (URL базы, секретный ключ JWT) хранятся в переменных окружения или .env файле — не в коде, не в Git.

# settings.py через pydantic-settings:
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str = 'sqlite:///blog.db'
    secret_key: str

    class Config:
        env_file = '.env'

settings = Settings()

8. Логирование и мониторинг

Как у нас: logger.info(...) в нескольких местах.

Как в продакшне: структурированные логи (JSON-формат для агрегаторов), метрики (Prometheus), трейсинг запросов, алерты при ошибках.


Итоговая таблица

Что готово:                     Что добавить для продакшна:
✓ Архитектура проекта           + Аутентификация (JWT)
✓ ORM-модели и миграции         + Авторизация (права доступа)
✓ Pydantic-валидация            + Пагинация
✓ Dependency Injection          + Тесты
✓ Обработка ошибок              + Переменные окружения
✓ REST API со всеми методами    + Хранение дат на сервере
✓ Связи между сущностями        + Email-валидация
✓ Документация /docs            + Логирование и мониторинг

Фундамент правильный. Всё перечисленное — это надстройки над уже готовой архитектурой, а не переписывание с нуля.


Часть 3. Что вы теперь умеете

Посмотрите на этот список не как на пройденные уроки, а как на реальные навыки:

SQL и базы данных:

  • Проектировать реляционные схемы с нормализацией
  • Писать запросы любой сложности: JOIN, GROUP BY, подзапросы, CTE
  • Понимать транзакции и целостность данных
  • Работать с SQLite и знать где он ограничен

Python + данные:

  • Управлять базой из кода через sqlite3 и SQLAlchemy
  • Строить слой данных через паттерн Repository
  • Генерировать реалистичные данные через Faker
  • Управлять схемой через миграции Alembic

Бэкенд-разработка:

  • Понимать HTTP, REST и JSON на уровне протокола
  • Строить API на FastAPI с правильной архитектурой
  • Валидировать данные через Pydantic
  • Изолировать зависимости через DI
  • Обрабатывать ошибки единообразно

Архитектура:

  • Разделять слои: роутеры, схемы, репозитории, модели
  • Понимать зачем каждый слой нужен и что он скрывает
  • Читать и писать код который поддерживаем

Вопросы для закрепления

1. Назовите три вещи которые нужно добавить в Blog API чтобы он был готов к реальному использованию.

Ответ

Любые три из: аутентификация (JWT), авторизация (права доступа), пагинация результатов, автоматические тесты, переменные окружения для секретов, валидация email через EmailStr, хранение дат на сервере, структурированное логирование.


2. Почему author_id передаётся в теле запроса в учебном проекте, и как это решается в продакшне?

Ответ

В учебном проекте нет аутентификации — мы не знаем кто делает запрос. В продакшне пользователь логинится и получает JWT-токен. При каждом запросе токен передаётся в заголовке Authorization. Сервер проверяет токен, извлекает из него user_id и передаёт в эндпоинт через зависимость Depends(get_current_user). Клиент author_id не передаёт — только токен.


3. Что такое курсорная пагинация и чем она лучше offset-пагинации при больших объёмах?

Ответ

Offset-пагинация (LIMIT 20 OFFSET 1000) работает медленнее при большом OFFSET — база данных всё равно читает 1020 строк и выбрасывает первые 1000. Курсорная пагинация использует значение последней записи как "курсор" — WHERE id > 1000 LIMIT 20. База читает ровно 20 строк независимо от глубины страницы. На миллионах записей разница ощутима.


4. Почему DATABASE_URL не должен быть прописан прямо в коде и как это исправить?

Ответ

URL базы данных содержит логин и пароль — это секрет. Если такой файл попадёт в Git (публичный или корпоративный) — секреты станут известны всем. Правильное решение: хранить чувствительные данные в переменных окружения или в .env файле, который добавлен в .gitignore. В коде читаем через os.getenv('DATABASE_URL') или через pydantic-settings.


5. Что означает "фундамент правильный" применительно к нашему Blog API?

Ответ

Архитектурные решения — разделение на слои, паттерн Repository, Dependency Injection, единый формат ошибок, ORM + миграции — сделаны правильно и не нуждаются в переписывании при добавлении продакшн-возможностей. Аутентификация добавляется как новая зависимость, пагинация — как дополнительные query-параметры, тесты — поверх уже правильно изолированных слоёв. Правильный фундамент означает что улучшения добавляются, а не заменяют существующее.


6. Почему понимание SQLAlchemy ORM помогает при изучении Django ORM?

Ответ

Концепции идентичны: модели-классы вместо таблиц, объекты вместо строк, session.query()Model.objects, .filter() = .filter(), relationshipForeignKey + related_name, Alembic-миграции ≈ Django-миграции. Django делает то же самое автоматически и с большим количеством встроенных инструментов — но понимание того что происходит под капотом приходит из SQLAlchemy.


7. Что такое from_attributes=True в Pydantic и зачем это нужно при работе с ORM?

Ответ

По умолчанию Pydantic создаёт объекты только из словарей. ORM-объекты — это экземпляры Python-классов с атрибутами, не словари. from_attributes=True (в Pydantic v2) говорит схеме читать данные через атрибуты объекта (obj.name) а не через ключи словаря (data['name']). Это позволяет передавать ORM-объекты напрямую в Pydantic-схемы без ручного преобразования через dict().


8. Какой тип тестов написали бы вы для эндпоинта POST /posts?

Ответ

Минимальный набор: тест успешного создания поста (проверить 201, проверить поля ответа), тест с несуществующим author_id (проверить 404), тест с пустым title (проверить 422), тест с несуществующими tag_ids (проверить корректное поведение), тест что созданный пост появляется в GET /posts. Для изоляции — использовать базу в памяти sqlite:///:memory: и откатывать транзакцию после каждого теста.


9. Почему каскадное удаление реализовано двумя способами одновременно — через ORM и через ondelete='CASCADE' в FK?

Ответ

Каскад ORM (cascade='all, delete-orphan') работает только когда удаление инициируется через SQLAlchemy. ondelete='CASCADE' в базе данных работает всегда — при прямом SQL, при другом инструменте, при удалении через скрипт миграции. Совместное использование — двойная защита целостности данных на разных уровнях. Если убрать один из них — возможны утечки данных в определённых сценариях.


10. Что принципиально нового вы узнаете в курсе по Django, чего нет в этом курсе?

Ответ

Полноценная аутентификация и авторизация из коробки (сессии, токены, группы и разрешения). Административная панель — готовый интерфейс управления данными без написания кода. Встроенная работа с формами и шаблонизатор. Полный стек — от HTML до базы данных в одном фреймворке. Батарейки: email, кэширование, сигналы, management-команды. PostgreSQL как основная СУБД вместо SQLite.


Предыдущий урок |