feat(telegram): improve command planning and testability (#158)
This commit is contained in:
@@ -5,6 +5,8 @@ __pycache__/
|
|||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.coverage
|
.coverage
|
||||||
|
.mutmut-cache/
|
||||||
|
mutants/
|
||||||
.worktrees/
|
.worktrees/
|
||||||
research/
|
research/
|
||||||
_site/
|
_site/
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ check:
|
|||||||
uv run ty check src tests
|
uv run ty check src tests
|
||||||
uv run pytest
|
uv run pytest
|
||||||
|
|
||||||
|
mutate:
|
||||||
|
uv run mutmut run
|
||||||
|
|
||||||
docs-serve:
|
docs-serve:
|
||||||
uv run --no-sync python scripts/docs_prebuild.py
|
uv run --no-sync python scripts/docs_prebuild.py
|
||||||
uv run --group docs zensical serve
|
uv run --group docs zensical serve
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Literal
|
from typing import TYPE_CHECKING, Literal
|
||||||
|
|
||||||
from ...context import RunContext
|
from ...context import RunContext
|
||||||
@@ -38,20 +39,45 @@ async def require_admin_or_private(
|
|||||||
denied: str,
|
denied: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
reply = make_reply(cfg, msg)
|
reply = make_reply(cfg, msg)
|
||||||
|
decision = await check_admin_or_private(
|
||||||
|
cfg,
|
||||||
|
msg,
|
||||||
|
missing_sender=missing_sender,
|
||||||
|
failed_member=failed_member,
|
||||||
|
denied=denied,
|
||||||
|
)
|
||||||
|
if decision.allowed:
|
||||||
|
return True
|
||||||
|
if decision.error_text is not None:
|
||||||
|
await reply(text=decision.error_text)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PermissionDecision:
|
||||||
|
allowed: bool
|
||||||
|
error_text: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
async def check_admin_or_private(
|
||||||
|
cfg: TelegramBridgeConfig,
|
||||||
|
msg: TelegramIncomingMessage,
|
||||||
|
*,
|
||||||
|
missing_sender: str,
|
||||||
|
failed_member: str,
|
||||||
|
denied: str,
|
||||||
|
) -> PermissionDecision:
|
||||||
sender_id = msg.sender_id
|
sender_id = msg.sender_id
|
||||||
if sender_id is None:
|
if sender_id is None:
|
||||||
await reply(text=missing_sender)
|
return PermissionDecision(allowed=False, error_text=missing_sender)
|
||||||
return False
|
|
||||||
if msg.is_private:
|
if msg.is_private:
|
||||||
return True
|
return PermissionDecision(allowed=True)
|
||||||
member = await cfg.bot.get_chat_member(msg.chat_id, sender_id)
|
member = await cfg.bot.get_chat_member(msg.chat_id, sender_id)
|
||||||
if member is None:
|
if member is None:
|
||||||
await reply(text=failed_member)
|
return PermissionDecision(allowed=False, error_text=failed_member)
|
||||||
return False
|
|
||||||
if member.status in {"creator", "administrator"}:
|
if member.status in {"creator", "administrator"}:
|
||||||
return True
|
return PermissionDecision(allowed=True)
|
||||||
await reply(text=denied)
|
return PermissionDecision(allowed=False, error_text=denied)
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def resolve_engine_selection(
|
async def resolve_engine_selection(
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ActionPlan:
|
||||||
|
reply_text: str | None
|
||||||
|
actions: tuple[Callable[[], Awaitable[None]], ...] = ()
|
||||||
|
|
||||||
|
async def execute(self, reply: Callable[..., Awaitable[None]]) -> None:
|
||||||
|
for action in self.actions:
|
||||||
|
await action()
|
||||||
|
if self.reply_text is not None:
|
||||||
|
await reply(text=self.reply_text)
|
||||||
@@ -8,7 +8,8 @@ from ..topic_state import TopicStateStore
|
|||||||
from ..topics import _topic_key
|
from ..topics import _topic_key
|
||||||
from ..trigger_mode import resolve_trigger_mode
|
from ..trigger_mode import resolve_trigger_mode
|
||||||
from ..types import TelegramIncomingMessage
|
from ..types import TelegramIncomingMessage
|
||||||
from .overrides import require_admin_or_private
|
from .overrides import check_admin_or_private
|
||||||
|
from .plan import ActionPlan
|
||||||
from .reply import make_reply
|
from .reply import make_reply
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -31,11 +32,27 @@ async def _handle_trigger_command(
|
|||||||
scope_chat_ids: frozenset[int] | None = None,
|
scope_chat_ids: frozenset[int] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
reply = make_reply(cfg, msg)
|
reply = make_reply(cfg, msg)
|
||||||
tkey = (
|
plan = await _plan_trigger_command(
|
||||||
_topic_key(msg, cfg, scope_chat_ids=scope_chat_ids)
|
cfg,
|
||||||
if topic_store is not None
|
msg,
|
||||||
else None
|
args_text=args_text,
|
||||||
|
topic_store=topic_store,
|
||||||
|
chat_prefs=chat_prefs,
|
||||||
|
scope_chat_ids=scope_chat_ids,
|
||||||
)
|
)
|
||||||
|
await plan.execute(reply)
|
||||||
|
|
||||||
|
|
||||||
|
async def _plan_trigger_command(
|
||||||
|
cfg: TelegramBridgeConfig,
|
||||||
|
msg: TelegramIncomingMessage,
|
||||||
|
*,
|
||||||
|
args_text: str,
|
||||||
|
topic_store: TopicStateStore | None,
|
||||||
|
chat_prefs: ChatPrefsStore | None,
|
||||||
|
scope_chat_ids: frozenset[int] | None,
|
||||||
|
) -> ActionPlan:
|
||||||
|
tkey = _topic_key(msg, cfg, scope_chat_ids=scope_chat_ids)
|
||||||
tokens = split_command_args(args_text)
|
tokens = split_command_args(args_text)
|
||||||
action = tokens[0].lower() if tokens else "show"
|
action = tokens[0].lower() if tokens else "show"
|
||||||
|
|
||||||
@@ -65,53 +82,62 @@ async def _handle_trigger_command(
|
|||||||
chat_label = "unavailable" if chat_prefs is None else chat_mode or "none"
|
chat_label = "unavailable" if chat_prefs is None else chat_mode or "none"
|
||||||
defaults_line = f"defaults: topic: {topic_label}, chat: {chat_label}"
|
defaults_line = f"defaults: topic: {topic_label}, chat: {chat_label}"
|
||||||
available_line = "available: all, mentions"
|
available_line = "available: all, mentions"
|
||||||
await reply(text="\n\n".join([trigger_line, defaults_line, available_line]))
|
return ActionPlan(
|
||||||
return
|
reply_text="\n\n".join([trigger_line, defaults_line, available_line])
|
||||||
|
)
|
||||||
|
|
||||||
if action in {"all", "mentions"}:
|
if action in {"all", "mentions"}:
|
||||||
if not await require_admin_or_private(
|
decision = await check_admin_or_private(
|
||||||
cfg,
|
cfg,
|
||||||
msg,
|
msg,
|
||||||
missing_sender="cannot verify sender for trigger settings.",
|
missing_sender="cannot verify sender for trigger settings.",
|
||||||
failed_member="failed to verify trigger permissions.",
|
failed_member="failed to verify trigger permissions.",
|
||||||
denied="changing trigger mode is restricted to group admins.",
|
denied="changing trigger mode is restricted to group admins.",
|
||||||
):
|
)
|
||||||
return
|
if not decision.allowed:
|
||||||
|
return ActionPlan(reply_text=decision.error_text or TRIGGER_USAGE)
|
||||||
if tkey is not None:
|
if tkey is not None:
|
||||||
if topic_store is None:
|
if topic_store is None:
|
||||||
await reply(text="topic trigger settings are unavailable.")
|
return ActionPlan(reply_text="topic trigger settings are unavailable.")
|
||||||
return
|
return ActionPlan(
|
||||||
await topic_store.set_trigger_mode(tkey[0], tkey[1], action)
|
reply_text=f"topic trigger mode set to `{action}`",
|
||||||
await reply(text=f"topic trigger mode set to `{action}`")
|
actions=(
|
||||||
return
|
lambda: topic_store.set_trigger_mode(tkey[0], tkey[1], action),
|
||||||
|
),
|
||||||
|
)
|
||||||
if chat_prefs is None:
|
if chat_prefs is None:
|
||||||
await reply(text="chat trigger settings are unavailable (no config path).")
|
return ActionPlan(
|
||||||
return
|
reply_text="chat trigger settings are unavailable (no config path)."
|
||||||
await chat_prefs.set_trigger_mode(msg.chat_id, action)
|
)
|
||||||
await reply(text=f"chat trigger mode set to `{action}`")
|
return ActionPlan(
|
||||||
return
|
reply_text=f"chat trigger mode set to `{action}`",
|
||||||
|
actions=(lambda: chat_prefs.set_trigger_mode(msg.chat_id, action),),
|
||||||
|
)
|
||||||
|
|
||||||
if action == "clear":
|
if action == "clear":
|
||||||
if not await require_admin_or_private(
|
decision = await check_admin_or_private(
|
||||||
cfg,
|
cfg,
|
||||||
msg,
|
msg,
|
||||||
missing_sender="cannot verify sender for trigger settings.",
|
missing_sender="cannot verify sender for trigger settings.",
|
||||||
failed_member="failed to verify trigger permissions.",
|
failed_member="failed to verify trigger permissions.",
|
||||||
denied="changing trigger mode is restricted to group admins.",
|
denied="changing trigger mode is restricted to group admins.",
|
||||||
):
|
)
|
||||||
return
|
if not decision.allowed:
|
||||||
|
return ActionPlan(reply_text=decision.error_text or TRIGGER_USAGE)
|
||||||
if tkey is not None:
|
if tkey is not None:
|
||||||
if topic_store is None:
|
if topic_store is None:
|
||||||
await reply(text="topic trigger settings are unavailable.")
|
return ActionPlan(reply_text="topic trigger settings are unavailable.")
|
||||||
return
|
return ActionPlan(
|
||||||
await topic_store.clear_trigger_mode(tkey[0], tkey[1])
|
reply_text="topic trigger mode cleared (using chat default).",
|
||||||
await reply(text="topic trigger mode cleared (using chat default).")
|
actions=(lambda: topic_store.clear_trigger_mode(tkey[0], tkey[1]),),
|
||||||
return
|
)
|
||||||
if chat_prefs is None:
|
if chat_prefs is None:
|
||||||
await reply(text="chat trigger settings are unavailable (no config path).")
|
return ActionPlan(
|
||||||
return
|
reply_text="chat trigger settings are unavailable (no config path)."
|
||||||
await chat_prefs.clear_trigger_mode(msg.chat_id)
|
)
|
||||||
await reply(text="chat trigger mode reset to `all`.")
|
return ActionPlan(
|
||||||
return
|
reply_text="chat trigger mode reset to `all`.",
|
||||||
|
actions=(lambda: chat_prefs.clear_trigger_mode(msg.chat_id),),
|
||||||
|
)
|
||||||
|
|
||||||
await reply(text=TRIGGER_USAGE)
|
return ActionPlan(reply_text=TRIGGER_USAGE)
|
||||||
|
|||||||
@@ -290,6 +290,8 @@ async def _drain_backlog(cfg: TelegramBridgeConfig, offset: int | None) -> int |
|
|||||||
|
|
||||||
async def poll_updates(
|
async def poll_updates(
|
||||||
cfg: TelegramBridgeConfig,
|
cfg: TelegramBridgeConfig,
|
||||||
|
*,
|
||||||
|
sleep: Callable[[float], Awaitable[None]] = anyio.sleep,
|
||||||
) -> AsyncIterator[TelegramIncomingUpdate]:
|
) -> AsyncIterator[TelegramIncomingUpdate]:
|
||||||
offset: int | None = None
|
offset: int | None = None
|
||||||
offset = await _drain_backlog(cfg, offset)
|
offset = await _drain_backlog(cfg, offset)
|
||||||
@@ -299,6 +301,7 @@ async def poll_updates(
|
|||||||
cfg.bot,
|
cfg.bot,
|
||||||
chat_ids=lambda: _allowed_chat_ids(cfg),
|
chat_ids=lambda: _allowed_chat_ids(cfg),
|
||||||
offset=offset,
|
offset=offset,
|
||||||
|
sleep=sleep,
|
||||||
):
|
):
|
||||||
yield msg
|
yield msg
|
||||||
|
|
||||||
@@ -419,11 +422,13 @@ class ForwardCoalescer:
|
|||||||
*,
|
*,
|
||||||
task_group: TaskGroup,
|
task_group: TaskGroup,
|
||||||
debounce_s: float,
|
debounce_s: float,
|
||||||
|
sleep: Callable[[float], Awaitable[None]] = anyio.sleep,
|
||||||
dispatch: Callable[[_PendingPrompt], Awaitable[None]],
|
dispatch: Callable[[_PendingPrompt], Awaitable[None]],
|
||||||
pending: dict[ForwardKey, _PendingPrompt],
|
pending: dict[ForwardKey, _PendingPrompt],
|
||||||
) -> None:
|
) -> None:
|
||||||
self._task_group = task_group
|
self._task_group = task_group
|
||||||
self._debounce_s = debounce_s
|
self._debounce_s = debounce_s
|
||||||
|
self._sleep = sleep
|
||||||
self._dispatch = dispatch
|
self._dispatch = dispatch
|
||||||
self._pending = pending
|
self._pending = pending
|
||||||
|
|
||||||
@@ -556,7 +561,7 @@ class ForwardCoalescer:
|
|||||||
try:
|
try:
|
||||||
with anyio.CancelScope() as scope:
|
with anyio.CancelScope() as scope:
|
||||||
pending.cancel_scope = scope
|
pending.cancel_scope = scope
|
||||||
await anyio.sleep(self._debounce_s)
|
await self._sleep(self._debounce_s)
|
||||||
except anyio.get_cancelled_exc_class():
|
except anyio.get_cancelled_exc_class():
|
||||||
return
|
return
|
||||||
if self._pending.get(key) is not pending:
|
if self._pending.get(key) is not pending:
|
||||||
@@ -673,6 +678,7 @@ class MediaGroupBuffer:
|
|||||||
*,
|
*,
|
||||||
task_group: TaskGroup,
|
task_group: TaskGroup,
|
||||||
debounce_s: float,
|
debounce_s: float,
|
||||||
|
sleep: Callable[[float], Awaitable[None]] = anyio.sleep,
|
||||||
cfg: TelegramBridgeConfig,
|
cfg: TelegramBridgeConfig,
|
||||||
chat_prefs: ChatPrefsStore | None,
|
chat_prefs: ChatPrefsStore | None,
|
||||||
topic_store: TopicStateStore | None,
|
topic_store: TopicStateStore | None,
|
||||||
@@ -690,6 +696,7 @@ class MediaGroupBuffer:
|
|||||||
) -> None:
|
) -> None:
|
||||||
self._task_group = task_group
|
self._task_group = task_group
|
||||||
self._debounce_s = debounce_s
|
self._debounce_s = debounce_s
|
||||||
|
self._sleep = sleep
|
||||||
self._cfg = cfg
|
self._cfg = cfg
|
||||||
self._chat_prefs = chat_prefs
|
self._chat_prefs = chat_prefs
|
||||||
self._topic_store = topic_store
|
self._topic_store = topic_store
|
||||||
@@ -718,7 +725,7 @@ class MediaGroupBuffer:
|
|||||||
if state is None:
|
if state is None:
|
||||||
return
|
return
|
||||||
token = state.token
|
token = state.token
|
||||||
await anyio.sleep(self._debounce_s)
|
await self._sleep(self._debounce_s)
|
||||||
state = self._groups.get(key)
|
state = self._groups.get(key)
|
||||||
if state is None:
|
if state is None:
|
||||||
return
|
return
|
||||||
@@ -880,6 +887,7 @@ async def run_main_loop(
|
|||||||
default_engine_override: str | None = None,
|
default_engine_override: str | None = None,
|
||||||
transport_id: str | None = None,
|
transport_id: str | None = None,
|
||||||
transport_config: TelegramTransportSettings | None = None,
|
transport_config: TelegramTransportSettings | None = None,
|
||||||
|
sleep: Callable[[float], Awaitable[None]] = anyio.sleep,
|
||||||
) -> None:
|
) -> None:
|
||||||
state = TelegramLoopState(
|
state = TelegramLoopState(
|
||||||
running_tasks={},
|
running_tasks={},
|
||||||
@@ -971,6 +979,18 @@ async def run_main_loop(
|
|||||||
else:
|
else:
|
||||||
logger.info("trigger_mode.bot_username.unavailable")
|
logger.info("trigger_mode.bot_username.unavailable")
|
||||||
async with anyio.create_task_group() as tg:
|
async with anyio.create_task_group() as tg:
|
||||||
|
poller_fn: Callable[
|
||||||
|
[TelegramBridgeConfig], AsyncIterator[TelegramIncomingUpdate]
|
||||||
|
]
|
||||||
|
if poller is poll_updates:
|
||||||
|
poller_fn = cast(
|
||||||
|
Callable[
|
||||||
|
[TelegramBridgeConfig], AsyncIterator[TelegramIncomingUpdate]
|
||||||
|
],
|
||||||
|
partial(poll_updates, sleep=sleep),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
poller_fn = poller
|
||||||
config_path = cfg.runtime.config_path
|
config_path = cfg.runtime.config_path
|
||||||
watch_enabled = bool(watch_config) and config_path is not None
|
watch_enabled = bool(watch_config) and config_path is not None
|
||||||
|
|
||||||
@@ -1418,6 +1438,7 @@ async def run_main_loop(
|
|||||||
forward_coalescer = ForwardCoalescer(
|
forward_coalescer = ForwardCoalescer(
|
||||||
task_group=tg,
|
task_group=tg,
|
||||||
debounce_s=state.forward_coalesce_s,
|
debounce_s=state.forward_coalesce_s,
|
||||||
|
sleep=sleep,
|
||||||
dispatch=_dispatch_pending_prompt,
|
dispatch=_dispatch_pending_prompt,
|
||||||
pending=state.pending_prompts,
|
pending=state.pending_prompts,
|
||||||
)
|
)
|
||||||
@@ -1451,6 +1472,7 @@ async def run_main_loop(
|
|||||||
media_group_buffer = MediaGroupBuffer(
|
media_group_buffer = MediaGroupBuffer(
|
||||||
task_group=tg,
|
task_group=tg,
|
||||||
debounce_s=state.media_group_debounce_s,
|
debounce_s=state.media_group_debounce_s,
|
||||||
|
sleep=sleep,
|
||||||
cfg=cfg,
|
cfg=cfg,
|
||||||
chat_prefs=state.chat_prefs,
|
chat_prefs=state.chat_prefs,
|
||||||
topic_store=state.topic_store,
|
topic_store=state.topic_store,
|
||||||
@@ -1736,7 +1758,7 @@ async def run_main_loop(
|
|||||||
return
|
return
|
||||||
await route_message(update)
|
await route_message(update)
|
||||||
|
|
||||||
async for update in poller(cfg):
|
async for update in poller_fn(cfg):
|
||||||
await route_update(update)
|
await route_update(update)
|
||||||
finally:
|
finally:
|
||||||
await cfg.exec_cfg.transport.close()
|
await cfg.exec_cfg.transport.close()
|
||||||
|
|||||||
@@ -232,20 +232,32 @@ def mask_token(token: str) -> str:
|
|||||||
return f"{token[:9]}...{token[-5:]}"
|
return f"{token[:9]}...{token[-5:]}"
|
||||||
|
|
||||||
|
|
||||||
async def get_bot_info(token: str) -> User | None:
|
async def get_bot_info(
|
||||||
|
token: str,
|
||||||
|
*,
|
||||||
|
sleep: Callable[[float], Awaitable[None]] | None = None,
|
||||||
|
) -> User | None:
|
||||||
|
if sleep is None:
|
||||||
|
sleep = anyio.sleep
|
||||||
bot = TelegramClient(token)
|
bot = TelegramClient(token)
|
||||||
try:
|
try:
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
try:
|
try:
|
||||||
return await bot.get_me()
|
return await bot.get_me()
|
||||||
except TelegramRetryAfter as exc:
|
except TelegramRetryAfter as exc:
|
||||||
await anyio.sleep(exc.retry_after)
|
await sleep(exc.retry_after)
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
await bot.close()
|
await bot.close()
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_chat(token: str) -> ChatInfo:
|
async def wait_for_chat(
|
||||||
|
token: str,
|
||||||
|
*,
|
||||||
|
sleep: Callable[[float], Awaitable[None]] | None = None,
|
||||||
|
) -> ChatInfo:
|
||||||
|
if sleep is None:
|
||||||
|
sleep = anyio.sleep
|
||||||
bot = TelegramClient(token)
|
bot = TelegramClient(token)
|
||||||
try:
|
try:
|
||||||
offset: int | None = None
|
offset: int | None = None
|
||||||
@@ -260,7 +272,7 @@ async def wait_for_chat(token: str) -> ChatInfo:
|
|||||||
offset=offset, timeout_s=50, allowed_updates=allowed_updates
|
offset=offset, timeout_s=50, allowed_updates=allowed_updates
|
||||||
)
|
)
|
||||||
if updates is None:
|
if updates is None:
|
||||||
await anyio.sleep(1)
|
await sleep(1)
|
||||||
continue
|
continue
|
||||||
if not updates:
|
if not updates:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import AsyncIterator, Callable, Iterable
|
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import msgspec
|
import msgspec
|
||||||
@@ -210,6 +210,7 @@ async def poll_incoming(
|
|||||||
chat_id: int | None = None,
|
chat_id: int | None = None,
|
||||||
chat_ids: Iterable[int] | Callable[[], Iterable[int]] | None = None,
|
chat_ids: Iterable[int] | Callable[[], Iterable[int]] | None = None,
|
||||||
offset: int | None = None,
|
offset: int | None = None,
|
||||||
|
sleep: Callable[[float], Awaitable[None]] = anyio.sleep,
|
||||||
) -> AsyncIterator[TelegramIncomingUpdate]:
|
) -> AsyncIterator[TelegramIncomingUpdate]:
|
||||||
while True:
|
while True:
|
||||||
updates = await bot.get_updates(
|
updates = await bot.get_updates(
|
||||||
@@ -219,7 +220,7 @@ async def poll_incoming(
|
|||||||
)
|
)
|
||||||
if updates is None:
|
if updates is None:
|
||||||
logger.info("loop.get_updates.failed")
|
logger.info("loop.get_updates.failed")
|
||||||
await anyio.sleep(2)
|
await sleep(2)
|
||||||
continue
|
continue
|
||||||
logger.debug("loop.updates", updates=updates)
|
logger.debug("loop.updates", updates=updates)
|
||||||
resolved_chat_ids = chat_ids() if callable(chat_ids) else chat_ids
|
resolved_chat_ids = chat_ids() if callable(chat_ids) else chat_ids
|
||||||
|
|||||||
@@ -223,3 +223,62 @@ async def test_trigger_set_clear_permissions(tmp_path: Path) -> None:
|
|||||||
)
|
)
|
||||||
assert await prefs.get_trigger_mode(msg.chat_id) is None
|
assert await prefs.get_trigger_mode(msg.chat_id) is None
|
||||||
assert "chat trigger mode reset" in _last_text(transport)
|
assert "chat trigger mode reset" in _last_text(transport)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_trigger_missing_sender_denied(tmp_path: Path) -> None:
|
||||||
|
transport = FakeTransport()
|
||||||
|
cfg = make_cfg(transport)
|
||||||
|
prefs = ChatPrefsStore(tmp_path / "prefs.json")
|
||||||
|
msg = _msg("/trigger all", chat_type="supergroup", sender_id=None)
|
||||||
|
|
||||||
|
await _handle_trigger_command(
|
||||||
|
cfg,
|
||||||
|
msg,
|
||||||
|
args_text="all",
|
||||||
|
_ambient_context=None,
|
||||||
|
topic_store=None,
|
||||||
|
chat_prefs=prefs,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await prefs.get_trigger_mode(msg.chat_id) is None
|
||||||
|
assert "cannot verify sender" in _last_text(transport)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_trigger_topic_unavailable() -> None:
|
||||||
|
transport = FakeTransport()
|
||||||
|
cfg = replace(
|
||||||
|
make_cfg(transport),
|
||||||
|
topics=TelegramTopicsSettings(enabled=True, scope="all"),
|
||||||
|
)
|
||||||
|
msg = _msg("/trigger mentions", chat_type="supergroup", thread_id=3)
|
||||||
|
|
||||||
|
await _handle_trigger_command(
|
||||||
|
cfg,
|
||||||
|
msg,
|
||||||
|
args_text="mentions",
|
||||||
|
_ambient_context=None,
|
||||||
|
topic_store=None,
|
||||||
|
chat_prefs=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "topic trigger settings are unavailable" in _last_text(transport)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_trigger_chat_prefs_unavailable() -> None:
|
||||||
|
transport = FakeTransport()
|
||||||
|
cfg = make_cfg(transport)
|
||||||
|
msg = _msg("/trigger mentions", chat_type="supergroup")
|
||||||
|
|
||||||
|
await _handle_trigger_command(
|
||||||
|
cfg,
|
||||||
|
msg,
|
||||||
|
args_text="mentions",
|
||||||
|
_ambient_context=None,
|
||||||
|
topic_store=None,
|
||||||
|
chat_prefs=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "chat trigger settings are unavailable" in _last_text(transport)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from takopi.telegram.api_models import Chat, Message, Update, User
|
||||||
|
from takopi.telegram.parsing import poll_incoming
|
||||||
|
from tests.telegram_fakes import FakeBot
|
||||||
|
|
||||||
|
|
||||||
|
class _Bot(FakeBot):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.calls = 0
|
||||||
|
|
||||||
|
async def get_updates(
|
||||||
|
self,
|
||||||
|
offset: int | None,
|
||||||
|
timeout_s: int = 50,
|
||||||
|
allowed_updates: list[str] | None = None,
|
||||||
|
) -> list[Update] | None:
|
||||||
|
_ = offset, timeout_s, allowed_updates
|
||||||
|
self.calls += 1
|
||||||
|
if self.calls == 1:
|
||||||
|
return None
|
||||||
|
return [
|
||||||
|
Update(
|
||||||
|
update_id=1,
|
||||||
|
message=Message(
|
||||||
|
message_id=10,
|
||||||
|
text="hello",
|
||||||
|
chat=Chat(id=123, type="private"),
|
||||||
|
from_=User(id=9),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_poll_incoming_retries_on_none() -> None:
|
||||||
|
bot = _Bot()
|
||||||
|
sleeps: list[float] = []
|
||||||
|
|
||||||
|
async def sleep(delay: float) -> None:
|
||||||
|
sleeps.append(delay)
|
||||||
|
|
||||||
|
msg = None
|
||||||
|
async for update in poll_incoming(bot, sleep=sleep):
|
||||||
|
msg = update
|
||||||
|
break
|
||||||
|
|
||||||
|
assert sleeps == [2]
|
||||||
|
assert msg is not None
|
||||||
|
assert msg.chat_id == 123
|
||||||
@@ -243,6 +243,45 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" },
|
{ url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libcst"
|
||||||
|
version = "1.8.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/60/4105441989e321f7ad0fd28ffccb83eb6aac0b7cfb0366dab855dcccfbe5/libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c", size = 2204202, upload-time = "2025-11-03T22:32:52.311Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/2f/51a6f285c3a183e50cfe5269d4a533c21625aac2c8de5cdf2d41f079320d/libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661", size = 2083581, upload-time = "2025-11-03T22:32:54.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/64/921b1c19b638860af76cdb28bc81d430056592910b9478eea49e31a7f47a/libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474", size = 2236495, upload-time = "2025-11-03T22:32:55.723Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/a8/b00592f9bede618cbb3df6ffe802fc65f1d1c03d48a10d353b108057d09c/libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8", size = 2301466, upload-time = "2025-11-03T22:32:57.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/df/790d9002f31580fefd0aec2f373a0f5da99070e04c5e8b1c995d0104f303/libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a", size = 2300264, upload-time = "2025-11-03T22:32:58.852Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/de/dc3f10e65bab461be5de57850d2910a02c24c3ddb0da28f0e6e4133c3487/libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47", size = 2408572, upload-time = "2025-11-03T22:33:00.552Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/3b/35645157a7590891038b077db170d6dd04335cd2e82a63bdaa78c3297dfe/libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4", size = 2193917, upload-time = "2025-11-03T22:33:02.354Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/a2/1034a9ba7d3e82f2c2afaad84ba5180f601aed676d92b76325797ad60951/libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9", size = 2078748, upload-time = "2025-11-03T22:33:03.707Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/a1/30bc61e8719f721a5562f77695e6154e9092d1bdf467aa35d0806dcd6cea/libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1", size = 2188980, upload-time = "2025-11-03T22:33:05.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/14/c660204532407c5628e3b615015a902ed2d0b884b77714a6bdbe73350910/libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4", size = 2074828, upload-time = "2025-11-03T22:33:06.864Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/e2/c497c354943dff644749f177ee9737b09ed811b8fc842b05709a40fe0d1b/libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28", size = 2225568, upload-time = "2025-11-03T22:33:08.354Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/ef/45999676d07bd6d0eefa28109b4f97124db114e92f9e108de42ba46a8028/libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa", size = 2286523, upload-time = "2025-11-03T22:33:10.206Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/6c/517d8bf57d9f811862f4125358caaf8cd3320a01291b3af08f7b50719db4/libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1", size = 2288044, upload-time = "2025-11-03T22:33:11.628Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/ce/24d7d49478ffb61207f229239879845da40a374965874f5ee60f96b02ddb/libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996", size = 2392605, upload-time = "2025-11-03T22:33:12.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/c3/829092ead738b71e96a4e96896c96f276976e5a8a58b4473ed813d7c962b/libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82", size = 2181581, upload-time = "2025-11-03T22:33:14.514Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/6d/5d6a790a02eb0d9d36c4aed4f41b277497e6178900b2fa29c35353aa45ed/libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f", size = 2065000, upload-time = "2025-11-03T22:33:16.257Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkify-it-py"
|
||||||
|
version = "2.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "uc-micro-py" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lxml"
|
name = "lxml"
|
||||||
version = "6.0.2"
|
version = "6.0.2"
|
||||||
@@ -308,6 +347,11 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
|
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
linkify = [
|
||||||
|
{ name = "linkify-it-py" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markupsafe"
|
name = "markupsafe"
|
||||||
version = "3.0.3"
|
version = "3.0.3"
|
||||||
@@ -338,6 +382,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdit-py-plugins"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mdurl"
|
name = "mdurl"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -463,6 +519,23 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" },
|
{ url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mutmut"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "coverage" },
|
||||||
|
{ name = "libcst" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "setproctitle" },
|
||||||
|
{ name = "textual" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/b2/813295383dacdcd05dd9104f462fa549c8213153340315be3c93bf999cf6/mutmut-3.4.0.tar.gz", hash = "sha256:b3b47e60828192c9f2e7737316469777f769a9163d0e384776129b80f7e8aa3e", size = 31922, upload-time = "2025-11-19T09:28:07.147Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/bf/391def84fbb3269dcb4322cb380f2bbd189c609464300d974dfdc00e491d/mutmut-3.4.0-py3-none-any.whl", hash = "sha256:77233282c0cfb198c0605d640aacf26f8c6f2540cec15a940e5c8f144672cd2f", size = 29628, upload-time = "2025-11-19T09:28:05.64Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "2.15.0"
|
version = "2.15.0"
|
||||||
@@ -773,6 +846,34 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setproctitle"
|
||||||
|
version = "1.3.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shellingham"
|
name = "shellingham"
|
||||||
version = "1.5.4"
|
version = "1.5.4"
|
||||||
@@ -845,6 +946,7 @@ dependencies = [
|
|||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "mutmut" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-anyio" },
|
{ name = "pytest-anyio" },
|
||||||
{ name = "pytest-cov" },
|
{ name = "pytest-cov" },
|
||||||
@@ -876,6 +978,7 @@ requires-dist = [
|
|||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "mutmut", specifier = ">=3.4.0" },
|
||||||
{ name = "pytest", specifier = ">=9.0.2" },
|
{ name = "pytest", specifier = ">=9.0.2" },
|
||||||
{ name = "pytest-anyio", specifier = ">=0.0.0" },
|
{ name = "pytest-anyio", specifier = ">=0.0.0" },
|
||||||
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
||||||
@@ -887,6 +990,23 @@ docs = [
|
|||||||
{ name = "zensical", specifier = ">=0.0.15" },
|
{ name = "zensical", specifier = ">=0.0.15" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "textual"
|
||||||
|
version = "7.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py", extra = ["linkify"] },
|
||||||
|
{ name = "mdit-py-plugins" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
{ name = "rich" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/ee/620c887bfad9d6eba062dfa3b6b0e735e0259102e2667b19f21625ef598d/textual-7.3.0.tar.gz", hash = "sha256:3169e8ba5518a979b0771e60be380ab1a6c344f30a2126e360e6f38d009a3de4", size = 1590692, upload-time = "2026-01-15T16:32:02.342Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/1f/abeb4e5cb36b99dd37db72beb2a74d58598ccb35aaadf14624ee967d4a6b/textual-7.3.0-py3-none-any.whl", hash = "sha256:db235cecf969c87fe5a9c04d83595f506affc9db81f3a53ab849534d726d330a", size = 716374, upload-time = "2026-01-15T16:31:58.233Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tomli-w"
|
name = "tomli-w"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -969,6 +1089,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uc-micro-py"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "watchdog"
|
name = "watchdog"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user