From aae6d8309a0358886d397e97fd0b86d2611a1174 Mon Sep 17 00:00:00 2001 From: Eduard Date: Wed, 21 May 2025 11:57:09 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=20=D0=BE=D1=82=D0=BB=D0=B0=D0=B4?= =?UTF-8?q?=D0=BA=D0=B8.=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D1=80=D0=BE=D0=B1=D0=BB=D0=B5=D0=BC=D1=8B=20?= =?UTF-8?q?=D1=81=20=D0=BB=D0=BE=D0=B3=D0=B3=D0=B5=D1=80=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - api/clockify.py | 29 +++++++------- api/redmine.py | 22 ++++++----- config/RedmineTracker.sql | 45 +++++++++++++++++++++ config/__init__.py | 9 ++++- db/db.py | 82 ++++++++++----------------------------- track.py | 51 ++++++++++++++---------- 7 files changed, 133 insertions(+), 106 deletions(-) create mode 100644 config/RedmineTracker.sql diff --git a/.gitignore b/.gitignore index 1afcbe2..afafcd3 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,6 @@ cover/ *.log *.db *.sqlite3 -*.sql local_settings.py db.sqlite3 db.sqlite3-journal diff --git a/api/clockify.py b/api/clockify.py index ae97fde..82109b7 100644 --- a/api/clockify.py +++ b/api/clockify.py @@ -11,6 +11,9 @@ from exceptions import InvalidToken from utils import time_to_hour +logger = logging.getLogger(__name__) + + class ClockifyAPI(ClockifyConfig): def __init__(self, token: str, *args, **kwargs): super().__init__(*args, **kwargs) @@ -25,13 +28,13 @@ class ClockifyAPI(ClockifyConfig): """ 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() - logging.info(f"Получено задач: {len(activities)}") + logger.info(f"Получено задач: {len(activities)}") time_worked_out_total = 0 result = [] for activity in tqdm(activities, desc="Парсинг задач"): if not activity.get("tagIds"): - logging.info(f'{activity["timeInterval"]["start"]}: {activity["description"]} - У задачи нет тегов') + logger.info(f'{activity["timeInterval"]["start"]}: {activity["description"]} - У задачи нет тегов') continue for tag_id in activity.get("tagIds", {"tagIds": []}): start = datetime.strptime(activity["timeInterval"]["start"], config.DATETIME_FORMAT) @@ -41,7 +44,7 @@ class ClockifyAPI(ClockifyConfig): try: task_id, task_title = tag.title.split(" - ") except ValueError: - logging.info(f'{activity["timeInterval"]["start"]}: {activity["description"]} - Неправильный формат тега {tag}') + logger.info(f'{activity["timeInterval"]["start"]}: {activity["description"]} - Неправильный формат тега {tag}') continue result.append( Activity( @@ -59,8 +62,8 @@ class ClockifyAPI(ClockifyConfig): is_tracked=activity["billable"] ) ) - logging.info(f"Сохранено задач: {len(result)}") - logging.info(f"Отработано часов: {time_worked_out_total}") + logger.info(f"Сохранено задач: {len(result)}") + logger.info(f"Отработано часов: {time_worked_out_total}") return result @@ -95,13 +98,13 @@ class ClockifyAPI(ClockifyConfig): } ) if resp.ok: - logging.debug(resp.json()) - logging.info(f"{activity} {Fore.GREEN} помечена как затреканная{Fore.RESET}") + logger.debug(resp.json()) + logger.info(f"{activity} {Fore.GREEN} помечена как затреканная{Fore.RESET}") else: - logging.debug(resp.json()) - logging.error(f"{activity} {Fore.RED} не помечена как затреканная{Fore.RESET}") + logger.debug(resp.json()) + logger.error(f"{activity} {Fore.RED} не помечена как затреканная{Fore.RESET}") except Exception as e: - logging.error(e) + logger.error(e) return @@ -109,8 +112,8 @@ class ClockifyAPI(ClockifyConfig): def _check_token(self, token: str) -> str: resp = requests.get(self.base_url + self.user_url, headers={"X-API-KEY": token}) if not resp.ok: - logging.error(resp.json()) + logger.error(resp.json()) raise InvalidToken resp = resp.json() - logging.debug({"id": resp["id"], "email": resp["email"], "name": resp["name"]}) - return token \ No newline at end of file + logger.debug({"id": resp["id"], "email": resp["email"], "name": resp["name"]}) + return token diff --git a/api/redmine.py b/api/redmine.py index 5c88c5e..7961748 100644 --- a/api/redmine.py +++ b/api/redmine.py @@ -8,6 +8,8 @@ from classes import Activity from enums import ActivityType from exceptions import InvalidToken +logger = logging.getLogger(__name__) + WORKSPACE_ID = config.CLOCKIFY_WORKSPACE_ID USER_ID = config.CLOCKIFY_USER_ID @@ -19,7 +21,7 @@ class RedmineAPI(RedmineConfig): def track_activities(self, activities: list[Activity]) -> list[Activity]: if len(activities) == 0: - logging.info("Нет задач для занесения трудочасов") + logger.info("Нет задач для занесения трудочасов") return tracked = [] for activity in tqdm(activities, desc="Занесение трудочасов"): @@ -41,25 +43,25 @@ class RedmineAPI(RedmineConfig): if resp.ok: activity.is_tracked = True tracked.append(activity) - logging.debug(activity) + logger.debug(activity) else: - logging.error(f"{activity} Не была затрекана") - logging.error(f"{resp.json()['errors']} {resp.status_code}") + logger.error(f"{activity} Не была затрекана") + logger.error(f"{resp.json()['errors']} {resp.status_code}") except Exception as e: - logging.error(e) + logger.error(e) continue if len(activities) == len(tracked): - logging.info("Все задачи были успешно занесены в Redmine") + logger.info("Все задачи были успешно занесены в Redmine") else: - logging.info(f"Не все задачи были занесены в Redmine. Занесено {len(tracked)}") + logger.info(f"Не все задачи были занесены в Redmine. Занесено {len(tracked)}") return tracked def _check_token(self, token): resp = requests.get(self.base_url + self.my_account_url, headers={"X-Redmine-Api-Key": token}) if not resp.ok: - logging.error("Invalid Token for RedmineAPI") + logger.error("Invalid Token for RedmineAPI") raise InvalidToken resp = resp.json()['user'] - logging.debug({"login": resp["login"], "email": resp["mail"], "full_name": f'{resp["firstname"]} {resp["lastname"]}'}) - return token \ No newline at end of file + logger.debug({"login": resp["login"], "email": resp["mail"], "full_name": f'{resp["firstname"]} {resp["lastname"]}'}) + return token diff --git a/config/RedmineTracker.sql b/config/RedmineTracker.sql new file mode 100644 index 0000000..69582c2 --- /dev/null +++ b/config/RedmineTracker.sql @@ -0,0 +1,45 @@ +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 "tags" ( + "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 "activity_types" ( + "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 ("task_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 diff --git a/config/__init__.py b/config/__init__.py index f326081..d5908d0 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -17,6 +17,7 @@ class Config(BaseSettings): CLOCKIFY_WORKSPACE_ID: str DB_NAME: str = "RedmineTracker.db" + SQL_PATH: str = "config/RedmineTracker.sql" DATETIME_FORMAT: str = "%Y-%m-%dT%H:%M:%SZ" @@ -59,8 +60,14 @@ def configure_argument_parser(): '--page-size', help='Количество задач для вывода' ) + parser.add_argument( + '-d', + '--debug', + action="store_true", + help='Режим отладки. Не загружает трудочасы в Redmine' + ) return parser arg_parser = configure_argument_parser() config = Config() -db = Database(config.DB_NAME) +db = Database(config.DB_NAME, config.SQL_PATH) diff --git a/db/db.py b/db/db.py index f492359..0adfba3 100644 --- a/db/db.py +++ b/db/db.py @@ -4,65 +4,25 @@ import sqlite3 as sq from classes import Activity, Tag from enums import ActivityType +logger = logging.getLogger(__name__) + class Database: - def __init__(self, db_name: str): - try: - self.con = sq.connect(db_name) - self.con.cursor().executescript(''' - 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") - FOREIGN KEY ("vetro_project_id") REFERENCES "vetro_projects"("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 "vetro_projects" ( - "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 "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 ("task_id") REFERENCES "activities"("task_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 - ); - ''') - except Exception as e: - raise(e) + def __init__(self, db_name: str, sql_path: str): + self.con = sq.connect(db_name) + cursor = self.con.cursor() + try: + with open(sql_path, 'r', encoding='utf-8') as f: + sql_script = f.read() + + cursor.executescript(sql_script) + self.con.commit() + logger.info("База данных успешно инициализирована!") + except Exception as e: + logger.error("Ошибка при инициализации БД:", e) + self.con.rollback() + def insert_activities(self, activities: list[Activity]): cur = self.con.cursor() for act in activities: @@ -83,7 +43,7 @@ class Database: ) self.con.commit() except Exception as e: - logging.error(e) + logger.debug(e) # Доделать def insert_time_entries(self, activities: list[Activity]): @@ -105,7 +65,7 @@ class Database: ) self.con.commit() except Exception as e: - logging.error(e) + logger.debug(e) def insert_tags(self, tags: list[Tag]): @@ -121,7 +81,7 @@ class Database: ) self.con.commit() except Exception as e: - logging.error(e) + logger.debug(e) def insert_vetro_projects(self, vetro_projects: dict): @@ -131,7 +91,7 @@ class Database: cur.execute("INSERT INTO vetro_projects VALUES(?, ?)", (k, v)) self.con.commit() except Exception as e: - logging.error(e) + logger.debug(e) def insert_activity_types(self, activity_types: dict): cur = self.con.cursor() @@ -140,4 +100,4 @@ class Database: cur.execute("INSERT INTO activity_types VALUES(?, ?)", (k, v)) self.con.commit() except Exception as e: - logging.error(e) + logger.debug(e) diff --git a/track.py b/track.py index e013891..d206ffb 100644 --- a/track.py +++ b/track.py @@ -1,9 +1,19 @@ import logging import sys -from api import ClockifyAPI, RedmineAPI -from config import arg_parser, config, db -from utils import format_date, str_to_date, today_start, today_end +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('app.log', encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] +) + + +from api import ClockifyAPI, RedmineAPI # noqa: E402 +from config import arg_parser, config, db # noqa: E402 +from utils import format_date, str_to_date, today_start, today_end # noqa: E402 WORKSPACE_ID = config.CLOCKIFY_WORKSPACE_ID @@ -12,24 +22,29 @@ USER_ID = config.CLOCKIFY_USER_ID def main(): args = arg_parser.parse_args() + + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + + logger = logging.getLogger(__name__) + start, end = None, None if args.start: start = str_to_date(args.start) if args.end: end = str_to_date(args.end) if args.start is None and args.end is None: - logging.info("Парсим задачи за сегодняшний день") + logger.info("Парсим задачи за сегодняшний день") start = today_start() end = today_end() else: - logging.info("Парсим задачи" + (f" с {start.date()} " if start else " ") + (f"до {end.date()}" if end else "")) + logger.info("Парсим задачи" + (f" с {start.date()} " if start else " ") + (f"до {end.date()}" if end else "")) clock = ClockifyAPI( token=config.CLOCKIFY_TOKEN, workspace_id=WORKSPACE_ID, user_id=USER_ID ) - red = RedmineAPI(config.REDMINE_TOKEN) db.insert_tags(clock.get_tags()) db.insert_vetro_projects(config.VETRO_PROJECTS) activities = clock.get_activities( @@ -40,7 +55,6 @@ def main(): }, ) db.insert_activities(activities) - db.insert_time_entries(activities) db.insert_activity_types({ 9: "Разработка", 8: "Бизнес-анализ", @@ -48,20 +62,17 @@ def main(): 58: "Внутренние консультации", 60: "Поддержка", }) - # tracked_activities = red.track_activities(activities) - # clock.mark_tracked(tracked_activities) + if args.debug: + logger.debug("Пропускаем этап загрузки трудочасов") + return + try: + red = RedmineAPI(config.REDMINE_TOKEN) + tracked_activities = red.track_activities(activities) + clock.mark_tracked(tracked_activities) + db.insert_time_entries(tracked_activities) + except Exception as e: + logger.error(e) if __name__ == "__main__": - logging.basicConfig( - level=logging.INFO, - format=('%(asctime)s, ' - '%(levelname)s, ' - '%(funcName)s, ' - '%(message)s' - ), - encoding='UTF-8', - handlers=[logging.FileHandler(__file__ + '.log'), - logging.StreamHandler(sys.stdout)] - ) main()