разделил создание зависимостей и сценарий переноса трудочасов, переименовал основную точку входа в main.py
This commit is contained in:
14
README.MD
14
README.MD
@@ -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)
|
||||||
@@ -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]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
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
|
"""Совместимая точка входа для старых запусков через 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()
|
||||||
|
|||||||
8
utils.py
8
utils.py
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user