Целый раздел ТЗ, от которого не было ни одной функции. - Учётные записи: завести, сменить роль и службу, заблокировать, сбросить пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль и проверка «это твой разбор». - Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия классификатора, число живых занятий. Отдельной строкой — секрет сессии: значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть её должен администратор, а не проверяющий на защите. - Журнал действий с фильтром, только на чтение. - Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный, а если его нет — из контейнера базы; если нет ни того ни другого, администратор видит, чего не хватает, а кнопка не молчит. Границы роли выражены отсутствием маршрутов, а не проверками внутри них: в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста проверяют это перебором маршрутов роутера — так запрет нельзя ослабить, случайно добавив обработчик. Найдено по ходу: тесты с базой падали в компании и проходили поодиночке. Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура, дающая движок на тест. 193 теста зелёных (12 новых).
42 lines
1.4 KiB
Python
42 lines
1.4 KiB
Python
"""make backup: снять резервную копию базы.
|
||
|
||
Та же операция, что кнопка на АРМ администратора, — но вызываемая из cron:
|
||
ТЗ требует копирования не реже раза в сутки, а полагаться на то, что кто-то
|
||
нажмёт кнопку, нельзя.
|
||
|
||
make backup снять копию
|
||
0 3 * * * cd … && make backup в crontab демо-машины
|
||
"""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from app.admin.backup import BackupError, create, listing # noqa: E402
|
||
|
||
#: Сколько копий держать. Место на демо-машине не бесконечно, а копия за
|
||
#: позапрошлую неделю не нужна никому.
|
||
KEEP = 14
|
||
|
||
|
||
def main() -> int:
|
||
try:
|
||
created = create()
|
||
except BackupError as exc:
|
||
print(f"копия не снята: {exc}")
|
||
return 1
|
||
|
||
print(f"{created['name']}: {created['size_bytes'] // 1024} КБ")
|
||
|
||
old = listing()[KEEP:]
|
||
for item in old:
|
||
(ROOT / "backups" / item["name"]).unlink(missing_ok=True)
|
||
if old:
|
||
print(f"удалено старых копий: {len(old)}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|