Шаг 01. Структура и настройка

📁 Серия: Капстоун A ⏱️ ~30 мин 🎯 Сложность: Начальная
#application-factory #config #flask3

⚡ Кратко: что делаем на этом шаге

Цель: Создать venv, установить зависимости, описать структуру директорий проекта, написать application factory (create_app()) и config-классы без устаревшего FLASK_ENV.

  • Файлы: config.py, app/__init__.py, run.py, requirements.txt
  • Команды: python -m venv venv, pip install flask flask-sqlalchemy flask-migrate pydantic
  • Результат: приложение стартует командой python run.py, отвечает 200 на GET /health

🎯 Цель этапа

На этом шаге мы закладываем фундамент проекта: правильную структуру директорий, виртуальное окружение, зависимости и application factory с config-классами. Закрываем callout-verify из урока 09: убираем устаревший FLASK_ENV и показываем правильный подход к конфигурации во Flask 3.x.

💡 Ключевая идея — Application Factory: Вместо того чтобы создавать приложение на уровне модуля (app = Flask(__name__)), мы оборачиваем его в функцию create_app(). Это позволяет создавать несколько экземпляров с разными конфигурациями (dev / test / prod) и правильно тестировать приложение.

После этого шага у нас будет

  • Виртуальное окружение и requirements.txt с закреплёнными версиями
  • Структура директорий: app/, app/models/, app/schemas/, app/api/
  • Три config-класса: DevelopmentConfig, TestingConfig, ProductionConfig
  • Application factory create_app() в app/__init__.py
  • Точка входа run.py
  • Health-check эндпоинт GET /health

📄 Затрагиваемые файлы

ФайлДействиеОписание
requirements.txtСоздатьСписок зависимостей с версиями
config.pyСоздатьConfig-классы для dev/test/prod
app/__init__.pyСоздатьApplication factory create_app()
app/models/__init__.pyСоздатьИнициализация пакета models (пустой)
app/schemas/__init__.pyСоздатьИнициализация пакета schemas (пустой)
app/api/__init__.pyСоздатьИнициализация пакета api (пустой)
run.pyСоздатьТочка входа: запуск Flask dev-сервера

🔨 Шаги

1. Создаём директорию и виртуальное окружение

💻 Терминал (PowerShell)
# Создаём папку проекта (выберите удобное место)
mkdir community_pulse
cd community_pulse

# Создаём виртуальное окружение
python -m venv venv

# Активируем (Windows PowerShell)
venv\Scripts\Activate.ps1

# Проверяем: в начале строки должно появиться (venv)
python --version

2. Устанавливаем зависимости

💻 Терминал (venv активирован)
pip install flask==3.1.0 flask-sqlalchemy==3.1.1 flask-migrate==4.0.7 pydantic==2.8.2 python-dotenv==1.0.1
📄 requirements.txt
# requirements.txt
flask==3.1.0
flask-sqlalchemy==3.1.1
flask-migrate==4.0.7
pydantic==2.8.2
python-dotenv==1.0.1
💡 Закреплённые версии: Фиксируем точные версии (==), а не диапазоны. Это гарантирует воспроизводимость: коллега установит ровно то, что вы тестировали.

3. Создаём структуру директорий

💻 Терминал
# Создаём пакеты (папки с __init__.py)
mkdir app
mkdir app\models
mkdir app\schemas
mkdir app\api

# Создаём пустые __init__.py
type nul > app\models\__init__.py
type nul > app\schemas\__init__.py
type nul > app\api\__init__.py

Итоговая структура проекта:

community_pulse/
├── venv/                   # виртуальное окружение (в git не попадает)
├── app/
│   ├── __init__.py         # application factory
│   ├── models/
│   │   └── __init__.py     # (пока пустой, модели — шаг 03)
│   ├── schemas/
│   │   └── __init__.py     # (пока пустой, схемы — шаг 04)
│   └── api/
│       └── __init__.py     # (пока пустой, blueprints — шаг 05)
├── migrations/             # создаётся flask-migrate на шаге 02
├── config.py               # конфигурация
├── requirements.txt        # зависимости
├── run.py                  # точка входа
└── .gitignore

4. Пишем конфигурацию

📄 config.py
# config.py
import os
from dotenv import load_dotenv

load_dotenv()  # читает .env если он есть


class Config:
    """Базовая конфигурация — общие настройки для всех окружений."""
    SECRET_KEY: str = os.environ.get("SECRET_KEY", "dev-secret-key-change-in-prod")
    SQLALCHEMY_TRACK_MODIFICATIONS: bool = False
    # DEBUG и TESTING НЕ ставим здесь — их переопределяют дочерние классы


class DevelopmentConfig(Config):
    """Конфигурация для разработки."""
    DEBUG: bool = True
    SQLALCHEMY_DATABASE_URI: str = os.environ.get(
        "DEV_DATABASE_URL",
        "sqlite:///community_pulse_dev.db"
    )


class TestingConfig(Config):
    """Конфигурация для тестов — БД в памяти, не пишет на диск."""
    TESTING: bool = True
    SQLALCHEMY_DATABASE_URI: str = "sqlite:///:memory:"
    # Отключаем CSRF и другие защиты для удобства тестирования
    WTF_CSRF_ENABLED: bool = False


class ProductionConfig(Config):
    """Конфигурация для продакшна — DATABASE_URL обязателен."""
    DEBUG: bool = False
    SQLALCHEMY_DATABASE_URI: str = os.environ.get("DATABASE_URL", "")

    def __init__(self) -> None:
        if not self.SQLALCHEMY_DATABASE_URI:
            raise RuntimeError("DATABASE_URL environment variable is not set")


# Словарь для выбора конфига по строке
config_map: dict[str, type[Config]] = {
    "development": DevelopmentConfig,
    "testing": TestingConfig,
    "production": ProductionConfig,
}
💡 Почему нет FLASK_ENV? В Flask 3.x переменная FLASK_ENV убрана (deprecated в 2.2, удалена в 3.0). Правильный подход — управлять окружением через собственную переменную (APP_ENV) и передавать нужный конфиг-класс в create_app(). Это закрывает callout-verify из урока 09.

5. Создаём application factory

📄 app/__init__.py
# app/__init__.py
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

from config import config_map, Config

# Создаём расширения БЕЗ привязки к приложению.
# Они будут инициализированы в create_app() через .init_app()
db: SQLAlchemy = SQLAlchemy()
migrate: Migrate = Migrate()


def create_app(config_name: str = "development") -> Flask:
    """
    Application factory — создаёт и настраивает экземпляр Flask.

    Args:
        config_name: ключ из config_map ('development', 'testing', 'production')

    Returns:
        Настроенный экземпляр Flask
    """
    app = Flask(__name__)

    # Загружаем конфиг из соответствующего класса
    config_class: type[Config] = config_map[config_name]
    app.config.from_object(config_class)

    # Инициализируем расширения с app
    db.init_app(app)
    migrate.init_app(app, db)

    # Health-check эндпоинт — проверяем, что сервер живой
    @app.route("/health")
    def health_check():
        return jsonify({"status": "ok", "env": config_name}), 200

    # Здесь будем регистрировать blueprints (шаг 05)
    # from app.api import register_blueprints
    # register_blueprints(app)

    return app
💡 db и migrate создаются вне create_app(): Это стандартный паттерн Flask-расширений. Объекты db и migrate существуют без привязки к конкретному приложению — их можно импортировать из моделей. Привязка к приложению происходит в create_app() через init_app().

6. Пишем точку входа

📄 run.py
# run.py
import os
from app import create_app

# Читаем окружение из переменной APP_ENV, по умолчанию 'development'
config_name: str = os.environ.get("APP_ENV", "development")
app = create_app(config_name)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

7. Создаём .gitignore

📄 .gitignore
# .gitignore
venv/
__pycache__/
*.pyc
*.pyo
.env
*.db
*.sqlite3
instance/
.pytest_cache/
.mypy_cache/

8. Создаём .env.example

📄 .env.example
# .env.example — скопируйте в .env и заполните своими значениями
APP_ENV=development
SECRET_KEY=your-secret-key-here
# DEV_DATABASE_URL=sqlite:///community_pulse_dev.db
# DATABASE_URL=postgresql://user:password@localhost/community_pulse

🧠 Объяснение логики

Application Factory vs глобальный app

В учебных примерах часто пишут app = Flask(__name__) прямо в модуле. Это работает, но создаёт проблемы при тестировании и когда нужны разные конфиги. Application factory решает это: вызвал create_app("testing") — получил тестовое приложение с БД в памяти; вызвал create_app("production") — получил продакшн-конфиг.

Почему config-классы, а не словарь

Классы позволяют использовать наследование: DevelopmentConfig и ProductionConfig наследуют общие настройки из Config. Переопределяем только то, что меняется. Словарь не умеет в наследование.

init_app() — паттерн расширений Flask

Расширения (SQLAlchemy, Migrate) создаются один раз на уровне модуля, без app. Привязываются к конкретному приложению через init_app(app). Это позволяет импортировать db из моделей, не создавая цикличных импортов.

⚠️ Частая ошибка: Создавать db = SQLAlchemy(app) прямо в create_app(). Тогда db привязан к одному экземпляру приложения и его нельзя импортировать в моделях до создания app. Всегда используйте db = SQLAlchemy() + db.init_app(app).

✅ Проверка

1. Запускаем приложение

💻 Терминал (venv активирован)
python run.py

2. Проверяем health-check

💻 Терминал (второй, или Postman)
curl http://localhost:5000/health

3. Ожидаемый результат

Успех — в терминале с сервером:
 * Running on http://0.0.0.0:5000
 * Debug mode: on
Успех — ответ на /health:
{"env": "development", "status": "ok"}

Диагностика: если что-то пошло не так

  • Ошибка: ModuleNotFoundError: No module named 'flask' — venv не активирован. Запустите venv\Scripts\Activate.ps1
  • Ошибка: Address already in use — порт 5000 занят другим процессом. Остановите другой сервер или смените порт в run.py
  • Ошибка: KeyError: 'development' — проверьте, что config_map содержит ключ "development"

➡️ Что дальше

На следующем шаге мы подключим Flask-SQLAlchemy и Flask-Migrate, инициализируем БД и создадим первую миграцию. После шага 02 у нас будет настроенная база данных под управлением Alembic.

  • Готово: venv, зависимости, структура, config-классы, application factory, health-check
  • Далее (шаг 02): Flask-SQLAlchemy + Flask-Migrate, команды flask db init/migrate/upgrade
  • Связь с уроком 09: application factory здесь — правильная версия того, что было в уроке