разделил создание зависимостей и сценарий переноса трудочасов, переименовал основную точку входа в main.py
This commit is contained in:
16
README.MD
16
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]` | Режим инициализации бд.<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)
|
||||
[Власов Эдуард](https://github.com/QuickLike)
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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
59
main.py
Normal 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
1
services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Сервисы бизнес-сценариев приложения."""
|
||||
66
services/tracking.py
Normal file
66
services/tracking.py
Normal 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])
|
||||
74
track.py
74
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()
|
||||
|
||||
8
utils.py
8
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}")
|
||||
|
||||
Reference in New Issue
Block a user