Files
schedule/src/backend/schedule/source.py
Vladislav Kopnin 34b975da69 Перенос в master всего, кроме админки: ТОП-группы, порядок групп, запуск без Docker
Админка осталась отдельной ветвью admin-panel; в master её нет (ни кода, ни
страниц, ни переменных окружения, ни тома настроек).

Что перенесено:
- parsing: имена групп вида «ИВТ-ТОП-11БО» распознаются, порядок групп = порядок
  столбцов таблицы (похожие заголовки с пробелами группой не считаются);
- main: GET /groups отдаёт группы в порядке столбцов; интервал обновления из
  SCHEDULE_INTERVAL_MINUTES (со старым SCHEDULE_CACHE_TTL как fallback);
- cache: по интервалу сверяем modified — если файл не менялся, не скачиваем;
  при загрузке считаем sha256 и пишем в лог и /status, изменился ли файл;
- source: запросы к Яндекс.Диску с no-cache и меткой времени;
- frontend: ответы API с Cache-Control: no-store, общий .env из корня проекта
  (и backend, и frontend), страницы/прокси админки нет;
- Dockerfile/compose/.env.example: без ADMIN_* и без тома настроек; в api-образ
  попадают все модули backend (проверяется тестом test_deploy);
- Makefile: make help/venv/run/run-api/run-front/test + docker-цели — «make run»
  поднимает всё локально без Docker;
- README: быстрый старт, полный список зависимостей, запуск локально и в Docker;
- deploy/: пример nginx и инструкция «второй стенд рядом с основным».

Тесты: 114 (удалены админские), все зелёные.
2026-10-06 15:15:44 +03:00

57 lines
2.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Загрузка файла расписания по публичной ссылке Яндекс.Диска."""
from __future__ import annotations
import logging
import time
import requests
from .errors import ScheduleError
log = logging.getLogger(__name__)
CLOUD_API = "https://cloud-api.yandex.net/v1/disk/public/resources"
# Публичная ссылка может кэшироваться на стороне Диска: просим не кэшировать
# и добавляем в запрос метку времени, чтобы получать текущую версию файла.
_NO_CACHE = {"Cache-Control": "no-cache", "Pragma": "no-cache"}
def _bust() -> dict:
return {"_": int(time.time() * 1000)}
def download_schedule(public_url: str, timeout: float = 60.0) -> bytes:
"""Скачивает файл с диска; при любой ошибке бросает ScheduleError."""
try:
resp = requests.get(
f"{CLOUD_API}/download",
params={"public_key": public_url, **_bust()},
headers=_NO_CACHE,
timeout=timeout,
)
resp.raise_for_status()
href = resp.json()["href"]
file_resp = requests.get(href, headers=_NO_CACHE, timeout=timeout)
file_resp.raise_for_status()
return file_resp.content
except (requests.RequestException, KeyError, ValueError) as exc:
raise ScheduleError(f"Не удалось скачать таблицу: {exc}") from exc
def fetch_table_modified(public_url: str, timeout: float = 20.0) -> str | None:
"""Время последнего изменения файла на диске или None, если не удалось узнать."""
try:
resp = requests.get(
CLOUD_API,
params={"public_key": public_url, **_bust()},
headers=_NO_CACHE,
timeout=timeout,
)
resp.raise_for_status()
return resp.json().get("modified")
except (requests.RequestException, ValueError) as exc:
log.debug("Не удалось получить modified таблицы: %s", exc)
return None