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 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 уже является 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 +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

@@ -18,6 +18,7 @@ logger = logging.getLogger(__name__)
from adapters.storage import MinIOStorageAdapter from adapters.storage import MinIOStorageAdapter
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser 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.models import UploadRequest, DataRequest
from core.services import ReportService, PARSERS from core.services import ReportService, PARSERS
@@ -28,7 +29,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 +934,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 +1162,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],
@@ -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], @app.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name],
summary="Асинхронная загрузка файла отчета сводки СА", summary="Асинхронная загрузка файла отчета сводки СА",
response_model=UploadResponse, 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_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

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