diff --git a/README.MD b/README.MD index f256645..c412fed 100644 --- a/README.MD +++ b/README.MD @@ -82,17 +82,17 @@ activeWorkspace - это `WORKSPACE_ID` ## Запуск. ``` -python track.py +python main.py ``` ## Аргументы запуска | Аргумент | Описание | Пример использования | |-------------------|----------------------------------------------------------------------------------------------|--------------------------------------| -| `-h [--help]` | Показать справку | `python track.py --help` | -| `-s [--start]` | Дата начала парсинга активностей в формате гггг-мм-дд | `python track.py --start 2025-05-19` | -| `-e [--end]` | Дата окончания парсинга активностей в формате гггг-мм-дд | `python track.py -e 2025-05-19` | -| `-p [--page-size]`| Количество задач для вывода. Выведет последние n записей | `python track.py --page-size 15` | -| `-d [--debug]` | Режим отладки. Не загружает трудочасы в Redmine | `python track.py -d` | -| `-i [--init-db]` | Режим инициализации бд.
Подгружает с серверов теги, виды деятельности и Проекты (ДО) Ветро| `python track.py -init-db` | +| `-h [--help]` | Показать справку | `python main.py --help` | +| `-s [--start]` | Дата начала парсинга активностей в формате гггг-мм-дд | `python main.py --start 2025-05-19` | +| `-e [--end]` | Дата окончания парсинга активностей в формате гггг-мм-дд | `python main.py -e 2025-05-19` | +| `-p [--page-size]`| Количество задач для вывода. Выведет последние n записей | `python main.py --page-size 15` | +| `-d [--debug]` | Режим отладки. Не загружает трудочасы в Redmine | `python main.py -d` | +| `-i [--init-db]` | Режим инициализации бд.
Подгружает с серверов теги, виды деятельности и Проекты (ДО) Ветро| `python main.py -init-db` | -[Власов Эдуард](https://github.com/QuickLike) \ No newline at end of file +[Власов Эдуард](https://github.com/QuickLike) diff --git a/api/clockify.py b/api/clockify.py index 908a3fe..8f764a7 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, db +from config import ClockifyConfig, config from classes import Activity, Tag, VetroProject from exceptions import InvalidToken from utils import parse_task_id, time_to_hour @@ -20,8 +20,10 @@ class ClockifyAPI(ClockifyConfig): Позволяет получать активности, теги, проекты ветро и помечать активности затрканными. """ - def __init__(self, token: str, *args, **kwargs): + def __init__(self, token: str, db, redmine_api=None, *args, **kwargs): super().__init__(*args, **kwargs) + self.db = db + self.redmine_api = redmine_api self.token = self._check_token(token) def get_activities(self, is_updating=False, **kwargs) -> list[Activity]: @@ -70,24 +72,26 @@ class ClockifyAPI(ClockifyConfig): if not tag: tag = self.get_tags(True, tag_id=tag_id, **{"page-size": self.tags_count}) if isinstance(tag, Tag): - db.insert_tags([tag]) + self.db.insert_tags([tag]) tags_by_id[tag.id] = tag - from config import red try: task_id = parse_task_id(tag.title) except Exception as e: logger.error(f"При обновлении тега произошла ошибка: {e}\nПопробуйте обновить флагом -i [--init-db]") continue - task = red.get_issue(task_id) + if not self.redmine_api: + logger.error("Не передан RedmineAPI для обновления задачи по новому тегу") + continue + task = self.redmine_api.get_issue(task_id) if task: - db.insert_tasks([task]) + self.db.insert_tasks([task]) else: logger.error(f"{Fore.RED} Тег не найден: {tag_id}{Fore.RESET}") continue else: task_id = parse_task_id(tag.title) - author = db.get_user_by("clockify_user_id", activity["userId"]) + author = self.db.get_user_by("clockify_user_id", activity["userId"]) if not author: logger.error(f"Не найден автор активности {activity['id']}") continue @@ -103,7 +107,7 @@ class ClockifyAPI(ClockifyConfig): time_spent=time_to_hour((end if end else datetime.utcnow()) - start), date_start=start, date_end=end, - is_tracked=db.has_time_entry(activity["id"]), + is_tracked=self.db.has_time_entry(activity["id"]), author_id=author["id"] ) ) @@ -161,7 +165,7 @@ class ClockifyAPI(ClockifyConfig): logger.debug(f"{Fore.YELLOW}Получено тегов: {len(result)}.{Fore.RESET}") return result - return db.get_tags() + return self.db.get_tags() def get_vetro_projects(self) -> list[VetroProject]: """ diff --git a/config/__init__.py b/config/__init__.py index e90a352..aeb2e26 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -3,8 +3,6 @@ from datetime import datetime from pydantic_settings import BaseSettings from dotenv import load_dotenv -from db.db import Database - from .clockify import ClockifyConfig # noqa: F401 from .redmine import RedmineConfig # noqa: F401 @@ -86,14 +84,3 @@ def configure_argument_parser(): arg_parser = configure_argument_parser() config = Config() -db = Database(config.DB_NAME, config.SQL_PATH) - -from api.clockify import ClockifyAPI -from api.redmine import RedmineAPI - -clock = ClockifyAPI( - token=config.CLOCKIFY_TOKEN, - workspace_id=config.WORKSPACE_ID, - user_id=config.USER_ID -) -red = RedmineAPI(config.REDMINE_TOKEN) diff --git a/main.py b/main.py new file mode 100644 index 0000000..804fb8d --- /dev/null +++ b/main.py @@ -0,0 +1,59 @@ +import logging +import sys + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(name)s:%(lineno)d %(levelname)s %(message)s', + handlers=[ + logging.FileHandler('app.log', encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] +) + + +from api.clockify import ClockifyAPI # noqa: E402 +from api.redmine import RedmineAPI # noqa: E402 +from config import arg_parser, config # noqa: E402 +from db.db import Database # noqa: E402 +from services.tracking import TrackingService # noqa: E402 +from utils import check_envs, db_update, get_start_end_dates # noqa: E402 + + +def main(): + args = arg_parser.parse_args() + + if is_debug := args.debug: + config.MODE = "debug" + logging.getLogger().setLevel(logging.DEBUG) + + logger = logging.getLogger(__name__) + db = Database(config.DB_NAME, config.SQL_PATH) + red = RedmineAPI(config.REDMINE_TOKEN) + clock = ClockifyAPI( + token=config.CLOCKIFY_TOKEN, + workspace_id=config.WORKSPACE_ID, + user_id=config.USER_ID, + db=db, + redmine_api=red, + ) + + start, end = get_start_end_dates(args) + + if not check_envs(clock): + return + + if args.init_db: + db_update(clock, db, red) + + try: + TrackingService( + clock=clock, + red=red, + db=db, + is_debug=is_debug, + ).track_period(start=start, end=end, page_size=args.page_size) + except Exception as e: + logger.error(e) + +if __name__ == "__main__": + main() diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..6478430 --- /dev/null +++ b/services/__init__.py @@ -0,0 +1 @@ +"""Сервисы бизнес-сценариев приложения.""" diff --git a/services/tracking.py b/services/tracking.py new file mode 100644 index 0000000..74c8067 --- /dev/null +++ b/services/tracking.py @@ -0,0 +1,66 @@ +"""Сервис переноса трудочасов из Clockify в Redmine.""" + +import logging + +from colorama import Fore + +from utils import format_date + +logger = logging.getLogger(__name__) + + +class TrackingService: + """Координирует загрузку, сохранение и перенос активностей в Redmine.""" + + def __init__(self, clock, red, db, is_debug: bool): + self.clock = clock + self.red = red + self.db = db + self.is_debug = is_debug + + def track_period(self, start, end, page_size: int | None): + """Переносит активности за период и возвращает список найденных активностей.""" + self._save_current_user() + activities = sorted( + self.clock.get_activities( + **{ + "start": format_date(start), + "end": format_date(end), + "page-size": page_size, + }, + ), + key=lambda x: x.date_start, + ) + if not activities: + logger.info(Fore.YELLOW + "Нет задач для занесения трудочасов" + Fore.RESET) + return [] + + self.db.insert_activities(activities) + self.red.track_activities( + activities, + on_success=None if self.is_debug else self._save_tracked_activity, + ) + return activities + + def _save_current_user(self): + """Сохраняет связку текущих пользователей Clockify и Redmine.""" + user = self.clock.get_user() + clock_user = { + "clockify_user_id": user["id"], + "clockify_email": user["email"], + "clockify_username": user["username"], + } + user = self.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"], + } + self.db.insert_user(**clock_user, **red_user) + + def _save_tracked_activity(self, activity): + """Сохраняет активность как успешно перенесенную в Redmine.""" + self.db.insert_activities([activity]) + self.db.insert_time_entries([activity]) diff --git a/track.py b/track.py index b043dff..df7c98d 100644 --- a/track.py +++ b/track.py @@ -1,77 +1,7 @@ -import logging -import sys +"""Совместимая точка входа для старых запусков через track.py.""" -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s %(name)s:%(lineno)d %(levelname)s %(message)s', - handlers=[ - logging.FileHandler('app.log', encoding='utf-8'), - logging.StreamHandler(sys.stdout) - ] -) +from main import main -from colorama import Fore # noqa: E402 - -from config import arg_parser, clock, config, db, red # noqa: E402 -from utils import check_envs, db_update, format_date, get_start_end_dates # noqa: E402 - - -def main(): - args = arg_parser.parse_args() - - if is_debug := args.debug: - config.MODE = "debug" - logging.getLogger().setLevel(logging.DEBUG) - - logger = logging.getLogger(__name__) - - start, end = get_start_end_dates(args) - - if not check_envs(): - return - - 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: - db_update() - - activities = sorted(clock.get_activities( - **{ - "start": format_date(start), - "end": format_date(end), - "page-size": args.page_size, - }, - ), key=lambda x: x.date_start) - if not activities: - logger.info(Fore.YELLOW + "Нет задач для занесения трудочасов" + Fore.RESET) - return - db.insert_activities(activities) - - def save_tracked_activity(activity): - db.insert_activities([activity]) - db.insert_time_entries([activity]) - - try: - red.track_activities( - activities, - on_success=None if is_debug else save_tracked_activity, - ) - except Exception as e: - logger.error(e) - if __name__ == "__main__": main() diff --git a/utils.py b/utils.py index 12e057e..f17d10f 100644 --- a/utils.py +++ b/utils.py @@ -41,8 +41,8 @@ def parse_task_id(tag: str) -> int: return int(match.group("task_id")) raise ValueError(f'Тег "{tag}" должен начинаться с номера задачи из 5 цифр') -def db_update(): - from config import clock, db, red +def db_update(clock, db, red): + """Обновляет локальную БД данными из Clockify и Redmine.""" logger.info(Fore.YELLOW + "Обновление базы данных..." + Fore.RESET) try: @@ -144,8 +144,8 @@ def get_start_end_dates(args: Namespace): return start, end -def check_envs(): - from config import config, clock +def check_envs(clock): + """Проверяет наличие обязательных переменных окружения Clockify.""" if not config.WORKSPACE_ID or not config.USER_ID: if not os.path.exists(config.ENV_TXT): logger.info(f"{Fore.YELLOW}Необходимые переменные не были обнаружены. Попытка получения...{Fore.RESET}")