feat(bot): точка входа, конфиг, главное меню, доступ для ADMIN_IDS, healthcheck
- config: pydantic-settings по подсистемам, ADMIN_IDS через запятую - texts.yaml + валидация при старте (лишний/пропущенный ключ — ошибка сразу) - меню: reply-клавиатура разделов (Участники — только админам), инлайн «Назад»/«Отмена» с редактированием сообщения на месте, /start, /menu, /help - AccessMiddleware (outer) на сообщениях и кнопках: до #25 пускает только ADMIN_IDS, постороннему — одна фраза в сутки, устаревшие кнопки молча гасятся - heartbeat-файл + HEALTHCHECK в Dockerfile; JSON-логи - FSM пока в памяти (фабрика), хранилище в БД — в #5 - тесты на фейковой сессии aiogram через настоящий Dispatcher Refs #4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
"""Liveness: a background task touches a file while the event loop runs; the Docker healthcheck checks its age.
|
||||
|
||||
A stuck loop stops touching the file, so the container turns unhealthy even though the process is still up.
|
||||
`python -m app.health` exits 0 when healthy, 1 otherwise.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from app.config import RuntimeSettings
|
||||
|
||||
|
||||
async def heartbeat(path: Path, interval: float) -> None:
|
||||
while True:
|
||||
await asyncio.to_thread(path.touch)
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
|
||||
def is_alive(path: Path, max_age: float, now: float | None = None) -> bool:
|
||||
try:
|
||||
age = (time.time() if now is None else now) - path.stat().st_mtime
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
return age <= max_age
|
||||
|
||||
|
||||
def main() -> None:
|
||||
rt = RuntimeSettings()
|
||||
sys.exit(0 if is_alive(rt.heartbeat_file, rt.heartbeat_max_age) else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user