7 Commits

Author SHA1 Message Date
2555fd80e0 pytests 2025-09-04 18:56:36 +03:00
847441842c Merge branch 'logging-upgrade' 2025-09-04 18:25:31 +03:00
00a01e99d7 Логгер работает 2025-09-04 18:24:53 +03:00
bbbfbbd508 Основа для логгера 2025-09-04 17:13:39 +03:00
0f3340c899 Merge branch 'add-new-parsers' 2025-09-04 15:54:15 +03:00
3c0fce128d oper_spravka_tech_pos реализован 2025-09-04 15:53:35 +03:00
b5c460bb6f monitoring_tar полностью функционален 2025-09-04 12:57:28 +03:00
29 changed files with 2822 additions and 142 deletions

1002
PARSER_DEVELOPMENT_GUIDE.md Normal file

File diff suppressed because it is too large Load Diff

BIN
monitoring_tar_correct.zip Normal file

Binary file not shown.

BIN
monitoring_tar_test.zip Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,13 +1,17 @@
from .monitoring_fuel import MonitoringFuelParser
from .monitoring_tar import MonitoringTarParser
from .svodka_ca import SvodkaCAParser
from .svodka_pm import SvodkaPMParser
from .svodka_repair_ca import SvodkaRepairCAParser
from .statuses_repair_ca import StatusesRepairCAParser
from .oper_spravka_tech_pos import OperSpravkaTechPosParser
__all__ = [
'MonitoringFuelParser',
'MonitoringTarParser',
'SvodkaCAParser',
'SvodkaPMParser',
'SvodkaRepairCAParser',
'StatusesRepairCAParser'
'StatusesRepairCAParser',
'OperSpravkaTechPosParser'
]

View File

@@ -1,12 +1,16 @@
import pandas as pd
import re
import zipfile
import logging
from typing import Dict, Tuple
from core.ports import ParserPort
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest
from adapters.pconfig import data_to_json
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
class MonitoringFuelParser(ParserPort):
"""Парсер для мониторинга топлива"""
@@ -157,19 +161,19 @@ class MonitoringFuelParser(ParserPort):
if len(candidates) == 1:
file = candidates[0]
print(f'Загрузка {file}')
logger.info(f'Загрузка {file}')
with zip_ref.open(file) as excel_file:
try:
df = self.parse_single(excel_file, 'Мониторинг потребления')
df_monitorings[mm] = df
print(f"✅ Данные за месяц {mm} загружены")
logger.info(f"✅ Данные за месяц {mm} загружены")
except Exception as e:
print(f"Ошибка при загрузке файла {file_temp}: {e}")
logger.error(f"Ошибка при загрузке файла {file_temp}: {e}")
else:
print(f"⚠️ Файл не найден: {file_temp}")
logger.warning(f"⚠️ Файл не найден: {file_temp}")
return df_monitorings
@@ -187,7 +191,7 @@ class MonitoringFuelParser(ParserPort):
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
for idx, row in df_temp.iterrows():
if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
logger.debug(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
return idx + 1 # возвращаем индекс строки (0-based)
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
@@ -237,7 +241,7 @@ class MonitoringFuelParser(ParserPort):
# Устанавливаем id как индекс
df_full.set_index('id', inplace=True)
print(f"Окончательное количество столбцов: {len(df_full.columns)}")
logger.debug(f"Окончательное количество столбцов: {len(df_full.columns)}")
return df_full
def aggregate_by_columns(self, df_dict: Dict[str, pd.DataFrame], columns: list) -> Tuple[pd.DataFrame, Dict[str, pd.DataFrame]]:
@@ -250,7 +254,7 @@ class MonitoringFuelParser(ParserPort):
for file_key, df in df_dict.items():
if col not in df.columns:
print(f"Колонка '{col}' не найдена в {file_key}, пропускаем.")
logger.warning(f"Колонка '{col}' не найдена в {file_key}, пропускаем.")
continue
# Берём колонку, оставляем id как индекс
@@ -302,7 +306,7 @@ class MonitoringFuelParser(ParserPort):
for file, df in df_dict.items():
if column not in df.columns:
print(f"Колонка '{column}' не найдена в {file}, пропускаем.")
logger.warning(f"Колонка '{column}' не найдена в {file}, пропускаем.")
continue
# Берём колонку и сохраняем как Series с именем месяца

View File

@@ -0,0 +1,306 @@
import os
import zipfile
import tempfile
import pandas as pd
import logging
from typing import Dict, Any, List
from core.ports import ParserPort
from adapters.pconfig import find_header_row, SNPZ_IDS, data_to_json
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
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 архив с файлами мониторинга ТЭР"""
logger.debug(f"🔍 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)
logger.debug(f"🔍 Создан DataFrame с {len(df)} записями")
return df
else:
logger.debug("🔍 Возвращаем пустой DataFrame")
return pd.DataFrame()
def _parse_zip_archive(self, zip_path: str) -> Dict[str, Any]:
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
logger.info(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("В архиве не найдены файлы мониторинга ТЭР")
logger.info(f"📁 Найдено {len(tar_files)} файлов мониторинга ТЭР")
# Обрабатываем каждый файл
all_data = {}
for file_path in tar_files:
logger.info(f"📁 Обработка файла: {file_path}")
# Извлекаем номер месяца из имени файла
filename = os.path.basename(file_path)
month_str = self._extract_month_from_filename(filename)
logger.debug(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:
logger.error(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:
logger.warning(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:
logger.warning(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()
logger.info(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:
logger.warning(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:
logger.error(f"Не найдена строка с заголовком '1' в файле {file}, лист '{sheet}'")
return {'total': None, 'last_day': None}
elif filename.startswith('monitoring_'):
# Для файлов monitoring_*.xlsm заголовок находится в строке 5
header_num = 5
else:
logger.error(f"❌ Неизвестный тип файла: {filename}")
return {'total': None, 'last_day': None}
logger.debug(f"🔍 Используем заголовок в строке {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:
logger.warning(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:
logger.error(f"❌ Ошибка при обработке файла {file}, лист '{sheet}': {e}")
return {'total': None, 'last_day': None}
def _get_tar_data_wrapper(self, params: Dict[str, Any] = None) -> str:
"""Обертка для получения данных мониторинга ТЭР с фильтрацией по режиму"""
logger.debug(f"🔍 _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:
logger.error(f"❌ Ошибка при конвертации данных в JSON: {e}")
return "{}"
def _get_tar_full_data_wrapper(self, params: Dict[str, Any] = None) -> str:
"""Обертка для получения всех данных мониторинга ТЭР"""
logger.debug(f"🔍 _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:
logger.error(f"❌ Ошибка при конвертации данных в JSON: {e}")
return "{}"

View File

@@ -0,0 +1,285 @@
import os
import tempfile
import zipfile
import pandas as pd
import logging
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
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
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 архив с файлами операционных справок технологических позиций"""
logger.debug(f"🔍 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)
logger.debug(f"🔍 Создан DataFrame с {len(df)} записями")
return df
else:
logger.debug("🔍 Возвращаем пустой DataFrame")
return pd.DataFrame()
def _parse_zip_archive(self, zip_path: str) -> Dict[str, pd.DataFrame]:
"""Парсит ZIP архив с файлами операционных справок"""
logger.info(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("В архиве не найдены файлы операционных справок технологических позиций")
logger.info(f"📁 Найдено {len(tech_pos_files)} файлов операционных справок")
# Обрабатываем каждый файл
all_data = {}
for file_path in tech_pos_files:
logger.info(f"📁 Обработка файла: {file_path}")
# Извлекаем ID ОГ из имени файла
filename = os.path.basename(file_path)
og_id = self._extract_og_id_from_filename(filename)
logger.debug(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)
logger.debug(f"📅 Актуальный лист: {actual_sheet}")
# Находим заголовок
header_row = self._find_header_row(file_path, actual_sheet)
logger.debug(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:
logger.warning(f"⚠️ Нет данных в файле {file_path}")
return {}
except Exception as e:
logger.error(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:
logger.warning(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():
logger.debug(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
return idx + 1 # возвращаем индекс строки (0-based), который будет использован как `header=`
raise ValueError(f"Не найдена строка с заголовком '{search_value}'.")
except Exception as e:
logger.error(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)
)
logger.debug(f"🔍 Прочитано {len(df_temp)} строк из Excel")
logger.debug(f"🔍 Колонки: {list(df_temp.columns)}")
# Фильтруем по валидным процессам
df_cleaned = df_temp[
df_temp['Процесс'].str.strip().isin(valid_processes) &
df_temp['Процесс'].notna()
].copy()
logger.debug(f"🔍 После фильтрации осталось {len(df_cleaned)} строк")
if df_cleaned.empty:
logger.warning("⚠️ Нет данных после фильтрации по процессам")
logger.debug(f"🔍 Доступные процессы в данных: {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)
logger.debug(f"🔍 Добавлены ID установок: {df_cleaned['id'].unique()}")
else:
logger.warning("⚠️ Колонка 'Установка' не найдена")
logger.info(f"✅ Получено {len(df_cleaned)} записей")
return df_cleaned
except Exception as e:
logger.error(f"❌ Ошибка при парсинге данных: {e}")
return pd.DataFrame()
def _get_tech_pos_wrapper(self, params: Dict[str, Any] = None) -> str:
"""Обертка для получения данных технологических позиций"""
logger.debug(f"🔍 _get_tech_pos_wrapper вызван с параметрами: {params}")
# Получаем ID ОГ из параметров
og_id = params.get('id') if params else None
if not og_id:
logger.error("Не указан ID ОГ")
return "{}"
# Получаем данные
tech_pos_data = {}
if hasattr(self, 'df') and self.df is not None and not self.df.empty:
# Данные из MinIO
logger.debug(f"🔍 Ищем данные для ОГ '{og_id}' в DataFrame с {len(self.df)} записями")
available_ogs = self.df['id'].tolist()
logger.debug(f"🔍 Доступные ОГ в данных: {available_ogs}")
for _, row in self.df.iterrows():
if row['id'] == og_id:
tech_pos_data = row['data']
logger.info(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
break
else:
logger.warning(f"❌ Данные для ОГ '{og_id}' не найдены")
elif hasattr(self, 'data_dict') and self.data_dict:
# Локальные данные
logger.debug(f"🔍 Ищем данные для ОГ '{og_id}' в data_dict")
available_ogs = list(self.data_dict.keys())
logger.debug(f"🔍 Доступные ОГ в data_dict: {available_ogs}")
if og_id in self.data_dict:
tech_pos_data = self.data_dict[og_id].to_dict(orient='records')
logger.info(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
else:
logger.warning(f"❌ Данные для ОГ '{og_id}' не найдены в data_dict")
# Конвертируем в список записей
try:
if isinstance(tech_pos_data, pd.DataFrame):
# Если это DataFrame, конвертируем в список словарей
result_list = tech_pos_data.to_dict(orient='records')
logger.debug(f"🔍 Конвертировано в список: {len(result_list)} записей")
return result_list
elif isinstance(tech_pos_data, list):
# Если уже список, возвращаем как есть
logger.debug(f"🔍 Уже список: {len(tech_pos_data)} записей")
return tech_pos_data
else:
logger.warning(f"🔍 Неожиданный тип данных: {type(tech_pos_data)}")
return []
except Exception as e:
logger.error(f"❌ Ошибка при конвертации данных: {e}")
return []

View File

@@ -2,12 +2,16 @@ import pandas as pd
import os
import tempfile
import zipfile
import logging
from typing import Dict, Any, List, Tuple, Optional
from core.ports import ParserPort
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
from app.schemas.statuses_repair_ca import StatusesRepairCARequest
from adapters.pconfig import find_header_row, get_og_by_name, data_to_json
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
class StatusesRepairCAParser(ParserPort):
"""Парсер для статусов ремонта СА"""
@@ -26,7 +30,7 @@ class StatusesRepairCAParser(ParserPort):
def parse(self, file_path: str, params: dict) -> Dict[str, Any]:
"""Парсинг файла статусов ремонта СА"""
print(f"🔍 DEBUG: StatusesRepairCAParser.parse вызван с файлом: {file_path}")
logger.debug(f"🔍 StatusesRepairCAParser.parse вызван с файлом: {file_path}")
try:
# Определяем тип файла
@@ -38,7 +42,7 @@ class StatusesRepairCAParser(ParserPort):
raise ValueError(f"Неподдерживаемый формат файла: {file_path}")
except Exception as e:
print(f"❌ Ошибка при парсинге файла {file_path}: {e}")
logger.error(f"❌ Ошибка при парсинге файла {file_path}: {e}")
raise
def _parse_zip_file(self, zip_path: str) -> Dict[str, Any]:
@@ -59,19 +63,19 @@ class StatusesRepairCAParser(ParserPort):
# Берем первый найденный Excel файл
excel_file = excel_files[0]
print(f"🔍 DEBUG: Найден Excel файл в архиве: {excel_file}")
logger.debug(f"🔍 Найден Excel файл в архиве: {excel_file}")
return self._parse_excel_file(excel_file)
def _parse_excel_file(self, file_path: str) -> Dict[str, Any]:
"""Парсинг Excel файла"""
print(f"🔍 DEBUG: Парсинг Excel файла: {file_path}")
logger.debug(f"🔍 Парсинг Excel файла: {file_path}")
# Парсим данные
df_statuses = self._parse_statuses_repair_ca(file_path, 0)
if df_statuses.empty:
print("⚠️ Нет данных после парсинга")
logger.warning("⚠️ Нет данных после парсинга")
return {"data": [], "records_count": 0}
# Преобразуем в список словарей для хранения
@@ -85,7 +89,7 @@ class StatusesRepairCAParser(ParserPort):
# Устанавливаем данные в парсер для использования в геттерах
self.data_dict = result
print(f"✅ Парсинг завершен. Получено {len(data_list)} записей")
logger.info(f"✅ Парсинг завершен. Получено {len(data_list)} записей")
return result
def _parse_statuses_repair_ca(self, file: str, sheet: int, header_num: Optional[int] = None) -> pd.DataFrame:
@@ -236,7 +240,7 @@ class StatusesRepairCAParser(ParserPort):
def _get_repair_statuses_wrapper(self, params: dict):
"""Обертка для получения статусов ремонта"""
print(f"🔍 DEBUG: _get_repair_statuses_wrapper вызван с параметрами: {params}")
logger.debug(f"🔍 _get_repair_statuses_wrapper вызван с параметрами: {params}")
# Валидация параметров
validated_params = validate_params_with_schema(params, StatusesRepairCARequest)
@@ -244,8 +248,8 @@ class StatusesRepairCAParser(ParserPort):
ids = validated_params.get('ids')
keys = validated_params.get('keys')
print(f"🔍 DEBUG: Запрошенные ОГ: {ids}")
print(f"🔍 DEBUG: Запрошенные ключи: {keys}")
logger.debug(f"🔍 Запрошенные ОГ: {ids}")
logger.debug(f"🔍 Запрошенные ключи: {keys}")
# Получаем данные из парсера
if hasattr(self, 'df') and self.df is not None:
@@ -265,15 +269,15 @@ class StatusesRepairCAParser(ParserPort):
# Данные из локального парсинга
data_source = self.data_dict.get('data', [])
else:
print("⚠️ Нет данных в парсере")
logger.warning("⚠️ Нет данных в парсере")
return []
print(f"🔍 DEBUG: Используем данные с {len(data_source)} записями")
logger.debug(f"🔍 Используем данные с {len(data_source)} записями")
# Фильтруем данные
filtered_data = self._filter_statuses_data(data_source, ids, keys)
print(f"🔍 DEBUG: Отфильтровано {len(filtered_data)} записей")
logger.debug(f"🔍 Отфильтровано {len(filtered_data)} записей")
return filtered_data
def _filter_statuses_data(self, data_source: List[Dict], ids: Optional[List[str]], keys: Optional[List[List[str]]]) -> List[Dict]:

View File

@@ -1,11 +1,15 @@
import pandas as pd
import numpy as np
import logging
from core.ports import ParserPort
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
from app.schemas.svodka_ca import SvodkaCARequest
from adapters.pconfig import get_og_by_name
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
class SvodkaCAParser(ParserPort):
"""Парсер для сводок СА"""
@@ -25,7 +29,7 @@ class SvodkaCAParser(ParserPort):
def _get_data_wrapper(self, params: dict):
"""Получение данных по режимам и таблицам"""
print(f"🔍 DEBUG: _get_data_wrapper вызван с параметрами: {params}")
logger.debug(f"🔍 _get_data_wrapper вызван с параметрами: {params}")
# Валидируем параметры с помощью схемы Pydantic
validated_params = validate_params_with_schema(params, SvodkaCARequest)
@@ -33,20 +37,20 @@ class SvodkaCAParser(ParserPort):
modes = validated_params["modes"]
tables = validated_params["tables"]
print(f"🔍 DEBUG: Запрошенные режимы: {modes}")
print(f"🔍 DEBUG: Запрошенные таблицы: {tables}")
logger.debug(f"🔍 Запрошенные режимы: {modes}")
logger.debug(f"🔍 Запрошенные таблицы: {tables}")
# Проверяем, есть ли данные в data_dict (из парсинга) или в df (из загрузки)
if hasattr(self, 'data_dict') and self.data_dict is not None:
# Данные из парсинга
data_source = self.data_dict
print(f"🔍 DEBUG: Используем data_dict с режимами: {list(data_source.keys())}")
logger.debug(f"🔍 Используем data_dict с режимами: {list(data_source.keys())}")
elif hasattr(self, 'df') and self.df is not None and not self.df.empty:
# Данные из загрузки - преобразуем DataFrame обратно в словарь
data_source = self._df_to_data_dict()
print(f"🔍 DEBUG: Используем df, преобразованный в data_dict с режимами: {list(data_source.keys())}")
logger.debug(f"🔍 Используем df, преобразованный в data_dict с режимами: {list(data_source.keys())}")
else:
print(f"🔍 DEBUG: Нет данных! data_dict={getattr(self, 'data_dict', 'None')}, df={getattr(self, 'df', 'None')}")
logger.warning(f"🔍 Нет данных! data_dict={getattr(self, 'data_dict', 'None')}, df={getattr(self, 'df', 'None')}")
return {}
# Фильтруем данные по запрошенным режимам и таблицам
@@ -55,18 +59,18 @@ class SvodkaCAParser(ParserPort):
if mode in data_source:
result_data[mode] = {}
available_tables = list(data_source[mode].keys())
print(f"🔍 DEBUG: Режим '{mode}' содержит таблицы: {available_tables}")
logger.debug(f"🔍 Режим '{mode}' содержит таблицы: {available_tables}")
for table_name, table_data in data_source[mode].items():
# Ищем таблицы по частичному совпадению
for requested_table in tables:
if requested_table in table_name:
result_data[mode][table_name] = table_data
print(f"🔍 DEBUG: Добавлена таблица '{table_name}' (совпадение с '{requested_table}') с {len(table_data)} записями")
logger.debug(f"🔍 Добавлена таблица '{table_name}' (совпадение с '{requested_table}') с {len(table_data)} записями")
break # Найдено совпадение, переходим к следующей таблице
else:
print(f"🔍 DEBUG: Режим '{mode}' не найден в data_source")
logger.warning(f"🔍 Режим '{mode}' не найден в data_source")
print(f"🔍 DEBUG: Итоговый результат содержит режимы: {list(result_data.keys())}")
logger.debug(f"🔍 Итоговый результат содержит режимы: {list(result_data.keys())}")
return result_data
def _df_to_data_dict(self):
@@ -91,7 +95,7 @@ class SvodkaCAParser(ParserPort):
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
"""Парсинг файла и возврат DataFrame"""
print(f"🔍 DEBUG: SvodkaCAParser.parse вызван с файлом: {file_path}")
logger.debug(f"🔍 SvodkaCAParser.parse вызван с файлом: {file_path}")
# Парсим данные и сохраняем словарь для использования в геттерах
self.data_dict = self.parse_svodka_ca(file_path, params)
@@ -114,17 +118,17 @@ class SvodkaCAParser(ParserPort):
if data_rows:
df = pd.DataFrame(data_rows)
self.df = df
print(f"🔍 DEBUG: Создан DataFrame с {len(data_rows)} записями")
logger.debug(f"🔍 Создан DataFrame с {len(data_rows)} записями")
return df
# Если данных нет, возвращаем пустой DataFrame
self.df = pd.DataFrame()
print(f"🔍 DEBUG: Возвращаем пустой DataFrame")
logger.debug(f"🔍 Возвращаем пустой DataFrame")
return self.df
def parse_svodka_ca(self, file_path: str, params: dict) -> dict:
"""Парсинг сводки СА - работает с тремя листами: План, Факт, Норматив"""
print(f"🔍 DEBUG: Начинаем парсинг сводки СА из файла: {file_path}")
logger.debug(f"🔍 Начинаем парсинг сводки СА из файла: {file_path}")
# === Точка входа. Нужно выгрузить три таблицы: План, Факт и Норматив ===
@@ -146,7 +150,7 @@ class SvodkaCAParser(ParserPort):
}
df_ca_plan = self.parse_sheet(file_path, 'План', inclusion_list_plan)
print(f"🔍 DEBUG: Объединённый и отсортированный План: {df_ca_plan.shape if df_ca_plan is not None else 'None'}")
logger.debug(f"🔍 Объединённый и отсортированный План: {df_ca_plan.shape if df_ca_plan is not None else 'None'}")
# Выгружаем Факт
inclusion_list_fact = {
@@ -166,7 +170,7 @@ class SvodkaCAParser(ParserPort):
}
df_ca_fact = self.parse_sheet(file_path, 'Факт', inclusion_list_fact)
print(f"🔍 DEBUG: Объединённый и отсортированный Факт: {df_ca_fact.shape if df_ca_fact is not None else 'None'}")
logger.debug(f"🔍 Объединённый и отсортированный Факт: {df_ca_fact.shape if df_ca_fact is not None else 'None'}")
# Выгружаем Норматив
inclusion_list_normativ = {
@@ -185,7 +189,7 @@ class SvodkaCAParser(ParserPort):
}
df_ca_normativ = self.parse_sheet(file_path, 'Норматив', inclusion_list_normativ)
print(f"🔍 DEBUG: Объединённый и отсортированный Норматив: {df_ca_normativ.shape if df_ca_normativ is not None else 'None'}")
logger.debug(f"🔍 Объединённый и отсортированный Норматив: {df_ca_normativ.shape if df_ca_normativ is not None else 'None'}")
# Преобразуем DataFrame в словарь по режимам и таблицам
data_dict = {}
@@ -211,9 +215,9 @@ class SvodkaCAParser(ParserPort):
table_data = group_df.drop('table', axis=1)
data_dict['normativ'][table_name] = table_data.to_dict('records')
print(f"🔍 DEBUG: Итоговый data_dict содержит режимы: {list(data_dict.keys())}")
logger.debug(f"🔍 Итоговый data_dict содержит режимы: {list(data_dict.keys())}")
for mode, tables in data_dict.items():
print(f"🔍 DEBUG: Режим '{mode}' содержит таблицы: {list(tables.keys())}")
logger.debug(f"🔍 Режим '{mode}' содержит таблицы: {list(tables.keys())}")
return data_dict
@@ -368,7 +372,7 @@ class SvodkaCAParser(ParserPort):
# Проверяем, что колонка 'name' существует
if 'name' not in df_cleaned.columns:
print(
logger.debug(
f"Внимание: колонка 'name' отсутствует в таблице для '{matched_key}'. Пропускаем добавление 'id'.")
continue # или обработать по-другому
else:

View File

@@ -6,10 +6,14 @@ import json
import zipfile
import tempfile
import shutil
import logging
from typing import Dict, Any, List, Optional
from core.ports import ParserPort
from adapters.pconfig import SINGLE_OGS, replace_id_in_path, find_header_row, data_to_json
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
class SvodkaPMParser(ParserPort):
"""Парсер для сводок ПМ (план и факт)"""
@@ -51,17 +55,17 @@ class SvodkaPMParser(ParserPort):
# Разархивируем файл
with zipfile.ZipFile(file_path, 'r') as zip_ref:
zip_ref.extractall(temp_dir)
print(f"📦 Архив разархивирован в: {temp_dir}")
logger.info(f"📦 Архив разархивирован в: {temp_dir}")
# Посмотрим, что находится в архиве
print(f"🔍 Содержимое архива:")
logger.debug(f"🔍 Содержимое архива:")
for root, dirs, files in os.walk(temp_dir):
level = root.replace(temp_dir, '').count(os.sep)
indent = ' ' * 2 * level
print(f"{indent}{os.path.basename(root)}/")
logger.debug(f"{indent}{os.path.basename(root)}/")
subindent = ' ' * 2 * (level + 1)
for file in files:
print(f"{subindent}{file}")
logger.debug(f"{subindent}{file}")
# Создаем словари для хранения данных как в оригинале
df_pm_facts = {} # Словарь с данными факта, ключ - ID ОГ
@@ -80,8 +84,8 @@ class SvodkaPMParser(ParserPort):
elif 'plan' in file.lower() or 'план' in file.lower():
plan_files.append(full_path)
print(f"📊 Найдено файлов факта: {len(fact_files)}")
print(f"📊 Найдено файлов плана: {len(plan_files)}")
logger.info(f"📊 Найдено файлов факта: {len(fact_files)}")
logger.info(f"📊 Найдено файлов плана: {len(plan_files)}")
# Обрабатываем найденные файлы
for fact_file in fact_files:
@@ -91,9 +95,9 @@ class SvodkaPMParser(ParserPort):
if 'svodka_fact_pm_' in filename:
og_id = filename.replace('svodka_fact_pm_', '').replace('.xlsx', '').replace('.xlsm', '')
if og_id in SINGLE_OGS:
print(f'📊 Загрузка факта: {fact_file} (ОГ: {og_id})')
logger.info(f'📊 Загрузка факта: {fact_file} (ОГ: {og_id})')
df_pm_facts[og_id] = self._parse_svodka_pm(fact_file, 'Сводка Нефтепереработка')
print(f"✅ Факт загружен для {og_id}")
logger.info(f"✅ Факт загружен для {og_id}")
for plan_file in plan_files:
# Извлекаем ID ОГ из имени файла
@@ -102,9 +106,9 @@ class SvodkaPMParser(ParserPort):
if 'svodka_plan_pm_' in filename:
og_id = filename.replace('svodka_plan_pm_', '').replace('.xlsx', '').replace('.xlsm', '')
if og_id in SINGLE_OGS:
print(f'📊 Загрузка плана: {plan_file} (ОГ: {og_id})')
logger.info(f'📊 Загрузка плана: {plan_file} (ОГ: {og_id})')
df_pm_plans[og_id] = self._parse_svodka_pm(plan_file, 'Сводка Нефтепереработка')
print(f"✅ План загружен для {og_id}")
logger.info(f"✅ План загружен для {og_id}")
# Инициализируем None для ОГ, для которых файлы не найдены
for og_id in SINGLE_OGS:
@@ -123,14 +127,14 @@ class SvodkaPMParser(ParserPort):
'df_pm_plans': df_pm_plans
}
print(f"🎯 Обработано ОГ: {len([k for k, v in df_pm_facts.items() if v is not None])} факт, {len([k for k, v in df_pm_plans.items() if v is not None])} план")
logger.info(f"🎯 Обработано ОГ: {len([k for k, v in df_pm_facts.items() if v is not None])} факт, {len([k for k, v in df_pm_plans.items() if v is not None])} план")
return result
finally:
# Удаляем временную директорию
shutil.rmtree(temp_dir, ignore_errors=True)
print(f"🗑️ Временная директория удалена: {temp_dir}")
logger.debug(f"🗑️ Временная директория удалена: {temp_dir}")
def _parse_svodka_pm(self, file_path: str, sheet_name: str, header_num: Optional[int] = None) -> pd.DataFrame:
"""Парсинг отчетов одного ОГ для БП, ПП и факта"""
@@ -226,19 +230,19 @@ class SvodkaPMParser(ParserPort):
def _get_svodka_value(self, df_svodka: pd.DataFrame, og_id: str, code: int, search_value: Optional[str] = None):
"""Служебная функция для простой выборке по сводке"""
print(f"🔍 DEBUG: Ищем код '{code}' для ОГ '{og_id}' в DataFrame с {len(df_svodka)} строками")
print(f"🔍 DEBUG: Первая строка данных: {df_svodka.iloc[0].tolist()}")
print(f"🔍 DEBUG: Доступные индексы: {list(df_svodka.index)}")
print(f"🔍 DEBUG: Доступные столбцы: {list(df_svodka.columns)}")
logger.debug(f"🔍 Ищем код '{code}' для ОГ '{og_id}' в DataFrame с {len(df_svodka)} строками")
logger.debug(f"🔍 Первая строка данных: {df_svodka.iloc[0].tolist()}")
logger.debug(f"🔍 Доступные индексы: {list(df_svodka.index)}")
logger.debug(f"🔍 Доступные столбцы: {list(df_svodka.columns)}")
# Проверяем, есть ли код в индексе
if code not in df_svodka.index:
print(f"⚠️ Код '{code}' не найден в индексе")
logger.warning(f"⚠️ Код '{code}' не найден в индексе")
return 0
# Получаем позицию строки с кодом
code_row_loc = df_svodka.index.get_loc(code)
print(f"🔍 DEBUG: Код '{code}' в позиции {code_row_loc}")
logger.debug(f"🔍 Код '{code}' в позиции {code_row_loc}")
# Определяем позиции для поиска
if search_value is None:
@@ -254,14 +258,14 @@ class SvodkaPMParser(ParserPort):
if col_name == search_value:
target_positions.append(i)
print(f"🔍 DEBUG: Найдены позиции для '{search_value}': {target_positions[:5]}...")
print(f"🔍 DEBUG: Позиции в первой строке: {target_positions[:5]}...")
logger.debug(f"🔍 Найдены позиции для '{search_value}': {target_positions[:5]}...")
logger.debug(f"🔍 Позиции в первой строке: {target_positions[:5]}...")
print(f"🔍 DEBUG: Ищем столбцы с названием '{search_value}'")
print(f"🔍 DEBUG: Целевые позиции: {target_positions[:10]}...")
logger.debug(f"🔍 Ищем столбцы с названием '{search_value}'")
logger.debug(f"🔍 Целевые позиции: {target_positions[:10]}...")
if not target_positions:
print(f"⚠️ Позиции '{search_value}' не найдены")
logger.warning(f"⚠️ Позиции '{search_value}' не найдены")
return 0
# Извлекаем значения из найденных позиций
@@ -285,7 +289,7 @@ class SvodkaPMParser(ParserPort):
# Преобразуем в числовой формат
numeric_values = pd.to_numeric(values, errors='coerce')
print(f"🔍 DEBUG: Числовые значения (первые 5): {numeric_values.tolist()[:5]}")
logger.debug(f"🔍 Числовые значения (первые 5): {numeric_values.tolist()[:5]}")
# Попробуем альтернативное преобразование
try:
@@ -301,10 +305,10 @@ class SvodkaPMParser(ParserPort):
except (ValueError, TypeError):
manual_values.append(0)
print(f"🔍 DEBUG: Ручное преобразование (первые 5): {manual_values[:5]}")
logger.debug(f"🔍 Ручное преобразование (первые 5): {manual_values[:5]}")
numeric_values = pd.Series(manual_values)
except Exception as e:
print(f"⚠️ Ошибка при ручном преобразовании: {e}")
logger.warning(f"⚠️ Ошибка при ручном преобразовании: {e}")
# Используем исходные значения
numeric_values = pd.Series([0 if pd.isna(v) or v is None else v for v in values])
@@ -338,7 +342,7 @@ class SvodkaPMParser(ParserPort):
# Получаем данные из сохраненных словарей (через self.df)
if not hasattr(self, 'df') or self.df is None:
print("❌ Данные не загружены. Сначала загрузите ZIP архив.")
logger.error("❌ Данные не загружены. Сначала загрузите ZIP архив.")
return {col: {str(code): None for code in codes} for col in columns}
# Извлекаем словари из сохраненных данных
@@ -349,10 +353,10 @@ class SvodkaPMParser(ParserPort):
fact_df = df_pm_facts.get(og_id)
plan_df = df_pm_plans.get(og_id)
print(f"🔍 ===== НАЧАЛО ОБРАБОТКИ ОГ {og_id} =====")
print(f"🔍 Коды: {codes}")
print(f"🔍 Столбцы: {columns}")
print(f"🔍 Получены данные для {og_id}: факт={'' if fact_df is not None else ''}, план={'' if plan_df is not None else ''}")
logger.debug(f"🔍 ===== НАЧАЛО ОБРАБОТКИ ОГ {og_id} =====")
logger.debug(f"🔍 Коды: {codes}")
logger.debug(f"🔍 Столбцы: {columns}")
logger.debug(f"🔍 Получены данные для {og_id}: факт={'' if fact_df is not None else ''}, план={'' if plan_df is not None else ''}")
# Определяем, какие столбцы из какого датафрейма брать
for col in columns:
@@ -360,24 +364,24 @@ class SvodkaPMParser(ParserPort):
if col in ['ПП', 'БП']:
if plan_df is None:
print(f"❌ Невозможно обработать '{col}': нет данных плана для {og_id}")
logger.warning(f"❌ Невозможно обработать '{col}': нет данных плана для {og_id}")
else:
print(f"🔍 DEBUG: ===== ОБРАБАТЫВАЕМ '{col}' ИЗ ДАННЫХ ПЛАНА =====")
logger.debug(f"🔍 ===== ОБРАБАТЫВАЕМ '{col}' ИЗ ДАННЫХ ПЛАНА =====")
for code in codes:
print(f"🔍 DEBUG: --- Код {code} для {col} ---")
logger.debug(f"🔍 --- Код {code} для {col} ---")
val = self._get_svodka_value(plan_df, og_id, code, col)
col_result[str(code)] = val
print(f"🔍 DEBUG: ===== ЗАВЕРШИЛИ ОБРАБОТКУ '{col}' =====")
logger.debug(f"🔍 ===== ЗАВЕРШИЛИ ОБРАБОТКУ '{col}' =====")
elif col in ['ТБ', 'СЭБ', 'НЭБ']:
if fact_df is None:
print(f"❌ Невозможно обработать '{col}': нет данных факта для {og_id}")
logger.warning(f"❌ Невозможно обработать '{col}': нет данных факта для {og_id}")
else:
for code in codes:
val = self._get_svodka_value(fact_df, og_id, code, col)
col_result[str(code)] = val
else:
print(f"⚠️ Неизвестный столбец: '{col}'. Пропускаем.")
logger.warning(f"⚠️ Неизвестный столбец: '{col}'. Пропускаем.")
col_result = {str(code): None for code in codes}
result[col] = col_result
@@ -443,7 +447,7 @@ class SvodkaPMParser(ParserPort):
data = self._get_svodka_og(og_id, codes, columns, search)
total_result[og_id] = data
except Exception as e:
print(f"❌ Ошибка при обработке {og_id}: {e}")
logger.error(f"❌ Ошибка при обработке {og_id}: {e}")
total_result[og_id] = None
json_result = data_to_json(total_result)

View File

@@ -4,6 +4,7 @@ import os
import tempfile
import shutil
import zipfile
import logging
from typing import Dict, List, Optional, Any
from core.ports import ParserPort
@@ -11,6 +12,9 @@ from core.schema_utils import register_getter_from_schema, validate_params_with_
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
from adapters.pconfig import SINGLE_OGS, find_header_row, get_og_by_name
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
class SvodkaRepairCAParser(ParserPort):
"""Парсер для сводок ремонта СА"""
@@ -29,7 +33,7 @@ class SvodkaRepairCAParser(ParserPort):
def _get_repair_data_wrapper(self, params: dict):
"""Получение данных о ремонтных работах"""
print(f"🔍 DEBUG: _get_repair_data_wrapper вызван с параметрами: {params}")
logger.debug(f"🔍 _get_repair_data_wrapper вызван с параметрами: {params}")
# Валидируем параметры с помощью схемы Pydantic
validated_params = validate_params_with_schema(params, SvodkaRepairCARequest)
@@ -39,21 +43,21 @@ class SvodkaRepairCAParser(ParserPort):
include_planned = validated_params.get("include_planned", True)
include_factual = validated_params.get("include_factual", True)
print(f"🔍 DEBUG: Запрошенные ОГ: {og_ids}")
print(f"🔍 DEBUG: Запрошенные типы ремонта: {repair_types}")
print(f"🔍 DEBUG: Включать плановые: {include_planned}, фактические: {include_factual}")
logger.debug(f"🔍 Запрошенные ОГ: {og_ids}")
logger.debug(f"🔍 Запрошенные типы ремонта: {repair_types}")
logger.debug(f"🔍 Включать плановые: {include_planned}, фактические: {include_factual}")
# Проверяем, есть ли данные в data_dict (из парсинга) или в df (из загрузки)
if hasattr(self, 'data_dict') and self.data_dict is not None:
# Данные из парсинга
data_source = self.data_dict
print(f"🔍 DEBUG: Используем data_dict с {len(data_source)} записями")
logger.debug(f"🔍 Используем data_dict с {len(data_source)} записями")
elif hasattr(self, 'df') and self.df is not None and not self.df.empty:
# Данные из загрузки - преобразуем DataFrame обратно в словарь
data_source = self._df_to_data_dict()
print(f"🔍 DEBUG: Используем df, преобразованный в data_dict с {len(data_source)} записями")
logger.debug(f"🔍 Используем df, преобразованный в data_dict с {len(data_source)} записями")
else:
print(f"🔍 DEBUG: Нет данных! data_dict={getattr(self, 'data_dict', 'None')}, df={getattr(self, 'df', 'None')}")
logger.warning(f"🔍 Нет данных! data_dict={getattr(self, 'data_dict', 'None')}, df={getattr(self, 'df', 'None')}")
return []
# Группируем данные по ОГ (как в оригинале)
@@ -86,8 +90,8 @@ class SvodkaRepairCAParser(ParserPort):
grouped_data[og_id].append(filtered_item)
total_records = sum(len(v) for v in grouped_data.values())
print(f"🔍 DEBUG: Отфильтровано {total_records} записей из {len(data_source)}")
print(f"🔍 DEBUG: Группировано по {len(grouped_data)} ОГ: {list(grouped_data.keys())}")
logger.debug(f"🔍 Отфильтровано {total_records} записей из {len(data_source)}")
logger.debug(f"🔍 Группировано по {len(grouped_data)} ОГ: {list(grouped_data.keys())}")
return grouped_data
def _df_to_data_dict(self):
@@ -109,7 +113,7 @@ class SvodkaRepairCAParser(ParserPort):
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
"""Парсинг файла и возврат DataFrame"""
print(f"🔍 DEBUG: SvodkaRepairCAParser.parse вызван с файлом: {file_path}")
logger.debug(f"🔍 SvodkaRepairCAParser.parse вызван с файлом: {file_path}")
# Определяем, это ZIP архив или одиночный файл
if file_path.lower().endswith('.zip'):
@@ -133,17 +137,17 @@ class SvodkaRepairCAParser(ParserPort):
if data_rows:
df = pd.DataFrame(data_rows)
self.df = df
print(f"🔍 DEBUG: Создан DataFrame с {len(data_rows)} записями")
logger.debug(f"🔍 Создан DataFrame с {len(data_rows)} записями")
return df
# Если данных нет, возвращаем пустой DataFrame
self.df = pd.DataFrame()
print(f"🔍 DEBUG: Возвращаем пустой DataFrame")
logger.debug(f"🔍 Возвращаем пустой DataFrame")
return self.df
def _parse_zip_archive(self, file_path: str, params: dict) -> List[Dict]:
"""Парсинг ZIP архива с файлами ремонта СА"""
print(f"🔍 DEBUG: Парсинг ZIP архива: {file_path}")
logger.info(f"🔍 Парсинг ZIP архива: {file_path}")
all_data = []
temp_dir = None
@@ -151,7 +155,7 @@ class SvodkaRepairCAParser(ParserPort):
try:
# Создаем временную директорию
temp_dir = tempfile.mkdtemp()
print(f"📦 Архив разархивирован в: {temp_dir}")
logger.debug(f"📦 Архив разархивирован в: {temp_dir}")
# Разархивируем файл
with zipfile.ZipFile(file_path, 'r') as zip_ref:
@@ -164,30 +168,30 @@ class SvodkaRepairCAParser(ParserPort):
if file.lower().endswith(('.xlsx', '.xlsm', '.xls')):
excel_files.append(os.path.join(root, file))
print(f"📊 Найдено Excel файлов: {len(excel_files)}")
logger.info(f"📊 Найдено Excel файлов: {len(excel_files)}")
# Обрабатываем каждый найденный файл
for excel_file in excel_files:
print(f"📊 Обработка файла: {excel_file}")
logger.info(f"📊 Обработка файла: {excel_file}")
file_data = self._parse_single_file(excel_file, params)
if file_data:
all_data.extend(file_data)
print(f"🎯 Всего обработано записей: {len(all_data)}")
logger.info(f"🎯 Всего обработано записей: {len(all_data)}")
return all_data
except Exception as e:
print(f"❌ Ошибка при обработке ZIP архива: {e}")
logger.error(f"❌ Ошибка при обработке ZIP архива: {e}")
return []
finally:
# Удаляем временную директорию
if temp_dir:
shutil.rmtree(temp_dir, ignore_errors=True)
print(f"🗑️ Временная директория удалена: {temp_dir}")
logger.debug(f"🗑️ Временная директория удалена: {temp_dir}")
def _parse_single_file(self, file_path: str, params: dict) -> List[Dict]:
"""Парсинг одиночного Excel файла"""
print(f"🔍 DEBUG: Парсинг файла: {file_path}")
logger.debug(f"🔍 Парсинг файла: {file_path}")
try:
# Получаем параметры
@@ -198,10 +202,10 @@ class SvodkaRepairCAParser(ParserPort):
if header_num is None:
header_num = find_header_row(file_path, sheet_name, search_value="ОГ")
if header_num is None:
print(f"Не найден заголовок в файле {file_path}")
logger.error(f"Не найден заголовок в файле {file_path}")
return []
print(f"🔍 DEBUG: Заголовок найден в строке {header_num}")
logger.debug(f"🔍 Заголовок найден в строке {header_num}")
# Читаем Excel файл
df = pd.read_excel(
@@ -213,23 +217,23 @@ class SvodkaRepairCAParser(ParserPort):
)
if df.empty:
print(f"❌ Файл {file_path} пуст")
logger.error(f"❌ Файл {file_path} пуст")
return []
if "ОГ" not in df.columns:
print(f"⚠️ Предупреждение: Колонка 'ОГ' не найдена в файле {file_path}")
logger.warning(f"⚠️ Предупреждение: Колонка 'ОГ' не найдена в файле {file_path}")
return []
# Обрабатываем данные
return self._process_repair_data(df)
except Exception as e:
print(f"❌ Ошибка при парсинге файла {file_path}: {e}")
logger.error(f"❌ Ошибка при парсинге файла {file_path}: {e}")
return []
def _process_repair_data(self, df: pd.DataFrame) -> List[Dict]:
"""Обработка данных о ремонте"""
print(f"🔍 DEBUG: Обработка данных с {len(df)} строками")
logger.debug(f"🔍 Обработка данных с {len(df)} строками")
# Шаг 1: Нормализация ОГ
def safe_replace(val):
@@ -254,7 +258,7 @@ class SvodkaRepairCAParser(ParserPort):
df = df[mask_og].copy()
if df.empty:
print(f"❌ Нет данных после фильтрации по ОГ")
logger.info(f"❌ Нет данных после фильтрации по ОГ")
return []
# Шаг 4: Удаление строк без "Вид простоя"
@@ -263,7 +267,7 @@ class SvodkaRepairCAParser(ParserPort):
mask_downtime = (downtime_clean != "") & (downtime_clean != "nan")
df = df[mask_downtime].copy()
else:
print("⚠️ Предупреждение: Колонка 'Вид простоя' не найдена.")
logger.info("⚠️ Предупреждение: Колонка 'Вид простоя' не найдена.")
return []
# Шаг 5: Удаление ненужных колонок
@@ -278,7 +282,7 @@ class SvodkaRepairCAParser(ParserPort):
# Шаг 6: Переименование первых 8 колонок по порядку
if df.shape[1] < 8:
print(f"⚠️ Внимание: В DataFrame только {df.shape[1]} колонок, требуется минимум 8.")
logger.info(f"⚠️ Внимание: В DataFrame только {df.shape[1]} колонок, требуется минимум 8.")
return []
new_names = ["id", "name", "type", "start_date", "end_date", "plan", "fact", "downtime"]
@@ -328,10 +332,10 @@ class SvodkaRepairCAParser(ParserPort):
result_data.append(record)
except Exception as e:
print(f"⚠️ Ошибка при обработке строки: {e}")
logger.info(f"⚠️ Ошибка при обработке строки: {e}")
continue
print(f"✅ Обработано {len(result_data)} записей")
logger.info(f"✅ Обработано {len(result_data)} записей")
return result_data
def _parse_date(self, value) -> Optional[str]:

View File

@@ -4,6 +4,10 @@ import json
import numpy as np
import pandas as pd
import os
import logging
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
OG_IDS = {
"Комсомольский НПЗ": "KNPZ",
@@ -163,7 +167,7 @@ def find_header_row(file, sheet, search_value="Итого", max_rows=50):
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
for idx, row in df_temp.iterrows():
if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
logger.debug(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
return idx # 0-based index — то, что нужно для header=
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")

View File

@@ -4,12 +4,16 @@
import os
import pickle
import io
import logging
from typing import Optional
from minio import Minio # boto3
import pandas as pd
from core.ports import StoragePort
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
class MinIOStorageAdapter(StoragePort):
"""Адаптер для MinIO хранилища"""
@@ -37,8 +41,8 @@ class MinIOStorageAdapter(StoragePort):
# Проверяем bucket только при первом использовании
self._ensure_bucket_exists()
except Exception as e:
print(f"⚠️ Не удалось подключиться к MinIO: {e}")
print("MinIO будет недоступен, но приложение продолжит работать")
logger.warning(f"⚠️ Не удалось подключиться к MinIO: {e}")
logger.warning("MinIO будет недоступен, но приложение продолжит работать")
return None
return self._client
@@ -50,16 +54,16 @@ class MinIOStorageAdapter(StoragePort):
try:
if not self.client.bucket_exists(self._bucket_name):
self.client.make_bucket(self._bucket_name)
print(f"✅ Bucket '{self._bucket_name}' создан")
logger.info(f"✅ Bucket '{self._bucket_name}' создан")
return True
except Exception as e:
print(f"❌ Ошибка при работе с bucket: {e}")
logger.error(f"❌ Ошибка при работе с bucket: {e}")
return False
def save_dataframe(self, df: pd.DataFrame, object_id: str) -> bool:
"""Сохранение DataFrame в MinIO"""
if self.client is None:
print("⚠️ MinIO недоступен, данные не сохранены")
logger.warning("⚠️ MinIO недоступен, данные не сохранены")
return False
try:
@@ -78,16 +82,16 @@ class MinIOStorageAdapter(StoragePort):
content_type='application/octet-stream'
)
print(f"✅ DataFrame успешно сохранен в MinIO: {self._bucket_name}/{object_id}")
logger.info(f"✅ DataFrame успешно сохранен в MinIO: {self._bucket_name}/{object_id}")
return True
except Exception as e:
print(f"❌ Ошибка при сохранении в MinIO: {e}")
logger.error(f"❌ Ошибка при сохранении в MinIO: {e}")
return False
def load_dataframe(self, object_id: str) -> Optional[pd.DataFrame]:
"""Загрузка DataFrame из MinIO"""
if self.client is None:
print("⚠️ MinIO недоступен, данные не загружены")
logger.warning("⚠️ MinIO недоступен, данные не загружены")
return None
try:
@@ -102,7 +106,7 @@ class MinIOStorageAdapter(StoragePort):
return df
except Exception as e:
print(f"❌ Ошибка при загрузке данных из MinIO: {e}")
logger.error(f"❌ Ошибка при загрузке данных из MinIO: {e}")
return None
finally:
if 'response' in locals():
@@ -112,15 +116,15 @@ class MinIOStorageAdapter(StoragePort):
def delete_object(self, object_id: str) -> bool:
"""Удаление объекта из MinIO"""
if self.client is None:
print("⚠️ MinIO недоступен, объект не удален")
logger.warning("⚠️ MinIO недоступен, объект не удален")
return False
try:
self.client.remove_object(self._bucket_name, object_id)
print(f"✅ Объект успешно удален из MinIO: {self._bucket_name}/{object_id}")
logger.info(f"✅ Объект успешно удален из MinIO: {self._bucket_name}/{object_id}")
return True
except Exception as e:
print(f"❌ Ошибка при удалении объекта из MinIO: {e}")
logger.error(f"❌ Ошибка при удалении объекта из MinIO: {e}")
return False
def object_exists(self, object_id: str) -> bool:

View File

@@ -1,12 +1,23 @@
import os
import multiprocessing
import uvicorn
import logging
from typing import Dict, List
from fastapi import FastAPI, File, UploadFile, HTTPException, status
from fastapi.responses import JSONResponse
# Настройка логирования
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(name)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
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.services import ReportService, PARSERS
@@ -18,8 +29,10 @@ from app.schemas import (
SvodkaCARequest,
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest
)
from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
from app.schemas.statuses_repair_ca import StatusesRepairCARequest
from app.schemas.monitoring_tar import MonitoringTarRequest, MonitoringTarFullRequest
# Парсеры
@@ -27,8 +40,10 @@ PARSERS.update({
'svodka_pm': SvodkaPMParser,
'svodka_ca': SvodkaCAParser,
'monitoring_fuel': MonitoringFuelParser,
'monitoring_tar': MonitoringTarParser,
'svodka_repair_ca': SvodkaRepairCAParser,
'statuses_repair_ca': StatusesRepairCAParser,
'oper_spravka_tech_pos': OperSpravkaTechPosParser,
# 'svodka_plan_sarnpz': SvodkaPlanSarnpzParser,
})
@@ -122,8 +137,8 @@ async def get_available_ogs(parser_name: str):
parser_class = PARSERS[parser_name]
# Для svodka_repair_ca возвращаем ОГ из загруженных данных
if parser_name == "svodka_repair_ca":
# Для парсеров с данными в MinIO возвращаем ОГ из загруженных данных
if parser_name in ["svodka_repair_ca", "oper_spravka_tech_pos"]:
try:
# Создаем экземпляр сервиса и загружаем данные из MinIO
report_service = get_report_service()
@@ -133,12 +148,24 @@ 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:
# Для 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}
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:
print(f"⚠️ Ошибка при получении ОГ: {e}")
logger.error(f"⚠️ Ошибка при получении ОГ: {e}")
import traceback
traceback.print_exc()
@@ -1163,5 +1190,258 @@ async def get_monitoring_fuel_month_by_code(
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 []
logger.debug(f"🔍 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__":
uvicorn.run(app, host="0.0.0.0", port=8080)

View 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": {}
}

View 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": "Данные успешно получены"
}
}

View File

@@ -3,11 +3,15 @@
"""
import tempfile
import os
import logging
from typing import Dict, Type
from core.models import UploadRequest, UploadResult, DataRequest, DataResult
from core.ports import ParserPort, StoragePort
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
# Глобальный словарь парсеров
PARSERS: Dict[str, Type[ParserPort]] = {}
@@ -51,7 +55,7 @@ class ReportService:
# Удаляем старый объект, если он существует и хранилище доступно
if self.storage.object_exists(object_id):
self.storage.delete_object(object_id)
print(f"Старый объект удален: {object_id}")
logger.debug(f"Старый объект удален: {object_id}")
# Сохраняем в хранилище
if self.storage.save_dataframe(parse_result, object_id):
@@ -102,18 +106,18 @@ class ReportService:
# Устанавливаем данные в парсер для использования в геттерах
parser.df = loaded_data
print(f"🔍 DEBUG: ReportService.get_data - установлены данные в парсер {request.report_type}")
logger.debug(f"🔍 ReportService.get_data - установлены данные в парсер {request.report_type}")
# Проверяем тип загруженных данных
if hasattr(loaded_data, 'shape'):
# Это DataFrame
print(f"🔍 DEBUG: DataFrame shape: {loaded_data.shape}")
print(f"🔍 DEBUG: DataFrame columns: {list(loaded_data.columns) if not loaded_data.empty else 'Empty'}")
logger.debug(f"🔍 DataFrame shape: {loaded_data.shape}")
logger.debug(f"🔍 DataFrame columns: {list(loaded_data.columns) if not loaded_data.empty else 'Empty'}")
elif isinstance(loaded_data, dict):
# Это словарь (для парсера ПМ)
print(f"🔍 DEBUG: Словарь с ключами: {list(loaded_data.keys())}")
logger.debug(f"🔍 Словарь с ключами: {list(loaded_data.keys())}")
else:
print(f"🔍 DEBUG: Неизвестный тип данных: {type(loaded_data)}")
logger.debug(f"🔍 Неизвестный тип данных: {type(loaded_data)}")
# Получаем параметры запроса
get_params = request.get_params or {}
@@ -142,6 +146,14 @@ class ReportService:
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)
@@ -150,7 +162,7 @@ class ReportService:
available_getters = list(parser.getters.keys())
if available_getters:
getter_name = available_getters[0]
print(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
else:
return DataResult(
success=False,
@@ -164,12 +176,15 @@ class ReportService:
available_getters = list(parser.getters.keys())
if available_getters:
getter_name = available_getters[0]
print(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
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)
@@ -178,7 +193,7 @@ class ReportService:
available_getters = list(parser.getters.keys())
if available_getters:
getter_name = available_getters[0]
print(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
else:
return DataResult(
success=False,

33
run_tests.py Normal file
View 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)

View File

@@ -115,12 +115,14 @@ def main():
st.write(f"{parser}")
# Основные вкладки - по одной на каждый парсер
tab1, tab2, tab3, tab4, tab5 = st.tabs([
tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs([
"📊 Сводки ПМ",
"🏭 Сводки СА",
"⛽ Мониторинг топлива",
"🔧 Ремонт СА",
"📋 Статусы ремонта СА"
"📋 Статусы ремонта СА",
"⚡ Мониторинг ТЭР",
"🏭 Операционные справки"
])
# Вкладка 1: Сводки ПМ - полный функционал
@@ -633,6 +635,189 @@ def main():
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("---")
st.markdown("### 📚 Документация API")
@@ -647,6 +832,7 @@ def main():
- 📊 Парсинг сводок ПМ (план и факт)
- 🏭 Парсинг сводок СА
- ⛽ Мониторинг топлива
- ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы)
- 🔧 Управление ремонтными работами СА
- 📋 Мониторинг статусов ремонта СА

BIN
test_repair_ca.zip Normal file

Binary file not shown.

44
tests/README.md Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
pytest>=7.0.0
pandas>=1.5.0
numpy>=1.20.0
openpyxl>=3.0.0

Binary file not shown.

Binary file not shown.

BIN
tests/test_data/pm_all.zip Normal file

Binary file not shown.

Binary file not shown.

394
tests/test_parsers.py Normal file
View 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__])