from typing import Any, Dict, List, Optional, Tuple import requests from redminelib import Redmine from redminelib.resources import Issue from urllib3.util.retry import Retry from .config import Config # Таймаут на один HTTP-запрос к Redmine (секунды). REQUEST_TIMEOUT = 30 # Размер чанка для запроса задач по issue_id, чтобы не превышать лимит длины URL (#21). ISSUE_ID_CHUNK_SIZE = 100 def _get_redmine_auth_kwargs() -> Dict[str, Any]: """Return Redmine auth kwargs. API key has priority over legacy password auth.""" api_key = Config.get_redmine_api_key() if api_key: return {"key": api_key} return { "username": Config.get_redmine_user(), "password": Config.get_redmine_password(), } def _make_retry_adapter() -> requests.adapters.HTTPAdapter: """Создаёт HTTPAdapter с retry для временных ошибок (#24).""" retry = Retry( total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET", "HEAD", "OPTIONS"], ) return requests.adapters.HTTPAdapter(max_retries=retry) def _create_redmine() -> Redmine: """Создаёт Redmine-клиент с таймаутом и retry-адаптером (#24).""" redmine = Redmine( Config.get_redmine_url(), **_get_redmine_auth_kwargs(), requests={ "verify": Config.get_redmine_verify(), "timeout": REQUEST_TIMEOUT, }, ) # Монтируем retry-адаптер на сессию для автоматических повторов retry_adapter = _make_retry_adapter() redmine.session.mount("https://", retry_adapter) redmine.session.mount("http://", retry_adapter) return redmine def _fetch_issues_chunked( redmine: Redmine, issue_ids: List[int] ) -> List[Issue]: """Загружает задачи чанками, чтобы не превышать лимит длины URL (#21).""" all_issues: List[Issue] = [] for i in range(0, len(issue_ids), ISSUE_ID_CHUNK_SIZE): chunk = issue_ids[i : i + ISSUE_ID_CHUNK_SIZE] issue_list_str = ",".join(str(x) for x in chunk) issues = redmine.issue.filter( issue_id=issue_list_str, status_id="*", sort="project:asc" ) all_issues.extend(issues) return all_issues def fetch_issues_with_spent_time( from_date: str, to_date: str ) -> Optional[List[Tuple[Issue, float]]]: """ Fetch unique issues linked to time entries of the current user in given date range, along with total spent hours per issue. Returns list of (issue, total_hours) tuples. """ redmine = _create_redmine() current_user = redmine.user.get("current") time_entries = redmine.time_entry.filter( user_id=current_user.id, from_date=from_date, to_date=to_date ) # Агрегируем часы по issue.id spent_time: Dict[int, float] = {} issue_ids = set() for entry in time_entries: if hasattr(entry, "issue") and entry.issue and hasattr(entry, "hours"): iid = entry.issue.id issue_ids.add(iid) spent_time[iid] = spent_time.get(iid, 0.0) + float(entry.hours) if not issue_ids: return None # Загружаем полные объекты задач чанками (#21) sorted_ids = sorted(issue_ids) issues = _fetch_issues_chunked(redmine, sorted_ids) # Сопоставляем задачи с суммарным временем. # Сортировка выполняется в report_builder.build_grouped_report, # здесь оставляем порядок API как есть. result = [] for issue in issues: total_hours = spent_time.get(issue.id, 0.0) result.append((issue, total_hours)) return result