разделил создание зависимостей и сценарий переноса трудочасов, переименовал основную точку входа в main.py

This commit is contained in:
2026-07-06 16:18:55 +03:00
parent 9a0ce15160
commit baf6e2f3a4
8 changed files with 153 additions and 106 deletions

View File

@@ -82,17 +82,17 @@ activeWorkspace - это `WORKSPACE_ID`
## Запуск. ## Запуск.
``` ```
python track.py python main.py
``` ```
## Аргументы запуска ## Аргументы запуска
| Аргумент | Описание | Пример использования | | Аргумент | Описание | Пример использования |
|-------------------|----------------------------------------------------------------------------------------------|--------------------------------------| |-------------------|----------------------------------------------------------------------------------------------|--------------------------------------|
| `-h [--help]` | Показать справку | `python track.py --help` | | `-h [--help]` | Показать справку | `python main.py --help` |
| `-s [--start]` | Дата начала парсинга активностей в формате гггг-мм-дд | `python track.py --start 2025-05-19` | | `-s [--start]` | Дата начала парсинга активностей в формате гггг-мм-дд | `python main.py --start 2025-05-19` |
| `-e [--end]` | Дата окончания парсинга активностей в формате гггг-мм-дд | `python track.py -e 2025-05-19` | | `-e [--end]` | Дата окончания парсинга активностей в формате гггг-мм-дд | `python main.py -e 2025-05-19` |
| `-p [--page-size]`| Количество задач для вывода. Выведет последние n записей | `python track.py --page-size 15` | | `-p [--page-size]`| Количество задач для вывода. Выведет последние n записей | `python main.py --page-size 15` |
| `-d [--debug]` | Режим отладки. Не загружает трудочасы в Redmine | `python track.py -d` | | `-d [--debug]` | Режим отладки. Не загружает трудочасы в Redmine | `python main.py -d` |
| `-i [--init-db]` | Режим инициализации бд.<br>Подгружает с серверов теги, виды деятельности и Проекты (ДО) Ветро| `python track.py -init-db` | | `-i [--init-db]` | Режим инициализации бд.<br>Подгружает с серверов теги, виды деятельности и Проекты (ДО) Ветро| `python main.py -init-db` |
[Власов Эдуард](https://github.com/QuickLike) [Власов Эдуард](https://github.com/QuickLike)

View File

@@ -5,7 +5,7 @@ from colorama import Fore
import requests import requests
from tqdm import tqdm from tqdm import tqdm
from config import ClockifyConfig, config, db from config import ClockifyConfig, config
from classes import Activity, Tag, VetroProject from classes import Activity, Tag, VetroProject
from exceptions import InvalidToken from exceptions import InvalidToken
from utils import parse_task_id, time_to_hour 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) super().__init__(*args, **kwargs)
self.db = db
self.redmine_api = redmine_api
self.token = self._check_token(token) self.token = self._check_token(token)
def get_activities(self, is_updating=False, **kwargs) -> list[Activity]: def get_activities(self, is_updating=False, **kwargs) -> list[Activity]:
@@ -70,24 +72,26 @@ class ClockifyAPI(ClockifyConfig):
if not tag: if not tag:
tag = self.get_tags(True, tag_id=tag_id, **{"page-size": self.tags_count}) tag = self.get_tags(True, tag_id=tag_id, **{"page-size": self.tags_count})
if isinstance(tag, Tag): if isinstance(tag, Tag):
db.insert_tags([tag]) self.db.insert_tags([tag])
tags_by_id[tag.id] = tag tags_by_id[tag.id] = tag
from config import red
try: try:
task_id = parse_task_id(tag.title) task_id = parse_task_id(tag.title)
except Exception as e: except Exception as e:
logger.error(f"При обновлении тега произошла ошибка: {e}\nПопробуйте обновить флагом -i [--init-db]") logger.error(f"При обновлении тега произошла ошибка: {e}\nПопробуйте обновить флагом -i [--init-db]")
continue 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: if task:
db.insert_tasks([task]) self.db.insert_tasks([task])
else: else:
logger.error(f"{Fore.RED} Тег не найден: {tag_id}{Fore.RESET}") logger.error(f"{Fore.RED} Тег не найден: {tag_id}{Fore.RESET}")
continue continue
else: else:
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 = self.db.get_user_by("clockify_user_id", activity["userId"])
if not author: if not author:
logger.error(f"Не найден автор активности {activity['id']}") logger.error(f"Не найден автор активности {activity['id']}")
continue continue
@@ -103,7 +107,7 @@ class ClockifyAPI(ClockifyConfig):
time_spent=time_to_hour((end if end else datetime.utcnow()) - start), time_spent=time_to_hour((end if end else datetime.utcnow()) - start),
date_start=start, date_start=start,
date_end=end, date_end=end,
is_tracked=db.has_time_entry(activity["id"]), is_tracked=self.db.has_time_entry(activity["id"]),
author_id=author["id"] author_id=author["id"]
) )
) )
@@ -161,7 +165,7 @@ class ClockifyAPI(ClockifyConfig):
logger.debug(f"{Fore.YELLOW}Получено тегов: {len(result)}.{Fore.RESET}") logger.debug(f"{Fore.YELLOW}Получено тегов: {len(result)}.{Fore.RESET}")
return result return result
return db.get_tags() return self.db.get_tags()
def get_vetro_projects(self) -> list[VetroProject]: def get_vetro_projects(self) -> list[VetroProject]:
""" """

View File

@@ -3,8 +3,6 @@ from datetime import datetime
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from dotenv import load_dotenv from dotenv import load_dotenv
from db.db import Database
from .clockify import ClockifyConfig # noqa: F401 from .clockify import ClockifyConfig # noqa: F401
from .redmine import RedmineConfig # noqa: F401 from .redmine import RedmineConfig # noqa: F401
@@ -86,14 +84,3 @@ def configure_argument_parser():
arg_parser = configure_argument_parser() arg_parser = configure_argument_parser()
config = Config() 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)

59
main.py Normal file
View File

@@ -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()

1
services/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Сервисы бизнес-сценариев приложения."""

66
services/tracking.py Normal file
View File

@@ -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])

View File

@@ -1,77 +1,7 @@
import logging """Совместимая точка входа для старых запусков через track.py."""
import sys
logging.basicConfig( from main import main
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 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__": if __name__ == "__main__":
main() main()

View File

@@ -41,8 +41,8 @@ def parse_task_id(tag: str) -> int:
return int(match.group("task_id")) return int(match.group("task_id"))
raise ValueError(f'Тег "{tag}" должен начинаться с номера задачи из 5 цифр') raise ValueError(f'Тег "{tag}" должен начинаться с номера задачи из 5 цифр')
def db_update(): def db_update(clock, db, red):
from config import clock, db, red """Обновляет локальную БД данными из Clockify и Redmine."""
logger.info(Fore.YELLOW + "Обновление базы данных..." + Fore.RESET) logger.info(Fore.YELLOW + "Обновление базы данных..." + Fore.RESET)
try: try:
@@ -144,8 +144,8 @@ def get_start_end_dates(args: Namespace):
return start, end return start, end
def check_envs(): def check_envs(clock):
from config import config, clock """Проверяет наличие обязательных переменных окружения Clockify."""
if not config.WORKSPACE_ID or not config.USER_ID: if not config.WORKSPACE_ID or not config.USER_ID:
if not os.path.exists(config.ENV_TXT): if not os.path.exists(config.ENV_TXT):
logger.info(f"{Fore.YELLOW}Необходимые переменные не были обнаружены. Попытка получения...{Fore.RESET}") logger.info(f"{Fore.YELLOW}Необходимые переменные не были обнаружены. Попытка получения...{Fore.RESET}")