5 Commits

Author SHA1 Message Date
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
8 changed files with 273 additions and 55 deletions

View File

@@ -5,8 +5,8 @@ import logging
from typing import Dict, Tuple
from core.ports import ParserPort
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest
from adapters.pconfig import data_to_json
from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest, MonitoringFuelSeriesRequest
from adapters.pconfig import data_to_json, get_object_by_name
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
@@ -35,6 +35,14 @@ class MonitoringFuelParser(ParserPort):
schema_class=MonitoringFuelMonthRequest,
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):
"""Агрегация данных по колонкам"""
@@ -92,16 +100,83 @@ class MonitoringFuelParser(ParserPort):
# Преобразуем в JSON-совместимый формат
result = {}
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:
value = row[col]
if pd.isna(value) or value == float('inf') or value == float('-inf'):
result[str(idx)][col] = None
result[object_id][col] = None
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
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):
"""Преобразование DataFrame обратно в словарь данных"""
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')
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
@@ -231,10 +311,10 @@ class MonitoringFuelParser(ParserPort):
# Проверяем, что колонка 'name' существует
if 'name' in df_full.columns:
# Применяем функцию get_id_by_name к каждой строке в колонке 'name'
# df_full['id'] = df_full['name'].apply(get_object_by_name) # This line was removed as per new_code
# Временно используем name как id
df_full['id'] = df_full['name']
# Применяем функцию get_object_by_name к каждой строке в колонке 'name'
df_full['id'] = df_full['name'].apply(get_object_by_name)
# Удаляем строки, где не удалось найти ID
df_full = df_full.dropna(subset=['id'])
else:
# Если нет колонки name, создаем id из индекса
df_full['id'] = df_full.index

View File

@@ -18,6 +18,7 @@ logger = logging.getLogger(__name__)
from adapters.storage import MinIOStorageAdapter
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser
from adapters.pconfig import SINGLE_OGS, OG_IDS
from core.models import UploadRequest, DataRequest
from core.services import ReportService, PARSERS
@@ -28,7 +29,7 @@ from app.schemas import (
UploadResponse, UploadErrorResponse,
SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest,
SvodkaCARequest,
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest
)
from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
@@ -933,40 +934,6 @@ async def get_statuses_repair_ca_data(
# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@app.post("/monitoring_fuel/get_data", tags=[MonitoringFuelParser.name])
async def get_monitoring_fuel_data(
request_data: dict
):
report_service = get_report_service()
"""
Получение данных из отчета мониторинга топлива
- column: Название колонки для агрегации (normativ, total, total_svod)
"""
try:
# Создаем запрос
request = DataRequest(
report_type='monitoring_fuel',
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)}")
# @app.post("/monitoring_fuel/upload_directory", tags=[MonitoringFuelParser.name])
# async def upload_monitoring_fuel_directory(
# request_data: dict
@@ -1195,6 +1162,66 @@ async def get_monitoring_fuel_month_by_code(
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@app.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)}")
# ====== MONITORING TAR ENDPOINTS ======
@app.post("/monitoring_tar/upload", tags=[MonitoringTarParser.name],
@@ -1512,6 +1539,17 @@ async def async_upload_svodka_pm_zip(
)
# ====== СИСТЕМНЫЕ ЭНДПОИНТЫ (НЕ ОТОБРАЖАЮТСЯ В SWAGGER) ======
@app.get("/system/ogs", include_in_schema=False)
async def get_system_ogs():
"""Системный эндпоинт для получения списка ОГ из pconfig"""
return {
"single_ogs": SINGLE_OGS,
"og_ids": OG_IDS
}
@app.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name],
summary="Асинхронная загрузка файла отчета сводки СА",
response_model=UploadResponse,

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

View File

@@ -32,3 +32,19 @@ class MonitoringFuelTotalRequest(BaseModel):
"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

@@ -37,6 +37,17 @@ def get_server_info() -> Dict[str, Any]:
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]:
"""Загрузка файла на API"""
try:

View File

@@ -2,6 +2,7 @@
UI модуль для мониторинга топлива
"""
import streamlit as st
import pandas as pd
from api_client import upload_file_to_api, make_api_request
from config import FUEL_COLUMNS
@@ -88,4 +89,74 @@ def render_monitoring_fuel_tab():
st.success("✅ Данные получены")
st.json(result)
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 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():
@@ -33,13 +33,14 @@ def render_statuses_repair_ca_tab():
# Секция получения данных
st.subheader("📊 Получение данных")
# Получаем доступные ОГ динамически
available_ogs = get_available_ogs("statuses_repair_ca")
# Получаем доступные ОГ из системного API
system_ogs = get_system_ogs()
available_ogs = system_ogs.get("single_ogs", [])
# Фильтр по ОГ
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"
)

View File

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