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}")