From f21acb23ab99e52d7dd6d47af56626aca45bae3d Mon Sep 17 00:00:00 2001 From: Eduard Date: Fri, 30 May 2025 12:19:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D0=B1?= =?UTF-8?q?=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/clockify.py | 27 +++++++++--- api/redmine.py | 24 +++++++---- classes/classes.py | 1 + config/RedmineTracker.sql | 88 +++++++++++++++++++++++---------------- db/db.py | 78 ++++++++++++++++++++++++++++------ track.py | 20 +++++++-- 6 files changed, 173 insertions(+), 65 deletions(-) diff --git a/api/clockify.py b/api/clockify.py index 858887a..2aba360 100644 --- a/api/clockify.py +++ b/api/clockify.py @@ -5,7 +5,7 @@ from colorama import Fore import requests from tqdm import tqdm -from config import ClockifyConfig, config +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 @@ -51,6 +51,9 @@ class ClockifyAPI(ClockifyConfig): except ValueError: logger.info(f'{activity["timeInterval"]["start"]}: {activity["description"]} - Неправильный формат тега {tag}') continue + + author = db.get_user_by("clockify_user_id", activity["userId"]) + result.append( Activity( id=activity["id"], @@ -62,7 +65,8 @@ class ClockifyAPI(ClockifyConfig): time_spent=time_to_hour((end if end else datetime.utcnow()) - start), date_start=start, date_end=end, - is_tracked=activity["billable"] + is_tracked=activity["billable"], + author_id=author["id"] ) ) logger.info(f"Сохранено задач: {len(result)}") @@ -135,16 +139,27 @@ class ClockifyAPI(ClockifyConfig): resp = resp.json() logger.debug({"id": resp["id"], "email": resp["email"], "name": resp["name"]}) return token - - def getenvs(self): - """Получает нужные USER_ID и WORKSPACE_ID с сервера Clockify""" + + def get_user(self): + """Получает нужные данные пользователя с сервера Clockify""" resp = requests.get(self.base_url + self.user_url, headers={"X-API-KEY": self.token}) if not resp.ok: logger.error(resp.json()) raise requests.exceptions.RequestException(Fore.RED + "Ошибка запроса." + Fore.RESET) + json = resp.json() + return { + "id": json['id'], + "workspace_id": json['activeWorkspace'], + "email": json["email"], + "username": json["name"] + } + + def getenvs(self): + """Получает нужные USER_ID и WORKSPACE_ID с сервера Clockify""" + user = self.get_user() with open(config.ENV_TXT, "w") as file: try: - file.write(f"USER_ID={resp.json()['id']}\nWORKSPACE_ID={resp.json()['activeWorkspace']}") + file.write(f"USER_ID={user['id']}\nWORKSPACE_ID={user['workspace_id']}") logger.info(f"{Fore.GREEN}Необходимые переменные были сохранены в файл {config.ENV_TXT}{Fore.RESET}") except Exception as e: logger.error(f"{Fore.RED}Не удалось сохранить необходимые переменные.{Fore.RESET}") diff --git a/api/redmine.py b/api/redmine.py index f5f8cc7..249740c 100644 --- a/api/redmine.py +++ b/api/redmine.py @@ -23,7 +23,8 @@ class RedmineAPI(RedmineConfig): """ def __init__(self, token): super().__init__() - self.token = self._check_token(token) + self.token = token + self._check_token() def track_activities(self, activities: list[Activity]) -> list[Activity]: """Заносит трудочасы на сервер Redmine""" @@ -75,12 +76,21 @@ class RedmineAPI(RedmineConfig): if act.get("active") ] - def _check_token(self, token): - """Проверяет токен""" - resp = requests.get(self.base_url + self.my_account_url, headers={"X-Redmine-Api-Key": token}) + def get_user(self): + """Проверяет токен и возвращает данные юзера""" + resp = requests.get(self.base_url + self.my_account_url, headers={"X-Redmine-Api-Key": self.token}) if not resp.ok: logger.error("Invalid Token for RedmineAPI") raise InvalidToken - resp = resp.json()['user'] - logger.debug({"login": resp["login"], "email": resp["mail"], "full_name": f'{resp["firstname"]} {resp["lastname"]}'}) - return token + json = resp.json()["user"] + return { + "id": json["id"], + "first_name": json["firstname"], + "last_name": json["lastname"], + "email": json["mail"], + "username": json["login"] + } + + def _check_token(self): + user = self.get_user() + logging.debug(user) diff --git a/classes/classes.py b/classes/classes.py index dab665d..e72d14a 100644 --- a/classes/classes.py +++ b/classes/classes.py @@ -7,6 +7,7 @@ from enums import ClockifyProjects, VetroProjects @dataclass class Activity: id: str + author_id: int vetro_project_id: VetroProjects task_id: int tag_id: str diff --git a/config/RedmineTracker.sql b/config/RedmineTracker.sql index 40914e7..bea228c 100644 --- a/config/RedmineTracker.sql +++ b/config/RedmineTracker.sql @@ -1,44 +1,60 @@ -CREATE TABLE IF NOT EXISTS "activities" ( - "id" VARCHAR NOT NULL UNIQUE, - "description" VARCHAR NOT NULL, - "time_spent" NUMERIC(3, 3) NOT NULL, - "date_start" DATETIME NOT NULL, - "date_end" DATETIME, - "is_tracked" BOOLEAN NOT NULL DEFAULT false, - "vetro_project_id" VARCHAR NOT NULL, - "tag_id" VARCHAR NOT NULL, - "project_id" VARCHAR NOT NULL, - PRIMARY KEY("id"), - FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON UPDATE NO ACTION ON DELETE NO ACTION, - FOREIGN KEY ("vetro_project_id") REFERENCES "vetro_projects"("id") ON UPDATE NO ACTION ON DELETE NO ACTION +CREATE TABLE IF NOT EXISTS "activity" ( + "id" VARCHAR NOT NULL UNIQUE, + "description" VARCHAR NOT NULL, + "author_id" INTEGER NOT NULL, + "time_spent" NUMERIC NOT NULL, + "date_start" DATETIME NOT NULL, + "date_end" DATETIME, + "is_tracked" BOOLEAN NOT NULL DEFAULT false, + "vetro_project_id" VARCHAR NOT NULL, + "tag_id" VARCHAR NOT NULL, + "project_id" VARCHAR NOT NULL, + PRIMARY KEY("id"), + FOREIGN KEY ("author_id") REFERENCES "user"("id") + ON UPDATE NO ACTION ON DELETE NO ACTION ); -CREATE TABLE IF NOT EXISTS "tags" ( - "id" VARCHAR NOT NULL UNIQUE, - "title" VARCHAR NOT NULL, - PRIMARY KEY("id") +CREATE TABLE IF NOT EXISTS "tag" ( + "id" VARCHAR NOT NULL UNIQUE, + "title" VARCHAR NOT NULL, + PRIMARY KEY("id") ); -CREATE TABLE IF NOT EXISTS "vetro_projects" ( - "id" VARCHAR NOT NULL UNIQUE, - "title" VARCHAR NOT NULL, - PRIMARY KEY("id") +CREATE TABLE IF NOT EXISTS "vetro_project" ( + "id" VARCHAR NOT NULL UNIQUE, + "title" VARCHAR NOT NULL, + PRIMARY KEY("id") ); - -CREATE TABLE IF NOT EXISTS "activity_types" ( - "id" INTEGER NOT NULL UNIQUE, - "title" VARCHAR NOT NULL, - PRIMARY KEY("id") + +CREATE TABLE IF NOT EXISTS "activity_type" ( + "id" INTEGER NOT NULL UNIQUE, + "title" VARCHAR NOT NULL, + PRIMARY KEY("id") ); CREATE TABLE IF NOT EXISTS "time_entry" ( - "id" INTEGER PRIMARY KEY, - "date" DATETIME NOT NULL, - "description" VARCHAR NOT NULL, - "activity_id" VARCHAR NOT NULL, - "time_spent" NUMERIC(3, 3) NOT NULL, - "task_id" INTEGER NOT NULL, - "activity_type_id" INTEGER NOT NULL, - FOREIGN KEY ("activity_id") REFERENCES "activities"("id") ON UPDATE NO ACTION ON DELETE NO ACTION, - FOREIGN KEY ("activity_type_id") REFERENCES "activity_types"("id") ON UPDATE NO ACTION ON DELETE NO ACTION -); \ No newline at end of file + "id" INTEGER, + "author_id" INTEGER NOT NULL, + "description" VARCHAR NOT NULL, + "date" DATETIME NOT NULL, + "activity_id" VARCHAR NOT NULL, + "time_spent" NUMERIC NOT NULL, + "task_id" INTEGER NOT NULL, + "activity_type_id" INTEGER NOT NULL, + PRIMARY KEY("id"), + FOREIGN KEY ("author_id") REFERENCES "user"("id") + ON UPDATE NO ACTION ON DELETE NO ACTION +); + +CREATE TABLE IF NOT EXISTS "user" ( + "id" INTEGER NOT NULL UNIQUE, + "first_name" VARCHAR, + "last_name" VARCHAR, + "clockify_user_id" VARCHAR NOT NULL UNIQUE, + "redmine_user_id" VARCHAR NOT NULL UNIQUE, + "clockify_email" VARCHAR UNIQUE, + "clockify_username" VARCHAR UNIQUE, + "redmine_email" VARCHAR UNIQUE, + "redmine_username" VARCHAR UNIQUE, + PRIMARY KEY("id") +); diff --git a/db/db.py b/db/db.py index 11baa8a..166fc66 100644 --- a/db/db.py +++ b/db/db.py @@ -34,16 +34,17 @@ class Database: is_updated = True for act in activities: if is_updated: - tags = self.cursor.execute("SELECT * FROM tags WHERE id = ?", (act.tag_id, )).fetchall() - vetro_projects = self.cursor.execute("SELECT * FROM vetro_projects WHERE id = ?", (act.vetro_project_id, )).fetchall() + tags = self.cursor.execute("SELECT * FROM tag WHERE id = ?", (act.tag_id, )).fetchall() + vetro_projects = self.cursor.execute("SELECT * FROM vetro_project WHERE id = ?", (act.vetro_project_id, )).fetchall() if not tags or not vetro_projects: is_updated = False try: self.cursor.execute( - "INSERT INTO activities VALUES(?,?,?,?,?,?,?,?,?)", + "INSERT INTO activity VALUES(?,?,?,?,?,?,?,?,?,?)", ( act.id, act.description, + act.author_id, act.time_spent, act.date_start, act.date_end, @@ -55,7 +56,7 @@ class Database: ) except sq.IntegrityError: self.cursor.execute( - """UPDATE activities + """UPDATE activity SET description = ?, time_spent = ?, date_start = ?, date_end = ?, is_tracked = ?, vetro_project_id = ?, tag_id = ?, project_id = ? WHERE id = ?""", ( @@ -81,15 +82,16 @@ class Database: try: self.cursor.execute( """INSERT INTO time_entry - (date, description, activity_id, time_spent, task_id, activity_type_id) - VALUES(?,?,?,?,?,?)""", + (date, description, activity_id, time_spent, task_id, activity_type_id, author_id) + VALUES(?,?,?,?,?,?,?)""", ( act.date_start.date(), act.description, act.id, act.time_spent, act.task_id, - ActivityTypes.DEVELOPMENT + ActivityTypes.DEVELOPMENT, + act.author_id ) ) self.con.commit() @@ -101,10 +103,10 @@ class Database: """Сохраняет в базу данных теги Clockify""" for tag in tags: try: - self.cursor.execute("INSERT INTO tags VALUES(?, ?)",(tag.id,tag.title)) + self.cursor.execute("INSERT INTO tag VALUES(?, ?)",(tag.id,tag.title)) except sq.IntegrityError: self.cursor.execute( - """UPDATE tags + """UPDATE tag SET title = ? WHERE id = ?""", (tag.title, tag.id) @@ -117,10 +119,10 @@ class Database: """Сохраняет в базу данных Проекты (ДО) Ветро Clockify""" for vetro_project in vetro_projects: try: - self.cursor.execute("INSERT INTO vetro_projects VALUES(?, ?)", (vetro_project.id, vetro_project.title)) + self.cursor.execute("INSERT INTO vetro_project VALUES(?, ?)", (vetro_project.id, vetro_project.title)) except sq.IntegrityError: self.cursor.execute(""" - UPDATE vetro_projects + UPDATE vetro_project SET title = ? WHERE id = ?""", (vetro_project.title, vetro_project.id) @@ -132,13 +134,63 @@ class Database: """Сохраняет в базу данных Деятельности Redmine""" for activity_type in activity_types: try: - self.cursor.execute("INSERT INTO activity_types VALUES(?, ?)", (activity_type.id, activity_type.title)) + self.cursor.execute("INSERT INTO activity_type VALUES(?, ?)", (activity_type.id, activity_type.title)) except sq.IntegrityError: self.cursor.execute(""" - UPDATE activity_types + UPDATE activity_type SET title = ? WHERE id = ?""", (activity_type.title, activity_type.id) ) finally: self.con.commit() + + def insert_user(self, **kwargs): + """Сохраняет в базу данных пользователя""" + print(kwargs.values()) + try: + self.cursor.execute("""INSERT INTO user( + clockify_user_id, clockify_email, clockify_username, + redmine_user_id, redmine_email, first_name, last_name, redmine_username) + VALUES(?, ?, ?, ?, ?, ?, ?, ?)""", [v for k, v in kwargs.items()]) + except sq.IntegrityError: + self.cursor.execute(""" + UPDATE user + SET clockify_email = ?, + clockify_username = ?, + redmine_email = ?, + first_name = ?, + last_name = ?, + redmine_username = ? + WHERE redmine_user_id = ? AND clockify_user_id = ?""", + ( + kwargs["clockify_email"], + kwargs["clockify_username"], + kwargs["redmine_email"], + kwargs["first_name"], + kwargs["last_name"], + kwargs["redmine_username"], + kwargs["redmine_user_id"], + kwargs["clockify_user_id"] + ) + ) + finally: + self.con.commit() + + def get_user_by(self, by_field: str, value: str): + """Получает пользователя из бд""" + try: + user = self.cursor.execute(f"SELECT * FROM user WHERE {by_field} = ?", (value,)).fetchone() + return { + "id": user[0], + "first_name": user[1], + "last_name": user[2], + "clockify_user_id": user[3], + "redmine_user_id": user[4], + "clockify_email": user[5], + "clockify_username": user[6], + "redmine_email": user[7], + "redmine_username": user[8] + } + except Exception as e: + logging.error(e) diff --git a/track.py b/track.py index 74c9261..5ae8c3e 100644 --- a/track.py +++ b/track.py @@ -62,7 +62,21 @@ def main(): logger.info(f'{Fore.YELLOW}Проверьте файл "{config.ENV_TXT}". Если необходимых переменных в нём нет, то просто удалите его.{Fore.RESET}') return red = RedmineAPI(config.REDMINE_TOKEN) - + user = clock.get_user() + clock_user = { + "clockify_user_id": user["id"], + "clockify_email": user["email"], + "clockify_username": user["username"], + } + user = red.get_user() + red_user = { + "redmine_user_id": user["id"], + "redmine_email": user["email"], + "first_name": user["first_name"], + "last_name": user["last_name"], + "redmine_username": user["username"] + } + db.insert_user(**clock_user, **red_user) if args.init_db: logger.info(Fore.YELLOW + "Обновление базы данных..." + Fore.RESET) try: @@ -74,13 +88,13 @@ def main(): else: logger.info(Fore.GREEN + "База данных теперь актуальна." + Fore.RESET) - activities = clock.get_activities( + activities = sorted(clock.get_activities( **{ "start": format_date(start), "end": format_date(end), "page-size": args.page_size, }, - ) + ), key=lambda x: x.date_start) db.insert_activities(activities) if args.debug: logger.debug("Пропускаем этап загрузки трудочасов")