повысил надежность обработки API-ответов и ошибок БД
This commit is contained in:
@@ -33,12 +33,16 @@ 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(
|
resp = requests.get(
|
||||||
self.base_url + time_entries_url,
|
self.base_url + time_entries_url,
|
||||||
params=kwargs,
|
params=kwargs,
|
||||||
headers={"X-API-KEY": self.token},
|
headers={"X-API-KEY": self.token},
|
||||||
timeout=REQUEST_TIMEOUT,
|
timeout=REQUEST_TIMEOUT,
|
||||||
).json()
|
)
|
||||||
|
if not resp.ok:
|
||||||
|
logger.error(f"Ошибка ClockifyAPI: {resp.text} {resp.status_code}")
|
||||||
|
resp.raise_for_status()
|
||||||
|
activities = resp.json()
|
||||||
if not activities:
|
if not activities:
|
||||||
return []
|
return []
|
||||||
logger.info(f"Получено задач: {len(activities)}")
|
logger.info(f"Получено задач: {len(activities)}")
|
||||||
@@ -84,6 +88,9 @@ class ClockifyAPI(ClockifyConfig):
|
|||||||
task_id = parse_task_id(tag.title)
|
task_id = parse_task_id(tag.title)
|
||||||
|
|
||||||
author = db.get_user_by("clockify_user_id", activity["userId"])
|
author = db.get_user_by("clockify_user_id", activity["userId"])
|
||||||
|
if not author:
|
||||||
|
logger.error(f"Не найден автор активности {activity['id']}")
|
||||||
|
continue
|
||||||
|
|
||||||
result.append(
|
result.append(
|
||||||
Activity(
|
Activity(
|
||||||
@@ -124,12 +131,16 @@ class ClockifyAPI(ClockifyConfig):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
"page": page
|
"page": page
|
||||||
}
|
}
|
||||||
tags = requests.get(
|
resp = 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,
|
timeout=REQUEST_TIMEOUT,
|
||||||
).json()
|
)
|
||||||
|
if not resp.ok:
|
||||||
|
logger.error(f"Ошибка ClockifyAPI: {resp.text} {resp.status_code}")
|
||||||
|
resp.raise_for_status()
|
||||||
|
tags = resp.json()
|
||||||
|
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
clockify_tag = Tag(
|
clockify_tag = Tag(
|
||||||
@@ -156,18 +167,26 @@ class ClockifyAPI(ClockifyConfig):
|
|||||||
"""
|
"""
|
||||||
Получает с сервера Clockify Проекты (ДО) Ветро
|
Получает с сервера Clockify Проекты (ДО) Ветро
|
||||||
"""
|
"""
|
||||||
projects = requests.get(
|
resp = requests.get(
|
||||||
self.base_url + self.projects_url,
|
self.base_url + self.projects_url,
|
||||||
headers={"X-API-KEY": self.token},
|
headers={"X-API-KEY": self.token},
|
||||||
timeout=REQUEST_TIMEOUT,
|
timeout=REQUEST_TIMEOUT,
|
||||||
).json()
|
)
|
||||||
|
if not resp.ok:
|
||||||
|
logger.error(f"Ошибка ClockifyAPI: {resp.text} {resp.status_code}")
|
||||||
|
resp.raise_for_status()
|
||||||
|
projects = resp.json()
|
||||||
for project in projects:
|
for project in projects:
|
||||||
if not project.get("archived"):
|
if not project.get("archived"):
|
||||||
vetro_projects = requests.get(
|
resp = requests.get(
|
||||||
self.base_url + self.tasks_url(project.get("id")),
|
self.base_url + self.tasks_url(project.get("id")),
|
||||||
headers={"X-API-KEY": self.token},
|
headers={"X-API-KEY": self.token},
|
||||||
timeout=REQUEST_TIMEOUT,
|
timeout=REQUEST_TIMEOUT,
|
||||||
).json()
|
)
|
||||||
|
if not resp.ok:
|
||||||
|
logger.error(f"Ошибка ClockifyAPI: {resp.text} {resp.status_code}")
|
||||||
|
resp.raise_for_status()
|
||||||
|
vetro_projects = resp.json()
|
||||||
return [VetroProject(
|
return [VetroProject(
|
||||||
id=vetro_project["id"],
|
id=vetro_project["id"],
|
||||||
title=vetro_project["name"]
|
title=vetro_project["name"]
|
||||||
@@ -200,7 +219,7 @@ class ClockifyAPI(ClockifyConfig):
|
|||||||
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) + "✅")
|
||||||
else:
|
else:
|
||||||
logger.debug(resp.json())
|
logger.debug(resp.text)
|
||||||
logger.error(f"{Fore.RED}{activity}{Fore.RESET}".ljust(60) + "❌")
|
logger.error(f"{Fore.RED}{activity}{Fore.RESET}".ljust(60) + "❌")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
@@ -214,7 +233,7 @@ class ClockifyAPI(ClockifyConfig):
|
|||||||
timeout=REQUEST_TIMEOUT,
|
timeout=REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
logger.error(resp.json())
|
logger.error(resp.text)
|
||||||
raise InvalidToken
|
raise InvalidToken
|
||||||
resp = resp.json()
|
resp = resp.json()
|
||||||
logger.debug({"id": resp["id"], "email": resp["email"], "name": resp["name"]})
|
logger.debug({"id": resp["id"], "email": resp["email"], "name": resp["name"]})
|
||||||
@@ -228,7 +247,7 @@ class ClockifyAPI(ClockifyConfig):
|
|||||||
timeout=REQUEST_TIMEOUT,
|
timeout=REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
logger.error(resp.json())
|
logger.error(resp.text)
|
||||||
raise requests.exceptions.RequestException(Fore.RED + "Ошибка запроса." + Fore.RESET)
|
raise requests.exceptions.RequestException(Fore.RED + "Ошибка запроса." + Fore.RESET)
|
||||||
json = resp.json()
|
json = resp.json()
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -80,18 +80,21 @@ class RedmineAPI(RedmineConfig):
|
|||||||
logger.info(f"{Fore.RED}{activity}{Fore.RESET}".ljust(60) + "❌")
|
logger.info(f"{Fore.RED}{activity}{Fore.RESET}".ljust(60) + "❌")
|
||||||
return tracked
|
return tracked
|
||||||
|
|
||||||
def get_time_entry_activities(self) -> list[ActivityTypes]:
|
def get_time_entry_activities(self) -> list[ActivityType]:
|
||||||
"""Получает виды Деятельности с сервера Redmine"""
|
"""Получает виды Деятельности с сервера Redmine"""
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
self.base_url + self.time_entry_activities,
|
self.base_url + self.time_entry_activities,
|
||||||
headers={"X-Redmine-API-KEY": self.token},
|
headers={"X-Redmine-API-KEY": self.token},
|
||||||
timeout=REQUEST_TIMEOUT,
|
timeout=REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
logger.debug(resp.json())
|
if not resp.ok:
|
||||||
if resp.ok:
|
logger.error(f"Ошибка RedmineAPI: {resp.text} {resp.status_code}")
|
||||||
|
return []
|
||||||
|
data = resp.json()
|
||||||
|
logger.debug(data)
|
||||||
return [
|
return [
|
||||||
ActivityType(id=act.get("id"), title=act.get("name"))
|
ActivityType(id=act.get("id"), title=act.get("name"))
|
||||||
for act in resp.json().get("time_entry_activities")
|
for act in data.get("time_entry_activities", [])
|
||||||
if act.get("active")
|
if act.get("active")
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -103,7 +106,7 @@ class RedmineAPI(RedmineConfig):
|
|||||||
timeout=REQUEST_TIMEOUT,
|
timeout=REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
logger.error("Invalid Token for RedmineAPI")
|
logger.error(f"Invalid Token for RedmineAPI: {resp.text}")
|
||||||
raise InvalidToken
|
raise InvalidToken
|
||||||
json = resp.json()["user"]
|
json = resp.json()["user"]
|
||||||
return {
|
return {
|
||||||
@@ -114,7 +117,7 @@ class RedmineAPI(RedmineConfig):
|
|||||||
"username": json["login"]
|
"username": json["login"]
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_issue(self, issue_id: int) -> Task:
|
def get_issue(self, issue_id: int) -> Task | None:
|
||||||
"""Получает задачу по её id с сервера Redmine"""
|
"""Получает задачу по её id с сервера Redmine"""
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
self.base_url + self.issue.format(**{"id": issue_id}),
|
self.base_url + self.issue.format(**{"id": issue_id}),
|
||||||
@@ -122,8 +125,8 @@ class RedmineAPI(RedmineConfig):
|
|||||||
timeout=REQUEST_TIMEOUT,
|
timeout=REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
logger.error("Ошибка RedmineAPI")
|
logger.error(f"Ошибка RedmineAPI: {resp.text} {resp.status_code}")
|
||||||
return {}
|
return None
|
||||||
issue = resp.json()["issue"]
|
issue = resp.json()["issue"]
|
||||||
return Task(**{
|
return Task(**{
|
||||||
"id": issue["id"],
|
"id": issue["id"],
|
||||||
@@ -146,10 +149,14 @@ class RedmineAPI(RedmineConfig):
|
|||||||
params={"limit": 100, "offset": 100 * i, "status_id": "*"},
|
params={"limit": 100, "offset": 100 * i, "status_id": "*"},
|
||||||
timeout=REQUEST_TIMEOUT,
|
timeout=REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
if not resp.ok or not resp.json():
|
if not resp.ok:
|
||||||
logger.error("Ошибка RedmineAPI")
|
logger.error(f"Ошибка RedmineAPI: {resp.text} {resp.status_code}")
|
||||||
return {}
|
return []
|
||||||
issues.extend(resp.json()["issues"])
|
data = resp.json()
|
||||||
|
if not data:
|
||||||
|
logger.error("Ошибка RedmineAPI: пустой ответ")
|
||||||
|
return []
|
||||||
|
issues.extend(data["issues"])
|
||||||
return [Task(**{
|
return [Task(**{
|
||||||
"id": issue["id"],
|
"id": issue["id"],
|
||||||
"subject": issue["subject"],
|
"subject": issue["subject"],
|
||||||
|
|||||||
13
db/constants.py
Normal file
13
db/constants.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""Константы слоя базы данных."""
|
||||||
|
|
||||||
|
USER_LOOKUP_FIELDS = frozenset(
|
||||||
|
{
|
||||||
|
"id",
|
||||||
|
"clockify_user_id",
|
||||||
|
"redmine_user_id",
|
||||||
|
"clockify_email",
|
||||||
|
"clockify_username",
|
||||||
|
"redmine_email",
|
||||||
|
"redmine_username",
|
||||||
|
}
|
||||||
|
)
|
||||||
11
db/db.py
11
db/db.py
@@ -5,6 +5,7 @@ from colorama import Fore
|
|||||||
|
|
||||||
from classes import Activity, ActivityType, Tag, VetroProject
|
from classes import Activity, ActivityType, Tag, VetroProject
|
||||||
from classes.classes import Task
|
from classes.classes import Task
|
||||||
|
from db.constants import USER_LOOKUP_FIELDS
|
||||||
from enums import ActivityTypes
|
from enums import ActivityTypes
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -102,8 +103,9 @@ class Database:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.con.commit()
|
self.con.commit()
|
||||||
except sq.IntegrityError:
|
except sq.IntegrityError as e:
|
||||||
pass
|
self.con.rollback()
|
||||||
|
logger.error(f"Не удалось сохранить трудочасы по активности {act.id}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def insert_tags(self, tags: list[Tag]):
|
def insert_tags(self, tags: list[Tag]):
|
||||||
@@ -238,8 +240,13 @@ class Database:
|
|||||||
|
|
||||||
def get_user_by(self, by_field: str, value: str):
|
def get_user_by(self, by_field: str, value: str):
|
||||||
"""Получает пользователя из бд"""
|
"""Получает пользователя из бд"""
|
||||||
|
if by_field not in USER_LOOKUP_FIELDS:
|
||||||
|
raise ValueError(f"Недопустимое поле поиска пользователя: {by_field}")
|
||||||
try:
|
try:
|
||||||
user = self.cursor.execute(f"SELECT * FROM user WHERE {by_field} = ?", (value,)).fetchone()
|
user = self.cursor.execute(f"SELECT * FROM user WHERE {by_field} = ?", (value,)).fetchone()
|
||||||
|
if not user:
|
||||||
|
logger.error(f"Пользователь не найден: {by_field}={value}")
|
||||||
|
return None
|
||||||
return {
|
return {
|
||||||
"id": user[0],
|
"id": user[0],
|
||||||
"first_name": user[1],
|
"first_name": user[1],
|
||||||
|
|||||||
2
utils.py
2
utils.py
@@ -25,7 +25,7 @@ def today_end() -> datetime:
|
|||||||
year = td.year
|
year = td.year
|
||||||
month = td.month
|
month = td.month
|
||||||
day = td.day
|
day = td.day
|
||||||
return datetime(year=year, month=month, day=day, hour=23)
|
return datetime(year=year, month=month, day=day, hour=23, minute=59, second=59)
|
||||||
|
|
||||||
|
|
||||||
def str_to_date(date: str) -> datetime | None:
|
def str_to_date(date: str) -> datetime | None:
|
||||||
|
|||||||
Reference in New Issue
Block a user