У админки появился собственный сервис, а не только место в плане
Каркас FastAPI живёт в admin/ и собирается отдельным образом: главная страница со списком будущих разделов и /healthz для healthcheck, пока без БД и логина. Сервис добавлен в оба compose (порт только на loopback до появления домена), CI теперь собирает и публикует два образа — бота и админку. Настройки админки наследуют общие, но не требуют BOT_TOKEN: сервис запускается, даже пока токен не заполнен.
This commit is contained in:
parent
16bd8c06b5
commit
b11676764e
|
|
@ -2,6 +2,10 @@
|
|||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# секреты локального окружения (в образ не попадают)
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# venv / кеши
|
||||
.venv
|
||||
.venv11
|
||||
|
|
|
|||
|
|
@ -42,3 +42,6 @@ CLICKHOUSE_PASSWORD=
|
|||
ANALYTICS_BATCH_SIZE=200
|
||||
ANALYTICS_FLUSH_INTERVAL=5.0
|
||||
ANALYTICS_BUFFER_MAX=5000
|
||||
|
||||
# Админка (FastAPI, Блок 4). Порт на loopback хоста для docker compose.
|
||||
ADMIN_HTTP_PORT=8000
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ concurrency:
|
|||
|
||||
env:
|
||||
REGISTRY: git.mpakosrv.ru
|
||||
IMAGE_NAME: mpakobecex/dispelevilbot-bot
|
||||
IMAGE_NAME_BOT: mpakobecex/dispelevilbot-bot
|
||||
IMAGE_NAME_ADMIN: mpakobecex/dispelevilbot-admin
|
||||
|
||||
jobs:
|
||||
lint-test:
|
||||
|
|
@ -40,16 +41,22 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- name: Логин в Gitea Registry
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
- name: Сборка и публикация образа
|
||||
- name: Сборка и публикация образов
|
||||
run: |
|
||||
set -e
|
||||
IMAGE="$REGISTRY/$IMAGE_NAME"
|
||||
SHA="${GITHUB_SHA:-unknown}"
|
||||
SHORT_SHA="$(echo "$SHA" | cut -c1-12)"
|
||||
SHORT_SHA="$(echo "${GITHUB_SHA:-unknown}" | cut -c1-12)"
|
||||
VERSION="$(sed -n 's/^__version__ = "\(.*\)"/\1/p' common/version.py)"
|
||||
TAGS="$IMAGE:$SHORT_SHA $IMAGE:latest"
|
||||
if [ -n "$VERSION" ]; then TAGS="$TAGS $IMAGE:$VERSION"; fi
|
||||
ARGS=""
|
||||
for t in $TAGS; do ARGS="$ARGS -t $t"; done
|
||||
docker build $ARGS .
|
||||
for t in $TAGS; do docker push "$t"; done
|
||||
|
||||
build_push() {
|
||||
image="$1"
|
||||
dockerfile="$2"
|
||||
tags="$image:$SHORT_SHA $image:latest"
|
||||
if [ -n "$VERSION" ]; then tags="$tags $image:$VERSION"; fi
|
||||
args=""
|
||||
for t in $tags; do args="$args -t $t"; done
|
||||
docker build $args -f "$dockerfile" .
|
||||
for t in $tags; do docker push "$t"; done
|
||||
}
|
||||
|
||||
build_push "$REGISTRY/$IMAGE_NAME_BOT" Dockerfile
|
||||
build_push "$REGISTRY/$IMAGE_NAME_ADMIN" admin/Dockerfile
|
||||
|
|
|
|||
19
DEPLOY.md
19
DEPLOY.md
|
|
@ -21,6 +21,7 @@ cp env/test.env.example env/test.env
|
|||
|
||||
Порт ClickHouse на хосте (`CLICKHOUSE_HTTP_PORT`) у контуров должен различаться:
|
||||
`prod` — `8123`, `test` — `8124` (иначе контейнеры конфликтуют на одной VPS).
|
||||
Порт админки (`ADMIN_HTTP_PORT`) — так же: `prod` — `8000`, `test` — `8001`.
|
||||
|
||||
## 3. Логин в registry (если пакет приватный)
|
||||
|
||||
|
|
@ -52,7 +53,8 @@ docker compose -p test --env-file env/test.env -f docker-compose.prod.yml logs -
|
|||
2. `migrate` (**Flyway**) накатывает миграции из `sql/` — для пустой БД с нуля;
|
||||
3. `clickhouse` поднимается и применяет `clickhouse/init/001_events.sql` —
|
||||
**только при первой инициализации** пустого data-каталога;
|
||||
4. `bot` стартует (`Запуск DispelEvilBot v<версия> (APP_ENV=...)`), аналитика и рассылки — в нём же.
|
||||
4. `bot` стартует (`Запуск DispelEvilBot v<версия> (APP_ENV=...)`), аналитика и рассылки — в нём же;
|
||||
5. `admin` стартует и отвечает на `GET /healthz` (порт на loopback хоста).
|
||||
|
||||
Проверка контура (сервисы, миграции, Postgres, ClickHouse):
|
||||
|
||||
|
|
@ -79,6 +81,21 @@ docker compose -p test --env-file env/test.env -f docker-compose.prod.yml exec -
|
|||
Изменения схемы: `clickhouse/init/*.sql` применяются лишь на пустом data-каталоге,
|
||||
поэтому на существующем контуре DDL нужно применить вручную (`make ch E=...`).
|
||||
|
||||
## Админка (FastAPI)
|
||||
|
||||
Сервис `admin` — отдельный образ `mpakobecex/dispelevilbot-admin`, общий пакет `common`.
|
||||
Наружу пока **не публикуется**: только loopback хоста (`127.0.0.1:${ADMIN_HTTP_PORT}`).
|
||||
Домен и TLS появятся вместе с входом через Telegram (Блоки 2.4/4.2).
|
||||
|
||||
```bash
|
||||
# проверка живости
|
||||
curl -s http://127.0.0.1:8001/healthz # test
|
||||
curl -s http://127.0.0.1:8000/healthz # prod
|
||||
```
|
||||
|
||||
Локально без Docker: `pip install -r requirements-admin.txt && python -m admin`
|
||||
(по умолчанию `0.0.0.0:8000`, переопределяется `ADMIN_HOST`/`ADMIN_PORT`).
|
||||
|
||||
## Бэкап событий (забирает домашний сервер)
|
||||
|
||||
Сырые события живут на VPS ограниченное время: раз в сутки домашний сервер **сам забирает**
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -8,6 +8,7 @@ Telegram-бот клана Dispel Evil (aiogram 3 + Postgres). Монорепо
|
|||
```
|
||||
common/ # общий пакет: config, logging, db, repositories, models, texts, analytics
|
||||
bot/ # сервис бота: handlers, services, keyboards, middlewares, notifications
|
||||
admin/ # сервис админки: FastAPI + Jinja2/HTMX (Блок 4), свой Dockerfile
|
||||
sql/ # миграции Flyway (V__*.sql, R__*.sql)
|
||||
clickhouse/ # init-скрипты ClickHouse (схема аналитики)
|
||||
docs/ # документация (docs/events.md — события аналитики)
|
||||
|
|
@ -43,6 +44,9 @@ APP_ENV=prod python -m bot # prod
|
|||
|
||||
или через `./start.sh` (режим берётся из `APP_ENV`).
|
||||
|
||||
Админка (FastAPI) запускается отдельным процессом: `python -m admin` (зависимости —
|
||||
`requirements-admin.txt`), по умолчанию слушает `0.0.0.0:8000`, проверка живучести — `GET /healthz`.
|
||||
|
||||
## База данных
|
||||
|
||||
Схема управляется миграциями **Flyway** в `sql/` (`V__*.sql` — versioned, `R__*.sql` — repeatable).
|
||||
|
|
@ -66,13 +70,17 @@ APP_ENV=prod python -m bot # prod
|
|||
|
||||
## Docker
|
||||
|
||||
Локальный запуск (postgres + migrate + clickhouse + bot):
|
||||
Локальный запуск (postgres + migrate + clickhouse + bot + admin):
|
||||
|
||||
```bash
|
||||
cp .env.example .env # заполнить
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
CI собирает два образа: `dispelevilbot-bot` и `dispelevilbot-admin`. Админка в compose
|
||||
публикуется только на loopback (`127.0.0.1:${ADMIN_HTTP_PORT}`), наружу выйдет вместе
|
||||
с доменом и TLS (Блок 2.4/4.2).
|
||||
|
||||
Прод/test-контуры — один `docker-compose.prod.yml`, разделяются Compose-проектом и env-файлом:
|
||||
|
||||
```bash
|
||||
|
|
@ -104,7 +112,7 @@ make backup E=test # на домашнем сервере: ./scripts/clickho
|
|||
|
||||
## Версионирование
|
||||
|
||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.3.9`.
|
||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.4.0`.
|
||||
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
||||
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
|
||||
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# Образ сервиса админки (FastAPI + Jinja2/HTMX).
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PIP_NO_CACHE_DIR=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Непривилегированный пользователь
|
||||
RUN useradd --create-home --uid 1000 app
|
||||
|
||||
COPY requirements-admin.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements-admin.txt
|
||||
|
||||
COPY common ./common
|
||||
COPY admin ./admin
|
||||
|
||||
USER app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Живость процесса: HTTP-проверка /healthz (порт берётся из ADMIN_PORT).
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD python -c "import os,urllib.request; urllib.request.urlopen('http://127.0.0.1:'+os.environ.get('ADMIN_PORT','8000')+'/healthz', timeout=3)"
|
||||
|
||||
CMD ["python", "-m", "admin"]
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
"""Сервис админки DispelEvilBot (FastAPI + HTMX).
|
||||
|
||||
Отдельный процесс/образ; общий код — пакет ``common``. Запуск: ``python -m admin``.
|
||||
"""
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
"""Точка входа админки: python -m admin"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import uvicorn
|
||||
|
||||
from common.config import get_admin_settings
|
||||
from common.logging_config import setup_logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Запустить uvicorn с приложением из ``admin.app``."""
|
||||
setup_logging(logging.INFO)
|
||||
settings = get_admin_settings()
|
||||
logger.info("Запуск админки (APP_ENV=%s) на %s:%s", settings.app_env, settings.admin_host, settings.admin_port)
|
||||
# log_config=None — чтобы uvicorn не перетирал единый формат логов.
|
||||
uvicorn.run(
|
||||
"admin.app:create_app",
|
||||
factory=True,
|
||||
host=settings.admin_host,
|
||||
port=settings.admin_port,
|
||||
log_config=None,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
"""Создание FastAPI-приложения админки."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from admin.routes import health, pages
|
||||
from common import __version__
|
||||
from common.config import AdminSettings, get_admin_settings
|
||||
from common.logging_config import setup_logging
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
TEMPLATES_DIR = BASE_DIR / "templates"
|
||||
|
||||
|
||||
def create_app(settings: AdminSettings | None = None) -> FastAPI:
|
||||
"""Собрать приложение: настройки, шаблоны, роутеры.
|
||||
|
||||
Настройки можно передать явно (тесты); по умолчанию читаются из окружения.
|
||||
"""
|
||||
setup_logging()
|
||||
settings = settings or get_admin_settings()
|
||||
|
||||
app = FastAPI(
|
||||
title="DispelEvilBot Admin",
|
||||
version=__version__,
|
||||
# Swagger/OpenAPI наружу пока не нужны: админка закрыта логином (Блок 4.2).
|
||||
docs_url=None,
|
||||
redoc_url=None,
|
||||
openapi_url=None,
|
||||
)
|
||||
app.state.settings = settings
|
||||
app.state.templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
|
||||
app.include_router(health.router)
|
||||
app.include_router(pages.router)
|
||||
return app
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""Роутеры админки."""
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
"""Служебные эндпоинты: healthcheck для Docker и мониторинга."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from common import __version__
|
||||
|
||||
router = APIRouter(tags=["service"])
|
||||
|
||||
|
||||
@router.get("/healthz")
|
||||
async def healthz(request: Request) -> dict[str, str]:
|
||||
"""Вернуть состояние сервиса (используется healthcheck'ом контейнера)."""
|
||||
settings = request.app.state.settings
|
||||
return {"status": "ok", "version": __version__, "app_env": settings.app_env}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
"""HTML-страницы админки (серверный рендер Jinja2 + HTMX)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from common import __version__
|
||||
|
||||
router = APIRouter(tags=["pages"])
|
||||
|
||||
#: Разделы админки по плану Блока 4 (пока заглушки на главной).
|
||||
SECTIONS: tuple[tuple[str, str, str], ...] = (
|
||||
("messages", "Сообщения", "Тексты бота и предпросмотр с тегами {tag}"),
|
||||
("schedules", "Расписания", "Время рассылок, дни недели и шаблон"),
|
||||
("users", "Пользователи", "Ники, звания, баланс и подписка"),
|
||||
("logs", "Логи", "События из ClickHouse с фильтрами"),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request) -> HTMLResponse:
|
||||
"""Главная страница: версия, окружение и список будущих разделов."""
|
||||
templates = request.app.state.templates
|
||||
settings = request.app.state.settings
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="index.html",
|
||||
context={
|
||||
"version": __version__,
|
||||
"app_env": settings.app_env,
|
||||
"sections": SECTIONS,
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{% block title %}Админка{% endblock %} — DispelEvilBot</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4" defer></script>
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-100 text-slate-900">
|
||||
<header class="border-b border-slate-200 bg-white">
|
||||
<div class="mx-auto flex max-w-5xl items-center justify-between px-4 py-4">
|
||||
<a href="/" class="text-lg font-semibold">DispelEvilBot</a>
|
||||
<span class="text-sm text-slate-500">админка · v{{ version }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto max-w-5xl px-4 py-8">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="mx-auto max-w-5xl px-4 pb-8 text-xs text-slate-400">DispelEvilBot v{{ version }}</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Главная{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-semibold">Панель администрирования</h1>
|
||||
<p class="mt-2 text-slate-600">
|
||||
Окружение: <span class="font-mono">{{ app_env }}</span>. Это каркас админки: разделы
|
||||
появятся по мере готовности (Блок 4.1–4.3).
|
||||
</p>
|
||||
|
||||
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||||
{% for key, title, description in sections %}
|
||||
<section class="rounded-lg border border-slate-200 bg-white p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium">{{ title }}</h2>
|
||||
<span class="rounded bg-slate-100 px-2 py-0.5 text-xs text-slate-500">скоро</span>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-slate-600">{{ description }}</p>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -105,5 +105,26 @@ class Settings(BaseSettings):
|
|||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
"""Вернуть singleton настроек."""
|
||||
"""Вернуть singleton настроек бота."""
|
||||
return Settings() # type: ignore[call-arg]
|
||||
|
||||
|
||||
class AdminSettings(Settings):
|
||||
"""Настройки сервиса админки.
|
||||
|
||||
Наследует общие поля (окружение, Postgres, таймзона, ClickHouse), но не требует
|
||||
``BOT_TOKEN``: на этапе каркаса он не нужен, а появится в проверке подписи
|
||||
Telegram-логина (Блок 4.2).
|
||||
"""
|
||||
|
||||
bot_token: str = ""
|
||||
|
||||
#: Адрес и порт, которые слушает uvicorn внутри контейнера.
|
||||
admin_host: str = "0.0.0.0"
|
||||
admin_port: int = 8000
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_admin_settings() -> AdminSettings:
|
||||
"""Вернуть singleton настроек админки."""
|
||||
return AdminSettings() # type: ignore[call-arg]
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "0.3.9"
|
||||
__version__ = "0.4.0"
|
||||
|
|
|
|||
|
|
@ -62,6 +62,31 @@ services:
|
|||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# Админка (FastAPI + Jinja2/HTMX), образ из Gitea Registry.
|
||||
# Наружу пока не публикуется: только loopback хоста; домен/TLS — Блок 2.4/4.2.
|
||||
admin:
|
||||
image: ${REGISTRY:-git.mpakosrv.ru}/mpakobecex/dispelevilbot-admin:${IMAGE_TAG:-latest}
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${ENV_FILE:-env/prod.env}
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PORT: "5432"
|
||||
ADMIN_HOST: 0.0.0.0
|
||||
ADMIN_PORT: "8000"
|
||||
ports:
|
||||
- "127.0.0.1:${ADMIN_HTTP_PORT:-8000}:8000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# Аналитика событий (docs/events.md). Наружу не публикуется:
|
||||
# только loopback хоста для бэкапа/туннеля; бот ходит внутрь сети compose.
|
||||
clickhouse:
|
||||
|
|
|
|||
|
|
@ -67,6 +67,34 @@ services:
|
|||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# Админка (FastAPI + HTMX). Порт — только loopback: наружу выйдет через reverse proxy
|
||||
# вместе с TLS и доменом (Блок 2.4/4.2).
|
||||
admin:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: admin/Dockerfile
|
||||
image: dispelevilbot-admin:local
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PORT: "5432"
|
||||
ADMIN_HOST: 0.0.0.0
|
||||
ADMIN_PORT: "8000"
|
||||
ports:
|
||||
- "127.0.0.1:${ADMIN_HTTP_PORT:-8000}:8000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# Аналитика событий (Блок 3). DDL — в clickhouse/init/.
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:25.3
|
||||
|
|
|
|||
|
|
@ -39,3 +39,7 @@ CLICKHOUSE_USER=de_bot
|
|||
CLICKHOUSE_PASSWORD=
|
||||
# Порт на хосте (для бэкапа). У test и prod он должен различаться.
|
||||
CLICKHOUSE_HTTP_PORT=8123
|
||||
|
||||
# Админка (FastAPI). Порт на loopback хоста; у test и prod должен различаться.
|
||||
# Наружу пока не публикуется (домен/TLS — Блок 2.4).
|
||||
ADMIN_HTTP_PORT=8000
|
||||
|
|
|
|||
|
|
@ -37,3 +37,7 @@ CLICKHOUSE_USER=de_bot
|
|||
CLICKHOUSE_PASSWORD=
|
||||
# Порт на хосте: у test он отличается от prod (8123), иначе конфликт на одной VPS.
|
||||
CLICKHOUSE_HTTP_PORT=8124
|
||||
|
||||
# Админка (FastAPI). Порт на loopback хоста; у test и prod должен различаться.
|
||||
# Наружу пока не публикуется (домен/TLS — Блок 2.4).
|
||||
ADMIN_HTTP_PORT=8001
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# Зависимости сервиса админки (FastAPI). Бот использует requirements.txt.
|
||||
# Python 3.12
|
||||
|
||||
fastapi>=0.115,<1.0
|
||||
uvicorn[standard]>=0.30,<1.0
|
||||
jinja2>=3.1,<4.0
|
||||
|
||||
pydantic>=2.7,<3.0
|
||||
pydantic-settings>=2.3,<3.0
|
||||
|
||||
# Postgres (async psycopg v3) — понадобится разделам шаблонов и пользователей.
|
||||
psycopg[binary,pool]>=3.2,<4.0
|
||||
|
||||
# Разбор form-data (HTMX-формы).
|
||||
python-multipart>=0.0.9,<1.0
|
||||
|
|
@ -5,3 +5,5 @@ black>=24.4,<25.0
|
|||
pytest>=8.2,<9.0
|
||||
pytest-asyncio>=0.23,<1.0
|
||||
pytest-cov>=5.0,<6.0
|
||||
# TestClient для FastAPI-админки.
|
||||
httpx>=0.27,<1.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
"""Тесты каркаса админки: healthcheck и главная страница (без БД и сети)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from admin.app import create_app
|
||||
from common.config import AdminSettings
|
||||
|
||||
|
||||
def _client() -> TestClient:
|
||||
settings = AdminSettings(_env_file=None, bot_token="", db_password="test", app_env="dev")
|
||||
return TestClient(create_app(settings))
|
||||
|
||||
|
||||
def test_healthz() -> None:
|
||||
response = _client().get("/healthz")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["status"] == "ok"
|
||||
assert payload["app_env"] == "dev"
|
||||
|
||||
|
||||
def test_index_renders_sections() -> None:
|
||||
response = _client().get("/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Панель администрирования" in response.text
|
||||
assert "Расписания" in response.text
|
||||
|
|
@ -7,4 +7,4 @@ from common import __version__
|
|||
|
||||
def test_version_is_set():
|
||||
assert __version__
|
||||
assert __version__ == "0.3.9"
|
||||
assert __version__ == "0.4.0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue