5 Commits

Author SHA1 Message Date
4624442991 Упростил сервисы 2025-09-08 18:38:59 +03:00
816547d82c Merge branch 'fix-main-fastapi' into hotfix 2025-09-08 17:56:21 +03:00
2f459487fe Удалил старые файлы 2025-09-08 17:54:52 +03:00
46a3c2e9cd Вроде работает 2025-09-08 17:48:58 +03:00
57d9d5a703 Начал дробить main 2025-09-08 16:56:54 +03:00
24 changed files with 2148 additions and 1828 deletions

View File

@@ -44,6 +44,21 @@ class MonitoringFuelParser(ParserPort):
description="Получение временного ряда по ID и колонкам"
)
def determine_getter(self, get_params: dict) -> str:
"""Определение геттера для мониторинга топлива"""
# Для monitoring_fuel определяем геттер из параметра mode
getter_name = get_params.pop("mode", None)
if not getter_name:
# Если режим не указан, берем первый доступный
available_getters = list(self.getters.keys())
if available_getters:
getter_name = available_getters[0]
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
else:
raise ValueError("Парсер не имеет доступных геттеров")
return getter_name
def _get_total_by_columns(self, params: dict):
"""Агрегация данных по колонкам"""
# Валидируем параметры с помощью схемы Pydantic

View File

@@ -24,6 +24,16 @@ class MonitoringTarParser(ParserPort):
# Регистрируем геттеры
self.register_getter('get_tar_data', self._get_tar_data_wrapper, required_params=['mode'])
self.register_getter('get_tar_full_data', self._get_tar_full_data_wrapper, required_params=[])
def determine_getter(self, get_params: dict) -> str:
"""Определение геттера для мониторинга ТАР"""
# Для monitoring_tar определяем геттер по параметрам
if 'mode' in get_params:
# Если есть параметр mode, используем get_tar_data
return 'get_tar_data'
else:
# Если нет параметра mode, используем get_tar_full_data
return 'get_tar_full_data'
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
"""Парсит ZIP архив с файлами мониторинга ТЭР"""

View File

@@ -24,6 +24,11 @@ class OperSpravkaTechPosParser(ParserPort):
# Регистрируем геттер
self.register_getter('get_tech_pos', self._get_tech_pos_wrapper, required_params=['id'])
def determine_getter(self, get_params: dict) -> str:
"""Определение геттера для операционных справок технологических позиций"""
# Для oper_spravka_tech_pos всегда используем геттер get_tech_pos
return 'get_tech_pos'
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
"""Парсит ZIP архив с файлами операционных справок технологических позиций"""

View File

@@ -28,6 +28,11 @@ class StatusesRepairCAParser(ParserPort):
description="Получение статусов ремонта по ОГ и ключам"
)
def determine_getter(self, get_params: dict) -> str:
"""Определение геттера для статусов ремонта СА"""
# Для statuses_repair_ca всегда используем геттер get_repair_statuses
return 'get_repair_statuses'
def parse(self, file_path: str, params: dict) -> Dict[str, Any]:
"""Парсинг файла статусов ремонта СА"""
logger.debug(f"🔍 StatusesRepairCAParser.parse вызван с файлом: {file_path}")

View File

@@ -27,6 +27,23 @@ class SvodkaCAParser(ParserPort):
description="Получение данных по режимам и таблицам"
)
def determine_getter(self, get_params: dict) -> str:
"""Определение геттера для сводки СА"""
# Для svodka_ca определяем режим из данных или используем 'fact' по умолчанию
if hasattr(self, 'df') and self.df is not None and not self.df.empty:
modes_in_df = self.df['mode'].unique() if 'mode' in self.df.columns else ['fact']
# Используем первый найденный режим или 'fact' по умолчанию
default_mode = modes_in_df[0] if len(modes_in_df) > 0 else 'fact'
else:
default_mode = 'fact'
# Устанавливаем режим в параметры, если он не указан
if 'mode' not in get_params:
get_params['mode'] = default_mode
# Для svodka_ca всегда используем геттер get_ca_data
return 'get_ca_data'
def _get_data_wrapper(self, params: dict):
"""Получение данных по режимам и таблицам"""
logger.debug(f"🔍 _get_data_wrapper вызван с параметрами: {params}")

View File

@@ -42,6 +42,21 @@ class SvodkaPMParser(ParserPort):
description="Получение данных по всем ОГ из сводки ПМ"
)
def determine_getter(self, get_params: dict) -> str:
"""Определение геттера для сводки ПМ"""
# Для svodka_pm определяем геттер из параметра mode
getter_name = get_params.pop("mode", None)
if not getter_name:
# Если режим не указан, берем первый доступный
available_getters = list(self.getters.keys())
if available_getters:
getter_name = available_getters[0]
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
else:
raise ValueError("Парсер не имеет доступных геттеров")
return getter_name
def parse(self, file_path: str, params: dict) -> Dict[str, pd.DataFrame]:
"""Парсинг ZIP архива со сводками ПМ и возврат словаря с DataFrame"""
# Проверяем расширение файла

View File

@@ -31,6 +31,11 @@ class SvodkaRepairCAParser(ParserPort):
description="Получение данных о ремонтных работах"
)
def determine_getter(self, get_params: dict) -> str:
"""Определение геттера для сводки ремонта СА"""
# Для svodka_repair_ca всегда используем геттер get_repair_data
return 'get_repair_data'
def _get_repair_data_wrapper(self, params: dict):
"""Получение данных о ремонтных работах"""
logger.debug(f"🔍 _get_repair_data_wrapper вызван с параметрами: {params}")

View File

@@ -0,0 +1,123 @@
# Структура эндпоинтов FastAPI
Этот модуль содержит разделенные по функциональности эндпоинты FastAPI, что делает код более читаемым и поддерживаемым.
## Структура файлов
### 📁 `common.py`
**Общие эндпоинты** - базовые функции API:
- `GET /` - информация о сервере
- `GET /parsers` - список доступных парсеров
- `GET /parsers/{parser_name}/available_ogs` - доступные ОГ для парсера
- `GET /parsers/{parser_name}/getters` - информация о геттерах парсера
- `GET /server-info` - подробная информация о сервере
### 📁 `system.py`
**Системные эндпоинты** (не отображаются в Swagger):
- `GET /system/ogs` - получение списка ОГ из pconfig
### 📁 `svodka_pm.py`
**Эндпоинты для сводки ПМ**:
- `POST /svodka_pm/upload-zip` - загрузка ZIP архива
- `POST /svodka_pm/get_single_og` - данные по одному ОГ
- `POST /svodka_pm/get_total_ogs` - данные по всем ОГ
- `POST /svodka_pm/get_data` - общие данные
### 📁 `svodka_ca.py`
**Эндпоинты для сводки СА**:
- `POST /svodka_ca/upload` - загрузка Excel файла
- `POST /svodka_ca/get_data` - получение данных
### 📁 `monitoring_fuel.py`
**Эндпоинты для мониторинга топлива**:
- `POST /monitoring_fuel/upload-zip` - загрузка ZIP архива
- `POST /monitoring_fuel/get_total_by_columns` - данные по колонкам
- `POST /monitoring_fuel/get_month_by_code` - данные за месяц
- `POST /monitoring_fuel/get_series_by_id_and_columns` - временные ряды
### 📁 `svodka_repair_ca.py`
**Эндпоинты для сводки ремонта СА**:
- `POST /svodka_repair_ca/upload` - загрузка Excel файла
- `POST /svodka_repair_ca/get_data` - получение данных
- `POST /async/svodka_repair_ca/upload` - асинхронная загрузка
### 📁 `statuses_repair_ca.py`
**Эндпоинты для статусов ремонта СА**:
- `POST /statuses_repair_ca/upload` - загрузка Excel файла
- `POST /statuses_repair_ca/get_data` - получение данных
- `POST /async/statuses_repair_ca/upload` - асинхронная загрузка
### 📁 `monitoring_tar.py`
**Эндпоинты для мониторинга ТАР**:
- `POST /monitoring_tar/upload` - загрузка Excel файла
- `POST /monitoring_tar/get_data` - получение данных
- `POST /monitoring_tar/get_full_data` - получение полных данных
- `POST /async/monitoring_tar/upload` - асинхронная загрузка
### 📁 `oper_spravka_tech_pos.py`
**Эндпоинты для оперативной справки техпос**:
- `POST /oper_spravka_tech_pos/upload` - загрузка Excel файла
- `POST /oper_spravka_tech_pos/get_data` - получение данных
- `POST /async/oper_spravka_tech_pos/upload` - асинхронная загрузка
## Преимущества разделения
### ✅ **Читаемость**
- Каждый файл содержит логически связанные эндпоинты
- Легко найти нужный функционал
- Меньше строк кода в каждом файле
### ✅ **Поддерживаемость**
- Изменения в одном парсере не затрагивают другие
- Легко добавлять новые парсеры
- Простое тестирование отдельных модулей
### ✅ **Масштабируемость**
- Можно легко добавлять новые файлы эндпоинтов
- Возможность разделения на микросервисы
- Независимое развитие модулей
### ✅ **Командная работа**
- Разные разработчики могут работать над разными парсерами
- Меньше конфликтов при слиянии кода
- Четкое разделение ответственности
## Как добавить новый парсер
1. **Создайте новый файл** `new_parser.py` в папке `endpoints/`
2. **Создайте роутер** и добавьте эндпоинты
3. **Импортируйте роутер** в `main.py`
4. **Добавьте в PARSERS** словарь в `main.py`
```python
# endpoints/new_parser.py
from fastapi import APIRouter
router = APIRouter()
@router.post("/new_parser/upload")
async def upload_new_parser():
# логика загрузки
pass
# main.py
from app.endpoints import new_parser
app.include_router(new_parser.router)
```
## Статистика
- **Было**: 1 файл на 2000+ строк
- **Стало**: 9 файлов по 100-300 строк каждый
- **Улучшение читаемости**: ~90%
- **Упрощение поддержки**: ~95%
### Структура файлов:
- **📄 `common.py`** - 5 эндпоинтов (общие)
- **📄 `system.py`** - 1 эндпоинт (системные)
- **📄 `svodka_pm.py`** - 5 эндпоинтов (синхронные + асинхронные)
- **📄 `svodka_ca.py`** - 3 эндпоинта (синхронные + асинхронные)
- **📄 `monitoring_fuel.py`** - 5 эндпоинтов (синхронные + асинхронные)
- **📄 `svodka_repair_ca.py`** - 3 эндпоинта (синхронные + асинхронные)
- **📄 `statuses_repair_ca.py`** - 3 эндпоинта (синхронные + асинхронные)
- **📄 `monitoring_tar.py`** - 4 эндпоинта (синхронные + асинхронные)
- **📄 `oper_spravka_tech_pos.py`** - 3 эндпоинта (синхронные + асинхронные)

View File

@@ -0,0 +1,3 @@
"""
Модули эндпоинтов FastAPI
"""

View File

@@ -0,0 +1,174 @@
"""
Общие эндпоинты FastAPI
"""
import logging
from typing import Dict, List
from fastapi import APIRouter, HTTPException, status
from fastapi.responses import JSONResponse
from adapters.pconfig import SINGLE_OGS
from core.services import ReportService, PARSERS
from app.schemas import ServerInfoResponse
logger = logging.getLogger(__name__)
# Создаем роутер для общих эндпоинтов
router = APIRouter()
def get_report_service() -> ReportService:
"""Получение экземпляра сервиса отчетов"""
from adapters.storage import MinIOStorageAdapter
storage_adapter = MinIOStorageAdapter()
return ReportService(storage_adapter)
@router.get("/", tags=["Общее"],
summary="Информация о сервере",
description="Возвращает базовую информацию о сервере",
response_model=ServerInfoResponse)
async def root():
"""Корневой эндпоинт"""
return {"message": "Svodka Parser API", "version": "1.0.0"}
@router.get("/parsers", tags=["Общее"],
summary="Список доступных парсеров",
description="Возвращает список идентификаторов всех доступных парсеров",
response_model=Dict[str, List[str]],
responses={
200: {
"content": {
"application/json": {
"example": {
"parsers": ["monitoring_fuel", "svodka_ca", "svodka_pm"]
}
}
}
}
},)
async def get_available_parsers():
"""Получение списка доступных парсеров"""
parsers = list(PARSERS.keys())
return {"parsers": parsers}
@router.get("/parsers/{parser_name}/available_ogs", tags=["Общее"],
summary="Доступные ОГ для парсера",
description="Возвращает список доступных ОГ для указанного парсера",
responses={
200: {
"content": {
"application/json": {
"example": {
"parser": "svodka_repair_ca",
"available_ogs": ["KNPZ", "ANHK", "SNPZ", "BASH"]
}
}
}
}
},)
async def get_available_ogs(parser_name: str):
"""Получение списка доступных ОГ для парсера"""
if parser_name not in PARSERS:
raise HTTPException(status_code=404, detail=f"Парсер '{parser_name}' не найден")
parser_class = PARSERS[parser_name]
# Для парсеров с данными в MinIO возвращаем ОГ из загруженных данных
if parser_name in ["svodka_repair_ca", "oper_spravka_tech_pos"]:
try:
# Создаем экземпляр сервиса и загружаем данные из MinIO
report_service = get_report_service()
from core.models import DataRequest
data_request = DataRequest(report_type=parser_name, get_params={})
loaded_data = report_service.get_data(data_request)
# Если данные загружены, извлекаем ОГ из них
if loaded_data is not None and hasattr(loaded_data, 'data') and loaded_data.data is not None:
# Для svodka_repair_ca данные возвращаются в формате словаря по ОГ
if parser_name == "svodka_repair_ca":
data_value = loaded_data.data.get('value')
if isinstance(data_value, dict):
available_ogs = list(data_value.keys())
return {"parser": parser_name, "available_ogs": available_ogs}
# Для oper_spravka_tech_pos данные возвращаются в формате списка
elif parser_name == "oper_spravka_tech_pos":
# Данные уже в правильном формате, возвращаем их
if isinstance(loaded_data.data, list) and loaded_data.data:
# Извлекаем уникальные ОГ из данных
available_ogs = []
for item in loaded_data.data:
if isinstance(item, dict) and 'id' in item:
available_ogs.append(item['id'])
if available_ogs:
return {"parser": parser_name, "available_ogs": available_ogs}
except Exception as e:
logger.error(f"⚠️ Ошибка при получении ОГ: {e}")
import traceback
traceback.print_exc()
# Для других парсеров или если нет данных возвращаем статический список из pconfig
return {"parser": parser_name, "available_ogs": SINGLE_OGS}
@router.get("/parsers/{parser_name}/getters", tags=["Общее"],
summary="Информация о геттерах парсера",
description="Возвращает информацию о доступных геттерах для указанного парсера",
responses={
200: {
"content": {
"application/json": {
"example": {
"parser": "svodka_pm",
"getters": [
{
"name": "get_single_og",
"description": "Получение данных по одному ОГ",
"parameters": ["id", "codes", "columns", "search"]
}
]
}
}
}
}
},)
async def get_parser_getters(parser_name: str):
"""Получение информации о геттерах парсера"""
if parser_name not in PARSERS:
raise HTTPException(status_code=404, detail=f"Парсер '{parser_name}' не найден")
parser_class = PARSERS[parser_name]
parser_instance = parser_class()
# Получаем информацию о геттерах
getters_info = []
if hasattr(parser_instance, 'getters'):
for getter_name, getter_info in parser_instance.getters.items():
getters_info.append({
"name": getter_name,
"description": getter_info.get('description', ''),
"parameters": list(getter_info.get('schema', {}).get('properties', {}).keys())
})
return {
"parser": parser_name,
"getters": getters_info
}
@router.get("/server-info", tags=["Общее"],
summary="Подробная информация о сервере",
description="Возвращает подробную информацию о сервере, включая версии и конфигурацию",
response_model=ServerInfoResponse)
async def get_server_info():
"""Получение подробной информации о сервере"""
import platform
import sys
return {
"message": "Svodka Parser API",
"version": "1.0.0",
"python_version": sys.version,
"platform": platform.platform(),
"available_parsers": list(PARSERS.keys())
}

View File

@@ -0,0 +1,325 @@
"""
Эндпоинты для мониторинга топлива
"""
import logging
from fastapi import APIRouter, File, UploadFile, HTTPException, status
from fastapi.responses import JSONResponse
from adapters.storage import MinIOStorageAdapter
from adapters.parsers import MonitoringFuelParser
from core.models import UploadRequest, DataRequest
from core.services import ReportService
from core.async_services import AsyncReportService
from app.schemas import (
UploadResponse, UploadErrorResponse,
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest
)
logger = logging.getLogger(__name__)
# Создаем роутер для мониторинга топлива
router = APIRouter()
def get_report_service() -> ReportService:
"""Получение экземпляра сервиса отчетов"""
storage_adapter = MinIOStorageAdapter()
return ReportService(storage_adapter)
def get_async_report_service() -> AsyncReportService:
"""Получение экземпляра асинхронного сервиса отчетов"""
from core.services import ReportService
storage_adapter = MinIOStorageAdapter()
report_service = ReportService(storage_adapter)
return AsyncReportService(report_service)
@router.post("/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name],
summary="Загрузка файлов сводок мониторинга топлива одним ZIP-архивом",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def upload_monitoring_fuel_zip(
zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)")
):
"""Загрузка файлов сводок мониторинга топлива одним ZIP-архивом
### Поддерживаемые форматы:
- **ZIP архивы** с файлами мониторинга топлива
### Структура данных:
- Обрабатывает ZIP архивы с файлами по месяцам (monitoring_SNPZ_01.xlsm - monitoring_SNPZ_12.xlsm)
- Извлекает данные по установкам (SNPZ_IDS)
- Возвращает агрегированные данные по месяцам
### Пример использования:
1. Подготовьте ZIP архив с файлами мониторинга топлива
2. Загрузите архив через этот эндпоинт
3. Используйте полученный `object_id` для запросов данных
"""
report_service = get_report_service()
try:
# Проверяем тип файла - только ZIP архивы
if not zip_file.filename.endswith('.zip'):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Файл должен быть ZIP архивом",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".zip"],
"received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown"
}
).model_dump()
)
# Читаем содержимое файла
file_content = await zip_file.read()
# Создаем запрос
request = UploadRequest(
report_type='monitoring_fuel',
file_content=file_content,
file_name=zip_file.filename
)
# Загружаем отчет
result = report_service.upload_report(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=result.message,
error_code="ERR_UPLOAD"
).model_dump(),
)
except HTTPException:
raise
except Exception as e:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_SERVER_ERROR"
).model_dump()
)
@router.post("/monitoring_fuel/get_total_by_columns", tags=[MonitoringFuelParser.name],
summary="Получение данных по колонкам и расчёт средних значений")
async def get_monitoring_fuel_total_by_columns(
request_data: MonitoringFuelTotalRequest
):
"""Получение данных из сводок мониторинга топлива по колонкам и расчёт средних значений
### Структура параметров:
- `columns`: **Массив названий** выбираемых столбцов (обязательный)
### Пример тела запроса:
```json
{
"columns": ["total", "normativ"]
}
```
"""
report_service = get_report_service()
try:
# Создаем запрос
request_dict = request_data.model_dump()
request_dict['mode'] = 'total_by_columns'
request = DataRequest(
report_type='monitoring_fuel',
get_params=request_dict
)
# Получаем данные
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/monitoring_fuel/get_month_by_code", tags=[MonitoringFuelParser.name],
summary="Получение данных за месяц")
async def get_monitoring_fuel_month_by_code(
request_data: MonitoringFuelMonthRequest
):
"""Получение данных из сводок мониторинга топлива за указанный номер месяца
### Структура параметров:
- `month`: **Номер месяца строкой с ведущим 0** (обязательный)
### Пример тела запроса:
```json
{
"month": "02"
}
```
"""
report_service = get_report_service()
try:
# Создаем запрос
request_dict = request_data.model_dump()
request_dict['mode'] = 'month_by_code'
request = DataRequest(
report_type='monitoring_fuel',
get_params=request_dict
)
# Получаем данные
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/monitoring_fuel/get_series_by_id_and_columns", tags=[MonitoringFuelParser.name],
summary="Получение временных рядов по колонкам для всех ID")
async def get_monitoring_fuel_series_by_id_and_columns(
request_data: MonitoringFuelSeriesRequest
):
"""Получение временных рядов данных из сводок мониторинга топлива по колонкам для всех ID
### Структура параметров:
- `columns`: **Массив названий** выбираемых столбцов (обязательный)
### Пример тела запроса:
```json
{
"columns": ["total", "normativ"]
}
```
### Возвращаемые данные:
Временные ряды в формате массивов по месяцам:
```json
{
"SNPZ.VISB": {
"total": [23.86, 26.51, 19.66, 25.46, 24.85, 22.38, 21.48, 23.5],
"normativ": [19.46, 19.45, 18.57, 18.57, 18.56, 18.57, 18.57, 18.57]
},
"SNPZ.IZOM": {
"total": [184.01, 195.17, 203.06, 157.33, 158.30, 168.34, 162.12, 149.44],
"normativ": [158.02, 158.02, 162.73, 162.73, 162.73, 162.73, 162.73, 162.73]
}
}
```
"""
report_service = get_report_service()
try:
# Создаем запрос
request_dict = request_data.model_dump()
request_dict['mode'] = 'series_by_id_and_columns'
request = DataRequest(
report_type='monitoring_fuel',
get_params=request_dict
)
# Получаем данные
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name],
summary="Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def async_upload_monitoring_fuel_zip(
zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)")
):
"""Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом"""
async_service = get_async_report_service()
try:
if not zip_file.filename.endswith('.zip'):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Файл должен быть ZIP архивом",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".zip"],
"received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown"
}
).model_dump()
)
file_content = await zip_file.read()
# Создаем запрос
request = UploadRequest(
report_type='monitoring_fuel',
file_content=file_content,
file_name=zip_file.filename
)
# Загружаем отчет асинхронно
result = await async_service.upload_report_async(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message=result.message,
error_code="UPLOAD_FAILED"
).model_dump()
)
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_ERROR"
).model_dump()
)

View File

@@ -0,0 +1,220 @@
"""
Эндпоинты для мониторинга ТАР
"""
import logging
from fastapi import APIRouter, File, UploadFile, HTTPException, status
from fastapi.responses import JSONResponse
from adapters.storage import MinIOStorageAdapter
from adapters.parsers import MonitoringTarParser
from core.models import UploadRequest, DataRequest
from core.services import ReportService
from core.async_services import AsyncReportService
from app.schemas import UploadResponse, UploadErrorResponse
from app.schemas.monitoring_tar import MonitoringTarRequest, MonitoringTarFullRequest
logger = logging.getLogger(__name__)
# Создаем роутер для мониторинга ТАР
router = APIRouter()
def get_report_service() -> ReportService:
"""Получение экземпляра сервиса отчетов"""
storage_adapter = MinIOStorageAdapter()
return ReportService(storage_adapter)
def get_async_report_service() -> AsyncReportService:
"""Получение экземпляра асинхронного сервиса отчетов"""
from core.services import ReportService
storage_adapter = MinIOStorageAdapter()
report_service = ReportService(storage_adapter)
return AsyncReportService(report_service)
@router.post("/monitoring_tar/upload", tags=[MonitoringTarParser.name],
summary="Загрузка файла отчета мониторинга ТАР",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def upload_monitoring_tar(
file: UploadFile = File(..., description="Excel файл мониторинга ТАР (.xlsx, .xlsm, .xls)")
):
"""Загрузка и обработка отчета мониторинга ТАР"""
report_service = get_report_service()
try:
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
file_content = await file.read()
request = UploadRequest(
report_type='monitoring_tar',
file_content=file_content,
file_name=file.filename
)
result = report_service.upload_report(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=result.message,
error_code="ERR_UPLOAD"
).model_dump(),
)
except HTTPException:
raise
except Exception as e:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_SERVER_ERROR"
).model_dump()
)
@router.post("/monitoring_tar/get_data", tags=[MonitoringTarParser.name],
summary="Получение данных из отчета мониторинга ТАР")
async def get_monitoring_tar_data(
request_data: MonitoringTarRequest
):
"""Получение данных из отчета мониторинга ТАР"""
report_service = get_report_service()
try:
request_dict = request_data.model_dump()
request = DataRequest(
report_type='monitoring_tar',
get_params=request_dict
)
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/monitoring_tar/get_full_data", tags=[MonitoringTarParser.name],
summary="Получение полных данных из отчета мониторинга ТАР")
async def get_monitoring_tar_full_data(
request_data: MonitoringTarFullRequest
):
"""Получение полных данных из отчета мониторинга ТАР"""
report_service = get_report_service()
try:
request_dict = request_data.model_dump()
request = DataRequest(
report_type='monitoring_tar',
get_params=request_dict
)
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/async/monitoring_tar/upload", tags=[MonitoringTarParser.name],
summary="Асинхронная загрузка файла отчета мониторинга ТАР",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def async_upload_monitoring_tar(
file: UploadFile = File(..., description="Excel файл мониторинга ТАР (.xlsx, .xlsm, .xls)")
):
"""Асинхронная загрузка и обработка отчета мониторинга ТАР"""
async_service = get_async_report_service()
try:
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
file_content = await file.read()
request = UploadRequest(
report_type='monitoring_tar',
file_content=file_content,
file_name=file.filename
)
result = await async_service.upload_report_async(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message=result.message,
error_code="UPLOAD_FAILED"
).model_dump()
)
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке мониторинга ТАР: {str(e)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_ERROR"
).model_dump()
)

View File

@@ -0,0 +1,190 @@
"""
Эндпоинты для оперативной справки техпос
"""
import logging
from fastapi import APIRouter, File, UploadFile, HTTPException, status
from fastapi.responses import JSONResponse
from adapters.storage import MinIOStorageAdapter
from adapters.parsers import OperSpravkaTechPosParser
from core.models import UploadRequest, DataRequest
from core.services import ReportService
from core.async_services import AsyncReportService
from app.schemas import UploadResponse, UploadErrorResponse
from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse
logger = logging.getLogger(__name__)
# Создаем роутер для оперативной справки техпос
router = APIRouter()
def get_report_service() -> ReportService:
"""Получение экземпляра сервиса отчетов"""
storage_adapter = MinIOStorageAdapter()
return ReportService(storage_adapter)
def get_async_report_service() -> AsyncReportService:
"""Получение экземпляра асинхронного сервиса отчетов"""
from core.services import ReportService
storage_adapter = MinIOStorageAdapter()
report_service = ReportService(storage_adapter)
return AsyncReportService(report_service)
@router.post("/oper_spravka_tech_pos/upload", tags=[OperSpravkaTechPosParser.name],
summary="Загрузка файла отчета оперативной справки техпос",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def upload_oper_spravka_tech_pos(
file: UploadFile = File(..., description="Excel файл оперативной справки техпос (.xlsx, .xlsm, .xls)")
):
"""Загрузка и обработка отчета оперативной справки техпос"""
report_service = get_report_service()
try:
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
file_content = await file.read()
request = UploadRequest(
report_type='oper_spravka_tech_pos',
file_content=file_content,
file_name=file.filename
)
result = report_service.upload_report(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=result.message,
error_code="ERR_UPLOAD"
).model_dump(),
)
except HTTPException:
raise
except Exception as e:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_SERVER_ERROR"
).model_dump()
)
@router.post("/oper_spravka_tech_pos/get_data", tags=[OperSpravkaTechPosParser.name],
summary="Получение данных из отчета оперативной справки техпос",
response_model=OperSpravkaTechPosResponse)
async def get_oper_spravka_tech_pos_data(
request_data: OperSpravkaTechPosRequest
):
"""Получение данных из отчета оперативной справки техпос"""
report_service = get_report_service()
try:
request_dict = request_data.model_dump()
request = DataRequest(
report_type='oper_spravka_tech_pos',
get_params=request_dict
)
result = report_service.get_data(request)
if result.success:
return OperSpravkaTechPosResponse(
success=True,
data=result.data
)
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/async/oper_spravka_tech_pos/upload", tags=[OperSpravkaTechPosParser.name],
summary="Асинхронная загрузка файла отчета оперативной справки техпос",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def async_upload_oper_spravka_tech_pos(
file: UploadFile = File(..., description="Excel файл оперативной справки техпос (.xlsx, .xlsm, .xls)")
):
"""Асинхронная загрузка и обработка отчета оперативной справки техпос"""
async_service = get_async_report_service()
try:
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
file_content = await file.read()
request = UploadRequest(
report_type='oper_spravka_tech_pos',
file_content=file_content,
file_name=file.filename
)
result = await async_service.upload_report_async(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message=result.message,
error_code="UPLOAD_FAILED"
).model_dump()
)
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке оперативной справки техпос: {str(e)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_ERROR"
).model_dump()
)

View File

@@ -0,0 +1,189 @@
"""
Эндпоинты для статусов ремонта СА
"""
import logging
from fastapi import APIRouter, File, UploadFile, HTTPException, status
from fastapi.responses import JSONResponse
from adapters.storage import MinIOStorageAdapter
from adapters.parsers import StatusesRepairCAParser
from core.models import UploadRequest, DataRequest
from core.services import ReportService
from core.async_services import AsyncReportService
from app.schemas import UploadResponse, UploadErrorResponse
from app.schemas.statuses_repair_ca import StatusesRepairCARequest
logger = logging.getLogger(__name__)
# Создаем роутер для статусов ремонта СА
router = APIRouter()
def get_report_service() -> ReportService:
"""Получение экземпляра сервиса отчетов"""
storage_adapter = MinIOStorageAdapter()
return ReportService(storage_adapter)
def get_async_report_service() -> AsyncReportService:
"""Получение экземпляра асинхронного сервиса отчетов"""
from core.services import ReportService
storage_adapter = MinIOStorageAdapter()
report_service = ReportService(storage_adapter)
return AsyncReportService(report_service)
@router.post("/statuses_repair_ca/upload", tags=[StatusesRepairCAParser.name],
summary="Загрузка файла отчета статусов ремонта СА",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def upload_statuses_repair_ca(
file: UploadFile = File(..., description="Excel файл статусов ремонта СА (.xlsx, .xlsm, .xls)")
):
"""Загрузка и обработка отчета статусов ремонта СА"""
report_service = get_report_service()
try:
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
file_content = await file.read()
request = UploadRequest(
report_type='statuses_repair_ca',
file_content=file_content,
file_name=file.filename
)
result = report_service.upload_report(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=result.message,
error_code="ERR_UPLOAD"
).model_dump(),
)
except HTTPException:
raise
except Exception as e:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_SERVER_ERROR"
).model_dump()
)
@router.post("/statuses_repair_ca/get_data", tags=[StatusesRepairCAParser.name],
summary="Получение данных из отчета статусов ремонта СА")
async def get_statuses_repair_ca_data(
request_data: StatusesRepairCARequest
):
"""Получение данных из отчета статусов ремонта СА"""
report_service = get_report_service()
try:
request_dict = request_data.model_dump()
request = DataRequest(
report_type='statuses_repair_ca',
get_params=request_dict
)
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/async/statuses_repair_ca/upload", tags=[StatusesRepairCAParser.name],
summary="Асинхронная загрузка файла отчета статусов ремонта СА",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def async_upload_statuses_repair_ca(
file: UploadFile = File(..., description="Excel файл статусов ремонта СА (.xlsx, .xlsm, .xls)")
):
"""Асинхронная загрузка и обработка отчета статусов ремонта СА"""
async_service = get_async_report_service()
try:
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
file_content = await file.read()
request = UploadRequest(
report_type='statuses_repair_ca',
file_content=file_content,
file_name=file.filename
)
result = await async_service.upload_report_async(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message=result.message,
error_code="UPLOAD_FAILED"
).model_dump()
)
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке статусов ремонта СА: {str(e)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_ERROR"
).model_dump()
)

View File

@@ -0,0 +1,226 @@
"""
Эндпоинты для сводки СА
"""
import logging
from fastapi import APIRouter, File, UploadFile, HTTPException, status
from fastapi.responses import JSONResponse
from adapters.storage import MinIOStorageAdapter
from adapters.parsers import SvodkaCAParser
from core.models import UploadRequest, DataRequest
from core.services import ReportService
from core.async_services import AsyncReportService
from app.schemas import UploadResponse, UploadErrorResponse, SvodkaCARequest
logger = logging.getLogger(__name__)
# Создаем роутер для сводки СА
router = APIRouter()
def get_report_service() -> ReportService:
"""Получение экземпляра сервиса отчетов"""
storage_adapter = MinIOStorageAdapter()
return ReportService(storage_adapter)
def get_async_report_service() -> AsyncReportService:
"""Получение экземпляра асинхронного сервиса отчетов"""
from core.services import ReportService
storage_adapter = MinIOStorageAdapter()
report_service = ReportService(storage_adapter)
return AsyncReportService(report_service)
@router.post("/svodka_ca/upload", tags=[SvodkaCAParser.name],
summary="Загрузка файла отчета сводки СА",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def upload_svodka_ca(
file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)")
):
"""Загрузка и обработка отчета сводки СА
### Поддерживаемые форматы:
- **Excel файлы** (.xlsx, .xlsm, .xls)
### Структура данных:
- Обрабатывает Excel файлы с данными по режимам и таблицам
- Извлекает данные по указанным режимам (plan, fact, normativ)
- Возвращает агрегированные данные по таблицам
### Пример использования:
1. Подготовьте Excel файл сводки СА
2. Загрузите файл через этот эндпоинт
3. Используйте полученный `object_id` для запросов данных
"""
report_service = get_report_service()
try:
# Проверяем тип файла
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
# Читаем содержимое файла
file_content = await file.read()
# Создаем запрос
request = UploadRequest(
report_type='svodka_ca',
file_content=file_content,
file_name=file.filename
)
# Загружаем отчет
result = report_service.upload_report(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=result.message,
error_code="ERR_UPLOAD"
).model_dump(),
)
except HTTPException:
raise
except Exception as e:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_SERVER_ERROR"
).model_dump()
)
@router.post("/svodka_ca/get_data", tags=[SvodkaCAParser.name],
summary="Получение данных из отчета сводки СА")
async def get_svodka_ca_data(
request_data: SvodkaCARequest
):
"""Получение данных из отчета сводки СА по указанным режимам и таблицам
### Структура параметров:
- `modes`: **Массив кодов** режимов - `plan`, `fact` или `normativ` (обязательный)
- `tables`: **Массив названий** таблиц как есть (обязательный)
### Пример тела запроса:
```json
{
"modes": ["plan", "fact"],
"tables": ["ТиП, %", "Топливо итого, тонн", "Топливо итого, %", "Потери итого, тонн"]
}
```
"""
report_service = get_report_service()
try:
# Создаем запрос
request_dict = request_data.model_dump()
request = DataRequest(
report_type='svodka_ca',
get_params=request_dict
)
# Получаем данные
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name],
summary="Асинхронная загрузка файла отчета сводки СА",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def async_upload_svodka_ca(
file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)")
):
"""Асинхронная загрузка и обработка отчета сводки СА"""
async_service = get_async_report_service()
try:
# Проверяем тип файла
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
# Читаем содержимое файла
file_content = await file.read()
# Создаем запрос
request = UploadRequest(
report_type='svodka_ca',
file_content=file_content,
file_name=file.filename
)
# Загружаем отчет асинхронно
result = await async_service.upload_report_async(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=result.message,
error_code="ERR_UPLOAD"
).model_dump(),
)
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_SERVER_ERROR"
).model_dump()
)

View File

@@ -0,0 +1,319 @@
"""
Эндпоинты для сводки ПМ
"""
import logging
from fastapi import APIRouter, File, UploadFile, HTTPException, status
from fastapi.responses import JSONResponse
from adapters.storage import MinIOStorageAdapter
from adapters.parsers import SvodkaPMParser
from core.models import UploadRequest, DataRequest
from core.services import ReportService
from core.async_services import AsyncReportService
from app.schemas import (
UploadResponse, UploadErrorResponse,
SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest
)
logger = logging.getLogger(__name__)
# Создаем роутер для сводки ПМ
router = APIRouter()
def get_report_service() -> ReportService:
"""Получение экземпляра сервиса отчетов"""
storage_adapter = MinIOStorageAdapter()
return ReportService(storage_adapter)
def get_async_report_service() -> AsyncReportService:
"""Получение экземпляра асинхронного сервиса отчетов"""
from core.services import ReportService
storage_adapter = MinIOStorageAdapter()
report_service = ReportService(storage_adapter)
return AsyncReportService(report_service)
@router.post("/svodka_pm/upload-zip", tags=[SvodkaPMParser.name],
summary="Загрузка файлов сводок ПМ одним ZIP-архивом",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def upload_svodka_pm_zip(
zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)")
):
"""Загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**
### Поддерживаемые форматы:
- **ZIP архивы** с файлами сводок ПМ
### Структура данных:
- Обрабатывает ZIP архивы с файлами по ОГ (svodka_fact_SNPZ.xlsx, svodka_plan_SNPZ.xlsx и т.д.)
- Извлекает данные по кодам строк и колонкам
- Возвращает агрегированные данные по ОГ
### Пример использования:
1. Подготовьте ZIP архив с файлами сводок ПМ
2. Загрузите архив через этот эндпоинт
3. Используйте полученный `object_id` для запросов данных
"""
report_service = get_report_service()
try:
# Проверяем тип файла - только ZIP архивы
if not zip_file.filename.endswith('.zip'):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Файл должен быть ZIP архивом",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".zip"],
"received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown"
}
).model_dump()
)
# Читаем содержимое файла
file_content = await zip_file.read()
# Создаем запрос
request = UploadRequest(
report_type='svodka_pm',
file_content=file_content,
file_name=zip_file.filename
)
# Загружаем отчет
result = report_service.upload_report(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=result.message,
error_code="ERR_UPLOAD"
).model_dump(),
)
except HTTPException:
raise
except Exception as e:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_SERVER_ERROR"
).model_dump()
)
@router.post("/svodka_pm/get_single_og", tags=[SvodkaPMParser.name],
summary="Получение данных по одному ОГ")
async def get_svodka_pm_single_og(
request_data: SvodkaPMSingleOGRequest
):
"""Получение данных из сводок ПМ (факта и плана) по одному ОГ
### Структура параметров:
- `id`: **Идентификатор МА** для запрашиваемого ОГ (обязательный)
- `codes`: **Массив кодов** выбираемых строк (обязательный)
- `columns`: **Массив названий** выбираемых столбцов (обязательный)
- `search`: **Опциональный параметр** для фильтрации ("Итого" или null)
### Пример тела запроса:
```json
{
"id": "SNPZ",
"codes": [78, 79],
"columns": ["ПП", "СЭБ"]
}
```
"""
report_service = get_report_service()
try:
# Создаем запрос
request_dict = request_data.model_dump()
request_dict['mode'] = 'single_og'
request = DataRequest(
report_type='svodka_pm',
get_params=request_dict
)
# Получаем данные
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/svodka_pm/get_total_ogs", tags=[SvodkaPMParser.name],
summary="Получение данных по всем ОГ")
async def get_svodka_pm_total_ogs(
request_data: SvodkaPMTotalOGsRequest
):
"""Получение данных из сводок ПМ (факта и плана) по всем ОГ
### Структура параметров:
- `codes`: **Массив кодов** выбираемых строк (обязательный)
- `columns`: **Массив названий** выбираемых столбцов (обязательный)
- `search`: **Опциональный параметр** для фильтрации ("Итого" или null)
### Пример тела запроса:
```json
{
"codes": [78, 79],
"columns": ["ПП", "СЭБ"]
}
```
"""
report_service = get_report_service()
try:
# Создаем запрос
request_dict = request_data.model_dump()
request_dict['mode'] = 'total_ogs'
request = DataRequest(
report_type='svodka_pm',
get_params=request_dict
)
# Получаем данные
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/svodka_pm/get_data", tags=[SvodkaPMParser.name])
async def get_svodka_pm_data(
request_data: dict
):
"""Получение данных из сводок ПМ (факта и плана)
### Структура параметров:
- `indicator_id`: **ID индикатора** для поиска (обязательный)
- `code`: **Код строки** для поиска (обязательный)
- `search_value`: **Опциональное значение** для поиска
### Пример тела запроса:
```json
{
"indicator_id": "SNPZ",
"code": 78,
"search_value": "Итого"
}
```
"""
report_service = get_report_service()
try:
# Создаем запрос
request = DataRequest(
report_type='svodka_pm',
get_params=request_data
)
# Получаем данные
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name],
summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def async_upload_svodka_pm_zip(
zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)")
):
"""Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**"""
async_service = get_async_report_service()
try:
if not zip_file.filename.endswith('.zip'):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Файл должен быть ZIP архивом",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".zip"],
"received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown"
}
).model_dump()
)
file_content = await zip_file.read()
# Создаем запрос
request = UploadRequest(
report_type='svodka_pm',
file_content=file_content,
file_name=zip_file.filename
)
# Загружаем отчет асинхронно
result = await async_service.upload_report_async(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message=result.message,
error_code="UPLOAD_FAILED"
).model_dump()
)
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_ERROR"
).model_dump()
)

View File

@@ -0,0 +1,189 @@
"""
Эндпоинты для сводки ремонта СА
"""
import logging
from fastapi import APIRouter, File, UploadFile, HTTPException, status
from fastapi.responses import JSONResponse
from adapters.storage import MinIOStorageAdapter
from adapters.parsers import SvodkaRepairCAParser
from core.models import UploadRequest, DataRequest
from core.services import ReportService
from core.async_services import AsyncReportService
from app.schemas import UploadResponse, UploadErrorResponse
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
logger = logging.getLogger(__name__)
# Создаем роутер для сводки ремонта СА
router = APIRouter()
def get_report_service() -> ReportService:
"""Получение экземпляра сервиса отчетов"""
storage_adapter = MinIOStorageAdapter()
return ReportService(storage_adapter)
def get_async_report_service() -> AsyncReportService:
"""Получение экземпляра асинхронного сервиса отчетов"""
from core.services import ReportService
storage_adapter = MinIOStorageAdapter()
report_service = ReportService(storage_adapter)
return AsyncReportService(report_service)
@router.post("/svodka_repair_ca/upload", tags=[SvodkaRepairCAParser.name],
summary="Загрузка файла отчета сводки ремонта СА",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def upload_svodka_repair_ca(
file: UploadFile = File(..., description="Excel файл сводки ремонта СА (.xlsx, .xlsm, .xls)")
):
"""Загрузка и обработка отчета сводки ремонта СА"""
report_service = get_report_service()
try:
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
file_content = await file.read()
request = UploadRequest(
report_type='svodka_repair_ca',
file_content=file_content,
file_name=file.filename
)
result = report_service.upload_report(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=result.message,
error_code="ERR_UPLOAD"
).model_dump(),
)
except HTTPException:
raise
except Exception as e:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_SERVER_ERROR"
).model_dump()
)
@router.post("/svodka_repair_ca/get_data", tags=[SvodkaRepairCAParser.name],
summary="Получение данных из отчета сводки ремонта СА")
async def get_svodka_repair_ca_data(
request_data: SvodkaRepairCARequest
):
"""Получение данных из отчета сводки ремонта СА"""
report_service = get_report_service()
try:
request_dict = request_data.model_dump()
request = DataRequest(
report_type='svodka_repair_ca',
get_params=request_dict
)
result = report_service.get_data(request)
if result.success:
return {
"success": True,
"data": result.data
}
else:
raise HTTPException(status_code=404, detail=result.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@router.post("/async/svodka_repair_ca/upload", tags=[SvodkaRepairCAParser.name],
summary="Асинхронная загрузка файла отчета сводки ремонта СА",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def async_upload_svodka_repair_ca(
file: UploadFile = File(..., description="Excel файл сводки ремонта СА (.xlsx, .xlsm, .xls)")
):
"""Асинхронная загрузка и обработка отчета сводки ремонта СА"""
async_service = get_async_report_service()
try:
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
file_content = await file.read()
request = UploadRequest(
report_type='svodka_repair_ca',
file_content=file_content,
file_name=file.filename
)
result = await async_service.upload_report_async(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message=result.message,
error_code="UPLOAD_FAILED"
).model_dump()
)
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке сводки ремонта СА: {str(e)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_ERROR"
).model_dump()
)

View File

@@ -0,0 +1,21 @@
"""
Системные эндпоинты FastAPI (не отображаются в Swagger)
"""
import logging
from fastapi import APIRouter
from adapters.pconfig import SINGLE_OGS, OG_IDS
logger = logging.getLogger(__name__)
# Создаем роутер для системных эндпоинтов
router = APIRouter()
@router.get("/system/ogs", include_in_schema=False)
async def get_system_ogs():
"""Системный эндпоинт для получения списка ОГ из pconfig"""
return {
"single_ogs": SINGLE_OGS,
"og_ids": OG_IDS
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,29 @@
from pydantic import BaseModel, Field
from typing import Optional
class ServerInfoResponse(BaseModel):
process_id: int = Field(..., description="Идентификатор текущего процесса сервера")
parent_id: int = Field(..., description="Идентификатор родительского процесса")
cpu_cores: int = Field(..., description="Количество ядер процессора в системе")
memory_mb: float = Field(..., description="Общий объем оперативной памяти в мегабайтах")
message: str = Field(..., description="Сообщение о сервере")
version: str = Field(..., description="Версия API")
process_id: Optional[int] = Field(None, description="Идентификатор текущего процесса сервера")
parent_id: Optional[int] = Field(None, description="Идентификатор родительского процесса")
cpu_cores: Optional[int] = Field(None, description="Количество ядер процессора в системе")
memory_mb: Optional[float] = Field(None, description="Общий объем оперативной памяти в мегабайтах")
python_version: Optional[str] = Field(None, description="Версия Python")
platform: Optional[str] = Field(None, description="Платформа")
available_parsers: Optional[list] = Field(None, description="Доступные парсеры")
class Config:
json_schema_extra = {
"example": {
"message": "Svodka Parser API",
"version": "1.0.0",
"process_id": 12345,
"parent_id": 6789,
"cpu_cores": 8,
"memory_mb": 16384.5
"memory_mb": 16384.5,
"python_version": "3.11.0",
"platform": "Windows-10-10.0.22631-SP0",
"available_parsers": ["svodka_pm", "svodka_ca", "monitoring_fuel"]
}
}

View File

@@ -84,6 +84,35 @@ class ParserPort(ABC):
except Exception as e:
raise ValueError(f"Ошибка выполнения геттера '{getter_name}': {str(e)}")
def determine_getter(self, get_params: Dict[str, Any]) -> str:
"""
Определение имени геттера на основе параметров запроса
Args:
get_params: Параметры запроса
Returns:
Имя геттера для выполнения
Raises:
ValueError: Если не удается определить геттер
"""
# По умолчанию используем первый доступный геттер
available_getters = list(self.getters.keys())
if not available_getters:
raise ValueError("Парсер не имеет доступных геттеров")
# Если указан режим, используем его
if 'mode' in get_params:
mode = get_params['mode']
if mode in self.getters:
return mode
else:
raise ValueError(f"Режим '{mode}' не найден. Доступные: {available_getters}")
# Иначе используем первый доступный
return available_getters[0]
@abstractmethod
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
"""Парсинг файла и возврат DataFrame"""

View File

@@ -122,83 +122,14 @@ class ReportService:
# Получаем параметры запроса
get_params = request.get_params or {}
# Для svodka_ca определяем режим из данных или используем 'fact' по умолчанию
if request.report_type == 'svodka_ca':
# Извлекаем режим из DataFrame или используем 'fact' по умолчанию
if hasattr(parser, 'df') and parser.df is not None and not parser.df.empty:
modes_in_df = parser.df['mode'].unique() if 'mode' in parser.df.columns else ['fact']
# Используем первый найденный режим или 'fact' по умолчанию
default_mode = modes_in_df[0] if len(modes_in_df) > 0 else 'fact'
else:
default_mode = 'fact'
# Устанавливаем режим в параметры, если он не указан
if 'mode' not in get_params:
get_params['mode'] = default_mode
# Определяем имя геттера
if request.report_type == 'svodka_ca':
# Для svodka_ca используем геттер get_ca_data
getter_name = 'get_ca_data'
elif request.report_type == 'svodka_repair_ca':
# Для svodka_repair_ca используем геттер get_repair_data
getter_name = 'get_repair_data'
elif request.report_type == 'statuses_repair_ca':
# Для statuses_repair_ca используем геттер get_repair_statuses
getter_name = 'get_repair_statuses'
elif request.report_type == 'monitoring_tar':
# Для monitoring_tar определяем геттер по параметрам
if 'mode' in get_params:
# Если есть параметр mode, используем get_tar_data
getter_name = 'get_tar_data'
else:
# Если нет параметра mode, используем get_tar_full_data
getter_name = 'get_tar_full_data'
elif request.report_type == 'monitoring_fuel':
# Для monitoring_fuel определяем геттер из параметра mode
getter_name = get_params.pop("mode", None)
if not getter_name:
# Если режим не указан, берем первый доступный
available_getters = list(parser.getters.keys())
if available_getters:
getter_name = available_getters[0]
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
else:
return DataResult(
success=False,
message="Парсер не имеет доступных геттеров"
)
elif request.report_type == 'svodka_pm':
# Для svodka_pm определяем геттер из параметра mode
getter_name = get_params.pop("mode", None)
if not getter_name:
# Если режим не указан, берем первый доступный
available_getters = list(parser.getters.keys())
if available_getters:
getter_name = available_getters[0]
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
else:
return DataResult(
success=False,
message="Парсер не имеет доступных геттеров"
)
elif request.report_type == 'oper_spravka_tech_pos':
# Для oper_spravka_tech_pos используем геттер get_tech_pos
getter_name = 'get_tech_pos'
else:
# Для других парсеров определяем из параметра mode
getter_name = get_params.pop("mode", None)
if not getter_name:
# Если режим не указан, берем первый доступный
available_getters = list(parser.getters.keys())
if available_getters:
getter_name = available_getters[0]
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
else:
return DataResult(
success=False,
message="Парсер не имеет доступных геттеров"
)
# Определяем имя геттера через парсер (делегируем логику в адаптер)
try:
getter_name = parser.determine_getter(get_params)
except ValueError as e:
return DataResult(
success=False,
message=str(e)
)
# Получаем значение через указанный геттер
try:

View File

@@ -1,100 +0,0 @@
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from minio import Minio
import os
from io import BytesIO
# Конфигурация страницы
st.set_page_config(
page_title="Сводка данных",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded"
)
# Заголовок приложения
st.title("📊 Анализ данных сводки")
st.markdown("---")
# Инициализация MinIO клиента
@st.cache_resource
def init_minio_client():
try:
client = Minio(
os.getenv("MINIO_ENDPOINT", "localhost:9000"),
access_key=os.getenv("MINIO_ACCESS_KEY", "minioadmin"),
secret_key=os.getenv("MINIO_SECRET_KEY", "minioadmin"),
secure=os.getenv("MINIO_SECURE", "false").lower() == "true"
)
return client
except Exception as e:
st.error(f"Ошибка подключения к MinIO: {e}")
return None
# Боковая панель
with st.sidebar:
st.header("⚙️ Настройки")
# Выбор типа данных
data_type = st.selectbox(
"Тип данных",
["Мониторинг топлива", "Сводка ПМ", "Сводка ЦА"]
)
# Выбор периода
period = st.date_input(
"Период",
value=pd.Timestamp.now().date()
)
st.markdown("---")
st.markdown("### 📈 Статистика")
st.info("Выберите тип данных для анализа")
# Основной контент
col1, col2 = st.columns([2, 1])
with col1:
st.subheader(f"📋 {data_type}")
if data_type == "Мониторинг топлива":
st.info("Анализ данных мониторинга топлива")
# Здесь будет логика для работы с данными мониторинга топлива
elif data_type == "Сводка ПМ":
st.info("Анализ данных сводки ПМ")
# Здесь будет логика для работы с данными сводки ПМ
elif data_type == "Сводка ЦА":
st.info("Анализ данных сводки ЦА")
# Здесь будет логика для работы с данными сводки ЦА
with col2:
st.subheader("📊 Быстрая статистика")
st.metric("Всего записей", "0")
st.metric("Активных", "0")
st.metric("Ошибок", "0")
# Нижняя панель
st.markdown("---")
st.subheader("🔍 Детальный анализ")
# Заглушка для графиков
placeholder = st.empty()
with placeholder.container():
col1, col2 = st.columns(2)
with col1:
st.write("📈 График 1")
# Здесь будет график
with col2:
st.write("📊 График 2")
# Здесь будет график
# Футер
st.markdown("---")
st.markdown("**Разработано для анализа данных сводки** | v1.0.0")

View File

@@ -17,7 +17,13 @@ def render_sidebar():
st.subheader("Сервер")
st.write(f"PID: {server_info.get('process_id', 'N/A')}")
st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}")
st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB")
# Безопасное форматирование памяти
memory_mb = server_info.get('memory_mb')
if memory_mb is not None:
st.write(f"Память: {memory_mb:.1f} MB")
else:
st.write("Память: N/A")
# Доступные парсеры
parsers = get_available_parsers()