diff --git a/python_parser/adapters/parsers/monitoring_fuel.py b/python_parser/adapters/parsers/monitoring_fuel.py index 6e51e6a..08ddfd2 100644 --- a/python_parser/adapters/parsers/monitoring_fuel.py +++ b/python_parser/adapters/parsers/monitoring_fuel.py @@ -5,7 +5,7 @@ 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 app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest, MonitoringFuelSeriesRequest from adapters.pconfig import data_to_json # Настройка логгера для модуля @@ -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): """Агрегация данных по колонкам""" @@ -102,6 +110,62 @@ class MonitoringFuelParser(ParserPort): 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: diff --git a/python_parser/app/main.py b/python_parser/app/main.py index a8ace37..d2744bf 100644 --- a/python_parser/app/main.py +++ b/python_parser/app/main.py @@ -28,7 +28,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 +933,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 +1161,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], diff --git a/python_parser/app/schemas/__init__.py b/python_parser/app/schemas/__init__.py index fa619ba..d9ab5d4 100644 --- a/python_parser/app/schemas/__init__.py +++ b/python_parser/app/schemas/__init__.py @@ -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', diff --git a/python_parser/app/schemas/monitoring_fuel.py b/python_parser/app/schemas/monitoring_fuel.py index 4239f05..d5b56bc 100644 --- a/python_parser/app/schemas/monitoring_fuel.py +++ b/python_parser/app/schemas/monitoring_fuel.py @@ -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"] + } + } diff --git a/streamlit_app/parsers_ui/monitoring_fuel_ui.py b/streamlit_app/parsers_ui/monitoring_fuel_ui.py index 8ee4fc1..00b6f7e 100644 --- a/streamlit_app/parsers_ui/monitoring_fuel_ui.py +++ b/streamlit_app/parsers_ui/monitoring_fuel_ui.py @@ -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,81 @@ def render_monitoring_fuel_tab(): st.success("✅ Данные получены") st.json(result) else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file + 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)} объектов") + + # Создаем DataFrame для отображения + df_series = pd.DataFrame(series_data).T + df_series.index.name = 'ID объекта' + + st.dataframe(df_series, use_container_width=True) + + # Показываем JSON для отладки + with st.expander("🔍 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, ...] + } + ``` + """) \ No newline at end of file