diff --git a/json/errors.json b/json/errors.json deleted file mode 100644 index a3a7afc..0000000 --- a/json/errors.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "no_time": "Не указано время таймера!", - "negative_time": "Время таймера не может быть отрицательным!", - "ss_mm_big": "Секунды и минуты не могут быть больше 59!", - "hh_big": "Часы не могут быть больше 23!" -} \ No newline at end of file diff --git a/kawai_focus/screens/timer_screen.py b/kawai_focus/screens/timer_screen.py index f8b1390..6f538a7 100644 --- a/kawai_focus/screens/timer_screen.py +++ b/kawai_focus/screens/timer_screen.py @@ -2,23 +2,22 @@ from kivy.uix.screenmanager import Screen from kivy.clock import Clock from kivy.core.audio import SoundLoader - -from kawai_focus.utils.utils import custom_timer -from kawai_focus.utils.data_json import read_json_timer +from kawai_focus.utils.utils import custom_timer, data_json +from kawai_focus.utils.validators import TimerValidator class TimerScreen(Screen): """Экран таймера""" - sound_timer_name = read_json_timer.get_text('sound_timer') + sound_timer_name = data_json.get_text('sound_timer') path_file = f'sounds/{sound_timer_name}' sound = SoundLoader.load(path_file) def __init__(self, **kwargs): super(TimerScreen, self).__init__(**kwargs) # Переменные для управления таймером - self.zero_time = read_json_timer.get_text('zero_time') - self.timer_generator = None + self.zero_time = data_json.get_text('zero_time') + self.timer_generator = None self.paused = False self.remaining_time = None self.sound_stop_event = None @@ -30,7 +29,8 @@ class TimerScreen(Screen): self.paused = False else: # Инициализация генератора таймера - self.timer_generator = custom_timer(ss=12) # Установите необходимое время + vaid_timer_data = TimerValidator(ss=12) # Установите необходимое время + self.timer_generator = custom_timer(valid_data=vaid_timer_data) self.remaining_time = next(self.timer_generator, self.zero_time) # Запуск обновления времени каждую секунду @@ -51,7 +51,7 @@ class TimerScreen(Screen): Clock.unschedule(self.update_time) self.paused = False # TODO: временное решение, время будет задаваться в интерфейсе - self.remaining_time = read_json_timer.get_text('custom_time') + self.remaining_time = data_json.get_text('custom_time') self.ids.time_label.text = self.remaining_time self.sound.stop() diff --git a/kawai_focus/tests/__init__.py b/kawai_focus/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kawai_focus/tests/validators_tests.py b/kawai_focus/tests/validators_tests.py new file mode 100644 index 0000000..fd3284e --- /dev/null +++ b/kawai_focus/tests/validators_tests.py @@ -0,0 +1,57 @@ +import pytest +from kawai_focus.utils.errors import ErrorMessage +from kawai_focus.utils.validators import TimerValidator + + +def test_valid_time(): + """Тест корректных данных""" + + timer = TimerValidator(hh=10, mm=30, ss=15) + assert timer.hh == 10 + assert timer.mm == 30 + assert timer.ss == 15 + + +def test_not_int_type(): + """Тест исключения для некорректного типа данных""" + + with pytest.raises(TypeError) as excinfo: + TimerValidator(hh="10", mm=30, ss=15) + + assert str(excinfo.value) == ErrorMessage.NOT_INT_TYPE.value + + +def test_no_time(): + """Тест исключения ситуации когда не указано время""" + + with pytest.raises(ValueError) as excinfo: + TimerValidator(hh=0, mm=0, ss=0) + + assert str(excinfo.value) == ErrorMessage.NO_TIME.value + + +def test_negative_time(): + """Тест исключения для отрицательного времени""" + + with pytest.raises(ValueError) as excinfo: + TimerValidator(hh=-1, mm=30, ss=15) + + assert str(excinfo.value) == ErrorMessage.NEGATIVE_TIME.value + + +def test_exceed_seconds_or_minutes(): + """Тест исключения для секунд/минут больше 59""" + + with pytest.raises(ValueError) as excinfo: + TimerValidator(hh=0, mm=60, ss=10) + + assert str(excinfo.value) == ErrorMessage.SS_MM_BIG.value + + +def test_exceed_hours(): + """Тест исключения для часов больше 23""" + + with pytest.raises(ValueError) as excinfo: + TimerValidator(hh=24, mm=0, ss=0) + + assert str(excinfo.value) == ErrorMessage.HH_BIG.value diff --git a/kawai_focus/utils/data_json.py b/kawai_focus/utils/data_json.py deleted file mode 100644 index a8acf8c..0000000 --- a/kawai_focus/utils/data_json.py +++ /dev/null @@ -1,36 +0,0 @@ -import json - -from kawai_focus.main import Logger - - -class ReadJson: - """Класс для чтения json файла""" - - def __init__(self, file_name: str): - self.file_name = file_name - self.json_data = self.read_json_as_dict() - - def read_json_as_dict(self) -> dict: - """Метод для чтения json файла как dict""" - - try: - file_path = f'json/{self.file_name}' - with open(file=file_path, mode='r') as file: - return json.load(file) - except (FileNotFoundError, json.decoder.JSONDecodeError) as err: - Logger.error(f'Logger: {err.__class__.__name__}: {err}') - - def get_text(self, name: str) -> str: - """Метод для возврата текста из файла json по ключу name""" - - text = self.json_data.get(name) - - if text: - if isinstance(text, list): - text = ''.join(text) - return text - return text - - -read_json_err = ReadJson(file_name='errors.json') -read_json_timer = ReadJson(file_name='timer.json') diff --git a/kawai_focus/utils/errors.py b/kawai_focus/utils/errors.py new file mode 100644 index 0000000..1b11bf7 --- /dev/null +++ b/kawai_focus/utils/errors.py @@ -0,0 +1,12 @@ +from enum import Enum + +class ErrorMessage(Enum): + NO_TIME = 'Не указано время таймера!' + NEGATIVE_TIME = 'Время таймера не может быть отрицательным!' + SS_MM_BIG = 'Секунды и минуты не могут быть больше 59!' + HH_BIG = 'Часы не могут быть больше 23!' + FOLDER_NOT_FOUND = 'Папка не найдена!' + JSON_NOT_FOUND = 'Файл json не найден!' + TYPE_DICT = 'Тип данных не является словарем!' + NOT_INT_TYPE = 'Тип данных не является целым числом!' + KEY_ERROR = 'Ключ не найден!' diff --git a/kawai_focus/utils/utils.py b/kawai_focus/utils/utils.py index b51aa18..80ba135 100644 --- a/kawai_focus/utils/utils.py +++ b/kawai_focus/utils/utils.py @@ -1,30 +1,59 @@ from typing import Generator +import json +from os import path, listdir from kawai_focus.main import Logger -from kawai_focus.utils.data_json import read_json_err +from kawai_focus.utils.errors import ErrorMessage +from kawai_focus.utils.validators import TimerValidator -def custom_timer(hh: int=0, mm: int=0, ss: int=0) -> Generator[str, None, None]: +class ReadJson: + """Класс для чтения всех JSON-файлов в директории и объединения их в словарь.""" + + def __init__(self, folder_json: str): + self.folder_json = folder_json + self._json_data = self._read_all_json() + + def _read_all_json(self) -> dict: + """Читает все JSON-файлы в директории и возвращает объединённый словарь.""" + + if not path.exists(self.folder_json): + raise FileNotFoundError(ErrorMessage.FOLDER_NOT_FOUND.value) + + # Получаем список всех файлов с расширением .json + json_files = [path.join(self.folder_json, file) for file in listdir(self.folder_json) if file.endswith('.json')] + + if not json_files: + raise FileNotFoundError(ErrorMessage.JSON_NOT_FOUND.value) + + data_dict = {} + + for file_path in json_files: + with open(file_path, 'r', encoding='utf-8') as file: + # Обновляем словарь с помощью .update() для эффективности + data_dict.update(json.load(file)) + + return data_dict + + def get_text(self, name: str) -> str: + """Возвращает текст из словаря по ключу.""" + + return self._json_data.get(name, ErrorMessage.KEY_ERROR.value) + + +data_json = ReadJson(folder_json='json') + + +def custom_timer(valid_data: TimerValidator) -> Generator[str, None, None]: """Функция отсчитывает время, установленное для таймера в формате 'hh:mm:ss'. Возвращает генератор, который возвращает текущее время в формате 'hh:mm:ss'.""" try: - if ss == 0 and mm == 0 and hh == 0: - raise ValueError(read_json_err.get_text('no_time')) - - if ss < 0 or mm < 0 or hh < 0: - raise ValueError(read_json_err.get_text('negative_time')) - - if ss > 59 or mm > 59: - raise ValueError(read_json_err.get_text('ss_mm_big')) - - if hh > 23: - raise ValueError(read_json_err.get_text('hh_big')) - - total_seconds = (int(hh or 0) * 3600) + (int(mm or 0) * 60) + (int(ss or 0)) + total_seconds = (int(valid_data.hh) * 3600) + (int(valid_data.mm) * 60) + (int(valid_data.ss)) for remaining in range(total_seconds, -1, -1): yield f"{remaining // 3600:02d}:{(remaining % 3600) // 60:02d}:{remaining % 60:02d}" except (TypeError, ValueError) as err: Logger.error(f'Logger: {err.__class__.__name__}: {err}') + \ No newline at end of file diff --git a/kawai_focus/utils/validators.py b/kawai_focus/utils/validators.py new file mode 100644 index 0000000..af893e6 --- /dev/null +++ b/kawai_focus/utils/validators.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from kawai_focus.utils.errors import ErrorMessage + + +@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) + diff --git a/poetry.lock b/poetry.lock index c87ff41..d20a9eb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -114,6 +114,19 @@ files = [ {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "docutils" version = "0.21.2" @@ -153,6 +166,18 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + [[package]] name = "kivy" version = "2.3.1" @@ -290,6 +315,34 @@ files = [ [package.dependencies] requests = "*" +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pygments" version = "2.19.1" @@ -321,6 +374,27 @@ files = [ [package.dependencies] pywin32 = ">=223" +[[package]] +name = "pytest" +version = "8.3.5" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + [[package]] name = "pywin32" version = "308" @@ -372,6 +446,18 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + [[package]] name = "urllib3" version = "2.3.0" @@ -393,4 +479,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "3fdbb9768a88f02f36e5874e93a09f51e4bbbd5c91a28267ab3651c3ac56af0e" +content-hash = "6d61809dec64d9e3c4bc761e94127766c1d194e07ecaf4ea1faf3b3f86ad5aa5" diff --git a/pyproject.toml b/pyproject.toml index e2d30b5..194b4b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,8 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "kivy (>=2.3.1,<3.0.0)", + "pytest (>=8.3.5,<9.0.0)", + "typing-extensions (>=4.13.2,<5.0.0)", ]