diff --git a/api/clockify.py b/api/clockify.py index 8fe6f2f..d88f739 100644 --- a/api/clockify.py +++ b/api/clockify.py @@ -1,6 +1,5 @@ from datetime import datetime import logging -from typing import Union from colorama import Fore import requests @@ -10,6 +9,7 @@ from config import ClockifyConfig, config, db from classes import Activity, Tag, VetroProject from exceptions import InvalidToken from utils import parse_task_id, time_to_hour +from .constants import REQUEST_TIMEOUT logger = logging.getLogger(__name__) @@ -33,7 +33,12 @@ class ClockifyAPI(ClockifyConfig): page-size: int - Размер страницы (по умолчанию 50) """ time_entries_url = self.time_entries_url - activities = requests.get(self.base_url + time_entries_url, params=kwargs, headers={"X-API-KEY": self.token}).json() + activities = requests.get( + self.base_url + time_entries_url, + params=kwargs, + headers={"X-API-KEY": self.token}, + timeout=REQUEST_TIMEOUT, + ).json() if not activities: return [] logger.info(f"Получено задач: {len(activities)}") @@ -100,7 +105,7 @@ class ClockifyAPI(ClockifyConfig): return result - def get_tags(self, is_updating=False, tag_id="", **kwargs) -> Union[list[Tag], Tag, None]: + def get_tags(self, is_updating=False, tag_id="", **kwargs) -> list[Tag] | Tag | None: """ Получает с сервера Clockify теги name: str - optional @@ -122,7 +127,8 @@ class ClockifyAPI(ClockifyConfig): tags = requests.get( self.base_url + self.tags_url, params=params, - headers={"X-API-KEY": self.token} + headers={"X-API-KEY": self.token}, + timeout=REQUEST_TIMEOUT, ).json() for tag in tags: @@ -150,10 +156,18 @@ class ClockifyAPI(ClockifyConfig): """ Получает с сервера Clockify Проекты (ДО) Ветро """ - projects = requests.get(self.base_url + self.projects_url, headers={"X-API-KEY": self.token}).json() + projects = requests.get( + self.base_url + self.projects_url, + headers={"X-API-KEY": self.token}, + timeout=REQUEST_TIMEOUT, + ).json() for project in projects: if not project.get("archived"): - vetro_projects = requests.get(self.base_url + self.tasks_url(project.get("id")), headers={"X-API-KEY": self.token}).json() + vetro_projects = requests.get( + self.base_url + self.tasks_url(project.get("id")), + headers={"X-API-KEY": self.token}, + timeout=REQUEST_TIMEOUT, + ).json() return [VetroProject( id=vetro_project["id"], title=vetro_project["name"] @@ -180,7 +194,8 @@ class ClockifyAPI(ClockifyConfig): "projectId": activity.project_id }, headers={ "X-API-KEY": self.token - } + }, + timeout=REQUEST_TIMEOUT, ) if config.is_debug or resp.ok: logger.info(f"{Fore.GREEN}{activity}{Fore.RESET}".ljust(60) + "✅") @@ -193,7 +208,11 @@ class ClockifyAPI(ClockifyConfig): def _check_token(self, token: str) -> str: """Проверяет токен""" - resp = requests.get(self.base_url + self.user_url, headers={"X-API-KEY": token}) + resp = requests.get( + self.base_url + self.user_url, + headers={"X-API-KEY": token}, + timeout=REQUEST_TIMEOUT, + ) if not resp.ok: logger.error(resp.json()) raise InvalidToken @@ -203,7 +222,11 @@ class ClockifyAPI(ClockifyConfig): def get_user(self): """Получает нужные данные пользователя с сервера Clockify""" - resp = requests.get(self.base_url + self.user_url, headers={"X-API-KEY": self.token}) + resp = requests.get( + self.base_url + self.user_url, + headers={"X-API-KEY": self.token}, + timeout=REQUEST_TIMEOUT, + ) if not resp.ok: logger.error(resp.json()) raise requests.exceptions.RequestException(Fore.RED + "Ошибка запроса." + Fore.RESET) diff --git a/api/constants.py b/api/constants.py new file mode 100644 index 0000000..5752f4b --- /dev/null +++ b/api/constants.py @@ -0,0 +1,3 @@ +"""Константы API-клиентов.""" + +REQUEST_TIMEOUT = 30 diff --git a/api/redmine.py b/api/redmine.py index a657804..737eebd 100644 --- a/api/redmine.py +++ b/api/redmine.py @@ -8,14 +8,11 @@ from config import RedmineConfig, config from classes import Activity, ActivityType from enums import ActivityTypes from exceptions import InvalidToken +from .constants import REQUEST_TIMEOUT logger = logging.getLogger(__name__) -WORKSPACE_ID = config.WORKSPACE_ID -USER_ID = config.USER_ID - - class RedmineAPI(RedmineConfig): """API клиент для взаимодействия с Redmine. @@ -52,7 +49,8 @@ class RedmineAPI(RedmineConfig): "comments": activity.description, "spent_on": activity.date_start.strftime(self.datetime_format) } - } + }, + timeout=REQUEST_TIMEOUT, ) if config.is_debug or resp.ok: activity.is_tracked = True @@ -84,7 +82,11 @@ class RedmineAPI(RedmineConfig): def get_time_entry_activities(self) -> list[ActivityTypes]: """Получает виды Деятельности с сервера Redmine""" - resp = requests.get(self.base_url + self.time_entry_activities, headers={"X-Redmine-API-KEY": self.token}) + resp = requests.get( + self.base_url + self.time_entry_activities, + headers={"X-Redmine-API-KEY": self.token}, + timeout=REQUEST_TIMEOUT, + ) logger.debug(resp.json()) if resp.ok: return [ @@ -95,7 +97,11 @@ class RedmineAPI(RedmineConfig): def get_user(self): """Проверяет токен и возвращает данные юзера""" - resp = requests.get(self.base_url + self.my_account_url, headers={"X-Redmine-Api-Key": self.token}) + resp = requests.get( + self.base_url + self.my_account_url, + headers={"X-Redmine-Api-Key": self.token}, + timeout=REQUEST_TIMEOUT, + ) if not resp.ok: logger.error("Invalid Token for RedmineAPI") raise InvalidToken @@ -113,7 +119,7 @@ class RedmineAPI(RedmineConfig): resp = requests.get( self.base_url + self.issue.format(**{"id": issue_id}), headers={"X-Redmine-Api-Key": self.token}, - timeout=None + timeout=REQUEST_TIMEOUT, ) if not resp.ok: logger.error("Ошибка RedmineAPI") @@ -138,7 +144,7 @@ class RedmineAPI(RedmineConfig): self.base_url + self.issues, headers={"X-Redmine-Api-Key": self.token}, params={"limit": 100, "offset": 100 * i, "status_id": "*"}, - timeout=None + timeout=REQUEST_TIMEOUT, ) if not resp.ok or not resp.json(): logger.error("Ошибка RedmineAPI") diff --git a/config/__init__.py b/config/__init__.py index 83977d4..e90a352 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -67,6 +67,7 @@ def configure_argument_parser(): parser.add_argument( '-p', '--page-size', + type=int, help='Количество задач для вывода' ) parser.add_argument( diff --git a/utils.py b/utils.py index bc12d7f..093bc88 100644 --- a/utils.py +++ b/utils.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta import logging import os import re -from typing import Optional from colorama import Fore @@ -29,16 +28,18 @@ def today_end() -> datetime: return datetime(year=year, month=month, day=day, hour=23) -def str_to_date(date: str) -> Optional[datetime]: +def str_to_date(date: str) -> datetime | None: if date: return datetime.strptime(date + "T00:00:00Z", config.DATETIME_FORMAT) -def format_date(date: datetime) -> datetime: +def format_date(date: datetime) -> str | None: if date: return date.strftime(config.DATETIME_FORMAT) def parse_task_id(tag: str) -> int: - return int(re.search(r"^(?P\d{5})", tag).group("task_id")) + if match := re.search(r"^(?P\d{5})", tag): + return int(match.group("task_id")) + raise ValueError(f'Тег "{tag}" должен начинаться с номера задачи из 5 цифр') def db_update(): from config import clock, db, red