SFM/main.py
2026-10-09 17:35:36 +03:00

161 lines
6.9 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.

import asyncio
import tempfile
from pathlib import Path
import streamlit as st
from dotenv import load_dotenv
from app.app_config import (
resolve_opencloud_access_token,
resolve_opencloud_username,
resolve_user_id,
)
from app.auth.service import AuthService, clear_session
from app.opencloud_token_store import OpenCloudTokenStoreError, add_token
from app.pipeline.service import run_batch
def main() -> None:
load_dotenv()
st.set_page_config(page_title="СФМ", page_icon=":lock:", layout="wide")
st.title("СФМ")
if not AuthService().run():
return
_, center_col, _ = st.columns([1, 2, 1])
with center_col:
_render_token_popover()
st.caption(
"Прикрепите XML-файлы и/или архивы с ними (можно несколько сразу, "
"вперемешку) — отчёт, протокол и лог загрузятся в Out вашего "
"личного пространства OpenCloud."
)
uploaded_files = st.file_uploader(
"XML-файлы и архивы",
type=["xml", "zip", "tar", "gz", "tgz", "bz2", "xz"],
accept_multiple_files=True,
)
if st.button(
"Запустить обработку xml в excel",
type="primary",
use_container_width=True,
disabled=not uploaded_files,
):
with tempfile.TemporaryDirectory() as tmp_dir:
input_paths = _save_attachments(uploaded_files, Path(tmp_dir))
progress_bar = st.progress(0.0, text="Подготовка файлов...")
status_placeholder = st.empty()
def _on_progress(done: int, total: int, file_name: str) -> None:
# Прогресс по файлам, а не один спиннер на весь батч —
# иначе при большом количестве файлов (были жалобы на
# батч в 230 штук) непонятно, идёт обработка или
# приложение зависло, и пользователь закрывает вкладку
# посреди обработки.
fraction = done / total if total else 1.0
progress_bar.progress(
fraction, text=f"Обработано {done} из {total}: {file_name}"
)
status_placeholder.caption(
f"Последний обработанный файл: {file_name}"
)
launcher_name = resolve_opencloud_username() or resolve_user_id()
batch_result = run_batch(
input_paths=input_paths,
launcher=launcher_name,
opencloud_username=resolve_opencloud_username(),
opencloud_access_token=resolve_opencloud_access_token(),
progress_callback=_on_progress,
)
progress_bar.progress(1.0, text="Готово")
if batch_result.status == "success":
st.success("Обработка завершена успешно.")
elif batch_result.status == "partial":
st.warning("Обработка завершена частично.")
else:
st.error("Обработка завершена с ошибками.")
st.write(
{
"processed": batch_result.processed,
"partial": batch_result.partial,
"errors": batch_result.errors,
"report_path": batch_result.report_path,
"protocol_path": batch_result.protocol_path,
"log_path": batch_result.log_path,
}
)
if batch_result.messages:
st.caption("Последние сообщения обработки:")
for message in batch_result.messages:
st.write(f"- {message}")
# Если обработка прошла, а загрузка в OpenCloud — нет, отдаём
# результат напрямую, чтобы не терять долгую обработку.
for file_name, payload in batch_result.fallback_files.items():
st.download_button(
f"Скачать {file_name}",
data=payload,
file_name=file_name,
use_container_width=True,
key=f"fallback_{file_name}",
)
if st.button("Выйти", type="secondary"):
clear_session()
st.rerun()
def _save_attachments(uploaded_files, target_dir: Path) -> list[Path]:
"""Складывает вложения на диск, каждое в свой подкаталог.
Подкаталог по индексу нужен потому, что имена вложений могут совпадать
(например, два архива "Файлы.zip" из разных папок), и одно перезаписало
бы другое.
"""
paths: list[Path] = []
for index, uploaded in enumerate(uploaded_files):
holder = target_dir / str(index)
holder.mkdir(parents=True, exist_ok=True)
path = holder / uploaded.name
path.write_bytes(uploaded.getvalue())
paths.append(path)
return paths
def _render_token_popover() -> None:
"""Самообслуживание: пользователь сам привязывает свой App Token
OpenCloud к своей учётке (по preferred_username из Keycloak), без
обращения к администратору с CLI-скриптом.
"""
with st.popover("Добавить токен"):
username = resolve_opencloud_username()
if not username:
st.error("Не удалось определить вашу учётную запись.")
return
token = st.text_input(
"App Token",
type="password",
help="Долгоживущий App Token из веб-интерфейса OpenCloud (6 слов через пробел).",
key="opencloud_token_input",
)
if st.button("Сохранить", key="opencloud_token_save"):
if not token:
st.error("Введите токен.")
else:
try:
asyncio.run(add_token(username, token, added_by=username))
except OpenCloudTokenStoreError as exc:
st.error(f"Не удалось сохранить токен: {exc}")
else:
st.success("Токен сохранён.")
if __name__ == "__main__":
main()