style: apply ruff format to all source files
This commit is contained in:
@@ -25,7 +25,9 @@ def parse_date_range(date_arg: str) -> tuple[str, str]:
|
||||
|
||||
from_date, to_date = parts[0].strip(), parts[1].strip()
|
||||
date_pattern = r"\d{4}-\d{2}-\d{2}"
|
||||
if not re.fullmatch(date_pattern, from_date) or not re.fullmatch(date_pattern, to_date):
|
||||
if not re.fullmatch(date_pattern, from_date) or not re.fullmatch(
|
||||
date_pattern, to_date
|
||||
):
|
||||
raise ValueError("Date range must be in format YYYY-MM-DD--YYYY-MM-DD")
|
||||
|
||||
try:
|
||||
@@ -46,7 +48,7 @@ def _run_init_config(config_path: str, force: bool) -> int:
|
||||
|
||||
if path.exists() and not force:
|
||||
print(
|
||||
f"⚠️ {path} already exists.\n" f" Use --init-config --force to overwrite.",
|
||||
f"⚠️ {path} already exists.\n Use --init-config --force to overwrite.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
@@ -90,7 +92,9 @@ def _run_init_config(config_path: str, force: bool) -> int:
|
||||
|
||||
ensure_config_dir(path.parent)
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
yaml.dump(data, fh, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
||||
yaml.dump(
|
||||
data, fh, allow_unicode=True, default_flow_style=False, sort_keys=False
|
||||
)
|
||||
path.chmod(0o600)
|
||||
|
||||
sections_found = [s for s in data if data[s]]
|
||||
@@ -222,7 +226,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
"--no-time", action="store_true", help="Do not include spent time into table"
|
||||
)
|
||||
parser.add_argument("--url", help="Override Redmine URL from .env (REDMINE_URL)")
|
||||
parser.add_argument("--api-key", help="Override Redmine API key from .env (REDMINE_API_KEY)")
|
||||
parser.add_argument(
|
||||
"--api-key", help="Override Redmine API key from .env (REDMINE_API_KEY)"
|
||||
)
|
||||
parser.add_argument("--config", help="Path to .env config file")
|
||||
parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
|
||||
parser.add_argument("--debug", action="store_true", help="Enable debug output")
|
||||
@@ -417,7 +423,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
)
|
||||
|
||||
if output_arg is None:
|
||||
print("❌ Не удалось определить путь для сохранения отчёта.", file=sys.stderr)
|
||||
print(
|
||||
"❌ Не удалось определить путь для сохранения отчёта.", file=sys.stderr
|
||||
)
|
||||
return 1
|
||||
|
||||
ret = _save_and_maybe_send(
|
||||
@@ -446,7 +454,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
)
|
||||
|
||||
if output_arg is None:
|
||||
print("❌ Не удалось определить путь для сохранения отчёта.", file=sys.stderr)
|
||||
print(
|
||||
"❌ Не удалось определить путь для сохранения отчёта.", file=sys.stderr
|
||||
)
|
||||
return 1
|
||||
|
||||
ret = _save_and_maybe_send(
|
||||
@@ -493,7 +503,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
to_str = to_date
|
||||
|
||||
try:
|
||||
save_period_to_config(args.config_path, from_str, to_str, precision, dynamic)
|
||||
save_period_to_config(
|
||||
args.config_path, from_str, to_str, precision, dynamic
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"❌ Не удалось сохранить период в конфиг: {e}",
|
||||
|
||||
@@ -170,7 +170,9 @@ def _fetch_issues_chunked(redmine: Redmine, issue_ids: List[int]) -> 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")
|
||||
issues = redmine.issue.filter(
|
||||
issue_id=issue_list_str, status_id="*", sort="project:asc"
|
||||
)
|
||||
all_issues.extend(issues)
|
||||
return all_issues
|
||||
|
||||
@@ -262,7 +264,9 @@ def fetch_issues_with_spent_time(
|
||||
)
|
||||
activities_lookup = _load_time_entry_activities(redmine) if by_activity else {}
|
||||
time_entries = list(
|
||||
redmine.time_entry.filter(user_id=target_user_id, from_date=from_date, to_date=to_date)
|
||||
redmine.time_entry.filter(
|
||||
user_id=target_user_id, from_date=from_date, to_date=to_date
|
||||
)
|
||||
)
|
||||
except RedmineAPIError:
|
||||
raise
|
||||
|
||||
@@ -106,7 +106,8 @@ class AppConfig:
|
||||
output_dir=cls._resolve_str(raw, "output", "dir"),
|
||||
output_filename=cls._resolve_str(raw, "output", "filename")
|
||||
or "{author}_{from}_{to}.{ext}",
|
||||
output_default_format=cls._resolve_str(raw, "output", "default_format") or "xlsx",
|
||||
output_default_format=cls._resolve_str(raw, "output", "default_format")
|
||||
or "xlsx",
|
||||
email=cls._resolve_email(raw),
|
||||
)
|
||||
|
||||
@@ -397,7 +398,9 @@ class Config:
|
||||
)
|
||||
|
||||
|
||||
def compute_next_period(last_from: str, last_to: str, precision: str) -> tuple[str, str]:
|
||||
def compute_next_period(
|
||||
last_from: str, last_to: str, precision: str
|
||||
) -> tuple[str, str]:
|
||||
"""Compute the next report period based on the last committed period.
|
||||
|
||||
- For a full calendar month → next full calendar month.
|
||||
|
||||
@@ -15,7 +15,9 @@ from .base import Formatter
|
||||
class ODTFormatter(Formatter):
|
||||
"""Форматтер для экспорта в ODT."""
|
||||
|
||||
def __init__(self, author: str = "", from_date: str = "", to_date: str = "", **_kwargs):
|
||||
def __init__(
|
||||
self, author: str = "", from_date: str = "", to_date: str = "", **_kwargs
|
||||
):
|
||||
"""
|
||||
Инициализирует форматтер с параметрами для шапки отчета.
|
||||
"""
|
||||
@@ -28,7 +30,11 @@ class ODTFormatter(Formatter):
|
||||
Форматирует данные в объект OpenDocument.
|
||||
"""
|
||||
|
||||
with resources.files("redmine_reporter").joinpath("templates/template.odt").open("rb") as f:
|
||||
with (
|
||||
resources.files("redmine_reporter")
|
||||
.joinpath("templates/template.odt")
|
||||
.open("rb") as f
|
||||
):
|
||||
doc = load(f)
|
||||
|
||||
# Удаляем все текстовые параграфы из шаблона, оставляя только
|
||||
@@ -52,7 +58,9 @@ class ODTFormatter(Formatter):
|
||||
# Стиль ячеек
|
||||
cell_style_name = "TableCellStyle"
|
||||
cell_style = Style(name=cell_style_name, family="table-cell")
|
||||
cell_props = TableCellProperties(padding="0.04in", border="0.05pt solid #000000")
|
||||
cell_props = TableCellProperties(
|
||||
padding="0.04in", border="0.05pt solid #000000"
|
||||
)
|
||||
cell_style.addElement(cell_props)
|
||||
doc.automaticstyles.addElement(cell_style)
|
||||
|
||||
@@ -100,7 +108,9 @@ class ODTFormatter(Formatter):
|
||||
# в остальных — covered-cell для валидности ODF (#13)
|
||||
if first_version_in_project and first_row_in_version:
|
||||
cell_project = TableCell(stylename=cell_style_name)
|
||||
cell_project.setAttribute("numberrowsspanned", str(total_project_rows))
|
||||
cell_project.setAttribute(
|
||||
"numberrowsspanned", str(total_project_rows)
|
||||
)
|
||||
p = P(stylename=para_style_name, text=project)
|
||||
cell_project.addElement(p)
|
||||
row.addElement(cell_project)
|
||||
@@ -111,7 +121,9 @@ class ODTFormatter(Formatter):
|
||||
# в остальных — covered-cell для валидности ODF (#13)
|
||||
if first_row_in_version:
|
||||
cell_version = TableCell(stylename=cell_style_name)
|
||||
cell_version.setAttribute("numberrowsspanned", str(row_span_version))
|
||||
cell_version.setAttribute(
|
||||
"numberrowsspanned", str(row_span_version)
|
||||
)
|
||||
p = P(stylename=para_style_name, text=version)
|
||||
cell_version.addElement(p)
|
||||
row.addElement(cell_version)
|
||||
|
||||
@@ -19,8 +19,12 @@ class XLSXFormatter(Formatter):
|
||||
и числовой столбец с часами для удобного суммирования.
|
||||
"""
|
||||
|
||||
_HEADER_FILL = PatternFill(start_color="D9E1F2", end_color="D9E1F2", fill_type="solid")
|
||||
_TOTAL_FILL = PatternFill(start_color="FFF2CC", end_color="FFF2CC", fill_type="solid")
|
||||
_HEADER_FILL = PatternFill(
|
||||
start_color="D9E1F2", end_color="D9E1F2", fill_type="solid"
|
||||
)
|
||||
_TOTAL_FILL = PatternFill(
|
||||
start_color="FFF2CC", end_color="FFF2CC", fill_type="solid"
|
||||
)
|
||||
_BORDER = Border(
|
||||
left=Side(style="thin"),
|
||||
right=Side(style="thin"),
|
||||
@@ -40,7 +44,15 @@ class XLSXFormatter(Formatter):
|
||||
else:
|
||||
ws.title = "Report"
|
||||
|
||||
headers = ["Project", "Version", "Issue ID", "Subject", "Status", "Hours", "Spent Time"]
|
||||
headers = [
|
||||
"Project",
|
||||
"Version",
|
||||
"Issue ID",
|
||||
"Subject",
|
||||
"Status",
|
||||
"Hours",
|
||||
"Spent Time",
|
||||
]
|
||||
ws.append(headers)
|
||||
self._style_header_row(ws, headers)
|
||||
|
||||
@@ -89,7 +101,10 @@ class XLSXFormatter(Formatter):
|
||||
)
|
||||
self._style_total_row(ws, current_row, bold=False)
|
||||
ws.merge_cells(
|
||||
start_row=current_row, start_column=2, end_row=current_row, end_column=5
|
||||
start_row=current_row,
|
||||
start_column=2,
|
||||
end_row=current_row,
|
||||
end_column=5,
|
||||
)
|
||||
current_row += 1
|
||||
|
||||
@@ -99,7 +114,8 @@ class XLSXFormatter(Formatter):
|
||||
|
||||
if not self.no_time:
|
||||
project_hours = sum(
|
||||
sum(r.get("hours", 0.0) for r in task_rows) for task_rows in versions.values()
|
||||
sum(r.get("hours", 0.0) for r in task_rows)
|
||||
for task_rows in versions.values()
|
||||
)
|
||||
ws.append(
|
||||
[
|
||||
@@ -114,7 +130,10 @@ class XLSXFormatter(Formatter):
|
||||
)
|
||||
self._style_total_row(ws, current_row, bold=True)
|
||||
ws.merge_cells(
|
||||
start_row=current_row, start_column=1, end_row=current_row, end_column=5
|
||||
start_row=current_row,
|
||||
start_column=1,
|
||||
end_row=current_row,
|
||||
end_column=5,
|
||||
)
|
||||
current_row += 1
|
||||
project_totals[project] = project_hours
|
||||
@@ -137,7 +156,9 @@ class XLSXFormatter(Formatter):
|
||||
]
|
||||
)
|
||||
self._style_total_row(ws, current_row, bold=True)
|
||||
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=5)
|
||||
ws.merge_cells(
|
||||
start_row=current_row, start_column=1, end_row=current_row, end_column=5
|
||||
)
|
||||
|
||||
for start, end in project_ranges:
|
||||
ws.merge_cells(start_row=start, start_column=1, end_row=end, end_column=1)
|
||||
@@ -167,7 +188,9 @@ class XLSXFormatter(Formatter):
|
||||
cell.font = Font(bold=True)
|
||||
cell.fill = self._HEADER_FILL
|
||||
cell.border = self._BORDER
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
cell.alignment = Alignment(
|
||||
horizontal="center", vertical="center", wrap_text=True
|
||||
)
|
||||
|
||||
def _style_data_row(self, ws: Worksheet, row: int) -> None:
|
||||
for col_idx in range(1, 8):
|
||||
@@ -190,7 +213,15 @@ class XLSXFormatter(Formatter):
|
||||
|
||||
def _apply_column_widths(self, ws: Worksheet) -> None:
|
||||
# Минимальные ширины по умолчанию
|
||||
widths: Dict[int, float] = {1: 18.0, 2: 16.0, 3: 12.0, 4: 45.0, 5: 14.0, 6: 10.0, 7: 14.0}
|
||||
widths: Dict[int, float] = {
|
||||
1: 18.0,
|
||||
2: 16.0,
|
||||
3: 12.0,
|
||||
4: 45.0,
|
||||
5: 14.0,
|
||||
6: 10.0,
|
||||
7: 14.0,
|
||||
}
|
||||
|
||||
for row in ws.iter_rows(min_row=2, max_row=ws.max_row):
|
||||
for col_idx, cell in enumerate(row, start=1):
|
||||
|
||||
@@ -40,8 +40,12 @@ def build_message(
|
||||
period: str,
|
||||
) -> MIMEMultipart:
|
||||
"""Формирует MIME-письмо с подстановками и вложением."""
|
||||
subject = email_config.subject.replace("{author}", author).replace("{period}", period)
|
||||
body = email_config.body_text.replace("{author}", author).replace("{period}", period)
|
||||
subject = email_config.subject.replace("{author}", author).replace(
|
||||
"{period}", period
|
||||
)
|
||||
body = email_config.body_text.replace("{author}", author).replace(
|
||||
"{period}", period
|
||||
)
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg["Subject"] = subject
|
||||
@@ -91,7 +95,9 @@ def send_report(
|
||||
"""
|
||||
smtp_cfg = email_config.smtp
|
||||
msg = build_message(email_config, file_path, author, period)
|
||||
all_recipients = list(email_config.to) + list(email_config.cc) + list(email_config.bcc)
|
||||
all_recipients = (
|
||||
list(email_config.to) + list(email_config.cc) + list(email_config.bcc)
|
||||
)
|
||||
|
||||
try:
|
||||
with smtplib.SMTP(smtp_cfg.host, smtp_cfg.port, timeout=SMTP_TIMEOUT) as server:
|
||||
@@ -100,21 +106,18 @@ def send_report(
|
||||
if smtp_cfg.user:
|
||||
server.login(smtp_cfg.user, smtp_cfg.password)
|
||||
|
||||
server.send_message(msg, from_addr=email_config.from_, to_addrs=all_recipients)
|
||||
server.send_message(
|
||||
msg, from_addr=email_config.from_, to_addrs=all_recipients
|
||||
)
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
raise RedmineAPIError(
|
||||
"Ошибка аутентификации SMTP. Проверьте логин и пароль."
|
||||
) from None
|
||||
except TimeoutError:
|
||||
raise RedmineAPIError(
|
||||
"Таймаут соединения с SMTP-сервером."
|
||||
) from None
|
||||
raise RedmineAPIError("Таймаут соединения с SMTP-сервером.") from None
|
||||
except smtplib.SMTPException as exc:
|
||||
raise RedmineAPIError(
|
||||
f"Ошибка отправки письма: {exc}"
|
||||
) from exc
|
||||
raise RedmineAPIError(f"Ошибка отправки письма: {exc}") from exc
|
||||
except OSError as exc:
|
||||
raise RedmineAPIError(
|
||||
f"Не удалось подключиться к SMTP-серверу {smtp_cfg.host}:{smtp_cfg.port}"
|
||||
) from exc
|
||||
|
||||
|
||||
@@ -45,7 +45,9 @@ def build_grouped_report(
|
||||
"""
|
||||
|
||||
# Защитная сортировка -- гарантирует корректную группировку независимо от порядка на входе
|
||||
issue_hours = sorted(issue_hours, key=lambda x: (str(x[0].project), get_version(x[0]), x[0].id))
|
||||
issue_hours = sorted(
|
||||
issue_hours, key=lambda x: (str(x[0].project), get_version(x[0]), x[0].id)
|
||||
)
|
||||
|
||||
rows: List[ReportRow] = []
|
||||
prev_project: str = ""
|
||||
@@ -67,7 +69,9 @@ def build_grouped_report(
|
||||
time_text = ""
|
||||
|
||||
display_project = project if project != prev_project else ""
|
||||
display_version = version if (project != prev_project or version != prev_version) else ""
|
||||
display_version = (
|
||||
version if (project != prev_project or version != prev_version) else ""
|
||||
)
|
||||
|
||||
rows.append(
|
||||
cast(
|
||||
@@ -122,7 +126,9 @@ def calculate_summary(
|
||||
**{f"version:{k}": round(v, 2) for k, v in by_project_version.items()},
|
||||
}
|
||||
if by_activity:
|
||||
result.update({f"activity:{k}": round(v, 2) for k, v in by_activity_name.items()})
|
||||
result.update(
|
||||
{f"activity:{k}": round(v, 2) for k, v in by_activity_name.items()}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ def resolve_env_vars(value: str) -> str:
|
||||
var_name = match.group(1)
|
||||
env_value = os.environ.get(var_name)
|
||||
if env_value is None:
|
||||
logger.warning("Environment variable %s is not set, using empty string", var_name)
|
||||
logger.warning(
|
||||
"Environment variable %s is not set, using empty string", var_name
|
||||
)
|
||||
return ""
|
||||
return env_value
|
||||
|
||||
@@ -182,5 +184,7 @@ def save_period_to_config(
|
||||
|
||||
ensure_config_dir(path.parent)
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
yaml.dump(raw, fh, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
||||
yaml.dump(
|
||||
raw, fh, allow_unicode=True, default_flow_style=False, sort_keys=False
|
||||
)
|
||||
path.chmod(0o600)
|
||||
|
||||
Reference in New Issue
Block a user