style: apply ruff format to all source files

This commit is contained in:
Кокос Артем Николаевич
2026-07-10 12:39:24 +07:00
parent 25425901b1
commit 0968560090
14 changed files with 286 additions and 107 deletions

View File

@@ -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}",

View File

@@ -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

View File

@@ -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.

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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)