разделил создание зависимостей и сценарий переноса трудочасов, переименовал основную точку входа в 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` |
| `-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]` | Режим инициализации бд.<br>Подгружает с серверов теги, виды деятельности и Проекты (ДО) Ветро| `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]` | Режим инициализации бд.<br>Подгружает с серверов теги, виды деятельности и Проекты (ДО) Ветро| `python main.py -init-db` |
[Власов Эдуард](https://github.com/QuickLike)

View File

@@ -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]:
"""

View File

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

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

View File

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