Добавил таймауты для API-запросов, уточнил парсинг тегов и типизацию аргументов
This commit is contained in:
@@ -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
3
api/constants.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""Константы API-клиентов."""
|
||||||
|
|
||||||
|
REQUEST_TIMEOUT = 30
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
9
utils.py
9
utils.py
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user