feat(user): report on another user's time entries
- Add user_id parameter to fetch_issues_with_spent_time(). - Support numeric ID, login, or full name resolution. - Reject ambiguous names and unknown users with clear messages. - Add CLI flags: --user-id, --user-login, --user-name. - Only allow one user flag at a time. - Add ResourceNotFoundError handling. - Update README with usage examples. - Add tests for user resolution and CLI flags. Closes #40
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import requests
|
||||
from redminelib import Redmine
|
||||
from redminelib.exceptions import AuthError, ForbiddenError
|
||||
from redminelib.exceptions import AuthError, ForbiddenError, ResourceNotFoundError
|
||||
from redminelib.resources import Issue
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
@@ -78,6 +78,8 @@ def _format_redmine_error(exc: Exception) -> str:
|
||||
"Access denied: your Redmine account does not have permission "
|
||||
"to read time entries or issues."
|
||||
)
|
||||
if isinstance(exc, ResourceNotFoundError):
|
||||
return "Requested Redmine resource not found: check user/project identifiers."
|
||||
|
||||
# requests HTTPError может быть обёрнуто в python-redmine
|
||||
original = getattr(exc, "response", None)
|
||||
@@ -120,22 +122,91 @@ def _fetch_issues_chunked(redmine: Redmine, issue_ids: List[int]) -> List[Issue]
|
||||
return all_issues
|
||||
|
||||
|
||||
def _resolve_user_id(redmine: Redmine, user_arg: Union[int, str]) -> int:
|
||||
"""Преобразует строковый идентификатор пользователя в числовой ID.
|
||||
|
||||
Если аргумент — число, возвращает его как есть.
|
||||
Если строка, пытается найти пользователя по логину или имени.
|
||||
"""
|
||||
if isinstance(user_arg, int):
|
||||
return user_arg
|
||||
|
||||
text = str(user_arg).strip()
|
||||
if not text:
|
||||
raise RedmineAPIError("User identifier cannot be empty.")
|
||||
|
||||
# Сначала пробуем интерпретировать как числовой ID
|
||||
if text.isdigit():
|
||||
return int(text)
|
||||
|
||||
# Затем ищем по логину
|
||||
try:
|
||||
users = redmine.user.filter(login=text)
|
||||
if users:
|
||||
return int(users[0].id)
|
||||
except Exception as exc:
|
||||
raise RedmineAPIError(
|
||||
f"Cannot resolve user login '{text}': {_format_redmine_error(exc)}",
|
||||
original=exc,
|
||||
) from exc
|
||||
|
||||
# Потом по имени
|
||||
try:
|
||||
users = redmine.user.filter(name=text)
|
||||
if len(users) == 1:
|
||||
return int(users[0].id)
|
||||
if len(users) > 1:
|
||||
matches = ", ".join(str(getattr(u, "login", u.id)) for u in users[:5])
|
||||
raise RedmineAPIError(
|
||||
f"Multiple users match '{text}': {matches}. Use --user-id with numeric ID."
|
||||
)
|
||||
except RedmineAPIError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise RedmineAPIError(
|
||||
f"Cannot resolve user name '{text}': {_format_redmine_error(exc)}",
|
||||
original=exc,
|
||||
) from exc
|
||||
|
||||
raise RedmineAPIError(
|
||||
f"User '{text}' not found. Check the login/name or use --user-id with numeric Redmine ID."
|
||||
)
|
||||
|
||||
|
||||
def _get_current_user_id(redmine: Redmine) -> int:
|
||||
"""Возвращает ID текущего пользователя."""
|
||||
try:
|
||||
current_user = redmine.user.get("current")
|
||||
return int(current_user.id)
|
||||
except Exception as exc:
|
||||
raise RedmineAPIError(_format_redmine_error(exc), original=exc) from exc
|
||||
|
||||
|
||||
def fetch_issues_with_spent_time(
|
||||
from_date: str, to_date: str
|
||||
from_date: str,
|
||||
to_date: str,
|
||||
user_id: Optional[Union[int, str]] = None,
|
||||
) -> Optional[List[Tuple[Issue, float]]]:
|
||||
"""
|
||||
Fetch unique issues linked to time entries of the current user in given date range,
|
||||
Fetch unique issues linked to time entries of the given user in date range,
|
||||
along with total spent hours per issue.
|
||||
If user_id is None, uses current user.
|
||||
Returns list of (issue, total_hours) tuples.
|
||||
Raises RedmineAPIError on API/auth/network failures.
|
||||
"""
|
||||
|
||||
try:
|
||||
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
|
||||
target_user_id = (
|
||||
_resolve_user_id(redmine, user_id)
|
||||
if user_id is not None
|
||||
else _get_current_user_id(redmine)
|
||||
)
|
||||
time_entries = redmine.time_entry.filter(
|
||||
user_id=target_user_id, from_date=from_date, to_date=to_date
|
||||
)
|
||||
except RedmineAPIError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise RedmineAPIError(_format_redmine_error(exc), original=exc) from exc
|
||||
|
||||
|
||||
Reference in New Issue
Block a user