✅ Решения: Flask-проект

⚡ Краткие ответы

  • Идемпотентны: GET, PUT, DELETE (не POST, не PATCH)
  • Stateless: каждый запрос несёт всю необходимую информацию
  • init_app vs SQLAlchemy(app): init_app поддерживает несколько экземпляров приложения
  • from_attributes=True: позволяет Pydantic читать поля из ORM-объекта через атрибуты

1.1 — Идемпотентные HTTP-методы

Ответ: GET, PUT, DELETE — идемпотентны.
  • GET — повторный запрос возвращает те же данные, не изменяет состояние.
  • PUT — замена ресурса; повторный PUT с теми же данными не меняет результат.
  • DELETE — после удаления ресурс отсутствует; повторный DELETE не изменяет это.
  • POST — каждый POST может создавать новый ресурс → не идемпотентен.
  • PATCH — частичное обновление; зависит от реализации, часто не идемпотентен.

1.2 — Stateless в REST

Ответ: Сервер не хранит информацию о предыдущих запросах клиента. Каждый запрос самодостаточен.

Пример: для авторизованных запросов клиент должен передавать токен в каждом запросе (в заголовке Authorization), а не рассчитывать, что сервер «помнит» сессию.

# Каждый запрос несёт токен — сервер не хранит состояние
GET /questions/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...

1.3 — CRUD маршруты для Article

URLМетодДействие
/articlesGETСписок всех статей
/articlesPOSTСоздать статью
/articles/<id>GETПолучить статью
/articles/<id>PUTОбновить статью полностью
/articles/<id>PATCHЧастично обновить статью
/articles/<id>DELETEУдалить статью

2.1 — Конфигурационный файл

# config.py

class Config:
    DEBUG = False
    TESTING = False
    SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevelopmentConfig(Config):
    DEBUG = True

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'

class ProductionConfig(Config):
    pass

config_map = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
}

2.2 — FLASK_ENV устарела

Ответ: FLASK_ENV удалена в Flask 2.3+. В Flask 3.x используйте:
  • FLASK_DEBUG=1 — для включения дебаг-режима.
  • Кастомную переменную APP_ENV=development для выбора конфигурации в application factory.

3.1 — Blueprint users_bp

# app/routers/users.py
from flask import Blueprint, jsonify, request

users_bp = Blueprint('users', __name__)

@users_bp.route('/', methods=['GET'])
def list_users():
    return jsonify({"users": []})

@users_bp.route('/', methods=['POST'])
def create_user():
    data = request.get_json()
    return jsonify({"created": data}), 201

@users_bp.route('/<int:user_id>', methods=['GET'])
def get_user(user_id):
    return jsonify({"id": user_id, "name": "Example"})

# Регистрация в create_app():
# app.register_blueprint(users_bp, url_prefix='/users')

3.2 — Два Blueprint с одинаковым именем

Flask вызовет ошибку ValueError: The name 'name' is already registered for a different blueprint.

Решение: задать уникальные имена при создании Blueprint:

questions_bp = Blueprint('questions', __name__)
responses_bp = Blueprint('responses', __name__)  # разные имена!

3.3 — Application Factory Pattern

Application Factory — функция create_app(config), которая создаёт и настраивает приложение Flask, не привязывая его к глобальной переменной.

Преимущества:

  • Можно создать несколько экземпляров (например, один для тестов с тестовой БД).
  • Расширения инициализируются через init_app(app) — нет циклических импортов.
  • Легко менять конфигурацию без изменения кода.

4.1 — SQLAlchemy(app) vs init_app()

СпособКогда использовать
db = SQLAlchemy(app) Только в простых скриптах / обучении. Привязывает db к одному app.
db.init_app(app) В Application Factory и любом реальном проекте. Позволяет иметь несколько экземпляров приложения.

4.2 — Эндпоинт GET /questions/<id>

@questions_bp.route('/<int:question_id>', methods=['GET'])
def get_question(question_id):
    question = db.session.get(Question, question_id)
    if not question:
        return jsonify({"error": "Not found"}), 404
    return jsonify(QuestionOut.model_validate(question).model_dump())

5.1 — from_attributes в Pydantic v2

Без from_attributes=True Pydantic пытается читать данные из словаря / mapping-объекта. ORM-объекты SQLAlchemy — это не словари, а Python-объекты с атрибутами. При попытке QuestionOut.model_validate(orm_obj) без настройки возникнет ошибка PydanticUserError: You must set the config attribute 'from_attributes=True'.
class QuestionOut(BaseModel):
    id: int
    text: str

    model_config = {"from_attributes": True}  # обязательно для ORM!

5.2 — Blueprint responses_bp с Pydantic

# app/schemas/response.py
from pydantic import BaseModel, Field

class ResponseCreate(BaseModel):
    question_id: int
    agree: bool

# app/routers/responses.py
from flask import Blueprint, jsonify, request
from app import db
from app.models.responses import Response
from app.models.questions import Question
from app.schemas.response import ResponseCreate
from pydantic import ValidationError
from sqlalchemy import select

responses_bp = Blueprint('responses', __name__)

@responses_bp.route('/', methods=['GET'])
def list_responses():
    responses = db.session.scalars(select(Response)).all()
    result = [{"id": r.id, "question_id": r.question_id, "agree": r.agree}
              for r in responses]
    return jsonify(result)

@responses_bp.route('/', methods=['POST'])
def create_response():
    data = request.get_json()
    try:
        schema = ResponseCreate(**data)
    except ValidationError as e:
        return jsonify({"error": e.errors()}), 422

    question = db.session.get(Question, schema.question_id)
    if not question:
        return jsonify({"error": "Question not found"}), 404

    response = Response(question_id=schema.question_id, agree=schema.agree)
    db.session.add(response)
    db.session.commit()
    return jsonify({"id": response.id, "question_id": response.question_id,
                    "agree": response.agree}), 201