Блок 1: Pydantic — моделирование
Задача 1: Event — дата не в прошлом
Логика решения: @field_validator('date') с @classmethod получает значение поля date после приведения типа. Сравниваем с datetime.now() и выбрасываем ValueError если дата в прошлом.
from pydantic import BaseModel, field_validator
from datetime import datetime, timedelta
class Event(BaseModel):
title: str
date: datetime
location: str
@field_validator('date')
@classmethod
def date_must_be_future(cls, v: datetime) -> datetime:
if v < datetime.now():
raise ValueError("Event date must be in the future")
return v
# Пример использования
try:
future_event = Event(
title="New Year Party",
date=datetime.now() + timedelta(days=30),
location="New York"
)
print(future_event)
except ValueError as e:
print(e)
Почему @classmethod? В Pydantic v2 валидаторы полей — это методы класса. cls — это класс модели, v — значение поля. Без @classmethod Pydantic v2 выбросит ошибку конфигурации.
Задача 2: UserProfile — Field, EmailStr, ConfigDict
Логика решения: Field(min_length=8) добавляет ограничение на длину пароля. EmailStr — специальный тип Pydantic для автоматической валидации email. model_config = ConfigDict(json_schema_extra=...) — замена class Config: schema_extra из v1.
from pydantic import BaseModel, EmailStr, Field, ConfigDict
class UserProfile(BaseModel):
username: str
password: str = Field(
min_length=8,
description="Password must be at least 8 characters long"
)
email: EmailStr
model_config = ConfigDict(
json_schema_extra={
"example": {
"username": "john_doe",
"password": "securePassword123",
"email": "john.doe@example.com"
}
}
)
# Пример создания пользователя
user_profile = UserProfile(
username="john_doe",
password="securePassword123",
email="john.doe@example.com"
)
print(user_profile)
Задача 3: Transaction — Annotated[Decimal, Field], str_strip_whitespace
Логика решения: в Pydantic v2 нет condecimal и constr — вместо них используется Annotated[тип, Field(...)]. anystr_strip_whitespace из v1 заменяется на ConfigDict(str_strip_whitespace=True).
from pydantic import BaseModel, Field, ConfigDict
from typing import Annotated
from decimal import Decimal
class Transaction(BaseModel):
amount: Annotated[Decimal, Field(gt=0)]
transaction_type: Annotated[str, Field(pattern=r"^(debit|credit)$")]
currency: Annotated[str, Field(min_length=3, max_length=3)]
model_config = ConfigDict(str_strip_whitespace=True)
# Пример транзакции
transaction = Transaction(
amount=Decimal("150.50"),
transaction_type="debit",
currency="USD"
)
print(transaction)
Почему Decimal, а не float? Для финансовых операций важна точность. float имеет проблемы с представлением: 0.1 + 0.2 != 0.3. Decimal обеспечивает точные вычисления.
Задача 4: Appointment — ≥24ч вперёд
Логика решения: проверяем не просто что дата в будущем, а что она как минимум на 24 часа вперёд. timedelta(hours=24) точнее, чем timedelta(days=1) при учёте перехода суток.
from pydantic import BaseModel, field_validator
from datetime import datetime, timedelta
class Appointment(BaseModel):
patient_name: str
appointment_date: datetime
@field_validator('appointment_date')
@classmethod
def check_appointment_date(cls, v: datetime) -> datetime:
if v < datetime.now() + timedelta(hours=24):
raise ValueError(
"Appointment must be scheduled at least 24 hours in advance."
)
return v
# Пример использования
try:
appointment = Appointment(
patient_name="Alice Smith",
appointment_date=datetime.now() + timedelta(hours=25)
)
print(appointment)
except ValueError as e:
print(e)
Блок 2: Pydantic — найди ошибку
Задача 5: Product price=-100
Ошибка: значение price=-100 нарушает условие валидатора check_price, который требует value > 0. При попытке создать Product с такой ценой будет выброшен ValidationError с сообщением «Price must be positive».
Как исправить: передать положительное значение цены. Если нужно разрешить нулевую цену — изменить условие на value < 0.
# Ошибочный вызов:
product = Product(name="Laptop", price=-100)
# -> ValidationError: price - Price must be positive
# Исправление:
product = Product(name="Laptop", price=1500.00)
print(product) # name='Laptop' price=1500.0
Дополнительно: в Pydantic v2 рекомендуется добавить @classmethod к валидатору — в данном коде он отсутствует (это также ошибка стиля для v2).
Задача 6: User email="aliceexample.com" без @
Ошибка: строка "aliceexample.com" не является валидным email — в ней отсутствует символ @. Тип EmailStr в Pydantic проверяет формат email по стандарту RFC 5321. При создании объекта будет выброшен ValidationError.
Как исправить: передать корректный email с символом @.
# Ошибочный вызов:
user = User(name="Alice", email="aliceexample.com")
# -> ValidationError: email - value is not a valid email address
# Исправление:
user = User(name="Alice", email="alice@example.com")
print(user) # name='Alice' email='alice@example.com'
Задача 7: Account balance="100" при validate_assignment
Ошибка: поле balance ожидает float, но передана строка "100". Флаг validate_assignment=True (или его аналог в v2: ConfigDict(validate_assignment=True)) заставляет Pydantic строго проверять типы при присвоении. Строка не приводится к float автоматически в этом режиме.
Примечание: без validate_assignment=True Pydantic v2 может принять строку "100" и автоматически преобразовать её в 100.0. С этой настройкой — не принимает.
# Ошибочный вызов:
acc = Account(username="john_doe", balance="100")
# -> ValidationError: balance - Input should be a valid number
# Исправление (передать float или число):
acc = Account(username="john_doe", balance=100.0)
print(acc) # username='john_doe' balance=100.0
# Также стоит обновить Config до ConfigDict (Pydantic v2):
# model_config = ConfigDict(validate_assignment=True)
Задача 8: Item() без обязательного price
Ошибка: при создании Item() не передано значение для поля price. Поле price: float = Field(gt=0) — обязательное (нет дефолтного значения, только ограничение). Поле name имеет default=None, поэтому для него ошибки нет.
# Ошибочный вызов:
item = Item()
# -> ValidationError: price - Field required
# Исправление (передать price):
item = Item(price=9.99)
print(item) # name=None price=9.99
# Или:
item = Item(name="Widget", price=9.99)
print(item) # name='Widget' price=9.99
Блок 3: SQLAlchemy
Задача 9: Engine для MySQL
Логика: строка подключения для MySQL строится как mysql+pymysql://пользователь:пароль@хост/база. Диалект mysql+pymysql говорит SQLAlchemy использовать PyMySQL как DBAPI.
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://user:password@localhost/mydatabase')
# Установка pymysql: pip install pymysql
Задача 10: Engine SQLite :memory: с логированием
Логика: sqlite:///:memory: — специальный URL для SQLite в памяти (база исчезает при закрытии соединения). echo=True — удобный способ включить логирование SQL-запросов в stdout. Альтернатива — модуль logging.
from sqlalchemy import create_engine
import logging
logging.basicConfig(level=logging.INFO)
engine = create_engine('sqlite:///:memory:', echo=True)
Задача 11: Модель User на SQLAlchemy 2.x
Логика: в 2.x базовый класс создаётся наследованием от DeclarativeBase, поля — через Mapped[тип] = mapped_column(...) с явными аннотациями типов.
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Integer
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
age: Mapped[int] = mapped_column(Integer)
Задача 12: User + Post — one-to-many
Логика: ForeignKey('users.id') в Post создаёт ссылку на родительскую запись. relationship с back_populates связывает оба класса. back_populates создаёт двустороннюю связь — при обращении к user.posts получаем список постов.
from sqlalchemy import String, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import List
class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
age: Mapped[int] = mapped_column(Integer)
posts: Mapped[List["Post"]] = relationship("Post", back_populates="user")
class Post(Base):
__tablename__ = 'posts'
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(255))
user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))
user: Mapped["User"] = relationship("User", back_populates="posts")
Base.metadata.create_all(engine)
Задача 13: User + Address — one-to-many
Логика: аналогична задаче 12. Один User имеет много Address. Важно: в исходном коде лекции модель Address не имеет поля description — оно нужно для задачи 14. Добавим его как необязательное.
from sqlalchemy import String, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import List, Optional
class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
age: Mapped[int] = mapped_column(Integer)
addresses: Mapped[List["Address"]] = relationship(
"Address", back_populates="user"
)
class Address(Base):
__tablename__ = 'addresses'
id: Mapped[int] = mapped_column(primary_key=True)
description: Mapped[Optional[str]] = mapped_column(String(255))
user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))
user: Mapped["User"] = relationship("User", back_populates="addresses")
Base.metadata.create_all(engine)
Задача 14: Сессия — add/commit/delete/query
Логика: в SQLAlchemy 2.x используем контекстный менеджер with Session(engine) as session:. После commit() транзакция зафиксирована в БД. После delete(obj) + commit() запись удалена — повторный query вернёт None.
from sqlalchemy.orm import Session
# Создать таблицы
Base.metadata.create_all(engine)
with Session(engine) as session:
# Создание нового пользователя и адреса
new_user = User(name="John Doe", age=28)
new_address = Address(user=new_user, description="123 Elm Street")
# Добавление в базу данных
session.add(new_user)
session.add(new_address)
session.commit()
# Удаление пользователя и проверка
session.delete(new_user)
session.commit()
# Проверка, что пользователь удалён
result = session.query(User).filter_by(name="John Doe").first()
print(result) # None
Задача 15: Анализ ошибок — 4 проблемы
Найденные ошибки из конспекта:
- Нет
Base = declarative_base()(в 1.x) или классаclass Base(DeclarativeBase)(в 2.x) —Baseиспользуется, но не определён. - Классы
PersonиPetне наследуются отBase— написаноclass Person():вместоclass Person(Base):. SQLAlchemy не узнает эти классы как ORM-модели. - В
sessionmaker()отсутствует параметр движка — написаноsessionmaker()вместо привязки к engine. В 2.x используемwith Session(engine). - Не указан размер строки в
String—Stringбез аргумента может вызвать проблемы в некоторых СУБД (MySQL требует явную длину). НужноString(100)или другой размер.
Исправленный код на SQLAlchemy 2.x:
from sqlalchemy import Column, ForeignKey, Integer, String, create_engine
from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column,
relationship, Session
)
from typing import List, Optional
engine = create_engine('sqlite:///example.db')
# ИСПРАВЛЕНИЕ 1: определить базовый класс
class Base(DeclarativeBase):
pass
# ИСПРАВЛЕНИЕ 2: наследоваться от Base
class Person(Base):
__tablename__ = 'persons'
id: Mapped[int] = mapped_column(primary_key=True)
# ИСПРАВЛЕНИЕ 4: указать длину строки
name: Mapped[str] = mapped_column(String(100), nullable=False)
pets: Mapped[List["Pet"]] = relationship("Pet", back_populates="owner")
class Pet(Base):
__tablename__ = 'pets'
id: Mapped[int] = mapped_column(primary_key=True)
# ИСПРАВЛЕНИЕ 4: указать длину строки
name: Mapped[str] = mapped_column(String(100), nullable=False)
owner_id: Mapped[int] = mapped_column(ForeignKey('persons.id'))
owner: Mapped["Person"] = relationship("Person", back_populates="pets")
Base.metadata.create_all(engine)
# ИСПРАВЛЕНИЕ 3: использовать with Session(engine) вместо sessionmaker()
with Session(engine) as session:
new_person = Person(name='Alice')
new_pet = Pet(name='Fido', owner=new_person)
session.add(new_person)
session.add(new_pet)
session.commit()
# Сессия закрывается автоматически