refactor: telegram modules and tighten linting (#111)

This commit is contained in:
banteg
2026-01-13 05:14:26 +04:00
committed by GitHub
parent f060d3b59c
commit c1205cd5a8
63 changed files with 3257 additions and 3073 deletions
+3 -3
View File
@@ -14,7 +14,7 @@ from takopi.model import (
def session_started(engine: str, value: str, title: str = "Codex") -> TakopiEvent:
engine_id = EngineId(engine)
engine_id: EngineId = engine
return StartedEvent(
engine=engine_id,
resume=ResumeToken(engine=engine_id, value=value),
@@ -29,7 +29,7 @@ def action_started(
detail: dict[str, Any] | None = None,
engine: str = "codex",
) -> TakopiEvent:
engine_id = EngineId(engine)
engine_id: EngineId = engine
return ActionEvent(
engine=engine_id,
action=Action(
@@ -50,7 +50,7 @@ def action_completed(
detail: dict[str, Any] | None = None,
engine: str = "codex",
) -> TakopiEvent:
engine_id = EngineId(engine)
engine_id: EngineId = engine
return ActionEvent(
engine=engine_id,
action=Action(
+2 -1
View File
@@ -1,7 +1,8 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable, Iterable
from typing import Any
from collections.abc import Callable, Iterable
@dataclass(frozen=True, slots=True)
+2 -2
View File
@@ -5,7 +5,7 @@ import pytest
from takopi.runner_bridge import ExecBridgeConfig, IncomingMessage, handle_message
from takopi.markdown import MarkdownParts, MarkdownPresenter
from takopi.model import EngineId, ResumeToken, TakopiEvent
from takopi.model import ResumeToken, TakopiEvent
from takopi.telegram.render import prepare_telegram
from takopi.runners.codex import CodexRunner
from takopi.runners.mock import Advance, Emit, Raise, Return, ScriptRunner, Wait
@@ -13,7 +13,7 @@ from takopi.settings import load_settings, require_telegram
from takopi.transport import MessageRef, RenderedMessage, SendOptions
from tests.factories import action_completed, action_started
CODEX_ENGINE = EngineId("codex")
CODEX_ENGINE = "codex"
class _FakeTransport:
+1 -2
View File
@@ -7,14 +7,13 @@ from collections.abc import AsyncIterator
from takopi.model import (
ActionEvent,
CompletedEvent,
EngineId,
ResumeToken,
StartedEvent,
TakopiEvent,
)
from takopi.runners.codex import CodexRunner, find_exec_only_flag
CODEX_ENGINE = EngineId("codex")
CODEX_ENGINE = "codex"
@pytest.mark.anyio
+4 -5
View File
@@ -45,11 +45,10 @@ def test_resolve_default_base_prefers_master_over_main(monkeypatch) -> None:
return None
def _fake_ok(args, **kwargs):
if args == ["show-ref", "--verify", "--quiet", "refs/heads/master"]:
return True
if args == ["show-ref", "--verify", "--quiet", "refs/heads/main"]:
return True
return False
return args in (
["show-ref", "--verify", "--quiet", "refs/heads/master"],
["show-ref", "--verify", "--quiet", "refs/heads/main"],
)
monkeypatch.setattr("takopi.utils.git.git_stdout", _fake_stdout)
monkeypatch.setattr("takopi.utils.git.git_ok", _fake_ok)
+3 -4
View File
@@ -7,7 +7,6 @@ from takopi.model import (
Action,
ActionEvent,
CompletedEvent,
EngineId,
ResumeToken,
StartedEvent,
TakopiEvent,
@@ -15,7 +14,7 @@ from takopi.model import (
from takopi.runners.mock import Emit, Return, ScriptRunner, Wait
from tests.factories import action_started
CODEX_ENGINE = EngineId("codex")
CODEX_ENGINE = "codex"
@pytest.mark.anyio
@@ -84,7 +83,7 @@ async def test_runner_releases_lock_when_consumer_closes() -> None:
gate = anyio.Event()
runner = ScriptRunner([Wait(gate)], engine=CODEX_ENGINE, resume_value="sid")
gen = cast(AsyncGenerator[TakopiEvent, None], runner.run("hello", None))
gen = cast(AsyncGenerator[TakopiEvent], runner.run("hello", None))
try:
while True:
evt = await anext(gen)
@@ -94,7 +93,7 @@ async def test_runner_releases_lock_when_consumer_closes() -> None:
await gen.aclose()
gen2 = cast(
AsyncGenerator[TakopiEvent, None],
AsyncGenerator[TakopiEvent],
runner.run("again", ResumeToken(engine=CODEX_ENGINE, value="sid")),
)
try:
+7 -8
View File
@@ -8,7 +8,6 @@ import takopi.runner as runner_module
from takopi.model import (
ActionEvent,
CompletedEvent,
EngineId,
ResumeToken,
StartedEvent,
TakopiEvent,
@@ -22,7 +21,7 @@ from takopi.runner import (
class _DummyRunner(ResumeTokenMixin, BaseRunner):
engine = EngineId("dummy")
engine = "dummy"
resume_re = re.compile(r"(?im)^`?dummy resume (?P<token>[^`\s]+)`?$")
async def run_impl(
@@ -39,7 +38,7 @@ class _DummyRunner(ResumeTokenMixin, BaseRunner):
class _DummyJsonlRunner(JsonlSubprocessRunner):
engine = EngineId("dummy-jsonl")
engine = "dummy-jsonl"
def command(self) -> str:
return "dummy"
@@ -67,7 +66,7 @@ class _DummyJsonlRunner(JsonlSubprocessRunner):
class _BareJsonlRunner(JsonlSubprocessRunner):
engine = EngineId("bare-jsonl")
engine = "bare-jsonl"
class _RunJsonlRunner(_DummyJsonlRunner):
@@ -177,7 +176,7 @@ async def test_base_runner_run_locked_handles_resume() -> None:
@pytest.mark.anyio
async def test_base_runner_rejects_wrong_resume_engine() -> None:
runner = _DummyRunner()
bad_resume = ResumeToken(engine=EngineId("other"), value="oops")
bad_resume = ResumeToken(engine="other", value="oops")
with pytest.raises(RuntimeError):
_ = [evt async for evt in runner.run("hello", bad_resume)]
@@ -185,7 +184,7 @@ async def test_base_runner_rejects_wrong_resume_engine() -> None:
@pytest.mark.anyio
async def test_base_runner_run_impl_not_implemented() -> None:
class _BareRunner(BaseRunner):
engine = EngineId("bare")
engine = "bare"
runner = _BareRunner()
with pytest.raises(NotImplementedError):
@@ -204,7 +203,7 @@ def test_resume_token_format_and_extract() -> None:
assert runner.extract_resume(None) is None
with pytest.raises(RuntimeError):
runner.format_resume(ResumeToken(engine=EngineId("other"), value="bad"))
runner.format_resume(ResumeToken(engine="other", value="bad"))
def test_session_lock_reuse() -> None:
@@ -294,7 +293,7 @@ def test_jsonl_helpers() -> None:
assert found == resume
assert emit is False
mismatch = StartedEvent(engine=EngineId("other"), resume=resume, title="t")
mismatch = StartedEvent(engine="other", resume=resume, title="t")
with pytest.raises(RuntimeError):
runner.handle_started_event(mismatch, expected_session=None, found_session=None)
+6 -7
View File
@@ -6,7 +6,6 @@ from typing import Any
import pytest
from takopi.config import ProjectsConfig
from takopi.model import EngineId
from takopi.router import AutoRouter, RunnerEntry
from takopi.runners.mock import Return, ScriptRunner
from takopi.settings import (
@@ -19,8 +18,8 @@ from takopi.transport_runtime import TransportRuntime
def test_build_startup_message_includes_missing_engines(tmp_path: Path) -> None:
codex = EngineId("codex")
pi = EngineId("pi")
codex = "codex"
pi = "pi"
runner = ScriptRunner([Return(answer="ok")], engine=codex)
missing = ScriptRunner([Return(answer="ok")], engine=pi)
router = AutoRouter(
@@ -53,9 +52,9 @@ def test_build_startup_message_includes_missing_engines(tmp_path: Path) -> None:
def test_build_startup_message_surfaces_unavailable_engine_reasons(
tmp_path: Path,
) -> None:
codex = EngineId("codex")
pi = EngineId("pi")
claude = EngineId("claude")
codex = "codex"
pi = "pi"
claude = "claude"
runner = ScriptRunner([Return(answer="ok")], engine=codex)
bad_cfg = ScriptRunner([Return(answer="ok")], engine=pi)
load_err = ScriptRunner([Return(answer="ok")], engine=claude)
@@ -100,7 +99,7 @@ def test_telegram_backend_build_and_run_wires_config(
encoding="utf-8",
)
codex = EngineId("codex")
codex = "codex"
runner = ScriptRunner([Return(answer="ok")], engine=codex)
router = AutoRouter(
entries=[RunnerEntry(engine=codex, runner=runner)],
+13 -14
View File
@@ -6,8 +6,9 @@ import anyio
import pytest
from takopi import commands, plugins
from takopi.telegram.commands.executor import _CaptureTransport, _run_engine
from takopi.telegram.commands.file_transfer import _handle_file_get, _handle_file_put
import takopi.telegram.loop as telegram_loop
import takopi.telegram.commands as telegram_commands
import takopi.telegram.topics as telegram_topics
from takopi.directives import parse_directives
from takopi.telegram.api_models import (
@@ -39,7 +40,7 @@ from takopi.context import RunContext
from takopi.config import ProjectConfig, ProjectsConfig
from takopi.runner_bridge import ExecBridgeConfig, RunningTask
from takopi.markdown import MarkdownPresenter
from takopi.model import EngineId, ResumeToken
from takopi.model import ResumeToken
from takopi.progress import ProgressTracker
from takopi.router import AutoRouter, RunnerEntry
from takopi.transport_runtime import TransportRuntime
@@ -52,7 +53,7 @@ from takopi.telegram.types import (
from takopi.transport import MessageRef, RenderedMessage, SendOptions
from tests.plugin_fixtures import FakeEntryPoint, install_entrypoints
CODEX_ENGINE = EngineId("codex")
CODEX_ENGINE = "codex"
def _empty_projects() -> ProjectsConfig:
@@ -905,9 +906,7 @@ async def test_handle_file_put_writes_file(tmp_path: Path) -> None:
),
)
await telegram_commands._handle_file_put(
cfg, msg, "/proj uploads/hello.txt", None, None
)
await _handle_file_put(cfg, msg, "/proj uploads/hello.txt", None, None)
target = tmp_path / "uploads" / "hello.txt"
assert target.read_bytes() == payload
@@ -966,7 +965,7 @@ async def test_handle_file_get_sends_document_for_allowed_user(
chat_type="supergroup",
)
await telegram_commands._handle_file_get(cfg, msg, "/proj hello.txt", None, None)
await _handle_file_get(cfg, msg, "/proj hello.txt", None, None)
assert bot.document_calls
assert bot.document_calls[0]["filename"] == "hello.txt"
@@ -1263,7 +1262,7 @@ async def test_send_with_resume_reports_when_missing() -> None:
@pytest.mark.anyio
async def test_run_engine_hides_resume_line_in_topics() -> None:
transport = telegram_commands._CaptureTransport()
transport = _CaptureTransport()
runner = ScriptRunner(
[Return(answer="ok")],
engine=CODEX_ENGINE,
@@ -1279,7 +1278,7 @@ async def test_run_engine_hides_resume_line_in_topics() -> None:
projects=_empty_projects(),
)
await telegram_commands._run_engine(
await _run_engine(
exec_cfg=exec_cfg,
runtime=runtime,
running_tasks={},
@@ -1456,14 +1455,14 @@ async def test_run_main_loop_auto_resumes_topic_default_engine(
123, 77, ResumeToken(engine=CODEX_ENGINE, value="resume-codex")
)
await store.set_session_resume(
123, 77, ResumeToken(engine=EngineId("claude"), value="resume-claude")
123, 77, ResumeToken(engine="claude", value="resume-claude")
)
await store.set_default_engine(123, 77, "claude")
transport = _FakeTransport()
bot = _FakeBot()
codex_runner = ScriptRunner([Return(answer="ok")], engine=CODEX_ENGINE)
claude_runner = ScriptRunner([Return(answer="ok")], engine=EngineId("claude"))
claude_runner = ScriptRunner([Return(answer="ok")], engine="claude")
router = AutoRouter(
entries=[
RunnerEntry(engine=codex_runner.engine, runner=codex_runner),
@@ -1521,7 +1520,7 @@ async def test_run_main_loop_auto_resumes_topic_default_engine(
assert codex_runner.calls == []
assert len(claude_runner.calls) == 1
assert claude_runner.calls[0][1] == ResumeToken(
engine=EngineId("claude"), value="resume-claude"
engine="claude", value="resume-claude"
)
@@ -2443,7 +2442,7 @@ async def test_run_main_loop_command_uses_project_default_engine(
transport = _FakeTransport()
bot = _FakeBot()
codex_runner = ScriptRunner([Return(answer="ok")], engine=CODEX_ENGINE)
pi_runner = ScriptRunner([Return(answer="ok")], engine=EngineId("pi"))
pi_runner = ScriptRunner([Return(answer="ok")], engine="pi")
router = AutoRouter(
entries=[
RunnerEntry(engine=codex_runner.engine, runner=codex_runner),
@@ -2525,7 +2524,7 @@ async def test_run_main_loop_command_defaults_to_chat_project(
transport = _FakeTransport()
bot = _FakeBot()
codex_runner = ScriptRunner([Return(answer="ok")], engine=CODEX_ENGINE)
pi_runner = ScriptRunner([Return(answer="ok")], engine=EngineId("pi"))
pi_runner = ScriptRunner([Return(answer="ok")], engine="pi")
router = AutoRouter(
entries=[
RunnerEntry(engine=codex_runner.engine, runner=codex_runner),
+13 -12
View File
@@ -3,6 +3,7 @@ import pytest
from takopi.logging import setup_logging
from takopi.telegram.client import TelegramClient, TelegramRetryAfter
from takopi.telegram.client_api import HttpBotClient
@pytest.mark.anyio
@@ -25,9 +26,9 @@ async def test_telegram_429_no_retry() -> None:
client = httpx.AsyncClient(transport=transport)
try:
tg = TelegramClient("123:abcDEF_ghij", http_client=client)
api = HttpBotClient("123:abcDEF_ghij", http_client=client)
with pytest.raises(TelegramRetryAfter) as exc:
await tg._post("sendMessage", {"chat_id": 1, "text": "hi"})
await api._post("sendMessage", {"chat_id": 1, "text": "hi"})
finally:
await client.aclose()
@@ -49,8 +50,8 @@ async def test_no_token_in_logs_on_http_error(
client = httpx.AsyncClient(transport=transport)
try:
tg = TelegramClient(token, http_client=client)
await tg._post("getUpdates", {"timeout": 1})
api = HttpBotClient(token, http_client=client)
await api._post("getUpdates", {"timeout": 1})
finally:
await client.aclose()
@@ -79,9 +80,9 @@ async def test_telegram_429_no_retry_post_form() -> None:
client = httpx.AsyncClient(transport=transport)
try:
tg = TelegramClient("123:abcDEF_ghij", http_client=client)
api = HttpBotClient("123:abcDEF_ghij", http_client=client)
with pytest.raises(TelegramRetryAfter) as exc:
await tg._post_form(
await api._post_form(
"sendDocument",
{"chat_id": 1},
files={"document": ("note.txt", b"hi")},
@@ -102,9 +103,9 @@ async def test_telegram_429_defaults_retry_after_on_bad_body() -> None:
client = httpx.AsyncClient(transport=transport)
try:
tg = TelegramClient("123:abcDEF_ghij", http_client=client)
api = HttpBotClient("123:abcDEF_ghij", http_client=client)
with pytest.raises(TelegramRetryAfter) as exc:
await tg._post("sendMessage", {"chat_id": 1, "text": "hi"})
await api._post("sendMessage", {"chat_id": 1, "text": "hi"})
finally:
await client.aclose()
@@ -124,8 +125,8 @@ async def test_telegram_ok_false_returns_none() -> None:
client = httpx.AsyncClient(transport=transport)
try:
tg = TelegramClient("123:abcDEF_ghij", http_client=client)
result = await tg._post("getUpdates", {"timeout": 1})
api = HttpBotClient("123:abcDEF_ghij", http_client=client)
result = await api._post("getUpdates", {"timeout": 1})
finally:
await client.aclose()
@@ -141,8 +142,8 @@ async def test_telegram_invalid_payload_returns_none() -> None:
client = httpx.AsyncClient(transport=transport)
try:
tg = TelegramClient("123:abcDEF_ghij", http_client=client)
result = await tg._post("getUpdates", {"timeout": 1})
api = HttpBotClient("123:abcDEF_ghij", http_client=client)
result = await api._post("getUpdates", {"timeout": 1})
finally:
await client.aclose()
+3 -4
View File
@@ -4,7 +4,6 @@ import pytest
from takopi.config import ProjectConfig, ProjectsConfig
from takopi.context import RunContext
from takopi.model import EngineId
from takopi.router import AutoRouter, RunnerEntry
from takopi.runners.mock import Return, ScriptRunner
from takopi.telegram.chat_prefs import ChatPrefsStore
@@ -15,8 +14,8 @@ from takopi.transport_runtime import TransportRuntime
@pytest.mark.anyio
async def test_resolve_engine_for_message_sources(tmp_path) -> None:
codex = ScriptRunner([Return(answer="ok")], engine=EngineId("codex"))
pi = ScriptRunner([Return(answer="ok")], engine=EngineId("pi"))
codex = ScriptRunner([Return(answer="ok")], engine="codex")
pi = ScriptRunner([Return(answer="ok")], engine="pi")
router = AutoRouter(
entries=[
RunnerEntry(engine=codex.engine, runner=codex),
@@ -42,7 +41,7 @@ async def test_resolve_engine_for_message_sources(tmp_path) -> None:
resolved = await resolve_engine_for_message(
runtime=runtime,
context=RunContext(project="proj"),
explicit_engine=EngineId("codex"),
explicit_engine="codex",
chat_id=1,
topic_key=(1, 10),
topic_store=topic_store,
+2 -2
View File
@@ -15,8 +15,8 @@ class DummyTransport:
def interactive_setup(self, *, force: bool) -> bool:
raise NotImplementedError
def lock_token(self, *, transport_config: object, config_path):
_ = transport_config, config_path
def lock_token(self, *, transport_config: object, _config_path):
_ = transport_config, _config_path
raise NotImplementedError
def build_and_run(