feat: add --my-tasks mode for open assigned issues (#71)

List open Redmine issues assigned to the current (or overridden) user,
sorted by priority rank (P1..P5) then version due date with a stable id
tie-break. Renders an OSC 8 hyperlinked console table on stdout and a
counts/hours summary line on stderr; version due dates are fetched once
per unique version, unavailable versions degrade to an empty due date.

Also migrates the codebase to ruff 0.17.3 defaults (PEP 585/604
annotations, targeted noqa for intentional blind excepts and CLI root
logging). Behavior-neutral: 408 tests pass with -W error.
This commit is contained in:
Кокос Артем Николаевич
2026-10-28 15:26:41 +07:00
parent 3ce4f408ed
commit 5d1cf4ab74
27 changed files with 1255 additions and 137 deletions

View File

@@ -2,7 +2,6 @@ import smtplib
from unittest import mock
import pytest
from redmine_reporter.client import RedmineAPIError
from redmine_reporter.config import EmailConfig, SmtpConfig
from redmine_reporter.mailer import build_message, send_report
@@ -65,7 +64,7 @@ class TestBuildMessage:
data = buf.getvalue()
assert b"Content-Transfer-Encoding: 8bit" in data
assert "Автор: Иванов".encode("utf-8") in data
assert "Автор: Иванов".encode() in data
def test_plain_part_uses_8bit_utf8(self):
"""Тела письма кодируются 8bit UTF-8 (не base64), точечно на часть."""
@@ -74,7 +73,7 @@ class TestBuildMessage:
)
msg = build_message(cfg, "/tmp/r.xlsx", "Иванов", "Q1", [])
plain = [p for p in msg.walk() if p.get_content_type() == "text/plain"][0]
plain = next(p for p in msg.walk() if p.get_content_type() == "text/plain")
assert plain["Content-Transfer-Encoding"] == "8bit"
assert (
plain.get_payload(decode=True).decode("utf-8")
@@ -205,7 +204,7 @@ class TestBuildMessage:
]
msg = build_message(cfg, str(report), "A", "P", rows)
html_part = [p for p in msg.walk() if p.get_content_type() == "text/html"][0]
html_part = next(p for p in msg.walk() if p.get_content_type() == "text/html")
payload = html_part.get_payload(decode=True).decode("utf-8")
assert "Project X" in payload
assert "Important task" in payload
@@ -228,9 +227,9 @@ class TestBuildMessage:
]
msg = build_message(cfg, str(report), "A", "P", rows)
alternative = [
alternative = next(
p for p in msg.walk() if p.get_content_type() == "multipart/alternative"
][0]
)
payloads = alternative.get_payload()
assert payloads[0].get_content_type() == "text/plain"
assert payloads[1].get_content_type() == "text/html"
@@ -273,9 +272,11 @@ class TestSendReport:
report.write_text("fake content")
cfg = _make_email_config()
with mock.patch("smtplib.SMTP", side_effect=OSError("Connection refused")):
with pytest.raises(RedmineAPIError, match="Не удалось подключиться к SMTP"):
send_report(cfg, str(report), "A", "P", [])
with (
mock.patch("smtplib.SMTP", side_effect=OSError("Connection refused")),
pytest.raises(RedmineAPIError, match="Не удалось подключиться к SMTP"),
):
send_report(cfg, str(report), "A", "P", [])
def test_send_auth_error(self, tmp_path):
report = tmp_path / "report.xlsx"
@@ -297,9 +298,11 @@ class TestSendReport:
report.write_text("fake content")
cfg = _make_email_config()
with mock.patch("smtplib.SMTP", side_effect=TimeoutError()):
with pytest.raises(RedmineAPIError, match="Таймаут соединения с SMTP"):
send_report(cfg, str(report), "A", "P", [])
with (
mock.patch("smtplib.SMTP", side_effect=TimeoutError()),
pytest.raises(RedmineAPIError, match="Таймаут соединения с SMTP"),
):
send_report(cfg, str(report), "A", "P", [])
def test_send_generic_smtp_error(self, tmp_path):
report = tmp_path / "report.xlsx"