8 Commits

Author SHA1 Message Date
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
802cf5ffba Фильтр донастроил 2025-09-08 15:49:37 +03:00
3ffe547208 get_month_by_code работает корректно 2025-09-08 15:30:55 +03:00
9f9adce4f3 get_series_by_id_and_columns функционален 2025-09-08 15:21:52 +03:00
8ee1c816e2 ch 2025-09-08 15:10:32 +03:00
34937ec062 Merge branch 'to-faster' 2025-09-04 23:01:09 +03:00
22 changed files with 2239 additions and 1733 deletions

View File

@@ -5,8 +5,8 @@ import logging
from typing import Dict, Tuple from typing import Dict, Tuple
from core.ports import ParserPort from core.ports import ParserPort
from core.schema_utils import register_getter_from_schema, validate_params_with_schema from core.schema_utils import register_getter_from_schema, validate_params_with_schema
from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest, MonitoringFuelSeriesRequest
from adapters.pconfig import data_to_json from adapters.pconfig import data_to_json, get_object_by_name
# Настройка логгера для модуля # Настройка логгера для модуля
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -36,6 +36,14 @@ class MonitoringFuelParser(ParserPort):
description="Получение данных за конкретный месяц" description="Получение данных за конкретный месяц"
) )
register_getter_from_schema(
parser_instance=self,
getter_name="series_by_id_and_columns",
method=self._get_series_by_id_and_columns,
schema_class=MonitoringFuelSeriesRequest,
description="Получение временного ряда по ID и колонкам"
)
def _get_total_by_columns(self, params: dict): def _get_total_by_columns(self, params: dict):
"""Агрегация данных по колонкам""" """Агрегация данных по колонкам"""
# Валидируем параметры с помощью схемы Pydantic # Валидируем параметры с помощью схемы Pydantic
@@ -92,16 +100,83 @@ class MonitoringFuelParser(ParserPort):
# Преобразуем в JSON-совместимый формат # Преобразуем в JSON-совместимый формат
result = {} result = {}
for idx, row in df_month.iterrows(): for idx, row in df_month.iterrows():
result[str(idx)] = {} # Преобразуем название установки в ID, если это необходимо
if isinstance(idx, str) and not idx.startswith('SNPZ.'):
# Это название установки, нужно преобразовать в ID
object_id = get_object_by_name(idx)
if object_id is None:
# Если не удалось найти ID, используем название как есть
object_id = idx
else:
# Это уже ID или что-то другое
object_id = str(idx)
result[object_id] = {}
for col in df_month.columns: for col in df_month.columns:
value = row[col] value = row[col]
if pd.isna(value) or value == float('inf') or value == float('-inf'): if pd.isna(value) or value == float('inf') or value == float('-inf'):
result[str(idx)][col] = None result[object_id][col] = None
else: else:
result[str(idx)][col] = float(value) if isinstance(value, (int, float)) else value result[object_id][col] = float(value) if isinstance(value, (int, float)) else value
return result return result
def _get_series_by_id_and_columns(self, params: dict):
"""Получение временных рядов по колонкам для всех ID"""
# Валидируем параметры с помощью схемы Pydantic
validated_params = validate_params_with_schema(params, MonitoringFuelSeriesRequest)
columns = validated_params["columns"]
# Проверяем, есть ли данные в data_dict (из парсинга) или в df (из загрузки)
if hasattr(self, 'data_dict') and self.data_dict is not None:
# Данные из парсинга
data_source = self.data_dict
elif hasattr(self, 'df') and self.df is not None and not self.df.empty:
# Данные из загрузки - преобразуем DataFrame обратно в словарь
data_source = self._df_to_data_dict()
else:
return {}
# Проверяем, что все колонки существуют хотя бы в одном месяце
valid_columns = set()
for month_df in data_source.values():
valid_columns.update(month_df.columns)
for col in columns:
if col not in valid_columns:
raise ValueError(f"Колонка '{col}' не найдена ни в одном месяце")
# Подготавливаем результат: словарь id → {col: [значения по месяцам]}
result = {}
# Обрабатываем месяцы от 01 до 12
for month_key in [f"{i:02d}" for i in range(1, 13)]:
if month_key not in data_source:
logger.warning(f"Месяц '{month_key}' не найден в df_monitorings, пропускаем.")
continue
df = data_source[month_key]
for col in columns:
if col not in df.columns:
continue # Пропускаем, если в этом месяце нет колонки
for idx, value in df[col].items():
if pd.isna(value):
continue # Пропускаем NaN
if idx not in result:
result[idx] = {c: [] for c in columns}
# Добавляем значение в массив для данного ID и колонки
if not pd.isna(value) and value != float('inf') and value != float('-inf'):
result[idx][col].append(float(value) if isinstance(value, (int, float)) else value)
# Преобразуем ключи id в строки (для JSON-совместимости)
result_str_keys = {str(k): v for k, v in result.items()}
return result_str_keys
def _df_to_data_dict(self): def _df_to_data_dict(self):
"""Преобразование DataFrame обратно в словарь данных""" """Преобразование DataFrame обратно в словарь данных"""
if not hasattr(self, 'df') or self.df is None or self.df.empty: if not hasattr(self, 'df') or self.df is None or self.df.empty:
@@ -115,7 +190,12 @@ class MonitoringFuelParser(ParserPort):
data = row.get('data') data = row.get('data')
if month and data is not None: if month and data is not None:
data_dict[month] = data # data уже является DataFrame, поэтому используем его напрямую
if isinstance(data, pd.DataFrame):
data_dict[month] = data
else:
# Если data не DataFrame, пропускаем
logger.warning(f"Данные за месяц {month} не являются DataFrame, пропускаем")
return data_dict return data_dict
@@ -231,10 +311,10 @@ class MonitoringFuelParser(ParserPort):
# Проверяем, что колонка 'name' существует # Проверяем, что колонка 'name' существует
if 'name' in df_full.columns: if 'name' in df_full.columns:
# Применяем функцию get_id_by_name к каждой строке в колонке 'name' # Применяем функцию get_object_by_name к каждой строке в колонке 'name'
# df_full['id'] = df_full['name'].apply(get_object_by_name) # This line was removed as per new_code df_full['id'] = df_full['name'].apply(get_object_by_name)
# Временно используем name как id # Удаляем строки, где не удалось найти ID
df_full['id'] = df_full['name'] df_full = df_full.dropna(subset=['id'])
else: else:
# Если нет колонки name, создаем id из индекса # Если нет колонки name, создаем id из индекса
df_full['id'] = df_full.index df_full['id'] = df_full.index

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,4 +1,4 @@
from .monitoring_fuel import MonitoringFuelMonthRequest, MonitoringFuelTotalRequest from .monitoring_fuel import MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest
from .svodka_ca import SvodkaCARequest from .svodka_ca import SvodkaCARequest
from .svodka_pm import SvodkaPMSingleOGRequest, SvodkaPMTotalOGsRequest from .svodka_pm import SvodkaPMSingleOGRequest, SvodkaPMTotalOGsRequest
from .server import ServerInfoResponse from .server import ServerInfoResponse
@@ -6,7 +6,7 @@ from .upload import UploadResponse, UploadErrorResponse
__all__ = [ __all__ = [
'MonitoringFuelMonthRequest', 'MonitoringFuelTotalRequest', 'MonitoringFuelMonthRequest', 'MonitoringFuelTotalRequest', 'MonitoringFuelSeriesRequest',
'SvodkaCARequest', 'SvodkaCARequest',
'SvodkaPMSingleOGRequest', 'SvodkaPMTotalOGsRequest', 'SvodkaPMSingleOGRequest', 'SvodkaPMTotalOGsRequest',
'ServerInfoResponse', 'ServerInfoResponse',

View File

@@ -32,3 +32,19 @@ class MonitoringFuelTotalRequest(BaseModel):
"columns": ["total", "normativ"] "columns": ["total", "normativ"]
} }
} }
class MonitoringFuelSeriesRequest(BaseModel):
columns: List[str] = Field(
...,
description="Массив названий выбираемых столбцов",
example=["total", "normativ"],
min_items=1
)
class Config:
json_schema_extra = {
"example": {
"columns": ["total", "normativ"]
}
}

View File

@@ -1,18 +1,29 @@
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import Optional
class ServerInfoResponse(BaseModel): class ServerInfoResponse(BaseModel):
process_id: int = Field(..., description="Идентификатор текущего процесса сервера") message: str = Field(..., description="Сообщение о сервере")
parent_id: int = Field(..., description="Идентификатор родительского процесса") version: str = Field(..., description="Версия API")
cpu_cores: int = Field(..., description="Количество ядер процессора в системе") process_id: Optional[int] = Field(None, description="Идентификатор текущего процесса сервера")
memory_mb: float = Field(..., 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: class Config:
json_schema_extra = { json_schema_extra = {
"example": { "example": {
"message": "Svodka Parser API",
"version": "1.0.0",
"process_id": 12345, "process_id": 12345,
"parent_id": 6789, "parent_id": 6789,
"cpu_cores": 8, "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

@@ -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

@@ -37,6 +37,17 @@ def get_server_info() -> Dict[str, Any]:
return {} return {}
def get_system_ogs() -> Dict[str, Any]:
"""Получение системного списка ОГ из pconfig"""
try:
response = requests.get(f"{API_BASE_URL}/system/ogs")
if response.status_code == 200:
return response.json()
return {"single_ogs": [], "og_ids": {}}
except:
return {"single_ogs": [], "og_ids": {}}
def upload_file_to_api(endpoint: str, file_data: bytes, filename: str) -> Tuple[Dict[str, Any], int]: def upload_file_to_api(endpoint: str, file_data: bytes, filename: str) -> Tuple[Dict[str, Any], int]:
"""Загрузка файла на API""" """Загрузка файла на API"""
try: try:

View File

@@ -2,6 +2,7 @@
UI модуль для мониторинга топлива UI модуль для мониторинга топлива
""" """
import streamlit as st import streamlit as st
import pandas as pd
from api_client import upload_file_to_api, make_api_request from api_client import upload_file_to_api, make_api_request
from config import FUEL_COLUMNS from config import FUEL_COLUMNS
@@ -89,3 +90,73 @@ def render_monitoring_fuel_tab():
st.json(result) st.json(result)
else: else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
st.markdown("---")
# Новая секция для временных рядов
st.subheader("📈 Временные ряды")
col1, col2 = st.columns(2)
with col1:
st.subheader("Временные ряды по колонкам")
# Выбор колонок для временного ряда
series_columns = st.multiselect(
"Выберите столбцы для временного ряда",
FUEL_COLUMNS,
default=["total", "normativ"],
key="fuel_series_columns"
)
if st.button("📊 Получить временные ряды", key="fuel_series_btn"):
if series_columns:
with st.spinner("Получаю временные ряды..."):
data = {
"columns": series_columns
}
result, status = make_api_request("/monitoring_fuel/get_series_by_id_and_columns", data)
if status == 200:
st.success("✅ Временные ряды получены")
# Отображаем данные
if result.get('data'):
series_data = result['data']
# Показываем количество найденных ID
st.info(f"📊 Найдено {len(series_data)} объектов")
# Показываем JSON данные
st.json(result)
else:
st.warning("⚠️ Данные не найдены")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите столбцы")
with col2:
st.subheader(" Справка")
st.info("""
**Временные ряды** показывают изменение значений по месяцам для всех объектов.
**Формат данных:**
- Каждый ID объекта содержит массивы значений по месяцам
- Массивы упорядочены по месяцам (01, 02, 03, ..., 12)
- Отсутствующие месяцы пропускаются
**Доступные колонки:**
- `total` - общее потребление
- `normativ` - нормативное потребление
- И другие колонки из загруженных данных
**Пример результата:**
```
SNPZ.VISB: {
"total": [23.86, 26.51, 19.66, ...],
"normativ": [19.46, 19.45, 18.57, ...]
}
```
""")

View File

@@ -3,7 +3,7 @@ UI модуль для статусов ремонта СА
""" """
import streamlit as st import streamlit as st
import pandas as pd import pandas as pd
from api_client import upload_file_to_api, make_api_request, get_available_ogs from api_client import upload_file_to_api, make_api_request, get_available_ogs, get_system_ogs
def render_statuses_repair_ca_tab(): def render_statuses_repair_ca_tab():
@@ -33,13 +33,14 @@ def render_statuses_repair_ca_tab():
# Секция получения данных # Секция получения данных
st.subheader("📊 Получение данных") st.subheader("📊 Получение данных")
# Получаем доступные ОГ динамически # Получаем доступные ОГ из системного API
available_ogs = get_available_ogs("statuses_repair_ca") system_ogs = get_system_ogs()
available_ogs = system_ogs.get("single_ogs", [])
# Фильтр по ОГ # Фильтр по ОГ
og_ids = st.multiselect( og_ids = st.multiselect(
"Выберите ОГ (оставьте пустым для всех)", "Выберите ОГ (оставьте пустым для всех)",
available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback available_ogs if available_ogs else get_available_ogs(), # fallback
key="statuses_repair_ca_og_ids" key="statuses_repair_ca_og_ids"
) )

View File

@@ -3,7 +3,7 @@ UI модуль для ремонта СА
""" """
import streamlit as st import streamlit as st
import pandas as pd import pandas as pd
from api_client import upload_file_to_api, make_api_request, get_available_ogs from api_client import upload_file_to_api, make_api_request, get_system_ogs, get_available_ogs
from config import REPAIR_TYPES from config import REPAIR_TYPES
@@ -42,8 +42,9 @@ def render_svodka_repair_ca_tab():
with col1: with col1:
st.subheader("Фильтры") st.subheader("Фильтры")
# Получаем доступные ОГ динамически # Получаем доступные ОГ из системного API
available_ogs = get_available_ogs("svodka_repair_ca") system_ogs = get_system_ogs()
available_ogs = system_ogs.get("single_ogs", [])
# Фильтр по ОГ # Фильтр по ОГ
og_ids = st.multiselect( og_ids = st.multiselect(

View File

@@ -17,7 +17,13 @@ def render_sidebar():
st.subheader("Сервер") st.subheader("Сервер")
st.write(f"PID: {server_info.get('process_id', 'N/A')}") 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"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() parsers = get_available_parsers()