Compare commits
2 Commits
4aca4ed6c6
...
add-new-pa
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c0fce128d | |||
| b5c460bb6f |
1002
PARSER_DEVELOPMENT_GUIDE.md
Normal file
1002
PARSER_DEVELOPMENT_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
BIN
monitoring_tar_correct.zip
Normal file
BIN
monitoring_tar_correct.zip
Normal file
Binary file not shown.
BIN
monitoring_tar_test.zip
Normal file
BIN
monitoring_tar_test.zip
Normal file
Binary file not shown.
BIN
python_parser/adapters/monitoring_tar_test.zip
Normal file
BIN
python_parser/adapters/monitoring_tar_test.zip
Normal file
Binary file not shown.
@@ -1,13 +1,17 @@
|
|||||||
from .monitoring_fuel import MonitoringFuelParser
|
from .monitoring_fuel import MonitoringFuelParser
|
||||||
|
from .monitoring_tar import MonitoringTarParser
|
||||||
from .svodka_ca import SvodkaCAParser
|
from .svodka_ca import SvodkaCAParser
|
||||||
from .svodka_pm import SvodkaPMParser
|
from .svodka_pm import SvodkaPMParser
|
||||||
from .svodka_repair_ca import SvodkaRepairCAParser
|
from .svodka_repair_ca import SvodkaRepairCAParser
|
||||||
from .statuses_repair_ca import StatusesRepairCAParser
|
from .statuses_repair_ca import StatusesRepairCAParser
|
||||||
|
from .oper_spravka_tech_pos import OperSpravkaTechPosParser
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'MonitoringFuelParser',
|
'MonitoringFuelParser',
|
||||||
|
'MonitoringTarParser',
|
||||||
'SvodkaCAParser',
|
'SvodkaCAParser',
|
||||||
'SvodkaPMParser',
|
'SvodkaPMParser',
|
||||||
'SvodkaRepairCAParser',
|
'SvodkaRepairCAParser',
|
||||||
'StatusesRepairCAParser'
|
'StatusesRepairCAParser',
|
||||||
|
'OperSpravkaTechPosParser'
|
||||||
]
|
]
|
||||||
|
|||||||
302
python_parser/adapters/parsers/monitoring_tar.py
Normal file
302
python_parser/adapters/parsers/monitoring_tar.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
import tempfile
|
||||||
|
import pandas as pd
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
from core.ports import ParserPort
|
||||||
|
from adapters.pconfig import find_header_row, SNPZ_IDS, data_to_json
|
||||||
|
|
||||||
|
|
||||||
|
class MonitoringTarParser(ParserPort):
|
||||||
|
"""Парсер для мониторинга ТЭР (топливно-энергетических ресурсов)"""
|
||||||
|
|
||||||
|
name = "monitoring_tar"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.data_dict = {}
|
||||||
|
self.df = None
|
||||||
|
|
||||||
|
# Регистрируем геттеры
|
||||||
|
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=[])
|
||||||
|
|
||||||
|
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
|
||||||
|
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
|
||||||
|
print(f"🔍 DEBUG: MonitoringTarParser.parse вызван с файлом: {file_path}")
|
||||||
|
|
||||||
|
if not file_path.endswith('.zip'):
|
||||||
|
raise ValueError("MonitoringTarParser поддерживает только ZIP архивы")
|
||||||
|
|
||||||
|
# Обрабатываем ZIP архив
|
||||||
|
result = self._parse_zip_archive(file_path)
|
||||||
|
|
||||||
|
# Конвертируем результат в DataFrame для совместимости с ReportService
|
||||||
|
if result:
|
||||||
|
data_list = []
|
||||||
|
for id, data in result.items():
|
||||||
|
data_list.append({
|
||||||
|
'id': id,
|
||||||
|
'data': data,
|
||||||
|
'records_count': len(data.get('total', [])) + len(data.get('last_day', []))
|
||||||
|
})
|
||||||
|
|
||||||
|
df = pd.DataFrame(data_list)
|
||||||
|
print(f"🔍 DEBUG: Создан DataFrame с {len(df)} записями")
|
||||||
|
return df
|
||||||
|
else:
|
||||||
|
print("🔍 DEBUG: Возвращаем пустой DataFrame")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
def _parse_zip_archive(self, zip_path: str) -> Dict[str, Any]:
|
||||||
|
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
|
||||||
|
print(f"📦 Обработка ZIP архива: {zip_path}")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(temp_dir)
|
||||||
|
|
||||||
|
# Ищем файлы мониторинга ТЭР
|
||||||
|
tar_files = []
|
||||||
|
for root, dirs, files in os.walk(temp_dir):
|
||||||
|
for file in files:
|
||||||
|
# Поддерживаем файлы svodka_tar_*.xlsx (основные) и monitoring_*.xlsm (альтернативные)
|
||||||
|
if (file.startswith('svodka_tar_') and file.endswith('.xlsx')) or (file.startswith('monitoring_') and file.endswith('.xlsm')):
|
||||||
|
tar_files.append(os.path.join(root, file))
|
||||||
|
|
||||||
|
if not tar_files:
|
||||||
|
raise ValueError("В архиве не найдены файлы мониторинга ТЭР")
|
||||||
|
|
||||||
|
print(f"📁 Найдено {len(tar_files)} файлов мониторинга ТЭР")
|
||||||
|
|
||||||
|
# Обрабатываем каждый файл
|
||||||
|
all_data = {}
|
||||||
|
for file_path in tar_files:
|
||||||
|
print(f"📁 Обработка файла: {file_path}")
|
||||||
|
|
||||||
|
# Извлекаем номер месяца из имени файла
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
month_str = self._extract_month_from_filename(filename)
|
||||||
|
print(f"📅 Месяц: {month_str}")
|
||||||
|
|
||||||
|
# Парсим файл
|
||||||
|
file_data = self._parse_single_file(file_path, month_str)
|
||||||
|
if file_data:
|
||||||
|
all_data.update(file_data)
|
||||||
|
|
||||||
|
return all_data
|
||||||
|
|
||||||
|
def _extract_month_from_filename(self, filename: str) -> str:
|
||||||
|
"""Извлекает номер месяца из имени файла"""
|
||||||
|
# Для файлов типа svodka_tar_SNPZ_01.xlsx или monitoring_SNPZ_01.xlsm
|
||||||
|
parts = filename.split('_')
|
||||||
|
if len(parts) >= 3:
|
||||||
|
month_part = parts[-1].split('.')[0] # Убираем расширение
|
||||||
|
if month_part.isdigit():
|
||||||
|
return month_part
|
||||||
|
return "01" # По умолчанию
|
||||||
|
|
||||||
|
def _parse_single_file(self, file_path: str, month_str: str) -> Dict[str, Any]:
|
||||||
|
"""Парсит один файл мониторинга ТЭР"""
|
||||||
|
try:
|
||||||
|
excel_file = pd.ExcelFile(file_path)
|
||||||
|
available_sheets = excel_file.sheet_names
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Не удалось открыть Excel-файл {file_path}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Словарь для хранения данных: id -> {'total': [], 'last_day': []}
|
||||||
|
df_svodka_tar = {}
|
||||||
|
|
||||||
|
# Определяем тип файла и обрабатываем соответственно
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
|
||||||
|
if filename.startswith('svodka_tar_'):
|
||||||
|
# Обрабатываем файлы svodka_tar_*.xlsx с SNPZ_IDS
|
||||||
|
for name, id in SNPZ_IDS.items():
|
||||||
|
if name not in available_sheets:
|
||||||
|
print(f"🟡 Лист '{name}' отсутствует в файле {file_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Парсим оба типа строк
|
||||||
|
result = self._parse_monitoring_tar_single(file_path, name, month_str)
|
||||||
|
|
||||||
|
# Инициализируем структуру для id
|
||||||
|
if id not in df_svodka_tar:
|
||||||
|
df_svodka_tar[id] = {'total': [], 'last_day': []}
|
||||||
|
|
||||||
|
if isinstance(result['total'], pd.DataFrame) and not result['total'].empty:
|
||||||
|
df_svodka_tar[id]['total'].append(result['total'])
|
||||||
|
|
||||||
|
if isinstance(result['last_day'], pd.DataFrame) and not result['last_day'].empty:
|
||||||
|
df_svodka_tar[id]['last_day'].append(result['last_day'])
|
||||||
|
|
||||||
|
elif filename.startswith('monitoring_'):
|
||||||
|
# Обрабатываем файлы monitoring_*.xlsm с альтернативными листами
|
||||||
|
monitoring_sheets = {
|
||||||
|
'Мониторинг потребления': 'SNPZ.MONITORING',
|
||||||
|
'Исходные данные': 'SNPZ.SOURCE_DATA'
|
||||||
|
}
|
||||||
|
|
||||||
|
for sheet_name, id in monitoring_sheets.items():
|
||||||
|
if sheet_name not in available_sheets:
|
||||||
|
print(f"🟡 Лист '{sheet_name}' отсутствует в файле {file_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Парсим оба типа строк
|
||||||
|
result = self._parse_monitoring_tar_single(file_path, sheet_name, month_str)
|
||||||
|
|
||||||
|
# Инициализируем структуру для id
|
||||||
|
if id not in df_svodka_tar:
|
||||||
|
df_svodka_tar[id] = {'total': [], 'last_day': []}
|
||||||
|
|
||||||
|
if isinstance(result['total'], pd.DataFrame) and not result['total'].empty:
|
||||||
|
df_svodka_tar[id]['total'].append(result['total'])
|
||||||
|
|
||||||
|
if isinstance(result['last_day'], pd.DataFrame) and not result['last_day'].empty:
|
||||||
|
df_svodka_tar[id]['last_day'].append(result['last_day'])
|
||||||
|
|
||||||
|
# Агрегация: объединяем списки в DataFrame
|
||||||
|
for id, data in df_svodka_tar.items():
|
||||||
|
if data['total']:
|
||||||
|
df_svodka_tar[id]['total'] = pd.concat(data['total'], ignore_index=True)
|
||||||
|
else:
|
||||||
|
df_svodka_tar[id]['total'] = pd.DataFrame()
|
||||||
|
|
||||||
|
if data['last_day']:
|
||||||
|
df_svodka_tar[id]['last_day'] = pd.concat(data['last_day'], ignore_index=True)
|
||||||
|
else:
|
||||||
|
df_svodka_tar[id]['last_day'] = pd.DataFrame()
|
||||||
|
|
||||||
|
print(f"✅ Агрегировано: {len(df_svodka_tar[id]['total'])} 'total' и "
|
||||||
|
f"{len(df_svodka_tar[id]['last_day'])} 'last_day' записей для id='{id}'")
|
||||||
|
|
||||||
|
return df_svodka_tar
|
||||||
|
|
||||||
|
def _parse_monitoring_tar_single(self, file: str, sheet: str, month_str: str) -> Dict[str, Any]:
|
||||||
|
"""Парсит один файл и лист"""
|
||||||
|
try:
|
||||||
|
# Проверяем наличие листа
|
||||||
|
if sheet not in pd.ExcelFile(file).sheet_names:
|
||||||
|
print(f"🟡 Лист '{sheet}' не найден в файле {file}")
|
||||||
|
return {'total': None, 'last_day': None}
|
||||||
|
|
||||||
|
# Определяем номер заголовка в зависимости от типа файла
|
||||||
|
filename = os.path.basename(file)
|
||||||
|
if filename.startswith('svodka_tar_'):
|
||||||
|
# Для файлов svodka_tar_*.xlsx ищем заголовок по значению "1"
|
||||||
|
header_num = find_header_row(file, sheet, search_value="1")
|
||||||
|
if header_num is None:
|
||||||
|
print(f"❌ Не найдена строка с заголовком '1' в файле {file}, лист '{sheet}'")
|
||||||
|
return {'total': None, 'last_day': None}
|
||||||
|
elif filename.startswith('monitoring_'):
|
||||||
|
# Для файлов monitoring_*.xlsm заголовок находится в строке 5
|
||||||
|
header_num = 5
|
||||||
|
else:
|
||||||
|
print(f"❌ Неизвестный тип файла: {filename}")
|
||||||
|
return {'total': None, 'last_day': None}
|
||||||
|
|
||||||
|
print(f"🔍 DEBUG: Используем заголовок в строке {header_num} для листа '{sheet}'")
|
||||||
|
|
||||||
|
# Читаем с двумя уровнями заголовков
|
||||||
|
df = pd.read_excel(
|
||||||
|
file,
|
||||||
|
sheet_name=sheet,
|
||||||
|
header=header_num,
|
||||||
|
index_col=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Убираем мультииндекс: оставляем первый уровень
|
||||||
|
df.columns = df.columns.get_level_values(0)
|
||||||
|
|
||||||
|
# Удаляем строки, где все значения — NaN
|
||||||
|
df = df.dropna(how='all').reset_index(drop=True)
|
||||||
|
if df.empty:
|
||||||
|
print(f"🟡 Нет данных после очистки в файле {file}, лист '{sheet}'")
|
||||||
|
return {'total': None, 'last_day': None}
|
||||||
|
|
||||||
|
# === 1. Обработка строки "Всего" ===
|
||||||
|
first_col = df.columns[0]
|
||||||
|
mask_total = df[first_col].astype(str).str.strip() == "Всего"
|
||||||
|
df_total = df[mask_total].copy()
|
||||||
|
|
||||||
|
if not df_total.empty:
|
||||||
|
# Заменяем "Всего" на номер месяца в первой колонке
|
||||||
|
df_total.loc[:, first_col] = df_total[first_col].astype(str).str.replace("Всего", month_str, regex=False)
|
||||||
|
df_total = df_total.reset_index(drop=True)
|
||||||
|
else:
|
||||||
|
df_total = pd.DataFrame()
|
||||||
|
|
||||||
|
# === 2. Обработка последней строки (не пустая) ===
|
||||||
|
# Берём последнюю строку из исходного df (не включая "Всего", если она внизу)
|
||||||
|
# Исключим строку "Всего" из "последней строки", если она есть
|
||||||
|
df_no_total = df[~mask_total].dropna(how='all')
|
||||||
|
if not df_no_total.empty:
|
||||||
|
df_last_day = df_no_total.tail(1).copy()
|
||||||
|
df_last_day = df_last_day.reset_index(drop=True)
|
||||||
|
else:
|
||||||
|
df_last_day = pd.DataFrame()
|
||||||
|
|
||||||
|
return {'total': df_total, 'last_day': df_last_day}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при обработке файла {file}, лист '{sheet}': {e}")
|
||||||
|
return {'total': None, 'last_day': None}
|
||||||
|
|
||||||
|
def _get_tar_data_wrapper(self, params: Dict[str, Any] = None) -> str:
|
||||||
|
"""Обертка для получения данных мониторинга ТЭР с фильтрацией по режиму"""
|
||||||
|
print(f"🔍 DEBUG: _get_tar_data_wrapper вызван с параметрами: {params}")
|
||||||
|
|
||||||
|
# Получаем режим из параметров
|
||||||
|
mode = params.get('mode', 'total') if params else 'total'
|
||||||
|
|
||||||
|
# Фильтруем данные по режиму
|
||||||
|
filtered_data = {}
|
||||||
|
if hasattr(self, 'df') and self.df is not None and not self.df.empty:
|
||||||
|
# Данные из MinIO
|
||||||
|
for _, row in self.df.iterrows():
|
||||||
|
id = row['id']
|
||||||
|
data = row['data']
|
||||||
|
if isinstance(data, dict) and mode in data:
|
||||||
|
filtered_data[id] = data[mode]
|
||||||
|
else:
|
||||||
|
filtered_data[id] = pd.DataFrame()
|
||||||
|
elif hasattr(self, 'data_dict') and self.data_dict:
|
||||||
|
# Локальные данные
|
||||||
|
for id, data in self.data_dict.items():
|
||||||
|
if isinstance(data, dict) and mode in data:
|
||||||
|
filtered_data[id] = data[mode]
|
||||||
|
else:
|
||||||
|
filtered_data[id] = pd.DataFrame()
|
||||||
|
|
||||||
|
# Конвертируем в JSON
|
||||||
|
try:
|
||||||
|
result_json = data_to_json(filtered_data)
|
||||||
|
return result_json
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при конвертации данных в JSON: {e}")
|
||||||
|
return "{}"
|
||||||
|
|
||||||
|
def _get_tar_full_data_wrapper(self, params: Dict[str, Any] = None) -> str:
|
||||||
|
"""Обертка для получения всех данных мониторинга ТЭР"""
|
||||||
|
print(f"🔍 DEBUG: _get_tar_full_data_wrapper вызван с параметрами: {params}")
|
||||||
|
|
||||||
|
# Получаем все данные
|
||||||
|
full_data = {}
|
||||||
|
if hasattr(self, 'df') and self.df is not None and not self.df.empty:
|
||||||
|
# Данные из MinIO
|
||||||
|
for _, row in self.df.iterrows():
|
||||||
|
id = row['id']
|
||||||
|
data = row['data']
|
||||||
|
full_data[id] = data
|
||||||
|
elif hasattr(self, 'data_dict') and self.data_dict:
|
||||||
|
# Локальные данные
|
||||||
|
full_data = self.data_dict
|
||||||
|
|
||||||
|
# Конвертируем в JSON
|
||||||
|
try:
|
||||||
|
result_json = data_to_json(full_data)
|
||||||
|
return result_json
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при конвертации данных в JSON: {e}")
|
||||||
|
return "{}"
|
||||||
281
python_parser/adapters/parsers/oper_spravka_tech_pos.py
Normal file
281
python_parser/adapters/parsers/oper_spravka_tech_pos.py
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
import pandas as pd
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
from datetime import datetime
|
||||||
|
from core.ports import ParserPort
|
||||||
|
from adapters.pconfig import find_header_row, get_object_by_name, data_to_json
|
||||||
|
|
||||||
|
|
||||||
|
class OperSpravkaTechPosParser(ParserPort):
|
||||||
|
"""Парсер для операционных справок технологических позиций"""
|
||||||
|
|
||||||
|
name = "oper_spravka_tech_pos"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.data_dict = {}
|
||||||
|
self.df = None
|
||||||
|
|
||||||
|
# Регистрируем геттер
|
||||||
|
self.register_getter('get_tech_pos', self._get_tech_pos_wrapper, required_params=['id'])
|
||||||
|
|
||||||
|
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
|
||||||
|
"""Парсит ZIP архив с файлами операционных справок технологических позиций"""
|
||||||
|
print(f"🔍 DEBUG: OperSpravkaTechPosParser.parse вызван с файлом: {file_path}")
|
||||||
|
|
||||||
|
if not file_path.endswith('.zip'):
|
||||||
|
raise ValueError("OperSpravkaTechPosParser поддерживает только ZIP архивы")
|
||||||
|
|
||||||
|
# Обрабатываем ZIP архив
|
||||||
|
result = self._parse_zip_archive(file_path)
|
||||||
|
|
||||||
|
# Конвертируем результат в DataFrame для совместимости с ReportService
|
||||||
|
if result:
|
||||||
|
data_list = []
|
||||||
|
for id, data in result.items():
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
records = data.to_dict(orient='records')
|
||||||
|
data_list.append({
|
||||||
|
'id': id,
|
||||||
|
'data': records,
|
||||||
|
'records_count': len(records)
|
||||||
|
})
|
||||||
|
|
||||||
|
df = pd.DataFrame(data_list)
|
||||||
|
print(f"🔍 DEBUG: Создан DataFrame с {len(df)} записями")
|
||||||
|
return df
|
||||||
|
else:
|
||||||
|
print("🔍 DEBUG: Возвращаем пустой DataFrame")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
def _parse_zip_archive(self, zip_path: str) -> Dict[str, pd.DataFrame]:
|
||||||
|
"""Парсит ZIP архив с файлами операционных справок"""
|
||||||
|
print(f"📦 Обработка ZIP архива: {zip_path}")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(temp_dir)
|
||||||
|
|
||||||
|
# Ищем файлы операционных справок
|
||||||
|
tech_pos_files = []
|
||||||
|
for root, dirs, files in os.walk(temp_dir):
|
||||||
|
for file in files:
|
||||||
|
if (file.startswith('oper_spavka_tech_pos_') or
|
||||||
|
file.startswith('oper_spravka_tech_pos_')) and file.endswith(('.xlsx', '.xls', '.xlsm')):
|
||||||
|
tech_pos_files.append(os.path.join(root, file))
|
||||||
|
|
||||||
|
if not tech_pos_files:
|
||||||
|
raise ValueError("В архиве не найдены файлы операционных справок технологических позиций")
|
||||||
|
|
||||||
|
print(f"📁 Найдено {len(tech_pos_files)} файлов операционных справок")
|
||||||
|
|
||||||
|
# Обрабатываем каждый файл
|
||||||
|
all_data = {}
|
||||||
|
for file_path in tech_pos_files:
|
||||||
|
print(f"📁 Обработка файла: {file_path}")
|
||||||
|
|
||||||
|
# Извлекаем ID ОГ из имени файла
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
og_id = self._extract_og_id_from_filename(filename)
|
||||||
|
print(f"🏭 ОГ ID: {og_id}")
|
||||||
|
|
||||||
|
# Парсим файл
|
||||||
|
file_data = self._parse_single_file(file_path)
|
||||||
|
if file_data:
|
||||||
|
all_data.update(file_data)
|
||||||
|
|
||||||
|
return all_data
|
||||||
|
|
||||||
|
def _extract_og_id_from_filename(self, filename: str) -> str:
|
||||||
|
"""Извлекает ID ОГ из имени файла"""
|
||||||
|
# Для файлов типа oper_spavka_tech_pos_SNPZ.xlsx
|
||||||
|
parts = filename.split('_')
|
||||||
|
if len(parts) >= 4:
|
||||||
|
og_id = parts[-1].split('.')[0] # Убираем расширение
|
||||||
|
return og_id
|
||||||
|
return "UNKNOWN"
|
||||||
|
|
||||||
|
def _parse_single_file(self, file_path: str) -> Dict[str, pd.DataFrame]:
|
||||||
|
"""Парсит один файл операционной справки"""
|
||||||
|
try:
|
||||||
|
# Находим актуальный лист
|
||||||
|
actual_sheet = self._find_actual_sheet_num(file_path)
|
||||||
|
print(f"📅 Актуальный лист: {actual_sheet}")
|
||||||
|
|
||||||
|
# Находим заголовок
|
||||||
|
header_row = self._find_header_row(file_path, actual_sheet)
|
||||||
|
print(f"📋 Заголовок найден в строке {header_row}")
|
||||||
|
|
||||||
|
# Парсим данные
|
||||||
|
df = self._parse_tech_pos_data(file_path, actual_sheet, header_row)
|
||||||
|
|
||||||
|
if df is not None and not df.empty:
|
||||||
|
# Извлекаем ID ОГ из имени файла
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
og_id = self._extract_og_id_from_filename(filename)
|
||||||
|
return {og_id: df}
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Нет данных в файле {file_path}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при обработке файла {file_path}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _find_actual_sheet_num(self, file_path: str) -> str:
|
||||||
|
"""Поиск номера актуального листа"""
|
||||||
|
current_day = datetime.now().day
|
||||||
|
current_month = datetime.now().month
|
||||||
|
|
||||||
|
actual_sheet = f"{current_day:02d}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Читаем все листы от 1 до текущего дня
|
||||||
|
all_sheets = {}
|
||||||
|
for day in range(1, current_day + 1):
|
||||||
|
sheet_num = f"{day:02d}"
|
||||||
|
try:
|
||||||
|
df_temp = pd.read_excel(file_path, sheet_name=sheet_num, usecols=[1], nrows=2, header=None)
|
||||||
|
all_sheets[sheet_num] = df_temp
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Идем от текущего дня к 1
|
||||||
|
for day in range(current_day, 0, -1):
|
||||||
|
sheet_num = f"{day:02d}"
|
||||||
|
if sheet_num in all_sheets:
|
||||||
|
df_temp = all_sheets[sheet_num]
|
||||||
|
if df_temp.shape[0] > 1:
|
||||||
|
date_str = df_temp.iloc[1, 0] # B2
|
||||||
|
|
||||||
|
if pd.notna(date_str):
|
||||||
|
try:
|
||||||
|
date = pd.to_datetime(date_str)
|
||||||
|
# Проверяем совпадение месяца даты с текущим месяцем
|
||||||
|
if date.month == current_month:
|
||||||
|
actual_sheet = sheet_num
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Ошибка при поиске актуального листа: {e}")
|
||||||
|
|
||||||
|
return actual_sheet
|
||||||
|
|
||||||
|
def _find_header_row(self, file_path: str, sheet_name: str, search_value: str = "Загрузка основных процессов") -> int:
|
||||||
|
"""Определение индекса заголовка в Excel по ключевому слову"""
|
||||||
|
try:
|
||||||
|
# Читаем первый столбец
|
||||||
|
df_temp = pd.read_excel(file_path, sheet_name=sheet_name, usecols=[0])
|
||||||
|
|
||||||
|
# Ищем строку с искомым значением
|
||||||
|
for idx, row in df_temp.iterrows():
|
||||||
|
if row.astype(str).str.contains(search_value, case=False, regex=False).any():
|
||||||
|
print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
|
||||||
|
return idx + 1 # возвращаем индекс строки (0-based), который будет использован как `header=`
|
||||||
|
|
||||||
|
raise ValueError(f"Не найдена строка с заголовком '{search_value}'.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при поиске заголовка: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _parse_tech_pos_data(self, file_path: str, sheet_name: str, header_row: int) -> pd.DataFrame:
|
||||||
|
"""Парсинг данных технологических позиций"""
|
||||||
|
try:
|
||||||
|
valid_processes = ['Первичная переработка', 'Гидроочистка топлив', 'Риформирование', 'Изомеризация']
|
||||||
|
|
||||||
|
df_temp = pd.read_excel(
|
||||||
|
file_path,
|
||||||
|
sheet_name=sheet_name,
|
||||||
|
header=header_row + 1, # Исправлено: добавляем +1 как в оригинале
|
||||||
|
usecols=range(1, 5)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"🔍 DEBUG: Прочитано {len(df_temp)} строк из Excel")
|
||||||
|
print(f"🔍 DEBUG: Колонки: {list(df_temp.columns)}")
|
||||||
|
|
||||||
|
# Фильтруем по валидным процессам
|
||||||
|
df_cleaned = df_temp[
|
||||||
|
df_temp['Процесс'].str.strip().isin(valid_processes) &
|
||||||
|
df_temp['Процесс'].notna()
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
print(f"🔍 DEBUG: После фильтрации осталось {len(df_cleaned)} строк")
|
||||||
|
|
||||||
|
if df_cleaned.empty:
|
||||||
|
print("⚠️ Нет данных после фильтрации по процессам")
|
||||||
|
print(f"🔍 DEBUG: Доступные процессы в данных: {df_temp['Процесс'].unique()}")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
df_cleaned['Процесс'] = df_cleaned['Процесс'].astype(str).str.strip()
|
||||||
|
|
||||||
|
# Добавляем ID установки
|
||||||
|
if 'Установка' in df_cleaned.columns:
|
||||||
|
df_cleaned['id'] = df_cleaned['Установка'].apply(get_object_by_name)
|
||||||
|
print(f"🔍 DEBUG: Добавлены ID установок: {df_cleaned['id'].unique()}")
|
||||||
|
else:
|
||||||
|
print("⚠️ Колонка 'Установка' не найдена")
|
||||||
|
|
||||||
|
print(f"✅ Получено {len(df_cleaned)} записей")
|
||||||
|
return df_cleaned
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при парсинге данных: {e}")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
def _get_tech_pos_wrapper(self, params: Dict[str, Any] = None) -> str:
|
||||||
|
"""Обертка для получения данных технологических позиций"""
|
||||||
|
print(f"🔍 DEBUG: _get_tech_pos_wrapper вызван с параметрами: {params}")
|
||||||
|
|
||||||
|
# Получаем ID ОГ из параметров
|
||||||
|
og_id = params.get('id') if params else None
|
||||||
|
if not og_id:
|
||||||
|
print("❌ Не указан ID ОГ")
|
||||||
|
return "{}"
|
||||||
|
|
||||||
|
# Получаем данные
|
||||||
|
tech_pos_data = {}
|
||||||
|
if hasattr(self, 'df') and self.df is not None and not self.df.empty:
|
||||||
|
# Данные из MinIO
|
||||||
|
print(f"🔍 DEBUG: Ищем данные для ОГ '{og_id}' в DataFrame с {len(self.df)} записями")
|
||||||
|
available_ogs = self.df['id'].tolist()
|
||||||
|
print(f"🔍 DEBUG: Доступные ОГ в данных: {available_ogs}")
|
||||||
|
|
||||||
|
for _, row in self.df.iterrows():
|
||||||
|
if row['id'] == og_id:
|
||||||
|
tech_pos_data = row['data']
|
||||||
|
print(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print(f"❌ Данные для ОГ '{og_id}' не найдены")
|
||||||
|
elif hasattr(self, 'data_dict') and self.data_dict:
|
||||||
|
# Локальные данные
|
||||||
|
print(f"🔍 DEBUG: Ищем данные для ОГ '{og_id}' в data_dict")
|
||||||
|
available_ogs = list(self.data_dict.keys())
|
||||||
|
print(f"🔍 DEBUG: Доступные ОГ в data_dict: {available_ogs}")
|
||||||
|
|
||||||
|
if og_id in self.data_dict:
|
||||||
|
tech_pos_data = self.data_dict[og_id].to_dict(orient='records')
|
||||||
|
print(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
|
||||||
|
else:
|
||||||
|
print(f"❌ Данные для ОГ '{og_id}' не найдены в data_dict")
|
||||||
|
|
||||||
|
# Конвертируем в список записей
|
||||||
|
try:
|
||||||
|
if isinstance(tech_pos_data, pd.DataFrame):
|
||||||
|
# Если это DataFrame, конвертируем в список словарей
|
||||||
|
result_list = tech_pos_data.to_dict(orient='records')
|
||||||
|
print(f"🔍 DEBUG: Конвертировано в список: {len(result_list)} записей")
|
||||||
|
return result_list
|
||||||
|
elif isinstance(tech_pos_data, list):
|
||||||
|
# Если уже список, возвращаем как есть
|
||||||
|
print(f"🔍 DEBUG: Уже список: {len(tech_pos_data)} записей")
|
||||||
|
return tech_pos_data
|
||||||
|
else:
|
||||||
|
print(f"🔍 DEBUG: Неожиданный тип данных: {type(tech_pos_data)}")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при конвертации данных: {e}")
|
||||||
|
return []
|
||||||
@@ -6,7 +6,7 @@ from fastapi import FastAPI, File, UploadFile, HTTPException, status
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from adapters.storage import MinIOStorageAdapter
|
from adapters.storage import MinIOStorageAdapter
|
||||||
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, SvodkaRepairCAParser, StatusesRepairCAParser
|
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser
|
||||||
|
|
||||||
from core.models import UploadRequest, DataRequest
|
from core.models import UploadRequest, DataRequest
|
||||||
from core.services import ReportService, PARSERS
|
from core.services import ReportService, PARSERS
|
||||||
@@ -18,8 +18,10 @@ from app.schemas import (
|
|||||||
SvodkaCARequest,
|
SvodkaCARequest,
|
||||||
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest
|
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest
|
||||||
)
|
)
|
||||||
|
from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse
|
||||||
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
|
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
|
||||||
from app.schemas.statuses_repair_ca import StatusesRepairCARequest
|
from app.schemas.statuses_repair_ca import StatusesRepairCARequest
|
||||||
|
from app.schemas.monitoring_tar import MonitoringTarRequest, MonitoringTarFullRequest
|
||||||
|
|
||||||
|
|
||||||
# Парсеры
|
# Парсеры
|
||||||
@@ -27,8 +29,10 @@ PARSERS.update({
|
|||||||
'svodka_pm': SvodkaPMParser,
|
'svodka_pm': SvodkaPMParser,
|
||||||
'svodka_ca': SvodkaCAParser,
|
'svodka_ca': SvodkaCAParser,
|
||||||
'monitoring_fuel': MonitoringFuelParser,
|
'monitoring_fuel': MonitoringFuelParser,
|
||||||
|
'monitoring_tar': MonitoringTarParser,
|
||||||
'svodka_repair_ca': SvodkaRepairCAParser,
|
'svodka_repair_ca': SvodkaRepairCAParser,
|
||||||
'statuses_repair_ca': StatusesRepairCAParser,
|
'statuses_repair_ca': StatusesRepairCAParser,
|
||||||
|
'oper_spravka_tech_pos': OperSpravkaTechPosParser,
|
||||||
# 'svodka_plan_sarnpz': SvodkaPlanSarnpzParser,
|
# 'svodka_plan_sarnpz': SvodkaPlanSarnpzParser,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,8 +126,8 @@ async def get_available_ogs(parser_name: str):
|
|||||||
|
|
||||||
parser_class = PARSERS[parser_name]
|
parser_class = PARSERS[parser_name]
|
||||||
|
|
||||||
# Для svodka_repair_ca возвращаем ОГ из загруженных данных
|
# Для парсеров с данными в MinIO возвращаем ОГ из загруженных данных
|
||||||
if parser_name == "svodka_repair_ca":
|
if parser_name in ["svodka_repair_ca", "oper_spravka_tech_pos"]:
|
||||||
try:
|
try:
|
||||||
# Создаем экземпляр сервиса и загружаем данные из MinIO
|
# Создаем экземпляр сервиса и загружаем данные из MinIO
|
||||||
report_service = get_report_service()
|
report_service = get_report_service()
|
||||||
@@ -133,10 +137,22 @@ async def get_available_ogs(parser_name: str):
|
|||||||
# Если данные загружены, извлекаем ОГ из них
|
# Если данные загружены, извлекаем ОГ из них
|
||||||
if loaded_data is not None and hasattr(loaded_data, 'data') and loaded_data.data is not None:
|
if loaded_data is not None and hasattr(loaded_data, 'data') and loaded_data.data is not None:
|
||||||
# Для svodka_repair_ca данные возвращаются в формате словаря по ОГ
|
# Для svodka_repair_ca данные возвращаются в формате словаря по ОГ
|
||||||
data_value = loaded_data.data.get('value')
|
if parser_name == "svodka_repair_ca":
|
||||||
if isinstance(data_value, dict):
|
data_value = loaded_data.data.get('value')
|
||||||
available_ogs = list(data_value.keys())
|
if isinstance(data_value, dict):
|
||||||
return {"parser": parser_name, "available_ogs": available_ogs}
|
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:
|
except Exception as e:
|
||||||
print(f"⚠️ Ошибка при получении ОГ: {e}")
|
print(f"⚠️ Ошибка при получении ОГ: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
@@ -1163,5 +1179,258 @@ async def get_monitoring_fuel_month_by_code(
|
|||||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# ====== MONITORING TAR ENDPOINTS ======
|
||||||
|
|
||||||
|
@app.post("/monitoring_tar/upload", tags=[MonitoringTarParser.name],
|
||||||
|
summary="Загрузка отчета мониторинга ТЭР")
|
||||||
|
async def upload_monitoring_tar(
|
||||||
|
file: UploadFile = File(...)
|
||||||
|
):
|
||||||
|
"""Загрузка и обработка отчета мониторинга ТЭР (Топливно-энергетических ресурсов)
|
||||||
|
|
||||||
|
### Поддерживаемые форматы:
|
||||||
|
- **ZIP архивы** с файлами мониторинга ТЭР
|
||||||
|
|
||||||
|
### Структура данных:
|
||||||
|
- Обрабатывает ZIP архивы с файлами по месяцам (svodka_tar_SNPZ_01.xlsx - svodka_tar_SNPZ_12.xlsx)
|
||||||
|
- Извлекает данные по установкам (SNPZ_IDS)
|
||||||
|
- Возвращает два типа данных: 'total' (строки "Всего") и 'last_day' (последние строки)
|
||||||
|
"""
|
||||||
|
report_service = get_report_service()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Проверяем тип файла - только ZIP архивы
|
||||||
|
if not file.filename.endswith('.zip'):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Неподдерживаемый тип файла. Ожидается только ZIP архив (.zip)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Читаем содержимое файла
|
||||||
|
file_content = await file.read()
|
||||||
|
|
||||||
|
# Создаем запрос на загрузку
|
||||||
|
upload_request = UploadRequest(
|
||||||
|
report_type='monitoring_tar',
|
||||||
|
file_content=file_content,
|
||||||
|
file_name=file.filename
|
||||||
|
)
|
||||||
|
|
||||||
|
# Загружаем отчет
|
||||||
|
result = report_service.upload_report(upload_request)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
return UploadResponse(
|
||||||
|
success=True,
|
||||||
|
message="Отчет успешно загружен и обработан",
|
||||||
|
report_id=result.object_id,
|
||||||
|
filename=file.filename
|
||||||
|
).model_dump()
|
||||||
|
else:
|
||||||
|
return UploadErrorResponse(
|
||||||
|
success=False,
|
||||||
|
message=result.message,
|
||||||
|
error_code="ERR_UPLOAD",
|
||||||
|
details=None
|
||||||
|
).model_dump()
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/monitoring_tar/get_data", tags=[MonitoringTarParser.name],
|
||||||
|
summary="Получение данных из отчета мониторинга ТЭР")
|
||||||
|
async def get_monitoring_tar_data(
|
||||||
|
request_data: MonitoringTarRequest
|
||||||
|
):
|
||||||
|
"""Получение данных из отчета мониторинга ТЭР
|
||||||
|
|
||||||
|
### Структура параметров:
|
||||||
|
- `mode`: **Режим получения данных** (опциональный)
|
||||||
|
- `"total"` - строки "Всего" (агрегированные данные)
|
||||||
|
- `"last_day"` - последние строки данных
|
||||||
|
- Если не указан, возвращаются все данные
|
||||||
|
|
||||||
|
### Пример тела запроса:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "total"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
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)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/monitoring_tar/get_full_data", tags=[MonitoringTarParser.name],
|
||||||
|
summary="Получение всех данных из отчета мониторинга ТЭР")
|
||||||
|
async def get_monitoring_tar_full_data():
|
||||||
|
"""Получение всех данных из отчета мониторинга ТЭР без фильтрации
|
||||||
|
|
||||||
|
### Возвращает:
|
||||||
|
- Все данные по всем установкам
|
||||||
|
- И данные 'total', и данные 'last_day'
|
||||||
|
- Полная структура данных мониторинга ТЭР
|
||||||
|
"""
|
||||||
|
report_service = get_report_service()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем запрос без параметров
|
||||||
|
request = DataRequest(
|
||||||
|
report_type='monitoring_tar',
|
||||||
|
get_params={}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Получаем данные
|
||||||
|
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)}")
|
||||||
|
|
||||||
|
|
||||||
|
# ====== OPER SPRAVKA TECH POS ENDPOINTS ======
|
||||||
|
|
||||||
|
@app.post("/oper_spravka_tech_pos/upload", tags=[OperSpravkaTechPosParser.name],
|
||||||
|
summary="Загрузка отчета операционной справки технологических позиций")
|
||||||
|
async def upload_oper_spravka_tech_pos(
|
||||||
|
file: UploadFile = File(...)
|
||||||
|
):
|
||||||
|
"""Загрузка и обработка отчета операционной справки технологических позиций
|
||||||
|
|
||||||
|
### Поддерживаемые форматы:
|
||||||
|
- **ZIP архивы** с файлами операционных справок
|
||||||
|
|
||||||
|
### Структура данных:
|
||||||
|
- Обрабатывает ZIP архивы с файлами операционных справок по технологическим позициям
|
||||||
|
- Извлекает данные по процессам: Первичная переработка, Гидроочистка топлив, Риформирование, Изомеризация
|
||||||
|
- Возвращает данные по установкам с планом и фактом
|
||||||
|
"""
|
||||||
|
report_service = get_report_service()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Проверяем тип файла - только ZIP архивы
|
||||||
|
if not file.filename.endswith('.zip'):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Неподдерживаемый тип файла. Ожидается только ZIP архив (.zip)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Читаем содержимое файла
|
||||||
|
file_content = await file.read()
|
||||||
|
|
||||||
|
# Создаем запрос на загрузку
|
||||||
|
upload_request = UploadRequest(
|
||||||
|
report_type="oper_spravka_tech_pos",
|
||||||
|
file_name=file.filename,
|
||||||
|
file_content=file_content,
|
||||||
|
parse_params={}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Загружаем и обрабатываем отчет
|
||||||
|
result = report_service.upload_report(upload_request)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
return UploadResponse(
|
||||||
|
success=True,
|
||||||
|
message="Отчет успешно загружен и обработан",
|
||||||
|
object_id=result.object_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return UploadErrorResponse(
|
||||||
|
success=False,
|
||||||
|
message=result.message,
|
||||||
|
error_code="ERR_UPLOAD",
|
||||||
|
details=None
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/oper_spravka_tech_pos/get_data", tags=[OperSpravkaTechPosParser.name],
|
||||||
|
summary="Получение данных операционной справки технологических позиций",
|
||||||
|
response_model=OperSpravkaTechPosResponse)
|
||||||
|
async def get_oper_spravka_tech_pos_data(request: OperSpravkaTechPosRequest):
|
||||||
|
"""Получение данных операционной справки технологических позиций по ОГ
|
||||||
|
|
||||||
|
### Параметры:
|
||||||
|
- **id** (str): ID ОГ (например, 'SNPZ', 'KNPZ')
|
||||||
|
|
||||||
|
### Возвращает:
|
||||||
|
- Данные по технологическим позициям для указанного ОГ
|
||||||
|
- Включает информацию о процессах, установках, плане и факте
|
||||||
|
"""
|
||||||
|
report_service = get_report_service()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем запрос на получение данных
|
||||||
|
data_request = DataRequest(
|
||||||
|
report_type="oper_spravka_tech_pos",
|
||||||
|
get_params={"id": request.id}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Получаем данные
|
||||||
|
result = report_service.get_data(data_request)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
# Извлекаем данные из результата
|
||||||
|
value_data = result.data.get("value", []) if isinstance(result.data.get("value"), list) else []
|
||||||
|
print(f"🔍 DEBUG: API возвращает данные: {type(value_data)}, длина: {len(value_data) if isinstance(value_data, (list, dict)) else 'N/A'}")
|
||||||
|
|
||||||
|
return OperSpravkaTechPosResponse(
|
||||||
|
success=True,
|
||||||
|
data=value_data,
|
||||||
|
message="Данные успешно получены"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return OperSpravkaTechPosResponse(
|
||||||
|
success=False,
|
||||||
|
data=None,
|
||||||
|
message=result.message
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8080)
|
uvicorn.run(app, host="0.0.0.0", port=8080)
|
||||||
|
|||||||
33
python_parser/app/schemas/monitoring_tar.py
Normal file
33
python_parser/app/schemas/monitoring_tar.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, Literal
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class TarMode(str, Enum):
|
||||||
|
"""Режимы получения данных мониторинга ТЭР"""
|
||||||
|
TOTAL = "total"
|
||||||
|
LAST_DAY = "last_day"
|
||||||
|
|
||||||
|
class MonitoringTarRequest(BaseModel):
|
||||||
|
"""Схема запроса для получения данных мониторинга ТЭР"""
|
||||||
|
mode: Optional[TarMode] = Field(
|
||||||
|
None,
|
||||||
|
description="Режим получения данных: 'total' (строки 'Всего') или 'last_day' (последние строки). Если не указан, возвращаются все данные",
|
||||||
|
example="total"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"mode": "total"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MonitoringTarFullRequest(BaseModel):
|
||||||
|
"""Схема запроса для получения всех данных мониторинга ТЭР"""
|
||||||
|
# Пустая схема - возвращает все данные без фильтрации
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {}
|
||||||
|
}
|
||||||
38
python_parser/app/schemas/oper_spravka_tech_pos.py
Normal file
38
python_parser/app/schemas/oper_spravka_tech_pos.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
class OperSpravkaTechPosRequest(BaseModel):
|
||||||
|
"""Запрос для получения данных операционной справки технологических позиций"""
|
||||||
|
id: str = Field(..., description="ID ОГ (например, 'SNPZ', 'KNPZ')")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"id": "SNPZ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OperSpravkaTechPosResponse(BaseModel):
|
||||||
|
"""Ответ с данными операционной справки технологических позиций"""
|
||||||
|
success: bool = Field(..., description="Статус успешности операции")
|
||||||
|
data: Optional[List[dict]] = Field(None, description="Данные по технологическим позициям")
|
||||||
|
message: Optional[str] = Field(None, description="Сообщение о результате операции")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"success": True,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"Процесс": "Первичная переработка",
|
||||||
|
"Установка": "ЭЛОУ-АВТ-6",
|
||||||
|
"План, т": 14855.0,
|
||||||
|
"Факт, т": 15149.647,
|
||||||
|
"id": "SNPZ.EAVT6"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message": "Данные успешно получены"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,6 +142,14 @@ class ReportService:
|
|||||||
elif request.report_type == 'statuses_repair_ca':
|
elif request.report_type == 'statuses_repair_ca':
|
||||||
# Для statuses_repair_ca используем геттер get_repair_statuses
|
# Для statuses_repair_ca используем геттер get_repair_statuses
|
||||||
getter_name = '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':
|
elif request.report_type == 'monitoring_fuel':
|
||||||
# Для monitoring_fuel определяем геттер из параметра mode
|
# Для monitoring_fuel определяем геттер из параметра mode
|
||||||
getter_name = get_params.pop("mode", None)
|
getter_name = get_params.pop("mode", None)
|
||||||
@@ -170,6 +178,9 @@ class ReportService:
|
|||||||
success=False,
|
success=False,
|
||||||
message="Парсер не имеет доступных геттеров"
|
message="Парсер не имеет доступных геттеров"
|
||||||
)
|
)
|
||||||
|
elif request.report_type == 'oper_spravka_tech_pos':
|
||||||
|
# Для oper_spravka_tech_pos используем геттер get_tech_pos
|
||||||
|
getter_name = 'get_tech_pos'
|
||||||
else:
|
else:
|
||||||
# Для других парсеров определяем из параметра mode
|
# Для других парсеров определяем из параметра mode
|
||||||
getter_name = get_params.pop("mode", None)
|
getter_name = get_params.pop("mode", None)
|
||||||
|
|||||||
@@ -115,12 +115,14 @@ def main():
|
|||||||
st.write(f"• {parser}")
|
st.write(f"• {parser}")
|
||||||
|
|
||||||
# Основные вкладки - по одной на каждый парсер
|
# Основные вкладки - по одной на каждый парсер
|
||||||
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs([
|
||||||
"📊 Сводки ПМ",
|
"📊 Сводки ПМ",
|
||||||
"🏭 Сводки СА",
|
"🏭 Сводки СА",
|
||||||
"⛽ Мониторинг топлива",
|
"⛽ Мониторинг топлива",
|
||||||
"🔧 Ремонт СА",
|
"🔧 Ремонт СА",
|
||||||
"📋 Статусы ремонта СА"
|
"📋 Статусы ремонта СА",
|
||||||
|
"⚡ Мониторинг ТЭР",
|
||||||
|
"🏭 Операционные справки"
|
||||||
])
|
])
|
||||||
|
|
||||||
# Вкладка 1: Сводки ПМ - полный функционал
|
# Вкладка 1: Сводки ПМ - полный функционал
|
||||||
@@ -633,6 +635,189 @@ def main():
|
|||||||
else:
|
else:
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
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("---")
|
st.markdown("---")
|
||||||
st.markdown("### 📚 Документация API")
|
st.markdown("### 📚 Документация API")
|
||||||
@@ -647,6 +832,7 @@ def main():
|
|||||||
- 📊 Парсинг сводок ПМ (план и факт)
|
- 📊 Парсинг сводок ПМ (план и факт)
|
||||||
- 🏭 Парсинг сводок СА
|
- 🏭 Парсинг сводок СА
|
||||||
- ⛽ Мониторинг топлива
|
- ⛽ Мониторинг топлива
|
||||||
|
- ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы)
|
||||||
- 🔧 Управление ремонтными работами СА
|
- 🔧 Управление ремонтными работами СА
|
||||||
- 📋 Мониторинг статусов ремонта СА
|
- 📋 Мониторинг статусов ремонта СА
|
||||||
|
|
||||||
|
|||||||
BIN
test_repair_ca.zip
Normal file
BIN
test_repair_ca.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user