
Kawai.Focus - приложение для фокусировки внимания (часть 5)
Данная статья посвящена:
- Работе с фреймворком Kivy в проекте Kawai.Focus;
- Подключению БД SQLite3 и хранение в ней настроек таймера;
- Созданию CRUD операций для БД;
- Созданию валидатора c использованием
pydantic
.
Дополнительные материалы

Для скачивания материалов необходимо войти или зарегистрироваться
Файлы также можно получить в Telegram-боте по коду: 748192
Реклама

Вступление
Всем доброго дня! В предыдущей статье 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
(ValueError
→ErrorMessage.NO_TIME.value
); - Проверяет, что переданные значения типа
int
(TypeError
).
Анонс на следующие статьи
Следующая моя статья выйдет 29 мая 2025 года. Прежде чем приступать к разработке экрана конструктора, я должен убедиться, что новый код работает корректно. Поэтому в следующей статье я планирую написать тесты для валидаторов, модели и CRUD-операций.
Если у вас есть мысли о том, как можно улучшить проект, пишите в комментариях — с удовольствием ознакомлюсь с вашими предложениями!
Читайте продолжение — не пропустите!
Заключение
- База данных SQLite3 подключена к проекту;
- Создана модель таймера;
- Подключены
alembic
иruff
для миграций; - Созданы функции CRUD операций для создания и получения таймера;
- Написаны два валидатора на
pydantic
; - Разобраны недостатки SQLIte3.
Ссылки к статье
- Мои статьи Arduinum628 на Код на салфетке;
- Репозиторий проекта Kawai.Focus.
Все статьи