81 lines
3.2 KiB
Python
81 lines
3.2 KiB
Python
|
|
import httpx
|
|||
|
|
import os
|
|||
|
|
import logging
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
logging.basicConfig(level=logging.WARNING)
|
|||
|
|
logger = logging.getLogger("YouTrackClient")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class YouTrackClient:
|
|||
|
|
def __init__(self):
|
|||
|
|
self.base_url = os.getenv("YOUTRACK_URL")
|
|||
|
|
self.token = os.getenv("YOUTRACK_TOKEN")
|
|||
|
|
self.ca_bundle = os.getenv("YOUTRACK_CA_BUNDLE")
|
|||
|
|
|
|||
|
|
if not self.base_url or not self.token:
|
|||
|
|
self.enabled = False
|
|||
|
|
logger.info("YouTrack: .env не настроен — синхронизация отключена")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Определяем verify параметр для httpx
|
|||
|
|
if self.ca_bundle:
|
|||
|
|
ca_path = Path(self.ca_bundle)
|
|||
|
|
if ca_path.exists():
|
|||
|
|
self.verify = str(ca_path.resolve())
|
|||
|
|
logger.info(f"YouTrack: использую CA bundle: {self.verify}")
|
|||
|
|
else:
|
|||
|
|
logger.error(f"YouTrack: CA bundle не найден: {ca_path}")
|
|||
|
|
self.verify = True # fallback на системные
|
|||
|
|
else:
|
|||
|
|
self.verify = True # стандартное поведение
|
|||
|
|
|
|||
|
|
self.enabled = True
|
|||
|
|
self.headers = {
|
|||
|
|
"Authorization": f"Bearer {self.token}",
|
|||
|
|
"Content-Type": "application/json",
|
|||
|
|
"Accept": "application/json"
|
|||
|
|
}
|
|||
|
|
self.base_url = self.base_url.rstrip('/')
|
|||
|
|
|
|||
|
|
def _get_client(self) -> httpx.Client:
|
|||
|
|
"""Создаёт HTTP-клиент с правильной настройкой SSL"""
|
|||
|
|
return httpx.Client(verify=self.verify, timeout=15)
|
|||
|
|
|
|||
|
|
def issue_exists(self, issue_id: str) -> bool:
|
|||
|
|
if not self.enabled:
|
|||
|
|
return False
|
|||
|
|
try:
|
|||
|
|
with self._get_client() as client:
|
|||
|
|
url = f"{self.base_url}/api/issues/{issue_id}"
|
|||
|
|
resp = client.get(url, headers=self.headers)
|
|||
|
|
exists = resp.status_code == 200
|
|||
|
|
if not exists:
|
|||
|
|
logger.warning(f"YouTrack: задача {issue_id} не найдена (HTTP {resp.status_code})")
|
|||
|
|
return exists
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"YouTrack: ошибка при проверке задачи {issue_id}: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def add_work_item(self, issue_id: str, minutes: float) -> bool:
|
|||
|
|
if not self.enabled:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
if minutes <= 0:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
payload = {"duration": {"minutes": round(minutes)}}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
with self._get_client() as client:
|
|||
|
|
url = f"{self.base_url}/api/issues/{issue_id}/timeTracking/workItems"
|
|||
|
|
resp = client.post(url, headers=self.headers, json=payload)
|
|||
|
|
if resp.status_code in (200, 201):
|
|||
|
|
logger.info(f"YouTrack: +{round(minutes)} мин к {issue_id}")
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
logger.error(f"YouTrack: ошибка отправки времени ({issue_id}): {resp.status_code} – {resp.text}")
|
|||
|
|
return False
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"YouTrack: сетевая ошибка при отправке времени ({issue_id}): {e}")
|
|||
|
|
return False
|