Cat

Kawai.Focus - приложение для фокусировки внимания (часть 5)

Данная статья посвящена:

  • Работе с фреймворком Kivy в проекте Kawai.Focus;
  • Подключению БД SQLite3 и хранение в ней настроек таймера;
  • Созданию CRUD операций для БД;
  • Созданию валидатора c использованием pydantic.
Kawai.Focus Arduinum628 08 Май 2025 Просмотров: 26

Вступление

Всем доброго дня! В предыдущей статье Kawai.Focus - приложение для фокусировки внимания (часть 4) я:

  • Улучшил код ReadJson (читалка JSON);
  • Написал класс для сообщений ошибок ErrorMessage на основе Enum;
  • Написал валидатор для таймера TimerValidator на основе dataclasses;
  • Улучшил функцию custom_timer();
  • Написал тесты для validators_tests.py.

Сегодня я подключу базу данных SQLite3, которая отлично подходит для хранения данных в небольших однопользовательских и синхронных приложениях. Это лёгкая база данных, не требующая отдельного сервера. Её часто используют в мобильных приложениях для работы в оффлайн-режиме.

Где используется SQLite3:

  • 📝 Личные дневники и заметки — сохранение записей пользователя;
  • 📅 Трекеры привычек — хранение прогресса и статистики;
  • 📚 Офлайн-читалки — книги, статьи без интернета;
  • 💰 Финансовые приложения — учет расходов без сети.

Также будут разработаны функции для CRUD-операций, включая получение таймера по id и создание нового таймера. Кроме того, я внедрю валидацию данных таймера с помощью pydantic.

Заваривайте чай, доставайте вкусняшки — пора “фаршировать помидор”! 🍅


База данных SQLite3

Взаимодействие с базой данных в Python удобнее реализовывать через ORM, а не с помощью непосредственных SQL-запросов. Я буду использовать популярную ORM-библиотеку SQLAlchemy, которая позволяет автоматически преобразовывать операции с Python-объектами в SQL-запросы.

Для начала я установлю SQLAlchemy:

poetry add sqlalchemy

Настройки для базы данных

Для хранения настроек проекта, включая параметры базы данных, я буду использовать библиотеку pydantic_settings. Она позволяет удобно управлять конфигурацией приложения, обеспечивая простую валидацию и загрузку настроек из разных источников.

pydantic_settings позволяет:

  • Загружать конфигурацию из .env, переменных окружения, файлов yaml или json;
  • Автоматически валидировать значения при загрузке настроек;
  • Использовать аннотации типов, что делает код читаемым и безопасным;
  • Применять дефолтные значения и обрабатывать отсутствующие параметры;
  • Преобразовывать данные (например, str → int или bool);
  • Использовать вложенные модели, что удобно для сложных конфигураций.

Устанавливаю pydantic_settings:

poetry add pydantic_settings

Далее я создам файл settings.py, в котором напишу три класса:

  • ModelConfig — модель конфигурации с настройками для .env;
  • SettingsDB — класс с настройками базыданных;
  • Settings — основной класс настроек.

Импорты

from pydantic_settings import BaseSettings, SettingsConfigDict

Разбор импортируемых модулей:

BaseSettings (pydantic_settings)

  • Базовый класс для создания моделей конфигурации;
  • Позволяет загружать настройки из .env, переменных окружения и других источников;
  • Автоматически валидирует данные на основе аннотаций типов;

SettingsConfigDict (pydantic_settings)

  • Позволяет задавать конфигурацию модели настроек;
  • Например, можно указать env_file=".env", чтобы загрузить данные из .env;
  • Используется для управления поведением модели конфигурации.

Класс ModelConfig

class ModelConfig(BaseSettings):
    """Модель конфигурации"""
    
    model_config = SettingsConfigDict(
        env_file='.env',
        env_file_encoding='utf-8',
        extra='ignore'
    )

Разбор класса:

Наследование от BaseSettings

  • ModelConfig получает функциональность для работы с переменными окружения и .env файлами.

Атрибут model_config

  • Определяет параметры конфигурации через SettingsConfigDict.
  • env_file=".env" — указывает, что настройки загружаются из файла .env.
  • env_file_encoding="utf-8" — устанавливает кодировку файла.
  • extra="ignore" — игнорирует неизвестные переменные окружения, чтобы избежать ошибок.

Класс SettingsDB

class SettingsDB(ModelConfig):
    """Класс для данных БД"""
    
    name_db: str
    
    @property
    def get_url_db(self) -> str:
        """Метод вернёт URL для подключения к БД"""
        
        return f'sqlite:///{self.name_db}'

Разбор класса:

Наследуется от ModelConfig:

  • Получает возможность загружать настройки из .env и использовать конфигурацию, определённую в ModelConfig;

Атрибут name_db: str:

  • Описывает имя базы данных в виде строки;
  • Это имя будет использовано для формирования строки подключения;

Метод get_url_db (с декоратором @property):

  • Автоматически формирует url для подключения к SQLite3;
  • Использует шаблон 'sqlite:///{self.name_db}', где self.name_db — название файла БД;
  • Позволяет обращаться к get_url_db как к атрибуту (settings.db_settings.get_url_db).

Класс Settings

class Settings(ModelConfig):
    """Класс для данных конфига"""
    
    db_settings: SettingsDB = SettingsDB()

Разбор класса:

Наследуется от ModelConfig:

  • Получает доступ к настройкам, загружаемым из .env;
  • Использует SettingsConfigDict для обработки переменных окружения;

Атрибут db_settings: SettingsDB:

  • Определяет объект настроек БД, который создаётся из SettingsDB();
  • Позволяет обращаться к данным БД (settings.db_settings.name_db);
  • Включает метод get_url_db, который формирует URL для подключения к SQLite3.

Инициализация настроек

settings = Settings()

Инициализация класса настроек для последующего использования.

Весь код settings.py

from pydantic_settings import BaseSettings, SettingsConfigDict


class ModelConfig(BaseSettings):
    """Модель конфигурации"""
        
    model_config = SettingsConfigDict(
        env_file='.env',
        env_file_encoding='utf-8',
        extra='ignore'
     )
    
class SettingsDb(ModelConfig):
    """Класс для данных БД"""
    
    name_db: str
    
    @property
    def get_url_db(self) -> str:
        """Метод вернёт URL для подключения к БД"""
        
        return f"sqlite:///{self.name_db}"

class Settings(ModelConfig):
    """Класс для данных конфига"""
    
    db_settings: SettingsDb = SettingsDb()


settings = Settings()

Сессия

Сессия базы данных представляет собой объект или механизм, управляющий взаимодействием с БД, обеспечивающий выполнение транзакций и запросов.

Для её реализации будет использоваться sessionmaker — фабрика сессий, основанная на шаблоне проектирования "Фабрика" (Factory Pattern). Этот паттерн применяется для создания объектов без явного указания их конкретного класса, что упрощает управление подключениями и повышает гибкость архитектуры.

Основные функции сессии:

  • Открывает соединение с базой данных;
  • Позволяет выполнять SQL-запросы;
  • Отслеживает изменения объектов перед сохранением;
  • Коммитит (сохраняет) или откатывает (rollback) транзакции;
  • Закрывает соединение после завершения работы.

Импорты

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from kawai_focus.settings import settings

Разбор импортируемых модулей:

create_engine (sqlalchemy):

  • Создаёт объект подключения к базе данных;
  • Позволяет управлять соединением с БД;
  • Используется для передачи данных в ORM;

sessionmaker (sqlalchemy.orm):

  • Фабрика для создания сессий (Session);
  • Позволяет управлять транзакциями, запросами и сохранением данных;

Session (sqlalchemy.orm):

  • Класс сессии, который используется для выполнения операций с БД;
  • Позволяет делать SQL-запросы, коммитить изменения и откатывать транзакции;

settings (kawai_focus.settings):

  • Кастомный модуль настроек приложения;
  • Используется для получения URL подключения к БД.

Класс SessionDB

class SessionDB:  
    """Класс для управления подключением к базе данных."""
    
    def __init__(self) -> None:  
        self._engine = create_engine(
            url=settings.db_settings.get_url_db, 
            echo=settings.db_settings.echo_db
        )  
        self._session_factory = sessionmaker(
            bind=self._engine, 
            expire_on_commit=False, 
            autocommit=False
        )  
    
    @property  
    def get_session(self) -> Session:  
        """Метод для получения сессии"""
        
        return self._session_factory

SessionDB — класс для управления подключением к базе данных, который инкапсулирует создание движка (engine) и фабрики сессий (sessionmaker) в SQLAlchemy.

Разбор класса:

Конструктор __init__:

  • Создаёт объект engine с параметрами подключения (url=settings.db_settings.get_url_db);
  • echo=settings.db_settings.echo_db — включает/отключает вывод SQL-запросов в консоль для дебага;
  • Создаёт фабрику сессий (sessionmaker), привязанную к engine;
  • expire_on_commit=False — делает так, чтобы объекты оставались активными после commit();
  • autocommit=False — отключает автоматическое коммитирование, требуя явного вызова commit().

Свойство get_session:

  • Возвращает готовую сессию.

Инициализация класса сессии

db = SessionDB()

Разбор кода:

db = SessionDB():

  • Создаёт экземпляр класса SessionDB;

Весь код session.py

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from kawai_focus.settings import settings


class SessionDB:  
    """Класс для управления подключением к базе данных."""
    
    def __init__(self) -> None:  
        self._engine = create_engine(
            url=settings.db_settings.get_url_db, 
            echo=settings.db_settings.echo_db
        )  
        self._session_factory = sessionmaker(
            bind=self._engine, 
            expire_on_commit=False, 
            autocommit=False
        )  
    
    @property  
    def get_session(self) -> Session:  
        """Метод для получения сессии"""
        
        return self._session_factory


db = SessionDB()

Модели

Теперь я создам модель, которая будет отвечать за хранение настроек таймера. В пакете database уже подготовлены два файла: models.py и mixins.py.

mixins.py

Миксины позволяют переиспользовать общие поля моделей, исключая дублирование кода. Одним из таких полей является id, который присутствует почти в каждой таблице базы данных.

В файле mixins.py создан класс IDMixin, предназначенный для повторного использования id поля в различных моделях.

Содержимое mixins.py:

from sqlalchemy.orm import Mapped, mapped_column  
from sqlalchemy import Integer


class IDMixin:
    """Базовый класс для моделей с id"""
    
    id: Mapped[int] = mapped_column(
        Integer,
        name="id",
        primary_key=True,
        autoincrement=True
    )

Разбор класса:

Наследование от Mapped[int]:

  • id объявляется как типизированное поле, соответствующее int;
  • Использует Mapped, который помогает работать с аннотированными типами в SQLAlchemy 2;

Использование mapped_column():

  • Integer — указывает, что поле id является целочисленным;
  • name="id" — явно задаёт имя столбца в таблице;
  • primary_key=True — делает id первичным ключом, который уникально идентифицирует запись;
  • autoincrement=True — указывает, что значения id будут автоматически увеличиваться при добавлении новых записей.

models.py

В файле models.py будут находится модели Base и Timer:

  • Base — базовая модель;
  • Timer — модель с данными таймера.

Импорты

from sqlalchemy.orm import DeclarativeBase, mapped_column, Mapped
from sqlalchemy import Integer, String
from kawai_focus.database.mixins import IDMixin

Разбор импортов:

DeclarativeBase (sqlalchemy.orm):

  • Базовый класс для декларативного объявления моделей в SQLAlchemy 2.0;
  • Позволяет создавать ORM-модели, используя Python-аннотации типов;

mapped_column (sqlalchemy.orm):

  • Функция для явного указания параметров столбца в ORM-модели;
  • Используется внутри аннотированных Mapped[...] полей;

Mapped (sqlalchemy.orm):

  • Позволяет аннотировать поля в ORM-модели, указывая их типы (Mapped[int], Mapped[str]);
  • Используется в SQLAlchemy 2.0 для улучшенной работы с типами данных;

Integer (sqlalchemy):

  • Определяет целочисленный (INTEGER) тип данных для столбца в БД;

String (sqlalchemy):

  • Определяет строковый (VARCHAR) тип данных для столбца в БД;

IDMixin (kawai_focus.database.mixins):

  • Кастомный миксин, который, вероятно, добавляет в модели поле id как первичный ключ;
  • Упрощает создание моделей с уникальным идентификатором, избегая дублирования кода.

Модель Base

class Base(DeclarativeBase):
    """Класс для корректной работы аннотаций"""
    
    pass

Разбор класса:

Наследование от DeclarativeBase:

  • DeclarativeBase заменяет declarative_base() и предоставляет удобный способ объявления моделей;
  • Позволяет корректно работать с аннотациями типов (Mapped[...]);
  • Упрощает наследование для всех моделей БД;

Почему pass ?

  • В этом классе нет дополнительных атрибутов, он просто определяет базу для всех моделей;
  • В дальнейшем все модели могут наследоваться от Base, получая доступ к механизмам ORM.

Модель Timer

class Timer(Base):
    """Модель настроек таймера"""
    
    __tablename__ = 'timer'
    
    title: Mapped[str] = mapped_column(
        String(length=200),
        name='название',
        nullable=False
    )
    pomodoro_time: Mapped[int] = mapped_column(
        Integer,
        name='время помидора',
        nullable=False
    )
    break_time: Mapped[int] = mapped_column(
        Integer,
        name='время перерыва',
        nullable=False
    )
    break_long_time: Mapped[int] = mapped_column(
        Integer,
        name='время долгого перерыва',
        nullable=False
    )
    count_pomodoro: Mapped[int] = mapped_column(
        Integer,
        name='количество помидоров',
        nullable=False
    )

Разбор кода:

__tablename__ = 'timer':

  • Определяет название таблицы в базе данных (timer);

Поле title (название таймера):

  • Mapped[str] — аннотация типа, указывает, что поле хранит строку;
  • mapped_column(String(length=200)) — ограничение длины до 200 символов;
  • name='название' — имя столбца в БД (явно указано на русском);
  • nullable=False — поле обязательное, оно не может быть NULL;

Поле pomodoro_time (время работы в Pomodoro);

  • Mapped[int] — тип данных Integer;
  • nullable=False — поле обязательно к заполнению;

Остальные поля break_time, break_long_time и count_pomodoro устроены так же как и поле pomodoro_time.

Весь код models.py

from sqlalchemy.orm import DeclarativeBase, mapped_column, Mapped
from sqlalchemy import Integer, String
from kawai_focus.database.mixins import IDMixin


class Base(DeclarativeBase):
    """Класс для корректной работы аннотаций"""
    
    pass
    
    
class Timer(Base):
    """Модель настроек таймера"""
    
    __tablename__ = 'timer'
    
    title: Mapped[str] = mapped_column(
        String(length=200),
        name='название',
        nullable=False
    )
    pomodoro_time: Mapped[int] = mapped_column(
        Integer,
        name='время помидора',
        nullable=False
    )
    break_time: Mapped[int] = mapped_column(
        Integer,
        name='время перерыва',
        nullable=False
    )
    break_long_time: Mapped[int] = mapped_column(
        Integer,
        name='время долгого перерыва',
        nullable=False
    )
    count_pomodoro: Mapped[int] = mapped_column(
        Integer,
        name='количество помидоров',
        nullable=False
    )

Миграции

Теперь самое время настроить миграции. Для этого я буду использовать Alembic — мощный инструмент для управления изменениями в базе данных, работающий поверх SQLAlchemy. Он помогает отслеживать версионность схемы, вносить изменения и управлять ими эффективно.

Также я буду использовать Ruff — быстрый линтер и форматтер для Python, который помогает поддерживать чистый и структурированный код. В паре с Alembic он полезен для обеспечения качества миграционных скриптов и предотвращения ошибок.

Устанавливаю alembic и ruff:

poetry add alembic ruff

Инициализация среды миграции:

alembic init kawai_focus/database/alembic

Основную информацию о настройке alembic можно найти в статье FastAPI 4. Модель пользователя, миксины и Alembic автора proDream на сайте Код на салфетке.

Создание миграции

alembic revision --autogenerate -m "migrration timer"

Результат выполнения команды:

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'timer'
  Generating /media/user_name/Files/Programming/Python/Projects/kawai-focus/kawai_focus/database/alembic/versions/2025_05_06_2026-e3e6ab4aede7_migrration_timer.py ...  done
  Running post write hook 'ruff' ...
1 file reformatted
  done

Разбор выполнения команды:

Context impl SQLiteImpl:

  • Alembic обнаружил, что используется SQLite3;

Will assume non-transactional DDL:

  • В SQLite3 DDL-команды (изменения структуры БД) не поддерживают транзакции, поэтому Alembic не будет применять их внутри BEGIN ... COMMIT;
  • У базы данных SQLite3 нет возможности откатывать изменения с помощью ROLLBACK;
  • Нельзя удалять и изменять столбцы напрямую;
  • Для глобальных изменений требуется создавать новую базу данных;
  • В моём случае таблицы довольно простые, и я не планирую их изменять;
  • Для добавления новых полей я буду создавать новую таблицу (конечно, не ради одного поля);

Detected added table 'timer':

  • Alembic обнаружил, что в моделях появилась новая таблица timer;

Generating ... migration_timer.py ... done:

  • Сгенерирован новый файл миграции в versions/, содержащий SQL-операции для добавления таблицы;

Running post write hook 'ruff':

  • Запущен Ruff для автоформатирования кода миграции;

1 file reformatted:

  • Ruff отформатировал файл, исправив стиль и возможные ошибки.

Применение миграции

alembic upgrade head

Результат работы команды:

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> e3e6ab4aede7, migrration timer

Разбор строки вывода INFO [alembic.runtime.migration] Running upgrade -> e3e6ab4aede7, migrration timer:

  • INFO [alembic.runtime.migration] → Alembic сообщает, что выполняет процесс миграции;
  • Running upgrade → Применяется новая миграция (добавление/изменение таблиц, колонок);
  • -> e3e6ab4aede7 → Это уникальный идентификатор миграции (revision ID);
  • migrration timer → Название миграции.

Валидация

Перед сохранением данных в модель необходимо проверять их типы и структуру. Поэтому я создал в пакете kawai_focus файл schemas.py, где теперь будут находиться все валидаторы приложения.

Внутри этого файла я создам класс TimerModel, который будет отвечать за проверку типов данных и определение схемы для модели Timer.

Содержимое schemas.py:

from pydantic import BaseModel


class TimerModel(BaseModel):
    """Модель для валидации данных таймера"""
    
    id: int | None = None
    title: str
    pomodoro_time: int
    break_time: int
    break_long_time: int
    count_pomodoro: int

Разбор кода:

Наследование от BaseModel:

  • BaseModel из Pydantic автоматически проверяет типы данных при создании объекта;

Поле id: int | None = None:

  • int | None → Поле может быть целым числом или None (например, если объект ещё не сохранён в БД);
  • = None -> Значение по умолчание для id (нужно для создания нового таймера).

Далее идут остальные поля.

CRUD операции

CRUD-операция — это процесс управления данными в базе, включающий создание (Create), чтение (Read), обновление (Update) и удаление (Delete). Сейчас мне нужно написать две функции с crud операциями:

  • get_timer — получение данных таймера по id (Read -> Select в SQL);
  • new_timer — создание нового таймера (Create -> Insert в SQL).

Импорты

from sqlalchemy import select, insert
from sqlalchemy.exc import SQLAlchemyError, OperationalError, NoResultFound
from pydantic import ValidationError
from kawai_focus.database.validators import TimerValidModel
from kawai_focus.database.session import db
from kawai_focus.database.models import Timer
from kawai_focus.main import Logger

Разбор импортов:

select, insert (sqlalchemy):

  • select — используется для выполнения SQL-запросов на чтение (SELECT * FROM ...);
  • insert — позволяет вставлять данные в таблицу (INSERT INTO ...);

SQLAlchemyError, OperationalError, NoResultFound (sqlalchemy.exc):

  • SQLAlchemyError — базовый класс ошибок SQLAlchemy;
  • OperationalError — ошибка, связанная с работой базы (например, проблемы с соединением);
  • NoResultFound — возникает, если SELECT не нашёл нужных данных;

ValidationError (pydantic):

  • Ошибка валидации данных, возникающая при проверке моделей Pydantic;
  • Полезна для проверки корректности входных данных перед записью в БД;

TimerValidModel (kawai_focus.database.validators):

  • Модель валидации таймера;

session (kawai_focus.database.session);

  • Объект сессии SQLAlchemy, используемый для выполнения запросов к БД.
  • Инкапсулирует логику работы с подключением и транзакциями;

Timer (kawai_focus.database.models):

  • ORM-модель таймера, представляющая таблицу timer в базе данных;
  • Содержит поля, такие как title, pomodoro_time, break_time и другие;

Logger (kawai_focus.main):

  • Объект для логирования событий, ошибок или операций в приложении.

Функция get_timer()

def get_timer(timer_id: int) -> TimerValidModel:
    """Функция для получения данных таймера"""
    
    try:
        with db.get_session() as session:
            timer_model = Timer 
            query = select(
                timer_model.id, 
                timer_model.title,
                timer_model.pomodoro_time, 
                timer_model.break_time, 
                timer_model.break_long_time,
                timer_model.count_pomodoro
            ).where(timer_id  timer_model.id)
            result = session.execute(query)
            timer = result.mappings().first()
            return TimerValidModel.model_validate(obj=timer, from_attributes=True)
    except (ConnectionError, SQLAlchemyError, TimeoutError, OperationalError, ValidationError, NoResultFound) as err:
        Logger.error(f'{err.__class__.__name__}: {err}')

Разбор кода:

Аргумент timer_id: int:

  • Ожидает int → идентификатор таймера;

Открытие сессии (with db.get_session() as session:):

  • Гарантирует, что соединение с БД будет корректно закрыто после выполнения запроса;

Формирование SQL-запроса (select):

  • timer_model = Timer → получает ORM-модель Timer;
  • select(...) → выбирает id, title, pomodoro_time и другие поля;
  • .where(timer_id timer_model.id) → фильтрует записи, оставляя только таймер с нужным id;

Выполнение запроса (session.execute(query)):

  • Запускает SQL-запрос, получая данные из таблицы timer;
  • .mappings().first() → извлекает первую найденную запись (None, если таймера с таким id нет);

Валидация данных через Pydantic:

  • TimerValidModel.model_validate(obj=timer, from_attributes=True);
  • Преобразует объект таймера в модель Pydantic, проверяя типы и значения;

Обработка ошибок (try-except):

  • Ловит ошибки подключения (ConnectionError, OperationalError);
  • Ошибки в SQLAlchemy (SQLAlchemyError, TimeoutError, NoResultFound);
  • Ошибка валидации (ValidationError);
  • Логирует ошибку через Logger.error(...).

Функция new_timer()

def new_timer(data: TimerValidModel) -> bool | None:
    """Функция для создания нового таймера"""
    
    try:
        with db.get_session() as session:
            timer_model = Timer
            query = insert(timer_model).values(**data.model_dump())
            session.execute(query)
            session.commit()
    except (ConnectionError, SQLAlchemyError, TimeoutError, OperationalError, ValidationError) as err:
        Logger.error(f'{err.__class__.__name__}: {err}')
    else:
        return True

Разбор кода:

Аргумент data: TimerValidModel:

  • Получает объект TimerValidModel, который содержит проверенные данные таймера;
  • model_dump() → Преобразует Pydantic-модель в словарь для SQLAlchemy;

Открытие сессии (with db.get_session() as session:):

  • Гарантирует, что соединение с БД будет автоматически закрыто после выполнения запросов;

Создание SQL-запроса insert:

  • query = insert(timer_model).values(**data.model_dump());
  • insert(timer_model) → вставляет новую запись в таблицу timer;
  • .values(**data.model_dump()) → передаёт данные таймера в виде dict;

Выполнение SQL-запроса (session.execute(query)):

  • Запускает команду INSERT INTO timer (...) VALUES (...);

Сохранение изменений (session.commit()):

  • Подтверждает транзакцию, записывая новый таймер в базу;

Обработка ошибок аналогична с функцией get_timer().


Улучшение TimerValidator

В моём проекте сейчас есть два файла: validators.py и schemas.py, которые находятся в разных частях программы. Однако их можно объединить в один, чтобы упростить структуру.

Класс TimerValidator написан с использованием декоратора @dataclass, тогда как TimerValidModel наследуется от BaseModel из pydantic.

Старый код класса TimerValidator

@dataclass
class TimerValidator:
    """Класс для валидации функции таймера."""
    
    hh: int = 0
    mm: int = 0
    ss: int = 0
    
    def __post_init__(self):
        if not all(isinstance(value, int) for value in (self.hh, self.mm, self.ss)):
            raise TypeError(ErrorMessage.NOT_INT_TYPE.value)
        if self.ss  0 and self.mm  0 and self.hh  0:
            raise ValueError(ErrorMessage.NO_TIME.value)
        if self.ss < 0 or self.mm < 0 or self.hh < 0:
            raise ValueError(ErrorMessage.NEGATIVE_TIME.value)
        if self.ss > 59 or self.mm > 59:
            raise ValueError(ErrorMessage.SS_MM_BIG.value)
        if self.hh > 23:
            raise ValueError(ErrorMessage.HH_BIG.value)

Поскольку в проекте используется pydantic для валидации типов данных, применение dataclasses не имеет большого смысла. Логичнее переписать TimerValidator, чтобы использовать возможности pydantic и превратить его из валидатора в схему данных.

TimerTimeModel

Удаляю старый validators.py из пакета utils.py, поскольку его логичнее разместить в разделе схем, а не утилит. Создаю класс TimerTimeModel в kawai_focus/schemas.py. Название TimerTimeModel лучше отражает его назначение, так как он отвечает за структуру и валидацию данных времени таймера.

Я буду использовать класс Field, который позволит избавиться от множества валидаторов, сократить объем кода и задействовать встроенные механизмы валидации.

Содержимое класса TimerTimeModel:

class TimerTimeModel(BaseModel):
    """Модель для валидации времени таймера"""
    
    hh: int = Field(0, ge=0, le=23)
    mm: int = Field(0, ge=0, le=59)
    ss: int = Field(0, ge=0, le=59)
    
    @field_validator('hh', 'mm', 'ss')
    @classmethod
    def check_all_time_fields(cls, value: int) -> int:
        """Метод валидирует все поля времени"""
        
        # гарантирует, что время не равно 00:00:00
        if value  0:
            raise ValueError(ErrorMessage.NO_TIME.value)
        
        return value

Разбор кода:

1. Определение базовой модели:

TimerTimeModel наследуется от BaseModel (Pydantic), что позволяет автоматически проверять данные на соответствие типам;

Поля:

  • hh: int = Field(0, ge=0, le=23) — Определяет поле hh (часы) с начальным значением 0. Задает границы допустимых значений: от 0 до 23;
  • mm: int = Field(0, ge=0, le=59) — Определяет поле mm (минуты) с начальным значением 0. Ограничивает диапазон значений от 0 до 59;
  • ss: int = Field(0, ge=0, le=59) — Определяет поле ss (секунды) с начальным значением 0. Устанавливает допустимый диапазон от 0 до 59;

2. Валидатор check_all_time_fields:

  • Проверяет все поля времени (hh, mm, ss);
  • Запрещает значение 00:00:00 (ValueErrorErrorMessage.NO_TIME.value);
  • Проверяет, что переданные значения типа int (TypeError).

Анонс на следующие статьи

Следующая моя статья выйдет 29 мая 2025 года. Прежде чем приступать к разработке экрана конструктора, я должен убедиться, что новый код работает корректно. Поэтому в следующей статье я планирую написать тесты для валидаторов, модели и CRUD-операций.

Если у вас есть мысли о том, как можно улучшить проект, пишите в комментариях — с удовольствием ознакомлюсь с вашими предложениями!

Читайте продолжение — не пропустите!


Заключение

  1. База данных SQLite3 подключена к проекту;
  2. Создана модель таймера;
  3. Подключены alembic и ruff для миграций;
  4. Созданы функции CRUD операций для создания и получения таймера;
  5. Написаны два валидатора на pydantic;
  6. Разобраны недостатки SQLIte3.

Ссылки к статье

Автор

    Нет комментариев

    Реклама