Добавил таймауты для API-запросов, уточнил парсинг тегов и типизацию аргументов

This commit is contained in:
2026-07-06 16:01:24 +03:00
parent 8d19e1d43f
commit bd20ef4d48
5 changed files with 56 additions and 22 deletions

View File

@@ -1,6 +1,5 @@
from datetime import datetime from datetime import datetime
import logging import logging
from typing import Union
from colorama import Fore from colorama import Fore
import requests import requests
@@ -10,6 +9,7 @@ from config import ClockifyConfig, config, db
from classes import Activity, Tag, VetroProject from classes import Activity, Tag, VetroProject
from exceptions import InvalidToken from exceptions import InvalidToken
from utils import parse_task_id, time_to_hour from utils import parse_task_id, time_to_hour
from .constants import REQUEST_TIMEOUT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -33,7 +33,12 @@ class ClockifyAPI(ClockifyConfig):
page-size: int - Размер страницы (по умолчанию 50) page-size: int - Размер страницы (по умолчанию 50)
""" """
time_entries_url = self.time_entries_url 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: if not activities:
return [] return []
logger.info(f"Получено задач: {len(activities)}") logger.info(f"Получено задач: {len(activities)}")
@@ -100,7 +105,7 @@ class ClockifyAPI(ClockifyConfig):
return result 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 теги Получает с сервера Clockify теги
name: str - optional name: str - optional
@@ -122,7 +127,8 @@ class ClockifyAPI(ClockifyConfig):
tags = requests.get( tags = requests.get(
self.base_url + self.tags_url, self.base_url + self.tags_url,
params=params, params=params,
headers={"X-API-KEY": self.token} headers={"X-API-KEY": self.token},
timeout=REQUEST_TIMEOUT,
).json() ).json()
for tag in tags: for tag in tags:
@@ -150,10 +156,18 @@ class ClockifyAPI(ClockifyConfig):
""" """
Получает с сервера Clockify Проекты (ДО) Ветро Получает с сервера 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: for project in projects:
if not project.get("archived"): 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( return [VetroProject(
id=vetro_project["id"], id=vetro_project["id"],
title=vetro_project["name"] title=vetro_project["name"]
@@ -180,7 +194,8 @@ class ClockifyAPI(ClockifyConfig):
"projectId": activity.project_id "projectId": activity.project_id
}, headers={ }, headers={
"X-API-KEY": self.token "X-API-KEY": self.token
} },
timeout=REQUEST_TIMEOUT,
) )
if config.is_debug or resp.ok: if config.is_debug or resp.ok:
logger.info(f"{Fore.GREEN}{activity}{Fore.RESET}".ljust(60) + "") logger.info(f"{Fore.GREEN}{activity}{Fore.RESET}".ljust(60) + "")
@@ -193,7 +208,11 @@ class ClockifyAPI(ClockifyConfig):
def _check_token(self, token: str) -> str: 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: if not resp.ok:
logger.error(resp.json()) logger.error(resp.json())
raise InvalidToken raise InvalidToken
@@ -203,7 +222,11 @@ class ClockifyAPI(ClockifyConfig):
def get_user(self): def get_user(self):
"""Получает нужные данные пользователя с сервера Clockify""" """Получает нужные данные пользователя с сервера 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: if not resp.ok:
logger.error(resp.json()) logger.error(resp.json())
raise requests.exceptions.RequestException(Fore.RED + "Ошибка запроса." + Fore.RESET) raise requests.exceptions.RequestException(Fore.RED + "Ошибка запроса." + Fore.RESET)

3
api/constants.py Normal file
View File

@@ -0,0 +1,3 @@
"""Константы API-клиентов."""
REQUEST_TIMEOUT = 30

View File

@@ -8,14 +8,11 @@ from config import RedmineConfig, config
from classes import Activity, ActivityType from classes import Activity, ActivityType
from enums import ActivityTypes from enums import ActivityTypes
from exceptions import InvalidToken from exceptions import InvalidToken
from .constants import REQUEST_TIMEOUT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
WORKSPACE_ID = config.WORKSPACE_ID
USER_ID = config.USER_ID
class RedmineAPI(RedmineConfig): class RedmineAPI(RedmineConfig):
"""API клиент для взаимодействия с Redmine. """API клиент для взаимодействия с Redmine.
@@ -52,7 +49,8 @@ class RedmineAPI(RedmineConfig):
"comments": activity.description, "comments": activity.description,
"spent_on": activity.date_start.strftime(self.datetime_format) "spent_on": activity.date_start.strftime(self.datetime_format)
} }
} },
timeout=REQUEST_TIMEOUT,
) )
if config.is_debug or resp.ok: if config.is_debug or resp.ok:
activity.is_tracked = True activity.is_tracked = True
@@ -84,7 +82,11 @@ class RedmineAPI(RedmineConfig):
def get_time_entry_activities(self) -> list[ActivityTypes]: def get_time_entry_activities(self) -> list[ActivityTypes]:
"""Получает виды Деятельности с сервера Redmine""" """Получает виды Деятельности с сервера 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()) logger.debug(resp.json())
if resp.ok: if resp.ok:
return [ return [
@@ -95,7 +97,11 @@ class RedmineAPI(RedmineConfig):
def get_user(self): 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: if not resp.ok:
logger.error("Invalid Token for RedmineAPI") logger.error("Invalid Token for RedmineAPI")
raise InvalidToken raise InvalidToken
@@ -113,7 +119,7 @@ class RedmineAPI(RedmineConfig):
resp = requests.get( resp = requests.get(
self.base_url + self.issue.format(**{"id": issue_id}), self.base_url + self.issue.format(**{"id": issue_id}),
headers={"X-Redmine-Api-Key": self.token}, headers={"X-Redmine-Api-Key": self.token},
timeout=None timeout=REQUEST_TIMEOUT,
) )
if not resp.ok: if not resp.ok:
logger.error("Ошибка RedmineAPI") logger.error("Ошибка RedmineAPI")
@@ -138,7 +144,7 @@ class RedmineAPI(RedmineConfig):
self.base_url + self.issues, self.base_url + self.issues,
headers={"X-Redmine-Api-Key": self.token}, headers={"X-Redmine-Api-Key": self.token},
params={"limit": 100, "offset": 100 * i, "status_id": "*"}, params={"limit": 100, "offset": 100 * i, "status_id": "*"},
timeout=None timeout=REQUEST_TIMEOUT,
) )
if not resp.ok or not resp.json(): if not resp.ok or not resp.json():
logger.error("Ошибка RedmineAPI") logger.error("Ошибка RedmineAPI")

View File

@@ -67,6 +67,7 @@ def configure_argument_parser():
parser.add_argument( parser.add_argument(
'-p', '-p',
'--page-size', '--page-size',
type=int,
help='Количество задач для вывода' help='Количество задач для вывода'
) )
parser.add_argument( parser.add_argument(

View File

@@ -3,7 +3,6 @@ from datetime import datetime, timedelta
import logging import logging
import os import os
import re import re
from typing import Optional
from colorama import Fore from colorama import Fore
@@ -29,16 +28,18 @@ def today_end() -> datetime:
return datetime(year=year, month=month, day=day, hour=23) 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: if date:
return datetime.strptime(date + "T00:00:00Z", config.DATETIME_FORMAT) 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: if date:
return date.strftime(config.DATETIME_FORMAT) return date.strftime(config.DATETIME_FORMAT)
def parse_task_id(tag: str) -> int: def parse_task_id(tag: str) -> int:
return int(re.search(r"^(?P<task_id>\d{5})", tag).group("task_id")) if match := re.search(r"^(?P<task_id>\d{5})", tag):
return int(match.group("task_id"))
raise ValueError(f'Тег "{tag}" должен начинаться с номера задачи из 5 цифр')
def db_update(): def db_update():
from config import clock, db, red from config import clock, db, red