2 Commits

Author SHA1 Message Date
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
5 changed files with 227 additions and 45 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
@@ -102,6 +110,62 @@ class MonitoringFuelParser(ParserPort):
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 +179,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 уже является DataFrame, поэтому используем его напрямую
if isinstance(data, pd.DataFrame):
data_dict[month] = data data_dict[month] = data
else:
# Если data не DataFrame, пропускаем
logger.warning(f"Данные за месяц {month} не являются DataFrame, пропускаем")
return data_dict return data_dict
@@ -231,10 +300,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

@@ -28,7 +28,7 @@ from app.schemas import (
UploadResponse, UploadErrorResponse, UploadResponse, UploadErrorResponse,
SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest, SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest,
SvodkaCARequest, SvodkaCARequest,
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest
) )
from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
@@ -933,40 +933,6 @@ async def get_statuses_repair_ca_data(
# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") # 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]) # @app.post("/monitoring_fuel/upload_directory", tags=[MonitoringFuelParser.name])
# async def upload_monitoring_fuel_directory( # async def upload_monitoring_fuel_directory(
# request_data: dict # request_data: dict
@@ -1195,6 +1161,66 @@ async def get_monitoring_fuel_month_by_code(
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") 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 ====== # ====== MONITORING TAR ENDPOINTS ======
@app.post("/monitoring_tar/upload", tags=[MonitoringTarParser.name], @app.post("/monitoring_tar/upload", tags=[MonitoringTarParser.name],

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

@@ -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, ...]
}
```
""")