Compare commits
23 Commits
847441842c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f600b7b4af | |||
| b01320acde | |||
| db3788ac69 | |||
| 31fad98133 | |||
| 88294de506 | |||
| cb8e8b2483 | |||
| 4624442991 | |||
| 816547d82c | |||
| 2f459487fe | |||
| 46a3c2e9cd | |||
| 57d9d5a703 | |||
| 802cf5ffba | |||
| 3ffe547208 | |||
| 9f9adce4f3 | |||
| 8ee1c816e2 | |||
| 34937ec062 | |||
| 55626490dd | |||
| 1bfe3c0cd8 | |||
| 36f37ffacb | |||
| 6a1f685ee3 | |||
| 2fcee9f065 | |||
| f54a36ab22 | |||
| 2555fd80e0 |
@@ -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,29 @@ 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 determine_getter(self, get_params: dict) -> str:
|
||||||
|
"""Определение геттера для мониторинга топлива"""
|
||||||
|
# Для monitoring_fuel определяем геттер из параметра mode
|
||||||
|
getter_name = get_params.pop("mode", None)
|
||||||
|
if not getter_name:
|
||||||
|
# Если режим не указан, берем первый доступный
|
||||||
|
available_getters = list(self.getters.keys())
|
||||||
|
if available_getters:
|
||||||
|
getter_name = available_getters[0]
|
||||||
|
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
||||||
|
else:
|
||||||
|
raise ValueError("Парсер не имеет доступных геттеров")
|
||||||
|
|
||||||
|
return getter_name
|
||||||
|
|
||||||
def _get_total_by_columns(self, params: dict):
|
def _get_total_by_columns(self, params: dict):
|
||||||
"""Агрегация данных по колонкам"""
|
"""Агрегация данных по колонкам"""
|
||||||
# Валидируем параметры с помощью схемы Pydantic
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
@@ -92,16 +115,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 +205,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 +326,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
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ class MonitoringTarParser(ParserPort):
|
|||||||
self.register_getter('get_tar_data', self._get_tar_data_wrapper, required_params=['mode'])
|
self.register_getter('get_tar_data', self._get_tar_data_wrapper, required_params=['mode'])
|
||||||
self.register_getter('get_tar_full_data', self._get_tar_full_data_wrapper, required_params=[])
|
self.register_getter('get_tar_full_data', self._get_tar_full_data_wrapper, required_params=[])
|
||||||
|
|
||||||
|
def determine_getter(self, get_params: dict) -> str:
|
||||||
|
"""Определение геттера для мониторинга ТАР"""
|
||||||
|
# Для monitoring_tar определяем геттер по параметрам
|
||||||
|
if 'mode' in get_params:
|
||||||
|
# Если есть параметр mode, используем get_tar_data
|
||||||
|
return 'get_tar_data'
|
||||||
|
else:
|
||||||
|
# Если нет параметра mode, используем get_tar_full_data
|
||||||
|
return 'get_tar_full_data'
|
||||||
|
|
||||||
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
|
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
|
||||||
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
|
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
|
||||||
logger.debug(f"🔍 MonitoringTarParser.parse вызван с файлом: {file_path}")
|
logger.debug(f"🔍 MonitoringTarParser.parse вызван с файлом: {file_path}")
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
# Регистрируем геттер
|
# Регистрируем геттер
|
||||||
self.register_getter('get_tech_pos', self._get_tech_pos_wrapper, required_params=['id'])
|
self.register_getter('get_tech_pos', self._get_tech_pos_wrapper, required_params=['id'])
|
||||||
|
|
||||||
|
def determine_getter(self, get_params: dict) -> str:
|
||||||
|
"""Определение геттера для операционных справок технологических позиций"""
|
||||||
|
# Для oper_spravka_tech_pos всегда используем геттер get_tech_pos
|
||||||
|
return 'get_tech_pos'
|
||||||
|
|
||||||
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
|
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
|
||||||
"""Парсит ZIP архив с файлами операционных справок технологических позиций"""
|
"""Парсит ZIP архив с файлами операционных справок технологических позиций"""
|
||||||
logger.debug(f"🔍 OperSpravkaTechPosParser.parse вызван с файлом: {file_path}")
|
logger.debug(f"🔍 OperSpravkaTechPosParser.parse вызван с файлом: {file_path}")
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ class StatusesRepairCAParser(ParserPort):
|
|||||||
description="Получение статусов ремонта по ОГ и ключам"
|
description="Получение статусов ремонта по ОГ и ключам"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def determine_getter(self, get_params: dict) -> str:
|
||||||
|
"""Определение геттера для статусов ремонта СА"""
|
||||||
|
# Для statuses_repair_ca всегда используем геттер get_repair_statuses
|
||||||
|
return 'get_repair_statuses'
|
||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> Dict[str, Any]:
|
def parse(self, file_path: str, params: dict) -> Dict[str, Any]:
|
||||||
"""Парсинг файла статусов ремонта СА"""
|
"""Парсинг файла статусов ремонта СА"""
|
||||||
logger.debug(f"🔍 StatusesRepairCAParser.parse вызван с файлом: {file_path}")
|
logger.debug(f"🔍 StatusesRepairCAParser.parse вызван с файлом: {file_path}")
|
||||||
|
|||||||
@@ -27,6 +27,23 @@ class SvodkaCAParser(ParserPort):
|
|||||||
description="Получение данных по режимам и таблицам"
|
description="Получение данных по режимам и таблицам"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def determine_getter(self, get_params: dict) -> str:
|
||||||
|
"""Определение геттера для сводки СА"""
|
||||||
|
# Для svodka_ca определяем режим из данных или используем 'fact' по умолчанию
|
||||||
|
if hasattr(self, 'df') and self.df is not None and not self.df.empty:
|
||||||
|
modes_in_df = self.df['mode'].unique() if 'mode' in self.df.columns else ['fact']
|
||||||
|
# Используем первый найденный режим или 'fact' по умолчанию
|
||||||
|
default_mode = modes_in_df[0] if len(modes_in_df) > 0 else 'fact'
|
||||||
|
else:
|
||||||
|
default_mode = 'fact'
|
||||||
|
|
||||||
|
# Устанавливаем режим в параметры, если он не указан
|
||||||
|
if 'mode' not in get_params:
|
||||||
|
get_params['mode'] = default_mode
|
||||||
|
|
||||||
|
# Для svodka_ca всегда используем геттер get_ca_data
|
||||||
|
return 'get_ca_data'
|
||||||
|
|
||||||
def _get_data_wrapper(self, params: dict):
|
def _get_data_wrapper(self, params: dict):
|
||||||
"""Получение данных по режимам и таблицам"""
|
"""Получение данных по режимам и таблицам"""
|
||||||
logger.debug(f"🔍 _get_data_wrapper вызван с параметрами: {params}")
|
logger.debug(f"🔍 _get_data_wrapper вызван с параметрами: {params}")
|
||||||
|
|||||||
@@ -42,6 +42,21 @@ class SvodkaPMParser(ParserPort):
|
|||||||
description="Получение данных по всем ОГ из сводки ПМ"
|
description="Получение данных по всем ОГ из сводки ПМ"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def determine_getter(self, get_params: dict) -> str:
|
||||||
|
"""Определение геттера для сводки ПМ"""
|
||||||
|
# Для svodka_pm определяем геттер из параметра mode
|
||||||
|
getter_name = get_params.pop("mode", None)
|
||||||
|
if not getter_name:
|
||||||
|
# Если режим не указан, берем первый доступный
|
||||||
|
available_getters = list(self.getters.keys())
|
||||||
|
if available_getters:
|
||||||
|
getter_name = available_getters[0]
|
||||||
|
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
||||||
|
else:
|
||||||
|
raise ValueError("Парсер не имеет доступных геттеров")
|
||||||
|
|
||||||
|
return getter_name
|
||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> Dict[str, pd.DataFrame]:
|
def parse(self, file_path: str, params: dict) -> Dict[str, pd.DataFrame]:
|
||||||
"""Парсинг ZIP архива со сводками ПМ и возврат словаря с DataFrame"""
|
"""Парсинг ZIP архива со сводками ПМ и возврат словаря с DataFrame"""
|
||||||
# Проверяем расширение файла
|
# Проверяем расширение файла
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
description="Получение данных о ремонтных работах"
|
description="Получение данных о ремонтных работах"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def determine_getter(self, get_params: dict) -> str:
|
||||||
|
"""Определение геттера для сводки ремонта СА"""
|
||||||
|
# Для svodka_repair_ca всегда используем геттер get_repair_data
|
||||||
|
return 'get_repair_data'
|
||||||
|
|
||||||
def _get_repair_data_wrapper(self, params: dict):
|
def _get_repair_data_wrapper(self, params: dict):
|
||||||
"""Получение данных о ремонтных работах"""
|
"""Получение данных о ремонтных работах"""
|
||||||
logger.debug(f"🔍 _get_repair_data_wrapper вызван с параметрами: {params}")
|
logger.debug(f"🔍 _get_repair_data_wrapper вызван с параметрами: {params}")
|
||||||
|
|||||||
123
python_parser/app/endpoints/README.md
Normal file
123
python_parser/app/endpoints/README.md
Normal 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 эндпоинта (синхронные + асинхронные)
|
||||||
3
python_parser/app/endpoints/__init__.py
Normal file
3
python_parser/app/endpoints/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Модули эндпоинтов FastAPI
|
||||||
|
"""
|
||||||
189
python_parser/app/endpoints/common.py
Normal file
189
python_parser/app/endpoints/common.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
"""
|
||||||
|
Общие эндпоинты 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
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
# Получаем информацию о процессе
|
||||||
|
process = psutil.Process()
|
||||||
|
parent_process = process.parent()
|
||||||
|
|
||||||
|
# Получаем информацию о системе
|
||||||
|
cpu_cores = psutil.cpu_count()
|
||||||
|
memory_info = psutil.virtual_memory()
|
||||||
|
memory_mb = memory_info.total / (1024 * 1024) # Конвертируем в MB
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Svodka Parser API",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"process_id": process.pid,
|
||||||
|
"parent_id": parent_process.pid if parent_process else None,
|
||||||
|
"cpu_cores": cpu_cores,
|
||||||
|
"memory_mb": memory_mb,
|
||||||
|
"python_version": sys.version,
|
||||||
|
"platform": platform.platform(),
|
||||||
|
"available_parsers": list(PARSERS.keys())
|
||||||
|
}
|
||||||
326
python_parser/app/endpoints/monitoring_fuel.py
Normal file
326
python_parser/app/endpoints/monitoring_fuel.py
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
"""
|
||||||
|
Эндпоинты для мониторинга топлива
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
include_in_schema=False,
|
||||||
|
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()
|
||||||
|
)
|
||||||
221
python_parser/app/endpoints/monitoring_tar.py
Normal file
221
python_parser/app/endpoints/monitoring_tar.py
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
"""
|
||||||
|
Эндпоинты для мониторинга ТАР
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
include_in_schema=False,
|
||||||
|
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()
|
||||||
|
)
|
||||||
193
python_parser/app/endpoints/oper_spravka_tech_pos.py
Normal file
193
python_parser/app/endpoints/oper_spravka_tech_pos.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
"""
|
||||||
|
Эндпоинты для оперативной справки техпос
|
||||||
|
"""
|
||||||
|
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="ZIP архив с файлами оперативной справки техпос (.zip)")
|
||||||
|
):
|
||||||
|
"""Загрузка и обработка отчета оперативной справки техпос"""
|
||||||
|
report_service = get_report_service()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not file.filename.endswith('.zip'):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content=UploadErrorResponse(
|
||||||
|
message="Поддерживаются только ZIP архивы (.zip)",
|
||||||
|
error_code="INVALID_FILE_TYPE",
|
||||||
|
details={
|
||||||
|
"expected_formats": [".zip"],
|
||||||
|
"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:
|
||||||
|
# Извлекаем данные из result.data["value"]
|
||||||
|
data = result.data.get("value", []) if isinstance(result.data, dict) else result.data
|
||||||
|
return OperSpravkaTechPosResponse(
|
||||||
|
success=True,
|
||||||
|
data=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,
|
||||||
|
include_in_schema=False,
|
||||||
|
responses={
|
||||||
|
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
|
||||||
|
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
|
||||||
|
},)
|
||||||
|
async def async_upload_oper_spravka_tech_pos(
|
||||||
|
file: UploadFile = File(..., description="ZIP архив с файлами оперативной справки техпос (.zip)")
|
||||||
|
):
|
||||||
|
"""Асинхронная загрузка и обработка отчета оперативной справки техпос"""
|
||||||
|
async_service = get_async_report_service()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not file.filename.endswith('.zip'):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content=UploadErrorResponse(
|
||||||
|
message="Поддерживаются только ZIP архивы (.zip)",
|
||||||
|
error_code="INVALID_FILE_TYPE",
|
||||||
|
details={
|
||||||
|
"expected_formats": [".zip"],
|
||||||
|
"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()
|
||||||
|
)
|
||||||
190
python_parser/app/endpoints/statuses_repair_ca.py
Normal file
190
python_parser/app/endpoints/statuses_repair_ca.py
Normal 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 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,
|
||||||
|
include_in_schema=False,
|
||||||
|
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()
|
||||||
|
)
|
||||||
227
python_parser/app/endpoints/svodka_ca.py
Normal file
227
python_parser/app/endpoints/svodka_ca.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
"""
|
||||||
|
Эндпоинты для сводки СА
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
include_in_schema=False,
|
||||||
|
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()
|
||||||
|
)
|
||||||
320
python_parser/app/endpoints/svodka_pm.py
Normal file
320
python_parser/app/endpoints/svodka_pm.py
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
"""
|
||||||
|
Эндпоинты для сводки ПМ
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
include_in_schema=False,
|
||||||
|
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()
|
||||||
|
)
|
||||||
190
python_parser/app/endpoints/svodka_repair_ca.py
Normal file
190
python_parser/app/endpoints/svodka_repair_ca.py
Normal 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 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,
|
||||||
|
include_in_schema=False,
|
||||||
|
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()
|
||||||
|
)
|
||||||
21
python_parser/app/endpoints/system.py
Normal file
21
python_parser/app/endpoints/system.py
Normal 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
@@ -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',
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
72
python_parser/core/async_services.py
Normal file
72
python_parser/core/async_services.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Асинхронные сервисы для работы с отчетами
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .services import ReportService
|
||||||
|
from .models import UploadRequest, UploadResult, DataRequest, DataResult
|
||||||
|
from .ports import StoragePort
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncReportService:
|
||||||
|
"""Асинхронный сервис для работы с отчетами"""
|
||||||
|
|
||||||
|
def __init__(self, report_service: ReportService):
|
||||||
|
self.report_service = report_service
|
||||||
|
self.executor = ThreadPoolExecutor(max_workers=4)
|
||||||
|
|
||||||
|
async def upload_report_async(self, request: UploadRequest) -> UploadResult:
|
||||||
|
"""Асинхронная загрузка отчета"""
|
||||||
|
try:
|
||||||
|
# Запускаем синхронную обработку в отдельном потоке
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
result = await loop.run_in_executor(
|
||||||
|
self.executor,
|
||||||
|
self._process_upload_sync,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при асинхронной загрузке отчета: {str(e)}")
|
||||||
|
return UploadResult(
|
||||||
|
success=False,
|
||||||
|
message=f"Ошибка при асинхронной загрузке отчета: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_upload_sync(self, request: UploadRequest) -> UploadResult:
|
||||||
|
"""Синхронная обработка загрузки (выполняется в отдельном потоке)"""
|
||||||
|
return self.report_service.upload_report(request)
|
||||||
|
|
||||||
|
async def get_data_async(self, request: DataRequest) -> DataResult:
|
||||||
|
"""Асинхронное получение данных"""
|
||||||
|
try:
|
||||||
|
# Запускаем синхронную обработку в отдельном потоке
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
result = await loop.run_in_executor(
|
||||||
|
self.executor,
|
||||||
|
self._process_get_data_sync,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при асинхронном получении данных: {str(e)}")
|
||||||
|
return DataResult(
|
||||||
|
success=False,
|
||||||
|
message=f"Ошибка при асинхронном получении данных: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_get_data_sync(self, request: DataRequest) -> DataResult:
|
||||||
|
"""Синхронное получение данных (выполняется в отдельном потоке)"""
|
||||||
|
return self.report_service.get_data(request)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""Очистка ресурсов"""
|
||||||
|
if hasattr(self, 'executor'):
|
||||||
|
self.executor.shutdown(wait=False)
|
||||||
@@ -84,6 +84,35 @@ class ParserPort(ABC):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Ошибка выполнения геттера '{getter_name}': {str(e)}")
|
raise ValueError(f"Ошибка выполнения геттера '{getter_name}': {str(e)}")
|
||||||
|
|
||||||
|
def determine_getter(self, get_params: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Определение имени геттера на основе параметров запроса
|
||||||
|
|
||||||
|
Args:
|
||||||
|
get_params: Параметры запроса
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Имя геттера для выполнения
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Если не удается определить геттер
|
||||||
|
"""
|
||||||
|
# По умолчанию используем первый доступный геттер
|
||||||
|
available_getters = list(self.getters.keys())
|
||||||
|
if not available_getters:
|
||||||
|
raise ValueError("Парсер не имеет доступных геттеров")
|
||||||
|
|
||||||
|
# Если указан режим, используем его
|
||||||
|
if 'mode' in get_params:
|
||||||
|
mode = get_params['mode']
|
||||||
|
if mode in self.getters:
|
||||||
|
return mode
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Режим '{mode}' не найден. Доступные: {available_getters}")
|
||||||
|
|
||||||
|
# Иначе используем первый доступный
|
||||||
|
return available_getters[0]
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||||
"""Парсинг файла и возврат DataFrame"""
|
"""Парсинг файла и возврат DataFrame"""
|
||||||
|
|||||||
@@ -122,82 +122,13 @@ class ReportService:
|
|||||||
# Получаем параметры запроса
|
# Получаем параметры запроса
|
||||||
get_params = request.get_params or {}
|
get_params = request.get_params or {}
|
||||||
|
|
||||||
# Для svodka_ca определяем режим из данных или используем 'fact' по умолчанию
|
# Определяем имя геттера через парсер (делегируем логику в адаптер)
|
||||||
if request.report_type == 'svodka_ca':
|
try:
|
||||||
# Извлекаем режим из DataFrame или используем 'fact' по умолчанию
|
getter_name = parser.determine_getter(get_params)
|
||||||
if hasattr(parser, 'df') and parser.df is not None and not parser.df.empty:
|
except ValueError as e:
|
||||||
modes_in_df = parser.df['mode'].unique() if 'mode' in parser.df.columns else ['fact']
|
|
||||||
# Используем первый найденный режим или 'fact' по умолчанию
|
|
||||||
default_mode = modes_in_df[0] if len(modes_in_df) > 0 else 'fact'
|
|
||||||
else:
|
|
||||||
default_mode = 'fact'
|
|
||||||
|
|
||||||
# Устанавливаем режим в параметры, если он не указан
|
|
||||||
if 'mode' not in get_params:
|
|
||||||
get_params['mode'] = default_mode
|
|
||||||
|
|
||||||
# Определяем имя геттера
|
|
||||||
if request.report_type == 'svodka_ca':
|
|
||||||
# Для svodka_ca используем геттер get_ca_data
|
|
||||||
getter_name = 'get_ca_data'
|
|
||||||
elif request.report_type == 'svodka_repair_ca':
|
|
||||||
# Для svodka_repair_ca используем геттер get_repair_data
|
|
||||||
getter_name = 'get_repair_data'
|
|
||||||
elif request.report_type == 'statuses_repair_ca':
|
|
||||||
# Для statuses_repair_ca используем геттер get_repair_statuses
|
|
||||||
getter_name = 'get_repair_statuses'
|
|
||||||
elif request.report_type == 'monitoring_tar':
|
|
||||||
# Для monitoring_tar определяем геттер по параметрам
|
|
||||||
if 'mode' in get_params:
|
|
||||||
# Если есть параметр mode, используем get_tar_data
|
|
||||||
getter_name = 'get_tar_data'
|
|
||||||
else:
|
|
||||||
# Если нет параметра mode, используем get_tar_full_data
|
|
||||||
getter_name = 'get_tar_full_data'
|
|
||||||
elif request.report_type == 'monitoring_fuel':
|
|
||||||
# Для monitoring_fuel определяем геттер из параметра mode
|
|
||||||
getter_name = get_params.pop("mode", None)
|
|
||||||
if not getter_name:
|
|
||||||
# Если режим не указан, берем первый доступный
|
|
||||||
available_getters = list(parser.getters.keys())
|
|
||||||
if available_getters:
|
|
||||||
getter_name = available_getters[0]
|
|
||||||
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
|
||||||
else:
|
|
||||||
return DataResult(
|
return DataResult(
|
||||||
success=False,
|
success=False,
|
||||||
message="Парсер не имеет доступных геттеров"
|
message=str(e)
|
||||||
)
|
|
||||||
elif request.report_type == 'svodka_pm':
|
|
||||||
# Для svodka_pm определяем геттер из параметра mode
|
|
||||||
getter_name = get_params.pop("mode", None)
|
|
||||||
if not getter_name:
|
|
||||||
# Если режим не указан, берем первый доступный
|
|
||||||
available_getters = list(parser.getters.keys())
|
|
||||||
if available_getters:
|
|
||||||
getter_name = available_getters[0]
|
|
||||||
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
|
||||||
else:
|
|
||||||
return DataResult(
|
|
||||||
success=False,
|
|
||||||
message="Парсер не имеет доступных геттеров"
|
|
||||||
)
|
|
||||||
elif request.report_type == 'oper_spravka_tech_pos':
|
|
||||||
# Для oper_spravka_tech_pos используем геттер get_tech_pos
|
|
||||||
getter_name = 'get_tech_pos'
|
|
||||||
else:
|
|
||||||
# Для других парсеров определяем из параметра mode
|
|
||||||
getter_name = get_params.pop("mode", None)
|
|
||||||
if not getter_name:
|
|
||||||
# Если режим не указан, берем первый доступный
|
|
||||||
available_getters = list(parser.getters.keys())
|
|
||||||
if available_getters:
|
|
||||||
getter_name = available_getters[0]
|
|
||||||
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
|
||||||
else:
|
|
||||||
return DataResult(
|
|
||||||
success=False,
|
|
||||||
message="Парсер не имеет доступных геттеров"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Получаем значение через указанный геттер
|
# Получаем значение через указанный геттер
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ requests>=2.31.0
|
|||||||
# pytest-mock>=3.10.0
|
# pytest-mock>=3.10.0
|
||||||
httpx>=0.24.0
|
httpx>=0.24.0
|
||||||
numpy
|
numpy
|
||||||
|
psutil>=5.9.0
|
||||||
33
run_tests.py
Normal file
33
run_tests.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Скрипт для запуска тестов парсеров
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def run_tests():
|
||||||
|
"""Запуск тестов"""
|
||||||
|
print(" Запуск тестов парсеров...")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Переходим в директорию проекта
|
||||||
|
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# Запускаем pytest
|
||||||
|
cmd = [sys.executable, "-m", "pytest", "tests/", "-v", "--tb=short"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||||
|
print(result.stdout)
|
||||||
|
print(" Все тесты прошли успешно!")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(" Некоторые тесты не прошли:")
|
||||||
|
print(e.stdout)
|
||||||
|
print(e.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = run_tests()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
@@ -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")
|
|
||||||
87
streamlit_app/api_client.py
Normal file
87
streamlit_app/api_client.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"""
|
||||||
|
Модуль для работы с API
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
from typing import Dict, Any, List, Tuple
|
||||||
|
from config import API_BASE_URL, API_PUBLIC_URL
|
||||||
|
|
||||||
|
|
||||||
|
def check_api_health() -> bool:
|
||||||
|
"""Проверка доступности API"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_BASE_URL}/", timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_parsers() -> List[str]:
|
||||||
|
"""Получение списка доступных парсеров"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_BASE_URL}/parsers")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()["parsers"]
|
||||||
|
return []
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_server_info() -> Dict[str, Any]:
|
||||||
|
"""Получение информации о сервере"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_BASE_URL}/server-info")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
return {}
|
||||||
|
except:
|
||||||
|
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:
|
||||||
|
# Определяем правильное имя поля в зависимости от эндпоинта
|
||||||
|
if "zip" in endpoint:
|
||||||
|
files = {"zip_file": (filename, file_data, "application/zip")}
|
||||||
|
else:
|
||||||
|
files = {"file": (filename, file_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
|
||||||
|
|
||||||
|
response = requests.post(f"{API_BASE_URL}{endpoint}", files=files)
|
||||||
|
return response.json(), response.status_code
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}, 500
|
||||||
|
|
||||||
|
|
||||||
|
def make_api_request(endpoint: str, data: Dict[str, Any]) -> Tuple[Dict[str, Any], int]:
|
||||||
|
"""Выполнение API запроса"""
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{API_BASE_URL}{endpoint}", json=data)
|
||||||
|
return response.json(), response.status_code
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}, 500
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_ogs(parser_name: str) -> List[str]:
|
||||||
|
"""Получение доступных ОГ для парсера"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/available_ogs")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
return data.get("available_ogs", [])
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Ошибка получения ОГ: {response.status_code}")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Ошибка при запросе ОГ: {e}")
|
||||||
|
return []
|
||||||
160
streamlit_app/async_upload_page.py
Normal file
160
streamlit_app/async_upload_page.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
"""
|
||||||
|
Страница асинхронной загрузки файлов
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from api_client import upload_file_to_api
|
||||||
|
from config import PARSER_TABS
|
||||||
|
|
||||||
|
# Глобальное хранилище задач (в реальном приложении лучше использовать Redis или БД)
|
||||||
|
TASKS_STORAGE = {}
|
||||||
|
|
||||||
|
|
||||||
|
def upload_file_async_background(endpoint, file_data, filename, task_id):
|
||||||
|
"""Асинхронная загрузка файла в фоновом режиме"""
|
||||||
|
global TASKS_STORAGE
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Обновляем статус на "running"
|
||||||
|
TASKS_STORAGE[task_id] = {
|
||||||
|
'status': 'running',
|
||||||
|
'filename': filename,
|
||||||
|
'endpoint': endpoint,
|
||||||
|
'started_at': time.time(),
|
||||||
|
'progress': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Имитируем асинхронную работу
|
||||||
|
time.sleep(1) # Небольшая задержка для демонстрации
|
||||||
|
|
||||||
|
# Выполняем загрузку
|
||||||
|
result, status = upload_file_to_api(endpoint, file_data, filename)
|
||||||
|
|
||||||
|
# Сохраняем результат в глобальном хранилище
|
||||||
|
TASKS_STORAGE[task_id] = {
|
||||||
|
'status': 'completed' if status == 200 else 'failed',
|
||||||
|
'result': result,
|
||||||
|
'status_code': status,
|
||||||
|
'filename': filename,
|
||||||
|
'endpoint': endpoint,
|
||||||
|
'started_at': TASKS_STORAGE.get(task_id, {}).get('started_at', time.time()),
|
||||||
|
'completed_at': time.time(),
|
||||||
|
'progress': 100
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Сохраняем ошибку
|
||||||
|
TASKS_STORAGE[task_id] = {
|
||||||
|
'status': 'failed',
|
||||||
|
'error': str(e),
|
||||||
|
'filename': filename,
|
||||||
|
'endpoint': endpoint,
|
||||||
|
'started_at': TASKS_STORAGE.get(task_id, {}).get('started_at', time.time()),
|
||||||
|
'completed_at': time.time(),
|
||||||
|
'progress': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render_async_upload_page():
|
||||||
|
"""Рендер страницы асинхронной загрузки"""
|
||||||
|
st.title("🚀 Асинхронная загрузка файлов")
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
st.info("""
|
||||||
|
**Асинхронная загрузка** позволяет загружать файлы без блокировки интерфейса.
|
||||||
|
После загрузки файл будет обработан в фоновом режиме, а вы сможете отслеживать прогресс на странице "Управление задачами".
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Выбор парсера
|
||||||
|
st.subheader("📋 Выбор парсера")
|
||||||
|
|
||||||
|
# Создаем словарь парсеров с их асинхронными эндпоинтами
|
||||||
|
parser_endpoints = {
|
||||||
|
"Сводки ПМ": "/async/svodka_pm/upload-zip",
|
||||||
|
"Сводки СА": "/async/svodka_ca/upload",
|
||||||
|
"Мониторинг топлива": "/async/monitoring_fuel/upload-zip",
|
||||||
|
"Ремонт СА": "/svodka_repair_ca/upload", # Пока синхронный
|
||||||
|
"Статусы ремонта СА": "/statuses_repair_ca/upload", # Пока синхронный
|
||||||
|
"Мониторинг ТЭР": "/monitoring_tar/upload", # Пока синхронный
|
||||||
|
"Операционные справки": "/oper_spravka_tech_pos/upload" # Пока синхронный
|
||||||
|
}
|
||||||
|
|
||||||
|
selected_parser = st.selectbox(
|
||||||
|
"Выберите тип парсера для загрузки:",
|
||||||
|
list(parser_endpoints.keys()),
|
||||||
|
key="async_parser_select"
|
||||||
|
)
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Загрузка файла
|
||||||
|
st.subheader("📤 Загрузка файла")
|
||||||
|
|
||||||
|
uploaded_file = st.file_uploader(
|
||||||
|
f"Выберите ZIP архив для парсера '{selected_parser}'",
|
||||||
|
type=['zip'],
|
||||||
|
key="async_file_upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_file is not None:
|
||||||
|
st.success(f"✅ Файл выбран: {uploaded_file.name}")
|
||||||
|
st.info(f"📊 Размер файла: {uploaded_file.size / 1024 / 1024:.2f} MB")
|
||||||
|
|
||||||
|
if st.button("🚀 Загрузить асинхронно", key="async_upload_btn", use_container_width=True):
|
||||||
|
# Создаем уникальный ID задачи
|
||||||
|
task_id = f"task_{int(time.time())}_{uploaded_file.name}"
|
||||||
|
|
||||||
|
# Показываем сообщение о создании задачи
|
||||||
|
st.success("✅ Задача загрузки создана!")
|
||||||
|
st.info(f"ID задачи: `{task_id}`")
|
||||||
|
st.info("📋 Перейдите на страницу 'Управление задачами' для отслеживания прогресса")
|
||||||
|
|
||||||
|
# Запускаем загрузку в фоновом потоке
|
||||||
|
endpoint = parser_endpoints[selected_parser]
|
||||||
|
file_data = uploaded_file.read()
|
||||||
|
|
||||||
|
# Создаем поток для асинхронной загрузки
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=upload_file_async_background,
|
||||||
|
args=(endpoint, file_data, uploaded_file.name, task_id)
|
||||||
|
)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
# Автоматически переключаемся на страницу задач
|
||||||
|
st.session_state.sidebar_tasks_clicked = True
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Информация о поддерживаемых форматах
|
||||||
|
with st.expander("ℹ️ Поддерживаемые форматы файлов"):
|
||||||
|
st.markdown("""
|
||||||
|
**Поддерживаемые форматы:**
|
||||||
|
- 📦 ZIP архивы с Excel файлами
|
||||||
|
- 📊 Excel файлы (.xlsx, .xls)
|
||||||
|
- 📋 CSV файлы (для некоторых парсеров)
|
||||||
|
|
||||||
|
**Ограничения:**
|
||||||
|
- Максимальный размер файла: 100 MB
|
||||||
|
- Количество файлов в архиве: до 50
|
||||||
|
- Поддерживаемые кодировки: UTF-8, Windows-1251
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Статистика загрузок
|
||||||
|
st.subheader("📈 Статистика загрузок")
|
||||||
|
|
||||||
|
col1, col2, col3 = st.columns(3)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.metric("Всего загружено", "0", "0")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.metric("В обработке", "0", "0")
|
||||||
|
|
||||||
|
with col3:
|
||||||
|
st.metric("Завершено", "0", "0")
|
||||||
58
streamlit_app/config.py
Normal file
58
streamlit_app/config.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""
|
||||||
|
Конфигурация приложения
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Конфигурация API
|
||||||
|
API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker
|
||||||
|
API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя
|
||||||
|
|
||||||
|
# Конфигурация страницы
|
||||||
|
def setup_page_config():
|
||||||
|
"""Настройка конфигурации страницы Streamlit"""
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="NIN Excel Parsers API Demo",
|
||||||
|
page_icon="📊",
|
||||||
|
layout="wide",
|
||||||
|
initial_sidebar_state="expanded"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Константы для парсеров
|
||||||
|
PARSER_TABS = [
|
||||||
|
"📊 Сводки ПМ",
|
||||||
|
"🏭 Сводки СА",
|
||||||
|
"⛽ Мониторинг топлива",
|
||||||
|
"🔧 Ремонт СА",
|
||||||
|
"📋 Статусы ремонта СА",
|
||||||
|
"⚡ Мониторинг ТЭР",
|
||||||
|
"🏭 Операционные справки"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Константы для ОГ
|
||||||
|
DEFAULT_OGS = [
|
||||||
|
"SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV",
|
||||||
|
"NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK",
|
||||||
|
"NVNPO", "KLNPZ", "PurNP", "YANOS"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Константы для кодов строк ПМ
|
||||||
|
PM_CODES = [78, 79, 394, 395, 396, 397, 81, 82, 83, 84]
|
||||||
|
|
||||||
|
# Константы для столбцов ПМ
|
||||||
|
PM_COLUMNS = ["БП", "ПП", "СЭБ", "Факт", "План"]
|
||||||
|
|
||||||
|
# Константы для режимов СА
|
||||||
|
CA_MODES = ["plan", "fact", "normativ"]
|
||||||
|
|
||||||
|
# Константы для таблиц СА
|
||||||
|
CA_TABLES = ["ТиП", "Топливо", "Потери"]
|
||||||
|
|
||||||
|
# Константы для столбцов мониторинга топлива
|
||||||
|
FUEL_COLUMNS = ["normativ", "total", "total_1"]
|
||||||
|
|
||||||
|
# Константы для типов ремонта
|
||||||
|
REPAIR_TYPES = ["КР", "КП", "ТР"]
|
||||||
|
|
||||||
|
# Константы для режимов мониторинга ТЭР
|
||||||
|
TAR_MODES = ["all", "total", "last_day"]
|
||||||
3
streamlit_app/parsers_ui/__init__.py
Normal file
3
streamlit_app/parsers_ui/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
UI модули для парсеров
|
||||||
|
"""
|
||||||
162
streamlit_app/parsers_ui/monitoring_fuel_ui.py
Normal file
162
streamlit_app/parsers_ui/monitoring_fuel_ui.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def render_monitoring_fuel_tab():
|
||||||
|
"""Рендер вкладки мониторинга топлива"""
|
||||||
|
st.header("⛽ Мониторинг топлива - Полный функционал")
|
||||||
|
|
||||||
|
# Секция загрузки файлов
|
||||||
|
st.subheader("📤 Загрузка файлов")
|
||||||
|
uploaded_fuel = st.file_uploader(
|
||||||
|
"Выберите ZIP архив с мониторингом топлива",
|
||||||
|
type=['zip'],
|
||||||
|
key="fuel_upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_fuel is not None:
|
||||||
|
if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"):
|
||||||
|
with st.spinner("Загружаю файл..."):
|
||||||
|
result, status = upload_file_to_api(
|
||||||
|
"/monitoring_fuel/upload-zip",
|
||||||
|
uploaded_fuel.read(),
|
||||||
|
uploaded_fuel.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
st.success(f"✅ {result.get('message', 'Файл загружен')}")
|
||||||
|
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Секция получения данных
|
||||||
|
st.subheader("🔍 Получение данных")
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.subheader("Агрегация по колонкам")
|
||||||
|
|
||||||
|
columns_fuel = st.multiselect(
|
||||||
|
"Выберите столбцы",
|
||||||
|
FUEL_COLUMNS,
|
||||||
|
default=["normativ", "total"],
|
||||||
|
key="fuel_columns"
|
||||||
|
)
|
||||||
|
|
||||||
|
if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"):
|
||||||
|
if columns_fuel:
|
||||||
|
with st.spinner("Получаю данные..."):
|
||||||
|
data = {
|
||||||
|
"columns": columns_fuel
|
||||||
|
}
|
||||||
|
|
||||||
|
result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
st.success("✅ Данные получены")
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
|
else:
|
||||||
|
st.warning("⚠️ Выберите столбцы")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.subheader("Данные за месяц")
|
||||||
|
|
||||||
|
month = st.selectbox(
|
||||||
|
"Выберите месяц",
|
||||||
|
[f"{i:02d}" for i in range(1, 13)],
|
||||||
|
key="fuel_month"
|
||||||
|
)
|
||||||
|
|
||||||
|
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
|
||||||
|
with st.spinner("Получаю данные..."):
|
||||||
|
data = {
|
||||||
|
"month": month
|
||||||
|
}
|
||||||
|
|
||||||
|
result, status = make_api_request("/monitoring_fuel/get_month_by_code", data)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
st.success("✅ Данные получены")
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
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, ...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
""")
|
||||||
108
streamlit_app/parsers_ui/monitoring_tar_ui.py
Normal file
108
streamlit_app/parsers_ui/monitoring_tar_ui.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
UI модуль для мониторинга ТЭР
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
from api_client import upload_file_to_api, make_api_request
|
||||||
|
from config import TAR_MODES
|
||||||
|
|
||||||
|
|
||||||
|
def render_monitoring_tar_tab():
|
||||||
|
"""Рендер вкладки мониторинга ТЭР"""
|
||||||
|
st.header("⚡ Мониторинг ТЭР (Топливно-энергетических ресурсов)")
|
||||||
|
|
||||||
|
# Секция загрузки файлов
|
||||||
|
st.subheader("📤 Загрузка файлов")
|
||||||
|
uploaded_file = st.file_uploader(
|
||||||
|
"Выберите ZIP архив с файлами мониторинга ТЭР",
|
||||||
|
type=['zip'],
|
||||||
|
key="monitoring_tar_upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_file is not None:
|
||||||
|
if st.button("📤 Загрузить файл", key="monitoring_tar_upload_btn"):
|
||||||
|
with st.spinner("Загружаем файл..."):
|
||||||
|
file_data = uploaded_file.read()
|
||||||
|
result, status_code = upload_file_to_api("/monitoring_tar/upload", file_data, uploaded_file.name)
|
||||||
|
|
||||||
|
if status_code == 200:
|
||||||
|
st.success("✅ Файл успешно загружен!")
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка загрузки: {result}")
|
||||||
|
|
||||||
|
# Секция получения данных
|
||||||
|
st.subheader("📊 Получение данных")
|
||||||
|
|
||||||
|
# Выбор формата отображения
|
||||||
|
display_format = st.radio(
|
||||||
|
"Формат отображения:",
|
||||||
|
["JSON", "Таблица"],
|
||||||
|
key="monitoring_tar_display_format",
|
||||||
|
horizontal=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Выбор режима данных
|
||||||
|
mode = st.selectbox(
|
||||||
|
"Выберите режим данных:",
|
||||||
|
TAR_MODES,
|
||||||
|
help="total - строки 'Всего' (агрегированные данные), last_day - последние строки данных, all - все данные",
|
||||||
|
key="monitoring_tar_mode"
|
||||||
|
)
|
||||||
|
|
||||||
|
if st.button("📊 Получить данные", key="monitoring_tar_get_data_btn"):
|
||||||
|
with st.spinner("Получаем данные..."):
|
||||||
|
# Выбираем эндпоинт в зависимости от режима
|
||||||
|
if mode == "all":
|
||||||
|
# Используем полный эндпоинт
|
||||||
|
result, status_code = make_api_request("/monitoring_tar/get_full_data", {})
|
||||||
|
else:
|
||||||
|
# Используем фильтрованный эндпоинт
|
||||||
|
request_data = {"mode": mode}
|
||||||
|
result, status_code = make_api_request("/monitoring_tar/get_data", request_data)
|
||||||
|
|
||||||
|
if status_code == 200 and result.get("success"):
|
||||||
|
st.success("✅ Данные успешно получены!")
|
||||||
|
|
||||||
|
# Показываем данные
|
||||||
|
data = result.get("data", {}).get("value", {})
|
||||||
|
if data:
|
||||||
|
st.subheader("📋 Результат:")
|
||||||
|
|
||||||
|
# Парсим данные, если они пришли как строка
|
||||||
|
if isinstance(data, str):
|
||||||
|
try:
|
||||||
|
data = json.loads(data)
|
||||||
|
st.write("✅ JSON успешно распарсен")
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
st.error(f"❌ Ошибка при парсинге JSON данных: {e}")
|
||||||
|
st.write("Сырые данные:", data)
|
||||||
|
return
|
||||||
|
|
||||||
|
if display_format == "JSON":
|
||||||
|
# Отображаем как JSON
|
||||||
|
st.json(data)
|
||||||
|
else:
|
||||||
|
# Отображаем как таблицы
|
||||||
|
if isinstance(data, dict):
|
||||||
|
# Показываем данные по установкам
|
||||||
|
for installation_id, installation_data in data.items():
|
||||||
|
with st.expander(f"🏭 {installation_id}"):
|
||||||
|
if isinstance(installation_data, dict):
|
||||||
|
# Показываем структуру данных
|
||||||
|
for data_type, type_data in installation_data.items():
|
||||||
|
st.write(f"**{data_type}:**")
|
||||||
|
if isinstance(type_data, list) and type_data:
|
||||||
|
df = pd.DataFrame(type_data)
|
||||||
|
st.dataframe(df)
|
||||||
|
else:
|
||||||
|
st.write("Нет данных")
|
||||||
|
else:
|
||||||
|
st.write("Нет данных")
|
||||||
|
else:
|
||||||
|
st.json(data)
|
||||||
|
else:
|
||||||
|
st.info("📋 Нет данных для отображения")
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
84
streamlit_app/parsers_ui/oper_spravka_tech_pos_ui.py
Normal file
84
streamlit_app/parsers_ui/oper_spravka_tech_pos_ui.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"""
|
||||||
|
UI модуль для операционных справок технологических позиций
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
import pandas as pd
|
||||||
|
from api_client import upload_file_to_api, make_api_request, get_available_ogs
|
||||||
|
|
||||||
|
|
||||||
|
def render_oper_spravka_tech_pos_tab():
|
||||||
|
"""Рендер вкладки операционных справок технологических позиций"""
|
||||||
|
st.header("🏭 Операционные справки технологических позиций")
|
||||||
|
|
||||||
|
# Секция загрузки файлов
|
||||||
|
st.subheader("📤 Загрузка файлов")
|
||||||
|
|
||||||
|
uploaded_file = st.file_uploader(
|
||||||
|
"Выберите ZIP архив с файлами операционных справок",
|
||||||
|
type=['zip'],
|
||||||
|
key="oper_spravka_tech_pos_upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_file is not None:
|
||||||
|
if st.button("📤 Загрузить файл", key="oper_spravka_tech_pos_upload_btn"):
|
||||||
|
with st.spinner("Загружаем файл..."):
|
||||||
|
file_data = uploaded_file.read()
|
||||||
|
result, status_code = upload_file_to_api("/oper_spravka_tech_pos/upload", file_data, uploaded_file.name)
|
||||||
|
|
||||||
|
if status_code == 200:
|
||||||
|
st.success("✅ Файл успешно загружен!")
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка загрузки: {result}")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Секция получения данных
|
||||||
|
st.subheader("📊 Получение данных")
|
||||||
|
|
||||||
|
# Выбор формата отображения
|
||||||
|
display_format = st.radio(
|
||||||
|
"Формат отображения:",
|
||||||
|
["JSON", "Таблица"],
|
||||||
|
key="oper_spravka_tech_pos_display_format",
|
||||||
|
horizontal=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Получаем доступные ОГ динамически
|
||||||
|
available_ogs = get_available_ogs("oper_spravka_tech_pos")
|
||||||
|
|
||||||
|
# Выбор ОГ
|
||||||
|
og_id = st.selectbox(
|
||||||
|
"Выберите ОГ:",
|
||||||
|
available_ogs if available_ogs else ["SNPZ", "KNPZ", "ANHK", "BASH", "UNH", "NOV"],
|
||||||
|
key="oper_spravka_tech_pos_og_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
if st.button("📊 Получить данные", key="oper_spravka_tech_pos_get_data_btn"):
|
||||||
|
with st.spinner("Получаем данные..."):
|
||||||
|
request_data = {"id": og_id}
|
||||||
|
result, status_code = make_api_request("/oper_spravka_tech_pos/get_data", request_data)
|
||||||
|
|
||||||
|
if status_code == 200 and result.get("success"):
|
||||||
|
st.success("✅ Данные успешно получены!")
|
||||||
|
|
||||||
|
# Показываем данные
|
||||||
|
data = result.get("data", [])
|
||||||
|
|
||||||
|
if data and len(data) > 0:
|
||||||
|
st.subheader("📋 Результат:")
|
||||||
|
|
||||||
|
if display_format == "JSON":
|
||||||
|
# Отображаем как JSON
|
||||||
|
st.json(data)
|
||||||
|
else:
|
||||||
|
# Отображаем как таблицу
|
||||||
|
if isinstance(data, list) and data:
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
st.dataframe(df, use_container_width=True)
|
||||||
|
else:
|
||||||
|
st.write("Нет данных")
|
||||||
|
else:
|
||||||
|
st.info("📋 Нет данных для отображения")
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
147
streamlit_app/parsers_ui/statuses_repair_ca_ui.py
Normal file
147
streamlit_app/parsers_ui/statuses_repair_ca_ui.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"""
|
||||||
|
UI модуль для статусов ремонта СА
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
import pandas as pd
|
||||||
|
from api_client import upload_file_to_api, make_api_request, get_available_ogs, get_system_ogs
|
||||||
|
|
||||||
|
|
||||||
|
def render_statuses_repair_ca_tab():
|
||||||
|
"""Рендер вкладки статусов ремонта СА"""
|
||||||
|
st.header("📋 Статусы ремонта СА")
|
||||||
|
|
||||||
|
# Секция загрузки файлов
|
||||||
|
st.subheader("📤 Загрузка файлов")
|
||||||
|
uploaded_file = st.file_uploader(
|
||||||
|
"Выберите файл статусов ремонта СА",
|
||||||
|
type=['xlsx', 'xlsm', 'xls', 'zip'],
|
||||||
|
key="statuses_repair_ca_upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_file is not None:
|
||||||
|
if st.button("📤 Загрузить файл", key="statuses_repair_ca_upload_btn"):
|
||||||
|
with st.spinner("Загружаем файл..."):
|
||||||
|
file_data = uploaded_file.read()
|
||||||
|
result, status_code = upload_file_to_api("/statuses_repair_ca/upload", file_data, uploaded_file.name)
|
||||||
|
|
||||||
|
if status_code == 200:
|
||||||
|
st.success("✅ Файл успешно загружен!")
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка загрузки: {result}")
|
||||||
|
|
||||||
|
# Секция получения данных
|
||||||
|
st.subheader("📊 Получение данных")
|
||||||
|
|
||||||
|
# Получаем доступные ОГ из системного API
|
||||||
|
system_ogs = get_system_ogs()
|
||||||
|
available_ogs = system_ogs.get("single_ogs", [])
|
||||||
|
|
||||||
|
# Фильтр по ОГ
|
||||||
|
og_ids = st.multiselect(
|
||||||
|
"Выберите ОГ (оставьте пустым для всех)",
|
||||||
|
available_ogs if available_ogs else get_available_ogs(), # fallback
|
||||||
|
key="statuses_repair_ca_og_ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Предустановленные ключи для извлечения
|
||||||
|
st.subheader("🔑 Ключи для извлечения данных")
|
||||||
|
|
||||||
|
# Основные ключи
|
||||||
|
include_basic_keys = st.checkbox("Основные данные", value=True, key="statuses_basic_keys")
|
||||||
|
include_readiness_keys = st.checkbox("Готовность к КР", value=True, key="statuses_readiness_keys")
|
||||||
|
include_contract_keys = st.checkbox("Заключение договоров", value=True, key="statuses_contract_keys")
|
||||||
|
include_supply_keys = st.checkbox("Поставка МТР", value=True, key="statuses_supply_keys")
|
||||||
|
|
||||||
|
# Формируем ключи на основе выбора
|
||||||
|
keys = []
|
||||||
|
if include_basic_keys:
|
||||||
|
keys.append(["Дата начала ремонта"])
|
||||||
|
keys.append(["Отставание / опережение подготовки к КР", "Отставание / опережение"])
|
||||||
|
keys.append(["Отставание / опережение подготовки к КР", "Динамика за прошедшую неделю"])
|
||||||
|
|
||||||
|
if include_readiness_keys:
|
||||||
|
keys.append(["Готовность к КР", "Факт"])
|
||||||
|
|
||||||
|
if include_contract_keys:
|
||||||
|
keys.append(["Заключение договоров на СМР", "Договор", "%"])
|
||||||
|
|
||||||
|
if include_supply_keys:
|
||||||
|
keys.append(["Поставка МТР", "На складе, позиций", "%"])
|
||||||
|
|
||||||
|
# Кнопка получения данных
|
||||||
|
if st.button("📊 Получить данные", key="statuses_repair_ca_get_data_btn"):
|
||||||
|
if not keys:
|
||||||
|
st.warning("⚠️ Выберите хотя бы одну группу ключей для извлечения")
|
||||||
|
else:
|
||||||
|
with st.spinner("Получаем данные..."):
|
||||||
|
request_data = {
|
||||||
|
"ids": og_ids if og_ids else None,
|
||||||
|
"keys": keys
|
||||||
|
}
|
||||||
|
|
||||||
|
result, status_code = make_api_request("/statuses_repair_ca/get_data", request_data)
|
||||||
|
|
||||||
|
if status_code == 200 and result.get("success"):
|
||||||
|
st.success("✅ Данные успешно получены!")
|
||||||
|
|
||||||
|
data = result.get("data", {}).get("value", [])
|
||||||
|
if data:
|
||||||
|
# Отображаем данные в виде таблицы
|
||||||
|
if isinstance(data, list) and len(data) > 0:
|
||||||
|
# Преобразуем в DataFrame для лучшего отображения
|
||||||
|
df_data = []
|
||||||
|
for item in data:
|
||||||
|
row = {
|
||||||
|
"ID": item.get("id", ""),
|
||||||
|
"Название": item.get("name", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавляем основные поля
|
||||||
|
if "Дата начала ремонта" in item:
|
||||||
|
row["Дата начала ремонта"] = item["Дата начала ремонта"]
|
||||||
|
|
||||||
|
# Добавляем готовность к КР
|
||||||
|
if "Готовность к КР" in item:
|
||||||
|
readiness = item["Готовность к КР"]
|
||||||
|
if isinstance(readiness, dict) and "Факт" in readiness:
|
||||||
|
row["Готовность к КР (Факт)"] = readiness["Факт"]
|
||||||
|
|
||||||
|
# Добавляем отставание/опережение
|
||||||
|
if "Отставание / опережение подготовки к КР" in item:
|
||||||
|
delay = item["Отставание / опережение подготовки к КР"]
|
||||||
|
if isinstance(delay, dict):
|
||||||
|
if "Отставание / опережение" in delay:
|
||||||
|
row["Отставание/опережение"] = delay["Отставание / опережение"]
|
||||||
|
if "Динамика за прошедшую неделю" in delay:
|
||||||
|
row["Динамика за неделю"] = delay["Динамика за прошедшую неделю"]
|
||||||
|
|
||||||
|
# Добавляем договоры
|
||||||
|
if "Заключение договоров на СМР" in item:
|
||||||
|
contracts = item["Заключение договоров на СМР"]
|
||||||
|
if isinstance(contracts, dict) and "Договор" in contracts:
|
||||||
|
contract = contracts["Договор"]
|
||||||
|
if isinstance(contract, dict) and "%" in contract:
|
||||||
|
row["Договоры (%)"] = contract["%"]
|
||||||
|
|
||||||
|
# Добавляем поставки МТР
|
||||||
|
if "Поставка МТР" in item:
|
||||||
|
supply = item["Поставка МТР"]
|
||||||
|
if isinstance(supply, dict) and "На складе, позиций" in supply:
|
||||||
|
warehouse = supply["На складе, позиций"]
|
||||||
|
if isinstance(warehouse, dict) and "%" in warehouse:
|
||||||
|
row["МТР на складе (%)"] = warehouse["%"]
|
||||||
|
|
||||||
|
df_data.append(row)
|
||||||
|
|
||||||
|
if df_data:
|
||||||
|
df = pd.DataFrame(df_data)
|
||||||
|
st.dataframe(df, use_container_width=True)
|
||||||
|
else:
|
||||||
|
st.info("📋 Нет данных для отображения")
|
||||||
|
else:
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.info("📋 Нет данных для отображения")
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
80
streamlit_app/parsers_ui/svodka_ca_ui.py
Normal file
80
streamlit_app/parsers_ui/svodka_ca_ui.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"""
|
||||||
|
UI модуль для парсера сводок СА
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
import requests
|
||||||
|
from api_client import make_api_request, API_BASE_URL
|
||||||
|
from config import CA_MODES, CA_TABLES
|
||||||
|
|
||||||
|
|
||||||
|
def render_svodka_ca_tab():
|
||||||
|
"""Рендер вкладки сводок СА"""
|
||||||
|
st.header("🏭 Сводки СА - Полный функционал")
|
||||||
|
|
||||||
|
# Секция загрузки файлов
|
||||||
|
st.subheader("📤 Загрузка файлов")
|
||||||
|
uploaded_ca = st.file_uploader(
|
||||||
|
"Выберите Excel файл сводки СА",
|
||||||
|
type=['xlsx', 'xlsm', 'xls'],
|
||||||
|
key="ca_upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_ca is not None:
|
||||||
|
if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"):
|
||||||
|
with st.spinner("Загружаю файл..."):
|
||||||
|
try:
|
||||||
|
files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
|
||||||
|
response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files)
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
st.success(f"✅ {result.get('message', 'Файл загружен')}")
|
||||||
|
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"❌ Ошибка: {str(e)}")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Секция получения данных
|
||||||
|
st.subheader("🔍 Получение данных")
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.subheader("Параметры запроса")
|
||||||
|
|
||||||
|
modes = st.multiselect(
|
||||||
|
"Выберите режимы",
|
||||||
|
CA_MODES,
|
||||||
|
default=["plan", "fact"],
|
||||||
|
key="ca_modes"
|
||||||
|
)
|
||||||
|
|
||||||
|
tables = st.multiselect(
|
||||||
|
"Выберите таблицы",
|
||||||
|
CA_TABLES,
|
||||||
|
default=["ТиП", "Топливо"],
|
||||||
|
key="ca_tables"
|
||||||
|
)
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.subheader("Результат")
|
||||||
|
if st.button("🔍 Получить данные СА", key="ca_btn"):
|
||||||
|
if modes and tables:
|
||||||
|
with st.spinner("Получаю данные..."):
|
||||||
|
data = {
|
||||||
|
"modes": modes,
|
||||||
|
"tables": tables
|
||||||
|
}
|
||||||
|
|
||||||
|
result, status = make_api_request("/svodka_ca/get_data", data)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
st.success("✅ Данные получены")
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
|
else:
|
||||||
|
st.warning("⚠️ Выберите режимы и таблицы")
|
||||||
118
streamlit_app/parsers_ui/svodka_pm_ui.py
Normal file
118
streamlit_app/parsers_ui/svodka_pm_ui.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
UI модуль для парсера сводок ПМ
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
from api_client import upload_file_to_api, make_api_request
|
||||||
|
from config import PM_CODES, PM_COLUMNS, DEFAULT_OGS
|
||||||
|
|
||||||
|
|
||||||
|
def render_svodka_pm_tab():
|
||||||
|
"""Рендер вкладки сводок ПМ"""
|
||||||
|
st.header("📊 Сводки ПМ - Полный функционал")
|
||||||
|
|
||||||
|
# Секция загрузки файлов
|
||||||
|
st.subheader("📤 Загрузка файлов")
|
||||||
|
uploaded_pm = st.file_uploader(
|
||||||
|
"Выберите ZIP архив со сводками ПМ",
|
||||||
|
type=['zip'],
|
||||||
|
key="pm_upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_pm is not None:
|
||||||
|
if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"):
|
||||||
|
with st.spinner("Загружаю файл..."):
|
||||||
|
result, status = upload_file_to_api(
|
||||||
|
"/svodka_pm/upload-zip",
|
||||||
|
uploaded_pm.read(),
|
||||||
|
uploaded_pm.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
st.success(f"✅ {result.get('message', 'Файл загружен')}")
|
||||||
|
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Секция получения данных
|
||||||
|
st.subheader("🔍 Получение данных")
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.subheader("Данные по одному ОГ")
|
||||||
|
|
||||||
|
og_id = st.selectbox(
|
||||||
|
"Выберите ОГ",
|
||||||
|
DEFAULT_OGS,
|
||||||
|
key="pm_single_og"
|
||||||
|
)
|
||||||
|
|
||||||
|
codes = st.multiselect(
|
||||||
|
"Выберите коды строк",
|
||||||
|
PM_CODES,
|
||||||
|
default=[78, 79],
|
||||||
|
key="pm_single_codes"
|
||||||
|
)
|
||||||
|
|
||||||
|
columns = st.multiselect(
|
||||||
|
"Выберите столбцы",
|
||||||
|
PM_COLUMNS,
|
||||||
|
default=["БП", "ПП"],
|
||||||
|
key="pm_single_columns"
|
||||||
|
)
|
||||||
|
|
||||||
|
if st.button("🔍 Получить данные по ОГ", key="pm_single_btn"):
|
||||||
|
if codes and columns:
|
||||||
|
with st.spinner("Получаю данные..."):
|
||||||
|
data = {
|
||||||
|
"id": og_id,
|
||||||
|
"codes": codes,
|
||||||
|
"columns": columns
|
||||||
|
}
|
||||||
|
|
||||||
|
result, status = make_api_request("/svodka_pm/get_single_og", data)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
st.success("✅ Данные получены")
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
|
else:
|
||||||
|
st.warning("⚠️ Выберите коды и столбцы")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.subheader("Данные по всем ОГ")
|
||||||
|
|
||||||
|
codes_total = st.multiselect(
|
||||||
|
"Выберите коды строк",
|
||||||
|
PM_CODES,
|
||||||
|
default=[78, 79, 394, 395],
|
||||||
|
key="pm_total_codes"
|
||||||
|
)
|
||||||
|
|
||||||
|
columns_total = st.multiselect(
|
||||||
|
"Выберите столбцы",
|
||||||
|
PM_COLUMNS,
|
||||||
|
default=["БП", "ПП", "СЭБ"],
|
||||||
|
key="pm_total_columns"
|
||||||
|
)
|
||||||
|
|
||||||
|
if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"):
|
||||||
|
if codes_total and columns_total:
|
||||||
|
with st.spinner("Получаю данные..."):
|
||||||
|
data = {
|
||||||
|
"codes": codes_total,
|
||||||
|
"columns": columns_total
|
||||||
|
}
|
||||||
|
|
||||||
|
result, status = make_api_request("/svodka_pm/get_total_ogs", data)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
st.success("✅ Данные получены")
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
|
else:
|
||||||
|
st.warning("⚠️ Выберите коды и столбцы")
|
||||||
111
streamlit_app/parsers_ui/svodka_repair_ca_ui.py
Normal file
111
streamlit_app/parsers_ui/svodka_repair_ca_ui.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"""
|
||||||
|
UI модуль для ремонта СА
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
import pandas as pd
|
||||||
|
from api_client import upload_file_to_api, make_api_request, get_system_ogs, get_available_ogs
|
||||||
|
from config import REPAIR_TYPES
|
||||||
|
|
||||||
|
|
||||||
|
def render_svodka_repair_ca_tab():
|
||||||
|
"""Рендер вкладки ремонта СА"""
|
||||||
|
st.header("🔧 Ремонт СА - Управление ремонтными работами")
|
||||||
|
|
||||||
|
# Секция загрузки файлов
|
||||||
|
st.subheader("📤 Загрузка файлов")
|
||||||
|
|
||||||
|
uploaded_file = st.file_uploader(
|
||||||
|
"Выберите Excel файл или ZIP архив с данными о ремонте СА",
|
||||||
|
type=['xlsx', 'xlsm', 'xls', 'zip'],
|
||||||
|
key="repair_ca_upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_file is not None:
|
||||||
|
if st.button("📤 Загрузить файл", key="repair_ca_upload_btn"):
|
||||||
|
with st.spinner("Загружаю файл..."):
|
||||||
|
file_data = uploaded_file.read()
|
||||||
|
result, status = upload_file_to_api("/svodka_repair_ca/upload", file_data, uploaded_file.name)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
st.success("✅ Файл успешно загружен")
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка загрузки: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Секция получения данных
|
||||||
|
st.subheader("🔍 Получение данных")
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.subheader("Фильтры")
|
||||||
|
|
||||||
|
# Получаем доступные ОГ из системного 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
|
||||||
|
key="repair_ca_og_ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Фильтр по типам ремонта
|
||||||
|
repair_types = st.multiselect(
|
||||||
|
"Выберите типы ремонта (оставьте пустым для всех)",
|
||||||
|
REPAIR_TYPES,
|
||||||
|
key="repair_ca_types"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Включение плановых/фактических данных
|
||||||
|
include_planned = st.checkbox("Включать плановые данные", value=True, key="repair_ca_planned")
|
||||||
|
include_factual = st.checkbox("Включать фактические данные", value=True, key="repair_ca_factual")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.subheader("Действия")
|
||||||
|
|
||||||
|
if st.button("🔍 Получить данные о ремонте", key="repair_ca_get_btn"):
|
||||||
|
with st.spinner("Получаю данные..."):
|
||||||
|
data = {
|
||||||
|
"include_planned": include_planned,
|
||||||
|
"include_factual": include_factual
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавляем фильтры только если они выбраны
|
||||||
|
if og_ids:
|
||||||
|
data["og_ids"] = og_ids
|
||||||
|
if repair_types:
|
||||||
|
data["repair_types"] = repair_types
|
||||||
|
|
||||||
|
result, status = make_api_request("/svodka_repair_ca/get_data", data)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
st.success("✅ Данные получены")
|
||||||
|
|
||||||
|
# Отображаем данные в виде таблицы, если возможно
|
||||||
|
if result.get("data") and isinstance(result["data"], list):
|
||||||
|
df_data = []
|
||||||
|
for item in result["data"]:
|
||||||
|
df_data.append({
|
||||||
|
"ID ОГ": item.get("id", ""),
|
||||||
|
"Наименование": item.get("name", ""),
|
||||||
|
"Тип ремонта": item.get("type", ""),
|
||||||
|
"Дата начала": item.get("start_date", ""),
|
||||||
|
"Дата окончания": item.get("end_date", ""),
|
||||||
|
"План": item.get("plan", ""),
|
||||||
|
"Факт": item.get("fact", ""),
|
||||||
|
"Простой": item.get("downtime", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
if df_data:
|
||||||
|
df = pd.DataFrame(df_data)
|
||||||
|
st.dataframe(df, use_container_width=True)
|
||||||
|
else:
|
||||||
|
st.info("📋 Нет данных для отображения")
|
||||||
|
else:
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
82
streamlit_app/sidebar.py
Normal file
82
streamlit_app/sidebar.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
Модуль для сайдбара
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
from api_client import get_server_info, get_available_parsers
|
||||||
|
from config import API_PUBLIC_URL
|
||||||
|
|
||||||
|
|
||||||
|
def render_sidebar():
|
||||||
|
"""Рендер боковой панели"""
|
||||||
|
with st.sidebar:
|
||||||
|
st.header("ℹ️ Информация1")
|
||||||
|
|
||||||
|
# Информация о сервере
|
||||||
|
server_info = get_server_info()
|
||||||
|
if server_info:
|
||||||
|
st.subheader("Сервер")
|
||||||
|
st.write(f"PID: {server_info.get('process_id', 'N/A')}")
|
||||||
|
st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}")
|
||||||
|
|
||||||
|
# Безопасное форматирование памяти
|
||||||
|
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()
|
||||||
|
if parsers:
|
||||||
|
st.subheader("Доступные парсеры")
|
||||||
|
for parser in parsers:
|
||||||
|
st.write(f"• {parser}")
|
||||||
|
|
||||||
|
# Навигация по страницам
|
||||||
|
st.markdown("---")
|
||||||
|
st.subheader("🧭 Навигация")
|
||||||
|
|
||||||
|
# Определяем активную страницу
|
||||||
|
active_page = st.session_state.get("active_page", 0)
|
||||||
|
|
||||||
|
# Кнопка для страницы синхронных парсеров
|
||||||
|
if st.button("📊 Синхронные парсеры", key="sidebar_sync_btn", use_container_width=True, type="primary" if active_page == 0 else "secondary"):
|
||||||
|
st.session_state.sidebar_sync_clicked = True
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
# Кнопка для страницы асинхронной загрузки
|
||||||
|
if st.button("🚀 Асинхронная загрузка", key="sidebar_async_btn", use_container_width=True, type="primary" if active_page == 1 else "secondary"):
|
||||||
|
st.session_state.sidebar_async_clicked = True
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
# Кнопка для страницы управления задачами
|
||||||
|
if st.button("📋 Управление задачами", key="sidebar_tasks_btn", use_container_width=True, type="primary" if active_page == 2 else "secondary"):
|
||||||
|
st.session_state.sidebar_tasks_clicked = True
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
|
||||||
|
def render_footer():
|
||||||
|
"""Рендер футера"""
|
||||||
|
st.markdown("---")
|
||||||
|
st.markdown("### 📚 Документация API")
|
||||||
|
st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs")
|
||||||
|
|
||||||
|
# Информация о проекте
|
||||||
|
with st.expander("ℹ️ О проекте"):
|
||||||
|
st.markdown("""
|
||||||
|
**NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов.
|
||||||
|
|
||||||
|
**Возможности:**
|
||||||
|
- 📊 Парсинг сводок ПМ (план и факт)
|
||||||
|
- 🏭 Парсинг сводок СА
|
||||||
|
- ⛽ Мониторинг топлива
|
||||||
|
- ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы)
|
||||||
|
- 🔧 Управление ремонтными работами СА
|
||||||
|
- 📋 Мониторинг статусов ремонта СА
|
||||||
|
|
||||||
|
**Технологии:**
|
||||||
|
- FastAPI
|
||||||
|
- Pandas
|
||||||
|
- MinIO (S3-совместимое хранилище)
|
||||||
|
- Streamlit (веб-интерфейс)
|
||||||
|
""")
|
||||||
@@ -1,847 +1,61 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
import requests
|
from config import setup_page_config, API_PUBLIC_URL
|
||||||
import json
|
from api_client import check_api_health
|
||||||
import pandas as pd
|
from sidebar import render_sidebar, render_footer
|
||||||
import io
|
from sync_parsers_page import render_sync_parsers_page
|
||||||
import zipfile
|
from async_upload_page import render_async_upload_page
|
||||||
from typing import Dict, Any, List
|
from tasks_page import render_tasks_page
|
||||||
import os
|
|
||||||
|
|
||||||
# Конфигурация страницы
|
# Конфигурация страницы
|
||||||
st.set_page_config(
|
setup_page_config()
|
||||||
page_title="NIN Excel Parsers API Demo",
|
|
||||||
page_icon="📊",
|
|
||||||
layout="wide",
|
|
||||||
initial_sidebar_state="expanded"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Конфигурация API
|
|
||||||
API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker
|
|
||||||
API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя
|
|
||||||
|
|
||||||
def check_api_health():
|
|
||||||
"""Проверка доступности API"""
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{API_BASE_URL}/", timeout=5)
|
|
||||||
return response.status_code == 200
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_available_parsers():
|
|
||||||
"""Получение списка доступных парсеров"""
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{API_BASE_URL}/parsers")
|
|
||||||
if response.status_code == 200:
|
|
||||||
return response.json()["parsers"]
|
|
||||||
return []
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_server_info():
|
|
||||||
"""Получение информации о сервере"""
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{API_BASE_URL}/server-info")
|
|
||||||
if response.status_code == 200:
|
|
||||||
return response.json()
|
|
||||||
return {}
|
|
||||||
except:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def upload_file_to_api(endpoint: str, file_data: bytes, filename: str):
|
|
||||||
"""Загрузка файла на API"""
|
|
||||||
try:
|
|
||||||
# Определяем правильное имя поля в зависимости от эндпоинта
|
|
||||||
if "zip" in endpoint:
|
|
||||||
files = {"zip_file": (filename, file_data, "application/zip")}
|
|
||||||
else:
|
|
||||||
files = {"file": (filename, file_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
|
|
||||||
|
|
||||||
response = requests.post(f"{API_BASE_URL}{endpoint}", files=files)
|
|
||||||
return response.json(), response.status_code
|
|
||||||
except Exception as e:
|
|
||||||
return {"error": str(e)}, 500
|
|
||||||
|
|
||||||
def make_api_request(endpoint: str, data: Dict[str, Any]):
|
|
||||||
"""Выполнение API запроса"""
|
|
||||||
try:
|
|
||||||
response = requests.post(f"{API_BASE_URL}{endpoint}", json=data)
|
|
||||||
return response.json(), response.status_code
|
|
||||||
except Exception as e:
|
|
||||||
return {"error": str(e)}, 500
|
|
||||||
|
|
||||||
def get_available_ogs(parser_name: str) -> List[str]:
|
|
||||||
"""Получение доступных ОГ для парсера"""
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/available_ogs")
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
return data.get("available_ogs", [])
|
|
||||||
else:
|
|
||||||
print(f"⚠️ Ошибка получения ОГ: {response.status_code}")
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Ошибка при запросе ОГ: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
st.title("🚀 NIN Excel Parsers API - Демонстрация")
|
# Определяем активную страницу для заголовка
|
||||||
|
active_page = st.session_state.get("active_page", 0)
|
||||||
|
page_titles = {
|
||||||
|
0: "Синхронные парсеры",
|
||||||
|
1: "Асинхронная загрузка",
|
||||||
|
2: "Управление задачами"
|
||||||
|
}
|
||||||
|
|
||||||
|
st.title(f"🚀 NIN Excel Parsers API - {page_titles.get(active_page, 'Демонстрация')}")
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# Проверка доступности API
|
# Проверка доступности API
|
||||||
if not check_api_health():
|
if not check_api_health():
|
||||||
st.error(f"❌ API недоступен по адресу {API_BASE_URL}")
|
st.error(f"❌ API недоступен по адресу {API_PUBLIC_URL}")
|
||||||
st.info("Убедитесь, что FastAPI сервер запущен")
|
st.info("Убедитесь, что FastAPI сервер запущен")
|
||||||
return
|
return
|
||||||
|
|
||||||
st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}")
|
st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}")
|
||||||
|
|
||||||
# Боковая панель с информацией
|
# Обрабатываем клики по кнопкам в сайдбаре ПЕРЕД рендером
|
||||||
with st.sidebar:
|
if st.session_state.get("sidebar_sync_clicked", False):
|
||||||
st.header("ℹ️ Информация")
|
st.session_state.sidebar_sync_clicked = False
|
||||||
|
st.session_state.active_page = 0
|
||||||
|
elif st.session_state.get("sidebar_async_clicked", False):
|
||||||
|
st.session_state.sidebar_async_clicked = False
|
||||||
|
st.session_state.active_page = 1
|
||||||
|
elif st.session_state.get("sidebar_tasks_clicked", False):
|
||||||
|
st.session_state.sidebar_tasks_clicked = False
|
||||||
|
st.session_state.active_page = 2
|
||||||
|
|
||||||
# Информация о сервере
|
# Определяем активную страницу
|
||||||
server_info = get_server_info()
|
active_page = st.session_state.get("active_page", 0)
|
||||||
if server_info:
|
|
||||||
st.subheader("Сервер")
|
|
||||||
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"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB")
|
|
||||||
|
|
||||||
# Доступные парсеры
|
# Боковая панель с информацией и навигацией
|
||||||
parsers = get_available_parsers()
|
render_sidebar()
|
||||||
if parsers:
|
|
||||||
st.subheader("Доступные парсеры")
|
|
||||||
for parser in parsers:
|
|
||||||
st.write(f"• {parser}")
|
|
||||||
|
|
||||||
# Основные вкладки - по одной на каждый парсер
|
# Рендерим соответствующую страницу
|
||||||
tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs([
|
if active_page == 0:
|
||||||
"📊 Сводки ПМ",
|
render_sync_parsers_page()
|
||||||
"🏭 Сводки СА",
|
elif active_page == 1:
|
||||||
"⛽ Мониторинг топлива",
|
render_async_upload_page()
|
||||||
"🔧 Ремонт СА",
|
|
||||||
"📋 Статусы ремонта СА",
|
|
||||||
"⚡ Мониторинг ТЭР",
|
|
||||||
"🏭 Операционные справки"
|
|
||||||
])
|
|
||||||
|
|
||||||
# Вкладка 1: Сводки ПМ - полный функционал
|
|
||||||
with tab1:
|
|
||||||
st.header("📊 Сводки ПМ - Полный функционал")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
|
||||||
st.subheader("📤 Загрузка файлов")
|
|
||||||
uploaded_pm = st.file_uploader(
|
|
||||||
"Выберите ZIP архив со сводками ПМ",
|
|
||||||
type=['zip'],
|
|
||||||
key="pm_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
if uploaded_pm is not None:
|
|
||||||
if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"):
|
|
||||||
with st.spinner("Загружаю файл..."):
|
|
||||||
result, status = upload_file_to_api(
|
|
||||||
"/svodka_pm/upload-zip",
|
|
||||||
uploaded_pm.read(),
|
|
||||||
uploaded_pm.name
|
|
||||||
)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success(f"✅ {result.get('message', 'Файл загружен')}")
|
|
||||||
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
|
|
||||||
else:
|
else:
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
render_tasks_page()
|
||||||
|
|
||||||
st.markdown("---")
|
|
||||||
|
|
||||||
# Секция получения данных
|
|
||||||
st.subheader("🔍 Получение данных")
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
|
|
||||||
with col1:
|
|
||||||
st.subheader("Данные по одному ОГ")
|
|
||||||
|
|
||||||
og_id = st.selectbox(
|
|
||||||
"Выберите ОГ",
|
|
||||||
["SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV",
|
|
||||||
"NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK",
|
|
||||||
"NVNPO", "KLNPZ", "PurNP", "YANOS"],
|
|
||||||
key="pm_single_og"
|
|
||||||
)
|
|
||||||
|
|
||||||
codes = st.multiselect(
|
|
||||||
"Выберите коды строк",
|
|
||||||
[78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
|
|
||||||
default=[78, 79],
|
|
||||||
key="pm_single_codes"
|
|
||||||
)
|
|
||||||
|
|
||||||
columns = st.multiselect(
|
|
||||||
"Выберите столбцы",
|
|
||||||
["БП", "ПП", "СЭБ", "Факт", "План"],
|
|
||||||
default=["БП", "ПП"],
|
|
||||||
key="pm_single_columns"
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.button("🔍 Получить данные по ОГ", key="pm_single_btn"):
|
|
||||||
if codes and columns:
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"id": og_id,
|
|
||||||
"codes": codes,
|
|
||||||
"columns": columns
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_pm/get_single_og", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
else:
|
|
||||||
st.warning("⚠️ Выберите коды и столбцы")
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("Данные по всем ОГ")
|
|
||||||
|
|
||||||
codes_total = st.multiselect(
|
|
||||||
"Выберите коды строк",
|
|
||||||
[78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
|
|
||||||
default=[78, 79, 394, 395],
|
|
||||||
key="pm_total_codes"
|
|
||||||
)
|
|
||||||
|
|
||||||
columns_total = st.multiselect(
|
|
||||||
"Выберите столбцы",
|
|
||||||
["БП", "ПП", "СЭБ", "Факт", "План"],
|
|
||||||
default=["БП", "ПП", "СЭБ"],
|
|
||||||
key="pm_total_columns"
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"):
|
|
||||||
if codes_total and columns_total:
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"codes": codes_total,
|
|
||||||
"columns": columns_total
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_pm/get_total_ogs", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
else:
|
|
||||||
st.warning("⚠️ Выберите коды и столбцы")
|
|
||||||
|
|
||||||
# Вкладка 2: Сводки СА - полный функционал
|
|
||||||
with tab2:
|
|
||||||
st.header("🏭 Сводки СА - Полный функционал")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
|
||||||
st.subheader("📤 Загрузка файлов")
|
|
||||||
uploaded_ca = st.file_uploader(
|
|
||||||
"Выберите Excel файл сводки СА",
|
|
||||||
type=['xlsx', 'xlsm', 'xls'],
|
|
||||||
key="ca_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
if uploaded_ca is not None:
|
|
||||||
if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"):
|
|
||||||
with st.spinner("Загружаю файл..."):
|
|
||||||
try:
|
|
||||||
files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
|
|
||||||
response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files)
|
|
||||||
result = response.json()
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
st.success(f"✅ {result.get('message', 'Файл загружен')}")
|
|
||||||
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
except Exception as e:
|
|
||||||
st.error(f"❌ Ошибка: {str(e)}")
|
|
||||||
|
|
||||||
st.markdown("---")
|
|
||||||
|
|
||||||
# Секция получения данных
|
|
||||||
st.subheader("🔍 Получение данных")
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
|
|
||||||
with col1:
|
|
||||||
st.subheader("Параметры запроса")
|
|
||||||
|
|
||||||
modes = st.multiselect(
|
|
||||||
"Выберите режимы",
|
|
||||||
["plan", "fact", "normativ"],
|
|
||||||
default=["plan", "fact"],
|
|
||||||
key="ca_modes"
|
|
||||||
)
|
|
||||||
|
|
||||||
tables = st.multiselect(
|
|
||||||
"Выберите таблицы",
|
|
||||||
["ТиП", "Топливо", "Потери"],
|
|
||||||
default=["ТиП", "Топливо"],
|
|
||||||
key="ca_tables"
|
|
||||||
)
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("Результат")
|
|
||||||
if st.button("🔍 Получить данные СА", key="ca_btn"):
|
|
||||||
if modes and tables:
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"modes": modes,
|
|
||||||
"tables": tables
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_ca/get_data", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
else:
|
|
||||||
st.warning("⚠️ Выберите режимы и таблицы")
|
|
||||||
|
|
||||||
# Вкладка 3: Мониторинг топлива - полный функционал
|
|
||||||
with tab3:
|
|
||||||
st.header("⛽ Мониторинг топлива - Полный функционал")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
|
||||||
st.subheader("📤 Загрузка файлов")
|
|
||||||
uploaded_fuel = st.file_uploader(
|
|
||||||
"Выберите ZIP архив с мониторингом топлива",
|
|
||||||
type=['zip'],
|
|
||||||
key="fuel_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
if uploaded_fuel is not None:
|
|
||||||
if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"):
|
|
||||||
with st.spinner("Загружаю файл..."):
|
|
||||||
result, status = upload_file_to_api(
|
|
||||||
"/monitoring_fuel/upload-zip",
|
|
||||||
uploaded_fuel.read(),
|
|
||||||
uploaded_fuel.name
|
|
||||||
)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success(f"✅ {result.get('message', 'Файл загружен')}")
|
|
||||||
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
|
|
||||||
st.markdown("---")
|
|
||||||
|
|
||||||
# Секция получения данных
|
|
||||||
st.subheader("🔍 Получение данных")
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
|
|
||||||
with col1:
|
|
||||||
st.subheader("Агрегация по колонкам")
|
|
||||||
|
|
||||||
columns_fuel = st.multiselect(
|
|
||||||
"Выберите столбцы",
|
|
||||||
["normativ", "total", "total_1"],
|
|
||||||
default=["normativ", "total"],
|
|
||||||
key="fuel_columns"
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"):
|
|
||||||
if columns_fuel:
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"columns": columns_fuel
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
else:
|
|
||||||
st.warning("⚠️ Выберите столбцы")
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("Данные за месяц")
|
|
||||||
|
|
||||||
month = st.selectbox(
|
|
||||||
"Выберите месяц",
|
|
||||||
[f"{i:02d}" for i in range(1, 13)],
|
|
||||||
key="fuel_month"
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"month": month
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status = make_api_request("/monitoring_fuel/get_month_by_code", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
|
|
||||||
# Вкладка 4: Ремонт СА
|
|
||||||
with tab4:
|
|
||||||
st.header("🔧 Ремонт СА - Управление ремонтными работами")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
|
||||||
st.subheader("📤 Загрузка файлов")
|
|
||||||
|
|
||||||
uploaded_file = st.file_uploader(
|
|
||||||
"Выберите Excel файл или ZIP архив с данными о ремонте СА",
|
|
||||||
type=['xlsx', 'xlsm', 'xls', 'zip'],
|
|
||||||
key="repair_ca_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
if uploaded_file is not None:
|
|
||||||
if st.button("📤 Загрузить файл", key="repair_ca_upload_btn"):
|
|
||||||
with st.spinner("Загружаю файл..."):
|
|
||||||
file_data = uploaded_file.read()
|
|
||||||
result, status = upload_file_to_api("/svodka_repair_ca/upload", file_data, uploaded_file.name)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Файл успешно загружен")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка загрузки: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
|
|
||||||
st.markdown("---")
|
|
||||||
|
|
||||||
# Секция получения данных
|
|
||||||
st.subheader("🔍 Получение данных")
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
|
|
||||||
with col1:
|
|
||||||
st.subheader("Фильтры")
|
|
||||||
|
|
||||||
# Получаем доступные ОГ динамически
|
|
||||||
available_ogs = get_available_ogs("svodka_repair_ca")
|
|
||||||
|
|
||||||
# Фильтр по ОГ
|
|
||||||
og_ids = st.multiselect(
|
|
||||||
"Выберите ОГ (оставьте пустым для всех)",
|
|
||||||
available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback
|
|
||||||
key="repair_ca_og_ids"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Фильтр по типам ремонта
|
|
||||||
repair_types = st.multiselect(
|
|
||||||
"Выберите типы ремонта (оставьте пустым для всех)",
|
|
||||||
["КР", "КП", "ТР"],
|
|
||||||
key="repair_ca_types"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Включение плановых/фактических данных
|
|
||||||
include_planned = st.checkbox("Включать плановые данные", value=True, key="repair_ca_planned")
|
|
||||||
include_factual = st.checkbox("Включать фактические данные", value=True, key="repair_ca_factual")
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("Действия")
|
|
||||||
|
|
||||||
if st.button("🔍 Получить данные о ремонте", key="repair_ca_get_btn"):
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"include_planned": include_planned,
|
|
||||||
"include_factual": include_factual
|
|
||||||
}
|
|
||||||
|
|
||||||
# Добавляем фильтры только если они выбраны
|
|
||||||
if og_ids:
|
|
||||||
data["og_ids"] = og_ids
|
|
||||||
if repair_types:
|
|
||||||
data["repair_types"] = repair_types
|
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_repair_ca/get_data", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
|
|
||||||
# Отображаем данные в виде таблицы, если возможно
|
|
||||||
if result.get("data") and isinstance(result["data"], list):
|
|
||||||
df_data = []
|
|
||||||
for item in result["data"]:
|
|
||||||
df_data.append({
|
|
||||||
"ID ОГ": item.get("id", ""),
|
|
||||||
"Наименование": item.get("name", ""),
|
|
||||||
"Тип ремонта": item.get("type", ""),
|
|
||||||
"Дата начала": item.get("start_date", ""),
|
|
||||||
"Дата окончания": item.get("end_date", ""),
|
|
||||||
"План": item.get("plan", ""),
|
|
||||||
"Факт": item.get("fact", ""),
|
|
||||||
"Простой": item.get("downtime", "")
|
|
||||||
})
|
|
||||||
|
|
||||||
if df_data:
|
|
||||||
df = pd.DataFrame(df_data)
|
|
||||||
st.dataframe(df, use_container_width=True)
|
|
||||||
else:
|
|
||||||
st.info("📋 Нет данных для отображения")
|
|
||||||
else:
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
|
|
||||||
# Вкладка 5: Статусы ремонта СА
|
|
||||||
with tab5:
|
|
||||||
st.header("📋 Статусы ремонта СА")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
|
||||||
st.subheader("📤 Загрузка файлов")
|
|
||||||
uploaded_file = st.file_uploader(
|
|
||||||
"Выберите файл статусов ремонта СА",
|
|
||||||
type=['xlsx', 'xlsm', 'xls', 'zip'],
|
|
||||||
key="statuses_repair_ca_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
if uploaded_file is not None:
|
|
||||||
if st.button("📤 Загрузить файл", key="statuses_repair_ca_upload_btn"):
|
|
||||||
with st.spinner("Загружаем файл..."):
|
|
||||||
file_data = uploaded_file.read()
|
|
||||||
result, status_code = upload_file_to_api("/statuses_repair_ca/upload", file_data, uploaded_file.name)
|
|
||||||
|
|
||||||
if status_code == 200:
|
|
||||||
st.success("✅ Файл успешно загружен!")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка загрузки: {result}")
|
|
||||||
|
|
||||||
# Секция получения данных
|
|
||||||
st.subheader("📊 Получение данных")
|
|
||||||
|
|
||||||
# Получаем доступные ОГ динамически
|
|
||||||
available_ogs = get_available_ogs("statuses_repair_ca")
|
|
||||||
|
|
||||||
# Фильтр по ОГ
|
|
||||||
og_ids = st.multiselect(
|
|
||||||
"Выберите ОГ (оставьте пустым для всех)",
|
|
||||||
available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback
|
|
||||||
key="statuses_repair_ca_og_ids"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Предустановленные ключи для извлечения
|
|
||||||
st.subheader("🔑 Ключи для извлечения данных")
|
|
||||||
|
|
||||||
# Основные ключи
|
|
||||||
include_basic_keys = st.checkbox("Основные данные", value=True, key="statuses_basic_keys")
|
|
||||||
include_readiness_keys = st.checkbox("Готовность к КР", value=True, key="statuses_readiness_keys")
|
|
||||||
include_contract_keys = st.checkbox("Заключение договоров", value=True, key="statuses_contract_keys")
|
|
||||||
include_supply_keys = st.checkbox("Поставка МТР", value=True, key="statuses_supply_keys")
|
|
||||||
|
|
||||||
# Формируем ключи на основе выбора
|
|
||||||
keys = []
|
|
||||||
if include_basic_keys:
|
|
||||||
keys.append(["Дата начала ремонта"])
|
|
||||||
keys.append(["Отставание / опережение подготовки к КР", "Отставание / опережение"])
|
|
||||||
keys.append(["Отставание / опережение подготовки к КР", "Динамика за прошедшую неделю"])
|
|
||||||
|
|
||||||
if include_readiness_keys:
|
|
||||||
keys.append(["Готовность к КР", "Факт"])
|
|
||||||
|
|
||||||
if include_contract_keys:
|
|
||||||
keys.append(["Заключение договоров на СМР", "Договор", "%"])
|
|
||||||
|
|
||||||
if include_supply_keys:
|
|
||||||
keys.append(["Поставка МТР", "На складе, позиций", "%"])
|
|
||||||
|
|
||||||
# Кнопка получения данных
|
|
||||||
if st.button("📊 Получить данные", key="statuses_repair_ca_get_data_btn"):
|
|
||||||
if not keys:
|
|
||||||
st.warning("⚠️ Выберите хотя бы одну группу ключей для извлечения")
|
|
||||||
else:
|
|
||||||
with st.spinner("Получаем данные..."):
|
|
||||||
request_data = {
|
|
||||||
"ids": og_ids if og_ids else None,
|
|
||||||
"keys": keys
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status_code = make_api_request("/statuses_repair_ca/get_data", request_data)
|
|
||||||
|
|
||||||
if status_code == 200 and result.get("success"):
|
|
||||||
st.success("✅ Данные успешно получены!")
|
|
||||||
|
|
||||||
data = result.get("data", {}).get("value", [])
|
|
||||||
if data:
|
|
||||||
# Отображаем данные в виде таблицы
|
|
||||||
if isinstance(data, list) and len(data) > 0:
|
|
||||||
# Преобразуем в DataFrame для лучшего отображения
|
|
||||||
df_data = []
|
|
||||||
for item in data:
|
|
||||||
row = {
|
|
||||||
"ID": item.get("id", ""),
|
|
||||||
"Название": item.get("name", ""),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Добавляем основные поля
|
|
||||||
if "Дата начала ремонта" in item:
|
|
||||||
row["Дата начала ремонта"] = item["Дата начала ремонта"]
|
|
||||||
|
|
||||||
# Добавляем готовность к КР
|
|
||||||
if "Готовность к КР" in item:
|
|
||||||
readiness = item["Готовность к КР"]
|
|
||||||
if isinstance(readiness, dict) and "Факт" in readiness:
|
|
||||||
row["Готовность к КР (Факт)"] = readiness["Факт"]
|
|
||||||
|
|
||||||
# Добавляем отставание/опережение
|
|
||||||
if "Отставание / опережение подготовки к КР" in item:
|
|
||||||
delay = item["Отставание / опережение подготовки к КР"]
|
|
||||||
if isinstance(delay, dict):
|
|
||||||
if "Отставание / опережение" in delay:
|
|
||||||
row["Отставание/опережение"] = delay["Отставание / опережение"]
|
|
||||||
if "Динамика за прошедшую неделю" in delay:
|
|
||||||
row["Динамика за неделю"] = delay["Динамика за прошедшую неделю"]
|
|
||||||
|
|
||||||
# Добавляем договоры
|
|
||||||
if "Заключение договоров на СМР" in item:
|
|
||||||
contracts = item["Заключение договоров на СМР"]
|
|
||||||
if isinstance(contracts, dict) and "Договор" in contracts:
|
|
||||||
contract = contracts["Договор"]
|
|
||||||
if isinstance(contract, dict) and "%" in contract:
|
|
||||||
row["Договоры (%)"] = contract["%"]
|
|
||||||
|
|
||||||
# Добавляем поставки МТР
|
|
||||||
if "Поставка МТР" in item:
|
|
||||||
supply = item["Поставка МТР"]
|
|
||||||
if isinstance(supply, dict) and "На складе, позиций" in supply:
|
|
||||||
warehouse = supply["На складе, позиций"]
|
|
||||||
if isinstance(warehouse, dict) and "%" in warehouse:
|
|
||||||
row["МТР на складе (%)"] = warehouse["%"]
|
|
||||||
|
|
||||||
df_data.append(row)
|
|
||||||
|
|
||||||
if df_data:
|
|
||||||
df = pd.DataFrame(df_data)
|
|
||||||
st.dataframe(df, use_container_width=True)
|
|
||||||
else:
|
|
||||||
st.info("📋 Нет данных для отображения")
|
|
||||||
else:
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.info("📋 Нет данных для отображения")
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
|
|
||||||
# Вкладка 6: Мониторинг ТЭР
|
|
||||||
with tab6:
|
|
||||||
st.header("⚡ Мониторинг ТЭР (Топливно-энергетических ресурсов)")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
|
||||||
st.subheader("📤 Загрузка файлов")
|
|
||||||
uploaded_file = st.file_uploader(
|
|
||||||
"Выберите ZIP архив с файлами мониторинга ТЭР",
|
|
||||||
type=['zip'],
|
|
||||||
key="monitoring_tar_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
if uploaded_file is not None:
|
|
||||||
if st.button("📤 Загрузить файл", key="monitoring_tar_upload_btn"):
|
|
||||||
with st.spinner("Загружаем файл..."):
|
|
||||||
file_data = uploaded_file.read()
|
|
||||||
result, status_code = upload_file_to_api("/monitoring_tar/upload", file_data, uploaded_file.name)
|
|
||||||
|
|
||||||
if status_code == 200:
|
|
||||||
st.success("✅ Файл успешно загружен!")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка загрузки: {result}")
|
|
||||||
|
|
||||||
# Секция получения данных
|
|
||||||
st.subheader("📊 Получение данных")
|
|
||||||
|
|
||||||
# Выбор формата отображения
|
|
||||||
display_format = st.radio(
|
|
||||||
"Формат отображения:",
|
|
||||||
["JSON", "Таблица"],
|
|
||||||
key="monitoring_tar_display_format",
|
|
||||||
horizontal=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Выбор режима данных
|
|
||||||
mode = st.selectbox(
|
|
||||||
"Выберите режим данных:",
|
|
||||||
["all", "total", "last_day"],
|
|
||||||
help="total - строки 'Всего' (агрегированные данные), last_day - последние строки данных, all - все данные",
|
|
||||||
key="monitoring_tar_mode"
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.button("📊 Получить данные", key="monitoring_tar_get_data_btn"):
|
|
||||||
with st.spinner("Получаем данные..."):
|
|
||||||
# Выбираем эндпоинт в зависимости от режима
|
|
||||||
if mode == "all":
|
|
||||||
# Используем полный эндпоинт
|
|
||||||
result, status_code = make_api_request("/monitoring_tar/get_full_data", {})
|
|
||||||
else:
|
|
||||||
# Используем фильтрованный эндпоинт
|
|
||||||
request_data = {"mode": mode}
|
|
||||||
result, status_code = make_api_request("/monitoring_tar/get_data", request_data)
|
|
||||||
|
|
||||||
if status_code == 200 and result.get("success"):
|
|
||||||
st.success("✅ Данные успешно получены!")
|
|
||||||
|
|
||||||
# Показываем данные
|
|
||||||
data = result.get("data", {}).get("value", {})
|
|
||||||
if data:
|
|
||||||
st.subheader("📋 Результат:")
|
|
||||||
|
|
||||||
# # Отладочная информация
|
|
||||||
# st.write(f"🔍 Тип данных: {type(data)}")
|
|
||||||
# if isinstance(data, str):
|
|
||||||
# st.write(f"🔍 Длина строки: {len(data)}")
|
|
||||||
# st.write(f"🔍 Первые 200 символов: {data[:200]}...")
|
|
||||||
|
|
||||||
# Парсим данные, если они пришли как строка
|
|
||||||
if isinstance(data, str):
|
|
||||||
try:
|
|
||||||
import json
|
|
||||||
data = json.loads(data)
|
|
||||||
st.write("✅ JSON успешно распарсен")
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
st.error(f"❌ Ошибка при парсинге JSON данных: {e}")
|
|
||||||
st.write("Сырые данные:", data)
|
|
||||||
return
|
|
||||||
|
|
||||||
if display_format == "JSON":
|
|
||||||
# Отображаем как JSON
|
|
||||||
st.json(data)
|
|
||||||
else:
|
|
||||||
# Отображаем как таблицы
|
|
||||||
if isinstance(data, dict):
|
|
||||||
# Показываем данные по установкам
|
|
||||||
for installation_id, installation_data in data.items():
|
|
||||||
with st.expander(f"🏭 {installation_id}"):
|
|
||||||
if isinstance(installation_data, dict):
|
|
||||||
# Показываем структуру данных
|
|
||||||
for data_type, type_data in installation_data.items():
|
|
||||||
st.write(f"**{data_type}:**")
|
|
||||||
if isinstance(type_data, list) and type_data:
|
|
||||||
df = pd.DataFrame(type_data)
|
|
||||||
st.dataframe(df)
|
|
||||||
else:
|
|
||||||
st.write("Нет данных")
|
|
||||||
else:
|
|
||||||
st.write("Нет данных")
|
|
||||||
else:
|
|
||||||
st.json(data)
|
|
||||||
else:
|
|
||||||
st.info("📋 Нет данных для отображения")
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
|
|
||||||
# Вкладка 7: Операционные справки технологических позиций
|
|
||||||
with tab7:
|
|
||||||
st.header("🏭 Операционные справки технологических позиций")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
|
||||||
st.subheader("📤 Загрузка файлов")
|
|
||||||
|
|
||||||
uploaded_file = st.file_uploader(
|
|
||||||
"Выберите ZIP архив с файлами операционных справок",
|
|
||||||
type=['zip'],
|
|
||||||
key="oper_spravka_tech_pos_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
if uploaded_file is not None:
|
|
||||||
if st.button("📤 Загрузить файл", key="oper_spravka_tech_pos_upload_btn"):
|
|
||||||
with st.spinner("Загружаем файл..."):
|
|
||||||
file_data = uploaded_file.read()
|
|
||||||
result, status_code = upload_file_to_api("/oper_spravka_tech_pos/upload", file_data, uploaded_file.name)
|
|
||||||
|
|
||||||
if status_code == 200:
|
|
||||||
st.success("✅ Файл успешно загружен!")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка загрузки: {result}")
|
|
||||||
|
|
||||||
st.markdown("---")
|
|
||||||
|
|
||||||
# Секция получения данных
|
|
||||||
st.subheader("📊 Получение данных")
|
|
||||||
|
|
||||||
# Выбор формата отображения
|
|
||||||
display_format = st.radio(
|
|
||||||
"Формат отображения:",
|
|
||||||
["JSON", "Таблица"],
|
|
||||||
key="oper_spravka_tech_pos_display_format",
|
|
||||||
horizontal=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Получаем доступные ОГ динамически
|
|
||||||
available_ogs = get_available_ogs("oper_spravka_tech_pos")
|
|
||||||
|
|
||||||
# Выбор ОГ
|
|
||||||
og_id = st.selectbox(
|
|
||||||
"Выберите ОГ:",
|
|
||||||
available_ogs if available_ogs else ["SNPZ", "KNPZ", "ANHK", "BASH", "UNH", "NOV"],
|
|
||||||
key="oper_spravka_tech_pos_og_id"
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.button("📊 Получить данные", key="oper_spravka_tech_pos_get_data_btn"):
|
|
||||||
with st.spinner("Получаем данные..."):
|
|
||||||
request_data = {"id": og_id}
|
|
||||||
result, status_code = make_api_request("/oper_spravka_tech_pos/get_data", request_data)
|
|
||||||
|
|
||||||
if status_code == 200 and result.get("success"):
|
|
||||||
st.success("✅ Данные успешно получены!")
|
|
||||||
|
|
||||||
# Показываем данные
|
|
||||||
data = result.get("data", [])
|
|
||||||
|
|
||||||
if data and len(data) > 0:
|
|
||||||
st.subheader("📋 Результат:")
|
|
||||||
|
|
||||||
if display_format == "JSON":
|
|
||||||
# Отображаем как JSON
|
|
||||||
st.json(data)
|
|
||||||
else:
|
|
||||||
# Отображаем как таблицу
|
|
||||||
if isinstance(data, list) and data:
|
|
||||||
df = pd.DataFrame(data)
|
|
||||||
st.dataframe(df, use_container_width=True)
|
|
||||||
else:
|
|
||||||
st.write("Нет данных")
|
|
||||||
else:
|
|
||||||
st.info("📋 Нет данных для отображения")
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
|
|
||||||
# Футер
|
# Футер
|
||||||
st.markdown("---")
|
render_footer()
|
||||||
st.markdown("### 📚 Документация API")
|
|
||||||
st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs")
|
|
||||||
|
|
||||||
# Информация о проекте
|
|
||||||
with st.expander("ℹ️ О проекте"):
|
|
||||||
st.markdown("""
|
|
||||||
**NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов.
|
|
||||||
|
|
||||||
**Возможности:**
|
|
||||||
- 📊 Парсинг сводок ПМ (план и факт)
|
|
||||||
- 🏭 Парсинг сводок СА
|
|
||||||
- ⛽ Мониторинг топлива
|
|
||||||
- ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы)
|
|
||||||
- 🔧 Управление ремонтными работами СА
|
|
||||||
- 📋 Мониторинг статусов ремонта СА
|
|
||||||
|
|
||||||
**Технологии:**
|
|
||||||
- FastAPI
|
|
||||||
- Pandas
|
|
||||||
- MinIO (S3-совместимое хранилище)
|
|
||||||
- Streamlit (веб-интерфейс)
|
|
||||||
""")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
54
streamlit_app/sync_parsers_page.py
Normal file
54
streamlit_app/sync_parsers_page.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Страница синхронных парсеров
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
from parsers_ui.svodka_pm_ui import render_svodka_pm_tab
|
||||||
|
from parsers_ui.svodka_ca_ui import render_svodka_ca_tab
|
||||||
|
from parsers_ui.monitoring_fuel_ui import render_monitoring_fuel_tab
|
||||||
|
from parsers_ui.svodka_repair_ca_ui import render_svodka_repair_ca_tab
|
||||||
|
from parsers_ui.statuses_repair_ca_ui import render_statuses_repair_ca_tab
|
||||||
|
from parsers_ui.monitoring_tar_ui import render_monitoring_tar_tab
|
||||||
|
from parsers_ui.oper_spravka_tech_pos_ui import render_oper_spravka_tech_pos_tab
|
||||||
|
from config import PARSER_TABS
|
||||||
|
|
||||||
|
|
||||||
|
def render_sync_parsers_page():
|
||||||
|
"""Рендер страницы синхронных парсеров"""
|
||||||
|
st.title("📊 Синхронные парсеры")
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
st.info("""
|
||||||
|
**Синхронные парсеры** обрабатывают файлы сразу после загрузки.
|
||||||
|
Интерфейс будет заблокирован до завершения обработки.
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Основные вкладки - по одной на каждый парсер
|
||||||
|
tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs(PARSER_TABS)
|
||||||
|
|
||||||
|
# Вкладка 1: Сводки ПМ - полный функционал
|
||||||
|
with tab1:
|
||||||
|
render_svodka_pm_tab()
|
||||||
|
|
||||||
|
# Вкладка 2: Сводки СА - полный функционал
|
||||||
|
with tab2:
|
||||||
|
render_svodka_ca_tab()
|
||||||
|
|
||||||
|
# Вкладка 3: Мониторинг топлива - полный функционал
|
||||||
|
with tab3:
|
||||||
|
render_monitoring_fuel_tab()
|
||||||
|
|
||||||
|
# Вкладка 4: Ремонт СА
|
||||||
|
with tab4:
|
||||||
|
render_svodka_repair_ca_tab()
|
||||||
|
|
||||||
|
# Вкладка 5: Статусы ремонта СА
|
||||||
|
with tab5:
|
||||||
|
render_statuses_repair_ca_tab()
|
||||||
|
|
||||||
|
# Вкладка 6: Мониторинг ТЭР
|
||||||
|
with tab6:
|
||||||
|
render_monitoring_tar_tab()
|
||||||
|
|
||||||
|
# Вкладка 7: Операционные справки технологических позиций
|
||||||
|
with tab7:
|
||||||
|
render_oper_spravka_tech_pos_tab()
|
||||||
186
streamlit_app/tasks_page.py
Normal file
186
streamlit_app/tasks_page.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
"""
|
||||||
|
Страница управления задачами загрузки
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
from async_upload_page import TASKS_STORAGE
|
||||||
|
|
||||||
|
|
||||||
|
def render_tasks_page():
|
||||||
|
"""Рендер страницы управления задачами"""
|
||||||
|
st.title("📋 Управление задачами загрузки")
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Кнопки управления
|
||||||
|
col1, col2, col3, col4 = st.columns([1, 1, 1, 2])
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
if st.button("🔄 Обновить", key="refresh_tasks_btn", use_container_width=True):
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
if st.button("🗑️ Очистить завершенные", key="clear_completed_btn", use_container_width=True):
|
||||||
|
# Удаляем завершенные и неудачные задачи
|
||||||
|
tasks_to_remove = []
|
||||||
|
for task_id, task in TASKS_STORAGE.items():
|
||||||
|
if task.get('status') in ['completed', 'failed']:
|
||||||
|
tasks_to_remove.append(task_id)
|
||||||
|
|
||||||
|
for task_id in tasks_to_remove:
|
||||||
|
del TASKS_STORAGE[task_id]
|
||||||
|
|
||||||
|
st.success(f"✅ Удалено {len(tasks_to_remove)} завершенных задач")
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
with col3:
|
||||||
|
auto_refresh = st.checkbox("🔄 Автообновление", key="auto_refresh_checkbox")
|
||||||
|
if auto_refresh:
|
||||||
|
time.sleep(2)
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
with col4:
|
||||||
|
st.caption("Последнее обновление: " + datetime.now().strftime("%H:%M:%S"))
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Статистика задач
|
||||||
|
st.subheader("📊 Статистика задач")
|
||||||
|
|
||||||
|
# Получаем задачи из глобального хранилища
|
||||||
|
tasks = TASKS_STORAGE
|
||||||
|
|
||||||
|
# Подсчитываем статистику
|
||||||
|
total_tasks = len(tasks)
|
||||||
|
pending_tasks = len([t for t in tasks.values() if t.get('status') == 'pending'])
|
||||||
|
running_tasks = len([t for t in tasks.values() if t.get('status') == 'running'])
|
||||||
|
completed_tasks = len([t for t in tasks.values() if t.get('status') == 'completed'])
|
||||||
|
failed_tasks = len([t for t in tasks.values() if t.get('status') == 'failed'])
|
||||||
|
|
||||||
|
col1, col2, col3, col4, col5 = st.columns(5)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.metric("Всего", total_tasks, f"+{total_tasks}")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.metric("Ожидают", pending_tasks, f"+{pending_tasks}")
|
||||||
|
|
||||||
|
with col3:
|
||||||
|
st.metric("Выполняются", running_tasks, f"+{running_tasks}")
|
||||||
|
|
||||||
|
with col4:
|
||||||
|
st.metric("Завершены", completed_tasks, f"+{completed_tasks}")
|
||||||
|
|
||||||
|
with col5:
|
||||||
|
st.metric("Ошибки", failed_tasks, f"+{failed_tasks}")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Список задач
|
||||||
|
st.subheader("📋 Список задач")
|
||||||
|
|
||||||
|
# Получаем задачи из глобального хранилища
|
||||||
|
tasks = TASKS_STORAGE
|
||||||
|
|
||||||
|
if tasks:
|
||||||
|
# Показываем задачи
|
||||||
|
for task_id, task in tasks.items():
|
||||||
|
status_emoji = {
|
||||||
|
'pending': '🟡',
|
||||||
|
'running': '🔵',
|
||||||
|
'completed': '🟢',
|
||||||
|
'failed': '🔴'
|
||||||
|
}.get(task.get('status', 'pending'), '⚪')
|
||||||
|
|
||||||
|
with st.expander(f"{status_emoji} {task.get('filename', 'Unknown')} - {task.get('status', 'unknown').upper()}", expanded=True):
|
||||||
|
col1, col2 = st.columns([3, 1])
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.write(f"**ID:** `{task_id}`")
|
||||||
|
st.write(f"**Статус:** {status_emoji} {task.get('status', 'unknown').upper()}")
|
||||||
|
st.write(f"**Файл:** {task.get('filename', 'Unknown')}")
|
||||||
|
st.write(f"**Эндпоинт:** {task.get('endpoint', 'Unknown')}")
|
||||||
|
|
||||||
|
# Показываем прогресс для выполняющихся задач
|
||||||
|
if task.get('status') == 'running':
|
||||||
|
progress = task.get('progress', 0)
|
||||||
|
st.write(f"**Прогресс:** {progress}%")
|
||||||
|
st.progress(progress / 100)
|
||||||
|
|
||||||
|
# Показываем время выполнения
|
||||||
|
if task.get('started_at'):
|
||||||
|
started_time = datetime.fromtimestamp(task['started_at']).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
st.write(f"**Начата:** {started_time}")
|
||||||
|
|
||||||
|
if task.get('completed_at'):
|
||||||
|
completed_time = datetime.fromtimestamp(task['completed_at']).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
st.write(f"**Завершена:** {completed_time}")
|
||||||
|
|
||||||
|
# Показываем длительность
|
||||||
|
if task.get('started_at'):
|
||||||
|
duration = task['completed_at'] - task['started_at']
|
||||||
|
st.write(f"**Длительность:** {duration:.1f} сек")
|
||||||
|
|
||||||
|
if task.get('result'):
|
||||||
|
result = task['result']
|
||||||
|
if task.get('status') == 'completed':
|
||||||
|
st.success(f"✅ {result.get('message', 'Задача выполнена')}")
|
||||||
|
if result.get('object_id'):
|
||||||
|
st.info(f"ID объекта: {result['object_id']}")
|
||||||
|
else:
|
||||||
|
st.error(f"❌ {result.get('message', 'Ошибка выполнения')}")
|
||||||
|
|
||||||
|
if task.get('error'):
|
||||||
|
st.error(f"❌ Ошибка: {task['error']}")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
if task.get('status') in ['pending', 'running']:
|
||||||
|
if st.button("❌ Отменить", key=f"cancel_{task_id}_btn", use_container_width=True):
|
||||||
|
st.info("Функция отмены будет реализована в следующих версиях")
|
||||||
|
else:
|
||||||
|
if st.button("🗑️ Удалить", key=f"delete_{task_id}_btn", use_container_width=True):
|
||||||
|
# Удаляем задачу из глобального хранилища
|
||||||
|
if task_id in TASKS_STORAGE:
|
||||||
|
del TASKS_STORAGE[task_id]
|
||||||
|
st.rerun()
|
||||||
|
else:
|
||||||
|
# Пустое состояние
|
||||||
|
st.info("""
|
||||||
|
**Нет активных задач**
|
||||||
|
|
||||||
|
Загрузите файл на странице "Асинхронная загрузка", чтобы создать новую задачу.
|
||||||
|
Здесь вы сможете отслеживать прогресс обработки и управлять задачами.
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Кнопка для создания тестовой задачи
|
||||||
|
if st.button("🧪 Создать тестовую задачу", key="create_test_task_btn"):
|
||||||
|
test_task_id = f"test_task_{int(time.time())}"
|
||||||
|
|
||||||
|
TASKS_STORAGE[test_task_id] = {
|
||||||
|
'status': 'completed',
|
||||||
|
'filename': 'test_file.zip',
|
||||||
|
'endpoint': '/test/upload',
|
||||||
|
'result': {'message': 'Тестовая задача выполнена', 'object_id': 'test-123'},
|
||||||
|
'started_at': time.time() - 5, # 5 секунд назад
|
||||||
|
'completed_at': time.time(),
|
||||||
|
'progress': 100
|
||||||
|
}
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Информация о статусах задач
|
||||||
|
with st.expander("ℹ️ Статусы задач"):
|
||||||
|
st.markdown("""
|
||||||
|
**Статусы задач:**
|
||||||
|
- 🟡 **Ожидает** - задача создана и ожидает выполнения
|
||||||
|
- 🔵 **Выполняется** - задача обрабатывается
|
||||||
|
- 🟢 **Завершена** - задача успешно выполнена
|
||||||
|
- 🔴 **Ошибка** - произошла ошибка при выполнении
|
||||||
|
- ⚫ **Отменена** - задача была отменена пользователем
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
- ❌ **Отменить** - отменить выполнение задачи
|
||||||
|
- 🔄 **Обновить** - обновить статус задачи
|
||||||
|
- 📊 **Детали** - просмотреть подробную информацию
|
||||||
|
""")
|
||||||
44
tests/README.md
Normal file
44
tests/README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Тесты для парсеров
|
||||||
|
|
||||||
|
Этот каталог содержит pytest тесты для всех парсеров и их геттеров.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
- est_parsers.py - Основные тесты для всех парсеров
|
||||||
|
- conftest.py - Конфигурация pytest
|
||||||
|
-
|
||||||
|
equirements.txt - Зависимости для тестов
|
||||||
|
- est_data/ - Тестовые данные
|
||||||
|
|
||||||
|
## Запуск тестов
|
||||||
|
|
||||||
|
`ash
|
||||||
|
# Установка зависимостей
|
||||||
|
pip install -r tests/requirements.txt
|
||||||
|
|
||||||
|
# Запуск всех тестов
|
||||||
|
pytest tests/
|
||||||
|
|
||||||
|
# Запуск конкретного теста
|
||||||
|
pytest tests/test_parsers.py::TestSvodkaPMParser
|
||||||
|
|
||||||
|
# Запуск с подробным выводом
|
||||||
|
pytest tests/ -v
|
||||||
|
|
||||||
|
# Запуск с покрытием кода
|
||||||
|
pytest tests/ --cov=python_parser
|
||||||
|
`
|
||||||
|
|
||||||
|
## Покрытие тестами
|
||||||
|
|
||||||
|
Тесты покрывают:
|
||||||
|
- Инициализацию всех парсеров
|
||||||
|
- Все геттеры каждого парсера
|
||||||
|
- Обработку валидных и невалидных параметров
|
||||||
|
- Интеграционные тесты
|
||||||
|
|
||||||
|
## Добавление новых тестов
|
||||||
|
|
||||||
|
При добавлении нового парсера:
|
||||||
|
1. Добавьте класс тестов в est_parsers.py
|
||||||
|
2. Создайте тесты для всех геттеров
|
||||||
28
tests/conftest.py
Normal file
28
tests/conftest.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
Конфигурация pytest для тестов парсеров
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем путь к проекту
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python_parser'))
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_data_dir():
|
||||||
|
"""Путь к директории с тестовыми данными"""
|
||||||
|
return os.path.join(os.path.dirname(__file__), 'test_data')
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_data():
|
||||||
|
"""Моковые данные для тестов"""
|
||||||
|
return {
|
||||||
|
'SNPZ': {
|
||||||
|
'data': 'test_data',
|
||||||
|
'records_count': 10
|
||||||
|
},
|
||||||
|
'KNPZ': {
|
||||||
|
'data': 'test_data_2',
|
||||||
|
'records_count': 5
|
||||||
|
}
|
||||||
|
}
|
||||||
4
tests/requirements.txt
Normal file
4
tests/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pytest>=7.0.0
|
||||||
|
pandas>=1.5.0
|
||||||
|
numpy>=1.20.0
|
||||||
|
openpyxl>=3.0.0
|
||||||
BIN
tests/test_data/monitoring.zip
Normal file
BIN
tests/test_data/monitoring.zip
Normal file
Binary file not shown.
BIN
tests/test_data/oper_spavka_tech_pos_SNPZ.zip
Normal file
BIN
tests/test_data/oper_spavka_tech_pos_SNPZ.zip
Normal file
Binary file not shown.
BIN
tests/test_data/pm_all.zip
Normal file
BIN
tests/test_data/pm_all.zip
Normal file
Binary file not shown.
BIN
tests/test_data/svodka_tar.zip
Normal file
BIN
tests/test_data/svodka_tar.zip
Normal file
Binary file not shown.
394
tests/test_parsers.py
Normal file
394
tests/test_parsers.py
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
"""
|
||||||
|
Тесты для всех парсеров и их геттеров
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import pandas as pd
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Добавляем путь к проекту
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python_parser'))
|
||||||
|
|
||||||
|
from adapters.parsers import (
|
||||||
|
SvodkaPMParser,
|
||||||
|
SvodkaCAParser,
|
||||||
|
MonitoringFuelParser,
|
||||||
|
MonitoringTarParser,
|
||||||
|
SvodkaRepairCAParser,
|
||||||
|
StatusesRepairCAParser,
|
||||||
|
OperSpravkaTechPosParser
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSvodkaPMParser:
|
||||||
|
"""Тесты для парсера Сводки ПМ"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = SvodkaPMParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'SNPZ': pd.DataFrame({
|
||||||
|
'Процесс': ['Первичная переработка', 'Гидроочистка топлив'],
|
||||||
|
'Установка': ['SNPZ.EAVT6', 'SNPZ.L24-6'],
|
||||||
|
'План, т': [100.0, 200.0],
|
||||||
|
'Факт, т': [95.0, 190.0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "Сводки ПМ"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 2
|
||||||
|
assert 'single_og' in self.parser.getters
|
||||||
|
assert 'total_ogs' in self.parser.getters
|
||||||
|
|
||||||
|
def test_single_og_getter(self):
|
||||||
|
"""Тест геттера single_og"""
|
||||||
|
params = {
|
||||||
|
'id': 'SNPZ',
|
||||||
|
'codes': [78, 79],
|
||||||
|
'columns': ['ПП', 'СЭБ']
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.parser.get_value('single_og', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, str) # Возвращает JSON строку
|
||||||
|
|
||||||
|
def test_total_ogs_getter(self):
|
||||||
|
"""Тест геттера total_ogs"""
|
||||||
|
params = {
|
||||||
|
'codes': [78, 79],
|
||||||
|
'columns': ['ПП', 'СЭБ']
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.parser.get_value('total_ogs', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, str) # Возвращает JSON строку
|
||||||
|
|
||||||
|
def test_getter_with_invalid_params(self):
|
||||||
|
"""Тест геттера с неверными параметрами"""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
self.parser.get_value('single_og', {'invalid': 'params'})
|
||||||
|
|
||||||
|
def test_getter_with_nonexistent_og(self):
|
||||||
|
"""Тест геттера с несуществующим ОГ"""
|
||||||
|
params = {
|
||||||
|
'id': 'NONEXISTENT',
|
||||||
|
'codes': [78, 79],
|
||||||
|
'columns': ['ПП', 'СЭБ']
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.parser.get_value('single_og', params)
|
||||||
|
# Должен вернуть пустой результат, но не упасть
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestSvodkaCAParser:
|
||||||
|
"""Тесты для парсера Сводки СА"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = SvodkaCAParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'plan': {
|
||||||
|
'ТиП': pd.DataFrame({
|
||||||
|
'ОГ': ['SNPZ', 'KNPZ'],
|
||||||
|
'Значение': [100.0, 200.0]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'fact': {
|
||||||
|
'ТиП': pd.DataFrame({
|
||||||
|
'ОГ': ['SNPZ', 'KNPZ'],
|
||||||
|
'Значение': [95.0, 190.0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "Сводки СА"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 1
|
||||||
|
assert 'get_ca_data' in self.parser.getters
|
||||||
|
|
||||||
|
def test_get_ca_data_getter(self):
|
||||||
|
"""Тест геттера get_ca_data"""
|
||||||
|
params = {
|
||||||
|
'modes': ['plan', 'fact'],
|
||||||
|
'tables': ['ТиП', 'Топливо']
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_ca_data', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, dict) # Возвращает словарь
|
||||||
|
|
||||||
|
|
||||||
|
class TestMonitoringFuelParser:
|
||||||
|
"""Тесты для парсера Мониторинга топлива"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = MonitoringFuelParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'SNPZ': pd.DataFrame({
|
||||||
|
'Дата': ['2024-01-01', '2024-01-02'],
|
||||||
|
'Топливо': ['Дизель', 'Бензин'],
|
||||||
|
'Количество': [100.0, 200.0],
|
||||||
|
'Объем': [50.0, 75.0] # Добавляем числовую колонку для агрегации
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "Мониторинг топлива"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
# Проверяем, что есть геттеры
|
||||||
|
assert len(self.parser.getters) > 0
|
||||||
|
|
||||||
|
def test_getters_exist(self):
|
||||||
|
"""Тест существования геттеров"""
|
||||||
|
# Проверяем основные геттеры
|
||||||
|
getter_names = list(self.parser.getters.keys())
|
||||||
|
assert len(getter_names) > 0
|
||||||
|
|
||||||
|
# Тестируем каждый геттер с правильными параметрами
|
||||||
|
for getter_name in getter_names:
|
||||||
|
if getter_name == 'total_by_columns':
|
||||||
|
params = {'columns': ['Количество', 'Объем']} # Используем числовые колонки
|
||||||
|
else:
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.parser.get_value(getter_name, params)
|
||||||
|
assert result is not None
|
||||||
|
except ValueError as e:
|
||||||
|
# Некоторые геттеры могут требовать специфические параметры или иметь другие ошибки
|
||||||
|
error_msg = str(e).lower()
|
||||||
|
assert any(keyword in error_msg for keyword in ["required", "missing", "отсутствуют", "обязательные", "ошибка выполнения"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestMonitoringTarParser:
|
||||||
|
"""Тесты для парсера Мониторинга ТЭР"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = MonitoringTarParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'total': [pd.DataFrame({
|
||||||
|
'Дата': ['2024-01-01'],
|
||||||
|
'Потребление': [100.0]
|
||||||
|
})],
|
||||||
|
'last_day': [pd.DataFrame({
|
||||||
|
'Дата': ['2024-01-02'],
|
||||||
|
'Потребление': [150.0]
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "monitoring_tar"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 2
|
||||||
|
assert 'get_tar_data' in self.parser.getters
|
||||||
|
assert 'get_tar_full_data' in self.parser.getters
|
||||||
|
|
||||||
|
def test_get_tar_data_getter(self):
|
||||||
|
"""Тест геттера get_tar_data"""
|
||||||
|
params = {'mode': 'total'}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_tar_data', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, str) # Возвращает JSON строку
|
||||||
|
|
||||||
|
def test_get_tar_full_data_getter(self):
|
||||||
|
"""Тест геттера get_tar_full_data"""
|
||||||
|
result = self.parser.get_value('get_tar_full_data', {})
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, str) # Возвращает JSON строку
|
||||||
|
|
||||||
|
|
||||||
|
class TestSvodkaRepairCAParser:
|
||||||
|
"""Тесты для парсера Сводки ремонта СА"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = SvodkaRepairCAParser()
|
||||||
|
# Создаем тестовые данные в правильном формате
|
||||||
|
self.test_data = [
|
||||||
|
{
|
||||||
|
'id': 'SNPZ',
|
||||||
|
'Тип_ремонта': 'Капитальный',
|
||||||
|
'Статус': 'Завершен',
|
||||||
|
'Дата': '2024-01-01'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'SNPZ',
|
||||||
|
'Тип_ремонта': 'Текущий',
|
||||||
|
'Статус': 'В работе',
|
||||||
|
'Дата': '2024-01-02'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "Сводки ремонта СА"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 1
|
||||||
|
assert 'get_repair_data' in self.parser.getters
|
||||||
|
|
||||||
|
def test_get_repair_data_getter(self):
|
||||||
|
"""Тест геттера get_repair_data"""
|
||||||
|
params = {
|
||||||
|
'og_ids': ['SNPZ'],
|
||||||
|
'repair_types': ['КР'],
|
||||||
|
'include_planned': True,
|
||||||
|
'include_factual': True
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_repair_data', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, dict) # Возвращает словарь
|
||||||
|
|
||||||
|
|
||||||
|
class TestStatusesRepairCAParser:
|
||||||
|
"""Тесты для парсера Статусов ремонта СА"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = StatusesRepairCAParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'SNPZ': pd.DataFrame({
|
||||||
|
'Статус': ['В работе', 'Завершен'],
|
||||||
|
'Процент': [50.0, 100.0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "Статусы ремонта СА"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 1
|
||||||
|
assert 'get_repair_statuses' in self.parser.getters
|
||||||
|
|
||||||
|
def test_get_repair_statuses_getter(self):
|
||||||
|
"""Тест геттера get_repair_statuses"""
|
||||||
|
params = {'ids': ['SNPZ']}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_repair_statuses', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, list) # Возвращает список
|
||||||
|
|
||||||
|
|
||||||
|
class TestOperSpravkaTechPosParser:
|
||||||
|
"""Тесты для парсера Операционных справок технологических позиций"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = OperSpravkaTechPosParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'SNPZ': pd.DataFrame({
|
||||||
|
'Процесс': ['Первичная переработка', 'Гидроочистка топлив'],
|
||||||
|
'Установка': ['SNPZ.EAVT6', 'SNPZ.L24-6'],
|
||||||
|
'План, т': [100.0, 200.0],
|
||||||
|
'Факт, т': [95.0, 190.0],
|
||||||
|
'id': ['SNPZ.EAVT6', 'SNPZ.L24-6']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "oper_spravka_tech_pos"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 1
|
||||||
|
assert 'get_tech_pos' in self.parser.getters
|
||||||
|
|
||||||
|
def test_get_tech_pos_getter(self):
|
||||||
|
"""Тест геттера get_tech_pos"""
|
||||||
|
params = {'id': 'SNPZ'}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_tech_pos', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, list) # Возвращает список словарей
|
||||||
|
|
||||||
|
def test_get_tech_pos_with_nonexistent_og(self):
|
||||||
|
"""Тест геттера с несуществующим ОГ"""
|
||||||
|
params = {'id': 'NONEXISTENT'}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_tech_pos', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, list)
|
||||||
|
assert len(result) == 0 # Пустой список для несуществующего ОГ
|
||||||
|
|
||||||
|
|
||||||
|
class TestParserIntegration:
|
||||||
|
"""Интеграционные тесты для всех парсеров"""
|
||||||
|
|
||||||
|
def test_all_parsers_have_getters(self):
|
||||||
|
"""Тест, что все парсеры имеют геттеры"""
|
||||||
|
parsers = [
|
||||||
|
SvodkaPMParser(),
|
||||||
|
SvodkaCAParser(),
|
||||||
|
MonitoringFuelParser(),
|
||||||
|
MonitoringTarParser(),
|
||||||
|
SvodkaRepairCAParser(),
|
||||||
|
StatusesRepairCAParser(),
|
||||||
|
OperSpravkaTechPosParser()
|
||||||
|
]
|
||||||
|
|
||||||
|
for parser in parsers:
|
||||||
|
assert hasattr(parser, 'getters')
|
||||||
|
assert len(parser.getters) > 0
|
||||||
|
assert hasattr(parser, 'name')
|
||||||
|
assert parser.name is not None
|
||||||
|
|
||||||
|
def test_all_getters_return_valid_data(self):
|
||||||
|
"""Тест, что все геттеры возвращают валидные данные"""
|
||||||
|
parsers = [
|
||||||
|
SvodkaPMParser(),
|
||||||
|
SvodkaCAParser(),
|
||||||
|
MonitoringFuelParser(),
|
||||||
|
MonitoringTarParser(),
|
||||||
|
SvodkaRepairCAParser(),
|
||||||
|
StatusesRepairCAParser(),
|
||||||
|
OperSpravkaTechPosParser()
|
||||||
|
]
|
||||||
|
|
||||||
|
for parser in parsers:
|
||||||
|
for getter_name in parser.getters.keys():
|
||||||
|
try:
|
||||||
|
result = parser.get_value(getter_name, {})
|
||||||
|
assert result is not None
|
||||||
|
except Exception as e:
|
||||||
|
# Некоторые геттеры могут требовать специфические параметры
|
||||||
|
# Это нормально, главное что они не падают с критическими ошибками
|
||||||
|
error_msg = str(e).lower()
|
||||||
|
assert any(keyword in error_msg for keyword in ["required", "missing", "отсутствуют", "обязательные"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
Reference in New Issue
Block a user