Compare commits
4 Commits
0f3340c899
...
create-tes
| Author | SHA1 | Date | |
|---|---|---|---|
| 2555fd80e0 | |||
| 847441842c | |||
| 00a01e99d7 | |||
| bbbfbbd508 |
@@ -1,12 +1,16 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import re
|
import re
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import logging
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple
|
||||||
from core.ports import ParserPort
|
from core.ports import ParserPort
|
||||||
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
||||||
from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest
|
from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest
|
||||||
from adapters.pconfig import data_to_json
|
from adapters.pconfig import data_to_json
|
||||||
|
|
||||||
|
# Настройка логгера для модуля
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MonitoringFuelParser(ParserPort):
|
class MonitoringFuelParser(ParserPort):
|
||||||
"""Парсер для мониторинга топлива"""
|
"""Парсер для мониторинга топлива"""
|
||||||
@@ -157,19 +161,19 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
if len(candidates) == 1:
|
if len(candidates) == 1:
|
||||||
file = candidates[0]
|
file = candidates[0]
|
||||||
|
|
||||||
print(f'Загрузка {file}')
|
logger.info(f'Загрузка {file}')
|
||||||
with zip_ref.open(file) as excel_file:
|
with zip_ref.open(file) as excel_file:
|
||||||
try:
|
try:
|
||||||
df = self.parse_single(excel_file, 'Мониторинг потребления')
|
df = self.parse_single(excel_file, 'Мониторинг потребления')
|
||||||
df_monitorings[mm] = df
|
df_monitorings[mm] = df
|
||||||
|
|
||||||
print(f"✅ Данные за месяц {mm} загружены")
|
logger.info(f"✅ Данные за месяц {mm} загружены")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка при загрузке файла {file_temp}: {e}")
|
logger.error(f"Ошибка при загрузке файла {file_temp}: {e}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(f"⚠️ Файл не найден: {file_temp}")
|
logger.warning(f"⚠️ Файл не найден: {file_temp}")
|
||||||
|
|
||||||
return df_monitorings
|
return df_monitorings
|
||||||
|
|
||||||
@@ -187,7 +191,7 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
||||||
for idx, row in df_temp.iterrows():
|
for idx, row in df_temp.iterrows():
|
||||||
if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
|
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)
|
return idx + 1 # возвращаем индекс строки (0-based)
|
||||||
|
|
||||||
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
||||||
@@ -237,7 +241,7 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
|
|
||||||
# Устанавливаем id как индекс
|
# Устанавливаем id как индекс
|
||||||
df_full.set_index('id', inplace=True)
|
df_full.set_index('id', inplace=True)
|
||||||
print(f"Окончательное количество столбцов: {len(df_full.columns)}")
|
logger.debug(f"Окончательное количество столбцов: {len(df_full.columns)}")
|
||||||
return df_full
|
return df_full
|
||||||
|
|
||||||
def aggregate_by_columns(self, df_dict: Dict[str, pd.DataFrame], columns: list) -> Tuple[pd.DataFrame, Dict[str, pd.DataFrame]]:
|
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():
|
for file_key, df in df_dict.items():
|
||||||
if col not in df.columns:
|
if col not in df.columns:
|
||||||
print(f"Колонка '{col}' не найдена в {file_key}, пропускаем.")
|
logger.warning(f"Колонка '{col}' не найдена в {file_key}, пропускаем.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Берём колонку, оставляем id как индекс
|
# Берём колонку, оставляем id как индекс
|
||||||
@@ -302,7 +306,7 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
|
|
||||||
for file, df in df_dict.items():
|
for file, df in df_dict.items():
|
||||||
if column not in df.columns:
|
if column not in df.columns:
|
||||||
print(f"Колонка '{column}' не найдена в {file}, пропускаем.")
|
logger.warning(f"Колонка '{column}' не найдена в {file}, пропускаем.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Берём колонку и сохраняем как Series с именем месяца
|
# Берём колонку и сохраняем как Series с именем месяца
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import os
|
|||||||
import zipfile
|
import zipfile
|
||||||
import tempfile
|
import tempfile
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import logging
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
from core.ports import ParserPort
|
from core.ports import ParserPort
|
||||||
from adapters.pconfig import find_header_row, SNPZ_IDS, data_to_json
|
from adapters.pconfig import find_header_row, SNPZ_IDS, data_to_json
|
||||||
|
|
||||||
|
# Настройка логгера для модуля
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MonitoringTarParser(ParserPort):
|
class MonitoringTarParser(ParserPort):
|
||||||
"""Парсер для мониторинга ТЭР (топливно-энергетических ресурсов)"""
|
"""Парсер для мониторинга ТЭР (топливно-энергетических ресурсов)"""
|
||||||
@@ -23,7 +27,7 @@ class MonitoringTarParser(ParserPort):
|
|||||||
|
|
||||||
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
|
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
|
||||||
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
|
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
|
||||||
print(f"🔍 DEBUG: MonitoringTarParser.parse вызван с файлом: {file_path}")
|
logger.debug(f"🔍 MonitoringTarParser.parse вызван с файлом: {file_path}")
|
||||||
|
|
||||||
if not file_path.endswith('.zip'):
|
if not file_path.endswith('.zip'):
|
||||||
raise ValueError("MonitoringTarParser поддерживает только ZIP архивы")
|
raise ValueError("MonitoringTarParser поддерживает только ZIP архивы")
|
||||||
@@ -42,15 +46,15 @@ class MonitoringTarParser(ParserPort):
|
|||||||
})
|
})
|
||||||
|
|
||||||
df = pd.DataFrame(data_list)
|
df = pd.DataFrame(data_list)
|
||||||
print(f"🔍 DEBUG: Создан DataFrame с {len(df)} записями")
|
logger.debug(f"🔍 Создан DataFrame с {len(df)} записями")
|
||||||
return df
|
return df
|
||||||
else:
|
else:
|
||||||
print("🔍 DEBUG: Возвращаем пустой DataFrame")
|
logger.debug("🔍 Возвращаем пустой DataFrame")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
def _parse_zip_archive(self, zip_path: str) -> Dict[str, Any]:
|
def _parse_zip_archive(self, zip_path: str) -> Dict[str, Any]:
|
||||||
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
|
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
|
||||||
print(f"📦 Обработка ZIP архива: {zip_path}")
|
logger.info(f"📦 Обработка ZIP архива: {zip_path}")
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
@@ -67,17 +71,17 @@ class MonitoringTarParser(ParserPort):
|
|||||||
if not tar_files:
|
if not tar_files:
|
||||||
raise ValueError("В архиве не найдены файлы мониторинга ТЭР")
|
raise ValueError("В архиве не найдены файлы мониторинга ТЭР")
|
||||||
|
|
||||||
print(f"📁 Найдено {len(tar_files)} файлов мониторинга ТЭР")
|
logger.info(f"📁 Найдено {len(tar_files)} файлов мониторинга ТЭР")
|
||||||
|
|
||||||
# Обрабатываем каждый файл
|
# Обрабатываем каждый файл
|
||||||
all_data = {}
|
all_data = {}
|
||||||
for file_path in tar_files:
|
for file_path in tar_files:
|
||||||
print(f"📁 Обработка файла: {file_path}")
|
logger.info(f"📁 Обработка файла: {file_path}")
|
||||||
|
|
||||||
# Извлекаем номер месяца из имени файла
|
# Извлекаем номер месяца из имени файла
|
||||||
filename = os.path.basename(file_path)
|
filename = os.path.basename(file_path)
|
||||||
month_str = self._extract_month_from_filename(filename)
|
month_str = self._extract_month_from_filename(filename)
|
||||||
print(f"📅 Месяц: {month_str}")
|
logger.debug(f"📅 Месяц: {month_str}")
|
||||||
|
|
||||||
# Парсим файл
|
# Парсим файл
|
||||||
file_data = self._parse_single_file(file_path, month_str)
|
file_data = self._parse_single_file(file_path, month_str)
|
||||||
@@ -102,7 +106,7 @@ class MonitoringTarParser(ParserPort):
|
|||||||
excel_file = pd.ExcelFile(file_path)
|
excel_file = pd.ExcelFile(file_path)
|
||||||
available_sheets = excel_file.sheet_names
|
available_sheets = excel_file.sheet_names
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Не удалось открыть Excel-файл {file_path}: {e}")
|
logger.error(f"❌ Не удалось открыть Excel-файл {file_path}: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Словарь для хранения данных: id -> {'total': [], 'last_day': []}
|
# Словарь для хранения данных: id -> {'total': [], 'last_day': []}
|
||||||
@@ -115,7 +119,7 @@ class MonitoringTarParser(ParserPort):
|
|||||||
# Обрабатываем файлы svodka_tar_*.xlsx с SNPZ_IDS
|
# Обрабатываем файлы svodka_tar_*.xlsx с SNPZ_IDS
|
||||||
for name, id in SNPZ_IDS.items():
|
for name, id in SNPZ_IDS.items():
|
||||||
if name not in available_sheets:
|
if name not in available_sheets:
|
||||||
print(f"🟡 Лист '{name}' отсутствует в файле {file_path}")
|
logger.warning(f"🟡 Лист '{name}' отсутствует в файле {file_path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Парсим оба типа строк
|
# Парсим оба типа строк
|
||||||
@@ -140,7 +144,7 @@ class MonitoringTarParser(ParserPort):
|
|||||||
|
|
||||||
for sheet_name, id in monitoring_sheets.items():
|
for sheet_name, id in monitoring_sheets.items():
|
||||||
if sheet_name not in available_sheets:
|
if sheet_name not in available_sheets:
|
||||||
print(f"🟡 Лист '{sheet_name}' отсутствует в файле {file_path}")
|
logger.warning(f"🟡 Лист '{sheet_name}' отсутствует в файле {file_path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Парсим оба типа строк
|
# Парсим оба типа строк
|
||||||
@@ -168,7 +172,7 @@ class MonitoringTarParser(ParserPort):
|
|||||||
else:
|
else:
|
||||||
df_svodka_tar[id]['last_day'] = pd.DataFrame()
|
df_svodka_tar[id]['last_day'] = pd.DataFrame()
|
||||||
|
|
||||||
print(f"✅ Агрегировано: {len(df_svodka_tar[id]['total'])} 'total' и "
|
logger.info(f"✅ Агрегировано: {len(df_svodka_tar[id]['total'])} 'total' и "
|
||||||
f"{len(df_svodka_tar[id]['last_day'])} 'last_day' записей для id='{id}'")
|
f"{len(df_svodka_tar[id]['last_day'])} 'last_day' записей для id='{id}'")
|
||||||
|
|
||||||
return df_svodka_tar
|
return df_svodka_tar
|
||||||
@@ -178,7 +182,7 @@ class MonitoringTarParser(ParserPort):
|
|||||||
try:
|
try:
|
||||||
# Проверяем наличие листа
|
# Проверяем наличие листа
|
||||||
if sheet not in pd.ExcelFile(file).sheet_names:
|
if sheet not in pd.ExcelFile(file).sheet_names:
|
||||||
print(f"🟡 Лист '{sheet}' не найден в файле {file}")
|
logger.warning(f"🟡 Лист '{sheet}' не найден в файле {file}")
|
||||||
return {'total': None, 'last_day': None}
|
return {'total': None, 'last_day': None}
|
||||||
|
|
||||||
# Определяем номер заголовка в зависимости от типа файла
|
# Определяем номер заголовка в зависимости от типа файла
|
||||||
@@ -187,16 +191,16 @@ class MonitoringTarParser(ParserPort):
|
|||||||
# Для файлов svodka_tar_*.xlsx ищем заголовок по значению "1"
|
# Для файлов svodka_tar_*.xlsx ищем заголовок по значению "1"
|
||||||
header_num = find_header_row(file, sheet, search_value="1")
|
header_num = find_header_row(file, sheet, search_value="1")
|
||||||
if header_num is None:
|
if header_num is None:
|
||||||
print(f"❌ Не найдена строка с заголовком '1' в файле {file}, лист '{sheet}'")
|
logger.error(f"❌ Не найдена строка с заголовком '1' в файле {file}, лист '{sheet}'")
|
||||||
return {'total': None, 'last_day': None}
|
return {'total': None, 'last_day': None}
|
||||||
elif filename.startswith('monitoring_'):
|
elif filename.startswith('monitoring_'):
|
||||||
# Для файлов monitoring_*.xlsm заголовок находится в строке 5
|
# Для файлов monitoring_*.xlsm заголовок находится в строке 5
|
||||||
header_num = 5
|
header_num = 5
|
||||||
else:
|
else:
|
||||||
print(f"❌ Неизвестный тип файла: {filename}")
|
logger.error(f"❌ Неизвестный тип файла: {filename}")
|
||||||
return {'total': None, 'last_day': None}
|
return {'total': None, 'last_day': None}
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Используем заголовок в строке {header_num} для листа '{sheet}'")
|
logger.debug(f"🔍 Используем заголовок в строке {header_num} для листа '{sheet}'")
|
||||||
|
|
||||||
# Читаем с двумя уровнями заголовков
|
# Читаем с двумя уровнями заголовков
|
||||||
df = pd.read_excel(
|
df = pd.read_excel(
|
||||||
@@ -212,7 +216,7 @@ class MonitoringTarParser(ParserPort):
|
|||||||
# Удаляем строки, где все значения — NaN
|
# Удаляем строки, где все значения — NaN
|
||||||
df = df.dropna(how='all').reset_index(drop=True)
|
df = df.dropna(how='all').reset_index(drop=True)
|
||||||
if df.empty:
|
if df.empty:
|
||||||
print(f"🟡 Нет данных после очистки в файле {file}, лист '{sheet}'")
|
logger.warning(f"🟡 Нет данных после очистки в файле {file}, лист '{sheet}'")
|
||||||
return {'total': None, 'last_day': None}
|
return {'total': None, 'last_day': None}
|
||||||
|
|
||||||
# === 1. Обработка строки "Всего" ===
|
# === 1. Обработка строки "Всего" ===
|
||||||
@@ -240,12 +244,12 @@ class MonitoringTarParser(ParserPort):
|
|||||||
return {'total': df_total, 'last_day': df_last_day}
|
return {'total': df_total, 'last_day': df_last_day}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при обработке файла {file}, лист '{sheet}': {e}")
|
logger.error(f"❌ Ошибка при обработке файла {file}, лист '{sheet}': {e}")
|
||||||
return {'total': None, 'last_day': None}
|
return {'total': None, 'last_day': None}
|
||||||
|
|
||||||
def _get_tar_data_wrapper(self, params: Dict[str, Any] = None) -> str:
|
def _get_tar_data_wrapper(self, params: Dict[str, Any] = None) -> str:
|
||||||
"""Обертка для получения данных мониторинга ТЭР с фильтрацией по режиму"""
|
"""Обертка для получения данных мониторинга ТЭР с фильтрацией по режиму"""
|
||||||
print(f"🔍 DEBUG: _get_tar_data_wrapper вызван с параметрами: {params}")
|
logger.debug(f"🔍 _get_tar_data_wrapper вызван с параметрами: {params}")
|
||||||
|
|
||||||
# Получаем режим из параметров
|
# Получаем режим из параметров
|
||||||
mode = params.get('mode', 'total') if params else 'total'
|
mode = params.get('mode', 'total') if params else 'total'
|
||||||
@@ -274,12 +278,12 @@ class MonitoringTarParser(ParserPort):
|
|||||||
result_json = data_to_json(filtered_data)
|
result_json = data_to_json(filtered_data)
|
||||||
return result_json
|
return result_json
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при конвертации данных в JSON: {e}")
|
logger.error(f"❌ Ошибка при конвертации данных в JSON: {e}")
|
||||||
return "{}"
|
return "{}"
|
||||||
|
|
||||||
def _get_tar_full_data_wrapper(self, params: Dict[str, Any] = None) -> str:
|
def _get_tar_full_data_wrapper(self, params: Dict[str, Any] = None) -> str:
|
||||||
"""Обертка для получения всех данных мониторинга ТЭР"""
|
"""Обертка для получения всех данных мониторинга ТЭР"""
|
||||||
print(f"🔍 DEBUG: _get_tar_full_data_wrapper вызван с параметрами: {params}")
|
logger.debug(f"🔍 _get_tar_full_data_wrapper вызван с параметрами: {params}")
|
||||||
|
|
||||||
# Получаем все данные
|
# Получаем все данные
|
||||||
full_data = {}
|
full_data = {}
|
||||||
@@ -298,5 +302,5 @@ class MonitoringTarParser(ParserPort):
|
|||||||
result_json = data_to_json(full_data)
|
result_json = data_to_json(full_data)
|
||||||
return result_json
|
return result_json
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при конвертации данных в JSON: {e}")
|
logger.error(f"❌ Ошибка при конвертации данных в JSON: {e}")
|
||||||
return "{}"
|
return "{}"
|
||||||
@@ -2,11 +2,15 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import logging
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.ports import ParserPort
|
from core.ports import ParserPort
|
||||||
from adapters.pconfig import find_header_row, get_object_by_name, data_to_json
|
from adapters.pconfig import find_header_row, get_object_by_name, data_to_json
|
||||||
|
|
||||||
|
# Настройка логгера для модуля
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class OperSpravkaTechPosParser(ParserPort):
|
class OperSpravkaTechPosParser(ParserPort):
|
||||||
"""Парсер для операционных справок технологических позиций"""
|
"""Парсер для операционных справок технологических позиций"""
|
||||||
@@ -23,7 +27,7 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
|
|
||||||
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
|
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
|
||||||
"""Парсит ZIP архив с файлами операционных справок технологических позиций"""
|
"""Парсит ZIP архив с файлами операционных справок технологических позиций"""
|
||||||
print(f"🔍 DEBUG: OperSpravkaTechPosParser.parse вызван с файлом: {file_path}")
|
logger.debug(f"🔍 OperSpravkaTechPosParser.parse вызван с файлом: {file_path}")
|
||||||
|
|
||||||
if not file_path.endswith('.zip'):
|
if not file_path.endswith('.zip'):
|
||||||
raise ValueError("OperSpravkaTechPosParser поддерживает только ZIP архивы")
|
raise ValueError("OperSpravkaTechPosParser поддерживает только ZIP архивы")
|
||||||
@@ -44,15 +48,15 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
})
|
})
|
||||||
|
|
||||||
df = pd.DataFrame(data_list)
|
df = pd.DataFrame(data_list)
|
||||||
print(f"🔍 DEBUG: Создан DataFrame с {len(df)} записями")
|
logger.debug(f"🔍 Создан DataFrame с {len(df)} записями")
|
||||||
return df
|
return df
|
||||||
else:
|
else:
|
||||||
print("🔍 DEBUG: Возвращаем пустой DataFrame")
|
logger.debug("🔍 Возвращаем пустой DataFrame")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
def _parse_zip_archive(self, zip_path: str) -> Dict[str, pd.DataFrame]:
|
def _parse_zip_archive(self, zip_path: str) -> Dict[str, pd.DataFrame]:
|
||||||
"""Парсит ZIP архив с файлами операционных справок"""
|
"""Парсит ZIP архив с файлами операционных справок"""
|
||||||
print(f"📦 Обработка ZIP архива: {zip_path}")
|
logger.info(f"📦 Обработка ZIP архива: {zip_path}")
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
@@ -69,17 +73,17 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
if not tech_pos_files:
|
if not tech_pos_files:
|
||||||
raise ValueError("В архиве не найдены файлы операционных справок технологических позиций")
|
raise ValueError("В архиве не найдены файлы операционных справок технологических позиций")
|
||||||
|
|
||||||
print(f"📁 Найдено {len(tech_pos_files)} файлов операционных справок")
|
logger.info(f"📁 Найдено {len(tech_pos_files)} файлов операционных справок")
|
||||||
|
|
||||||
# Обрабатываем каждый файл
|
# Обрабатываем каждый файл
|
||||||
all_data = {}
|
all_data = {}
|
||||||
for file_path in tech_pos_files:
|
for file_path in tech_pos_files:
|
||||||
print(f"📁 Обработка файла: {file_path}")
|
logger.info(f"📁 Обработка файла: {file_path}")
|
||||||
|
|
||||||
# Извлекаем ID ОГ из имени файла
|
# Извлекаем ID ОГ из имени файла
|
||||||
filename = os.path.basename(file_path)
|
filename = os.path.basename(file_path)
|
||||||
og_id = self._extract_og_id_from_filename(filename)
|
og_id = self._extract_og_id_from_filename(filename)
|
||||||
print(f"🏭 ОГ ID: {og_id}")
|
logger.debug(f"🏭 ОГ ID: {og_id}")
|
||||||
|
|
||||||
# Парсим файл
|
# Парсим файл
|
||||||
file_data = self._parse_single_file(file_path)
|
file_data = self._parse_single_file(file_path)
|
||||||
@@ -102,11 +106,11 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
try:
|
try:
|
||||||
# Находим актуальный лист
|
# Находим актуальный лист
|
||||||
actual_sheet = self._find_actual_sheet_num(file_path)
|
actual_sheet = self._find_actual_sheet_num(file_path)
|
||||||
print(f"📅 Актуальный лист: {actual_sheet}")
|
logger.debug(f"📅 Актуальный лист: {actual_sheet}")
|
||||||
|
|
||||||
# Находим заголовок
|
# Находим заголовок
|
||||||
header_row = self._find_header_row(file_path, actual_sheet)
|
header_row = self._find_header_row(file_path, actual_sheet)
|
||||||
print(f"📋 Заголовок найден в строке {header_row}")
|
logger.debug(f"📋 Заголовок найден в строке {header_row}")
|
||||||
|
|
||||||
# Парсим данные
|
# Парсим данные
|
||||||
df = self._parse_tech_pos_data(file_path, actual_sheet, header_row)
|
df = self._parse_tech_pos_data(file_path, actual_sheet, header_row)
|
||||||
@@ -117,11 +121,11 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
og_id = self._extract_og_id_from_filename(filename)
|
og_id = self._extract_og_id_from_filename(filename)
|
||||||
return {og_id: df}
|
return {og_id: df}
|
||||||
else:
|
else:
|
||||||
print(f"⚠️ Нет данных в файле {file_path}")
|
logger.warning(f"⚠️ Нет данных в файле {file_path}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при обработке файла {file_path}: {e}")
|
logger.error(f"❌ Ошибка при обработке файла {file_path}: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _find_actual_sheet_num(self, file_path: str) -> str:
|
def _find_actual_sheet_num(self, file_path: str) -> str:
|
||||||
@@ -160,7 +164,7 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Ошибка при поиске актуального листа: {e}")
|
logger.warning(f"⚠️ Ошибка при поиске актуального листа: {e}")
|
||||||
|
|
||||||
return actual_sheet
|
return actual_sheet
|
||||||
|
|
||||||
@@ -173,12 +177,12 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
# Ищем строку с искомым значением
|
# Ищем строку с искомым значением
|
||||||
for idx, row in df_temp.iterrows():
|
for idx, row in df_temp.iterrows():
|
||||||
if row.astype(str).str.contains(search_value, case=False, regex=False).any():
|
if row.astype(str).str.contains(search_value, case=False, regex=False).any():
|
||||||
print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
|
logger.debug(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
|
||||||
return idx + 1 # возвращаем индекс строки (0-based), который будет использован как `header=`
|
return idx + 1 # возвращаем индекс строки (0-based), который будет использован как `header=`
|
||||||
|
|
||||||
raise ValueError(f"Не найдена строка с заголовком '{search_value}'.")
|
raise ValueError(f"Не найдена строка с заголовком '{search_value}'.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при поиске заголовка: {e}")
|
logger.error(f"❌ Ошибка при поиске заголовка: {e}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def _parse_tech_pos_data(self, file_path: str, sheet_name: str, header_row: int) -> pd.DataFrame:
|
def _parse_tech_pos_data(self, file_path: str, sheet_name: str, header_row: int) -> pd.DataFrame:
|
||||||
@@ -193,8 +197,8 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
usecols=range(1, 5)
|
usecols=range(1, 5)
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Прочитано {len(df_temp)} строк из Excel")
|
logger.debug(f"🔍 Прочитано {len(df_temp)} строк из Excel")
|
||||||
print(f"🔍 DEBUG: Колонки: {list(df_temp.columns)}")
|
logger.debug(f"🔍 Колонки: {list(df_temp.columns)}")
|
||||||
|
|
||||||
# Фильтруем по валидным процессам
|
# Фильтруем по валидным процессам
|
||||||
df_cleaned = df_temp[
|
df_cleaned = df_temp[
|
||||||
@@ -202,11 +206,11 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
df_temp['Процесс'].notna()
|
df_temp['Процесс'].notna()
|
||||||
].copy()
|
].copy()
|
||||||
|
|
||||||
print(f"🔍 DEBUG: После фильтрации осталось {len(df_cleaned)} строк")
|
logger.debug(f"🔍 После фильтрации осталось {len(df_cleaned)} строк")
|
||||||
|
|
||||||
if df_cleaned.empty:
|
if df_cleaned.empty:
|
||||||
print("⚠️ Нет данных после фильтрации по процессам")
|
logger.warning("⚠️ Нет данных после фильтрации по процессам")
|
||||||
print(f"🔍 DEBUG: Доступные процессы в данных: {df_temp['Процесс'].unique()}")
|
logger.debug(f"🔍 Доступные процессы в данных: {df_temp['Процесс'].unique()}")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
df_cleaned['Процесс'] = df_cleaned['Процесс'].astype(str).str.strip()
|
df_cleaned['Процесс'] = df_cleaned['Процесс'].astype(str).str.strip()
|
||||||
@@ -214,68 +218,68 @@ class OperSpravkaTechPosParser(ParserPort):
|
|||||||
# Добавляем ID установки
|
# Добавляем ID установки
|
||||||
if 'Установка' in df_cleaned.columns:
|
if 'Установка' in df_cleaned.columns:
|
||||||
df_cleaned['id'] = df_cleaned['Установка'].apply(get_object_by_name)
|
df_cleaned['id'] = df_cleaned['Установка'].apply(get_object_by_name)
|
||||||
print(f"🔍 DEBUG: Добавлены ID установок: {df_cleaned['id'].unique()}")
|
logger.debug(f"🔍 Добавлены ID установок: {df_cleaned['id'].unique()}")
|
||||||
else:
|
else:
|
||||||
print("⚠️ Колонка 'Установка' не найдена")
|
logger.warning("⚠️ Колонка 'Установка' не найдена")
|
||||||
|
|
||||||
print(f"✅ Получено {len(df_cleaned)} записей")
|
logger.info(f"✅ Получено {len(df_cleaned)} записей")
|
||||||
return df_cleaned
|
return df_cleaned
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при парсинге данных: {e}")
|
logger.error(f"❌ Ошибка при парсинге данных: {e}")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
def _get_tech_pos_wrapper(self, params: Dict[str, Any] = None) -> str:
|
def _get_tech_pos_wrapper(self, params: Dict[str, Any] = None) -> str:
|
||||||
"""Обертка для получения данных технологических позиций"""
|
"""Обертка для получения данных технологических позиций"""
|
||||||
print(f"🔍 DEBUG: _get_tech_pos_wrapper вызван с параметрами: {params}")
|
logger.debug(f"🔍 _get_tech_pos_wrapper вызван с параметрами: {params}")
|
||||||
|
|
||||||
# Получаем ID ОГ из параметров
|
# Получаем ID ОГ из параметров
|
||||||
og_id = params.get('id') if params else None
|
og_id = params.get('id') if params else None
|
||||||
if not og_id:
|
if not og_id:
|
||||||
print("❌ Не указан ID ОГ")
|
logger.error("❌ Не указан ID ОГ")
|
||||||
return "{}"
|
return "{}"
|
||||||
|
|
||||||
# Получаем данные
|
# Получаем данные
|
||||||
tech_pos_data = {}
|
tech_pos_data = {}
|
||||||
if hasattr(self, 'df') and self.df is not None and not self.df.empty:
|
if hasattr(self, 'df') and self.df is not None and not self.df.empty:
|
||||||
# Данные из MinIO
|
# Данные из MinIO
|
||||||
print(f"🔍 DEBUG: Ищем данные для ОГ '{og_id}' в DataFrame с {len(self.df)} записями")
|
logger.debug(f"🔍 Ищем данные для ОГ '{og_id}' в DataFrame с {len(self.df)} записями")
|
||||||
available_ogs = self.df['id'].tolist()
|
available_ogs = self.df['id'].tolist()
|
||||||
print(f"🔍 DEBUG: Доступные ОГ в данных: {available_ogs}")
|
logger.debug(f"🔍 Доступные ОГ в данных: {available_ogs}")
|
||||||
|
|
||||||
for _, row in self.df.iterrows():
|
for _, row in self.df.iterrows():
|
||||||
if row['id'] == og_id:
|
if row['id'] == og_id:
|
||||||
tech_pos_data = row['data']
|
tech_pos_data = row['data']
|
||||||
print(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
|
logger.info(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print(f"❌ Данные для ОГ '{og_id}' не найдены")
|
logger.warning(f"❌ Данные для ОГ '{og_id}' не найдены")
|
||||||
elif hasattr(self, 'data_dict') and self.data_dict:
|
elif hasattr(self, 'data_dict') and self.data_dict:
|
||||||
# Локальные данные
|
# Локальные данные
|
||||||
print(f"🔍 DEBUG: Ищем данные для ОГ '{og_id}' в data_dict")
|
logger.debug(f"🔍 Ищем данные для ОГ '{og_id}' в data_dict")
|
||||||
available_ogs = list(self.data_dict.keys())
|
available_ogs = list(self.data_dict.keys())
|
||||||
print(f"🔍 DEBUG: Доступные ОГ в data_dict: {available_ogs}")
|
logger.debug(f"🔍 Доступные ОГ в data_dict: {available_ogs}")
|
||||||
|
|
||||||
if og_id in self.data_dict:
|
if og_id in self.data_dict:
|
||||||
tech_pos_data = self.data_dict[og_id].to_dict(orient='records')
|
tech_pos_data = self.data_dict[og_id].to_dict(orient='records')
|
||||||
print(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
|
logger.info(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
|
||||||
else:
|
else:
|
||||||
print(f"❌ Данные для ОГ '{og_id}' не найдены в data_dict")
|
logger.warning(f"❌ Данные для ОГ '{og_id}' не найдены в data_dict")
|
||||||
|
|
||||||
# Конвертируем в список записей
|
# Конвертируем в список записей
|
||||||
try:
|
try:
|
||||||
if isinstance(tech_pos_data, pd.DataFrame):
|
if isinstance(tech_pos_data, pd.DataFrame):
|
||||||
# Если это DataFrame, конвертируем в список словарей
|
# Если это DataFrame, конвертируем в список словарей
|
||||||
result_list = tech_pos_data.to_dict(orient='records')
|
result_list = tech_pos_data.to_dict(orient='records')
|
||||||
print(f"🔍 DEBUG: Конвертировано в список: {len(result_list)} записей")
|
logger.debug(f"🔍 Конвертировано в список: {len(result_list)} записей")
|
||||||
return result_list
|
return result_list
|
||||||
elif isinstance(tech_pos_data, list):
|
elif isinstance(tech_pos_data, list):
|
||||||
# Если уже список, возвращаем как есть
|
# Если уже список, возвращаем как есть
|
||||||
print(f"🔍 DEBUG: Уже список: {len(tech_pos_data)} записей")
|
logger.debug(f"🔍 Уже список: {len(tech_pos_data)} записей")
|
||||||
return tech_pos_data
|
return tech_pos_data
|
||||||
else:
|
else:
|
||||||
print(f"🔍 DEBUG: Неожиданный тип данных: {type(tech_pos_data)}")
|
logger.warning(f"🔍 Неожиданный тип данных: {type(tech_pos_data)}")
|
||||||
return []
|
return []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при конвертации данных: {e}")
|
logger.error(f"❌ Ошибка при конвертации данных: {e}")
|
||||||
return []
|
return []
|
||||||
@@ -2,12 +2,16 @@ import pandas as pd
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import logging
|
||||||
from typing import Dict, Any, List, Tuple, Optional
|
from typing import Dict, Any, List, Tuple, Optional
|
||||||
from core.ports import ParserPort
|
from core.ports import ParserPort
|
||||||
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
||||||
from app.schemas.statuses_repair_ca import StatusesRepairCARequest
|
from app.schemas.statuses_repair_ca import StatusesRepairCARequest
|
||||||
from adapters.pconfig import find_header_row, get_og_by_name, data_to_json
|
from adapters.pconfig import find_header_row, get_og_by_name, data_to_json
|
||||||
|
|
||||||
|
# Настройка логгера для модуля
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class StatusesRepairCAParser(ParserPort):
|
class StatusesRepairCAParser(ParserPort):
|
||||||
"""Парсер для статусов ремонта СА"""
|
"""Парсер для статусов ремонта СА"""
|
||||||
@@ -26,7 +30,7 @@ class StatusesRepairCAParser(ParserPort):
|
|||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> Dict[str, Any]:
|
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:
|
try:
|
||||||
# Определяем тип файла
|
# Определяем тип файла
|
||||||
@@ -38,7 +42,7 @@ class StatusesRepairCAParser(ParserPort):
|
|||||||
raise ValueError(f"Неподдерживаемый формат файла: {file_path}")
|
raise ValueError(f"Неподдерживаемый формат файла: {file_path}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при парсинге файла {file_path}: {e}")
|
logger.error(f"❌ Ошибка при парсинге файла {file_path}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _parse_zip_file(self, zip_path: str) -> Dict[str, Any]:
|
def _parse_zip_file(self, zip_path: str) -> Dict[str, Any]:
|
||||||
@@ -59,19 +63,19 @@ class StatusesRepairCAParser(ParserPort):
|
|||||||
|
|
||||||
# Берем первый найденный Excel файл
|
# Берем первый найденный Excel файл
|
||||||
excel_file = excel_files[0]
|
excel_file = excel_files[0]
|
||||||
print(f"🔍 DEBUG: Найден Excel файл в архиве: {excel_file}")
|
logger.debug(f"🔍 Найден Excel файл в архиве: {excel_file}")
|
||||||
|
|
||||||
return self._parse_excel_file(excel_file)
|
return self._parse_excel_file(excel_file)
|
||||||
|
|
||||||
def _parse_excel_file(self, file_path: str) -> Dict[str, Any]:
|
def _parse_excel_file(self, file_path: str) -> Dict[str, Any]:
|
||||||
"""Парсинг Excel файла"""
|
"""Парсинг Excel файла"""
|
||||||
print(f"🔍 DEBUG: Парсинг Excel файла: {file_path}")
|
logger.debug(f"🔍 Парсинг Excel файла: {file_path}")
|
||||||
|
|
||||||
# Парсим данные
|
# Парсим данные
|
||||||
df_statuses = self._parse_statuses_repair_ca(file_path, 0)
|
df_statuses = self._parse_statuses_repair_ca(file_path, 0)
|
||||||
|
|
||||||
if df_statuses.empty:
|
if df_statuses.empty:
|
||||||
print("⚠️ Нет данных после парсинга")
|
logger.warning("⚠️ Нет данных после парсинга")
|
||||||
return {"data": [], "records_count": 0}
|
return {"data": [], "records_count": 0}
|
||||||
|
|
||||||
# Преобразуем в список словарей для хранения
|
# Преобразуем в список словарей для хранения
|
||||||
@@ -85,7 +89,7 @@ class StatusesRepairCAParser(ParserPort):
|
|||||||
# Устанавливаем данные в парсер для использования в геттерах
|
# Устанавливаем данные в парсер для использования в геттерах
|
||||||
self.data_dict = result
|
self.data_dict = result
|
||||||
|
|
||||||
print(f"✅ Парсинг завершен. Получено {len(data_list)} записей")
|
logger.info(f"✅ Парсинг завершен. Получено {len(data_list)} записей")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _parse_statuses_repair_ca(self, file: str, sheet: int, header_num: Optional[int] = None) -> pd.DataFrame:
|
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):
|
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)
|
validated_params = validate_params_with_schema(params, StatusesRepairCARequest)
|
||||||
@@ -244,8 +248,8 @@ class StatusesRepairCAParser(ParserPort):
|
|||||||
ids = validated_params.get('ids')
|
ids = validated_params.get('ids')
|
||||||
keys = validated_params.get('keys')
|
keys = validated_params.get('keys')
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Запрошенные ОГ: {ids}")
|
logger.debug(f"🔍 Запрошенные ОГ: {ids}")
|
||||||
print(f"🔍 DEBUG: Запрошенные ключи: {keys}")
|
logger.debug(f"🔍 Запрошенные ключи: {keys}")
|
||||||
|
|
||||||
# Получаем данные из парсера
|
# Получаем данные из парсера
|
||||||
if hasattr(self, 'df') and self.df is not None:
|
if hasattr(self, 'df') and self.df is not None:
|
||||||
@@ -265,15 +269,15 @@ class StatusesRepairCAParser(ParserPort):
|
|||||||
# Данные из локального парсинга
|
# Данные из локального парсинга
|
||||||
data_source = self.data_dict.get('data', [])
|
data_source = self.data_dict.get('data', [])
|
||||||
else:
|
else:
|
||||||
print("⚠️ Нет данных в парсере")
|
logger.warning("⚠️ Нет данных в парсере")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Используем данные с {len(data_source)} записями")
|
logger.debug(f"🔍 Используем данные с {len(data_source)} записями")
|
||||||
|
|
||||||
# Фильтруем данные
|
# Фильтруем данные
|
||||||
filtered_data = self._filter_statuses_data(data_source, ids, keys)
|
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
|
return filtered_data
|
||||||
|
|
||||||
def _filter_statuses_data(self, data_source: List[Dict], ids: Optional[List[str]], keys: Optional[List[List[str]]]) -> List[Dict]:
|
def _filter_statuses_data(self, data_source: List[Dict], ids: Optional[List[str]], keys: Optional[List[List[str]]]) -> List[Dict]:
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import logging
|
||||||
|
|
||||||
from core.ports import ParserPort
|
from core.ports import ParserPort
|
||||||
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
||||||
from app.schemas.svodka_ca import SvodkaCARequest
|
from app.schemas.svodka_ca import SvodkaCARequest
|
||||||
from adapters.pconfig import get_og_by_name
|
from adapters.pconfig import get_og_by_name
|
||||||
|
|
||||||
|
# Настройка логгера для модуля
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SvodkaCAParser(ParserPort):
|
class SvodkaCAParser(ParserPort):
|
||||||
"""Парсер для сводок СА"""
|
"""Парсер для сводок СА"""
|
||||||
@@ -25,7 +29,7 @@ class SvodkaCAParser(ParserPort):
|
|||||||
|
|
||||||
def _get_data_wrapper(self, params: dict):
|
def _get_data_wrapper(self, params: dict):
|
||||||
"""Получение данных по режимам и таблицам"""
|
"""Получение данных по режимам и таблицам"""
|
||||||
print(f"🔍 DEBUG: _get_data_wrapper вызван с параметрами: {params}")
|
logger.debug(f"🔍 _get_data_wrapper вызван с параметрами: {params}")
|
||||||
|
|
||||||
# Валидируем параметры с помощью схемы Pydantic
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
validated_params = validate_params_with_schema(params, SvodkaCARequest)
|
validated_params = validate_params_with_schema(params, SvodkaCARequest)
|
||||||
@@ -33,20 +37,20 @@ class SvodkaCAParser(ParserPort):
|
|||||||
modes = validated_params["modes"]
|
modes = validated_params["modes"]
|
||||||
tables = validated_params["tables"]
|
tables = validated_params["tables"]
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Запрошенные режимы: {modes}")
|
logger.debug(f"🔍 Запрошенные режимы: {modes}")
|
||||||
print(f"🔍 DEBUG: Запрошенные таблицы: {tables}")
|
logger.debug(f"🔍 Запрошенные таблицы: {tables}")
|
||||||
|
|
||||||
# Проверяем, есть ли данные в data_dict (из парсинга) или в df (из загрузки)
|
# Проверяем, есть ли данные в data_dict (из парсинга) или в df (из загрузки)
|
||||||
if hasattr(self, 'data_dict') and self.data_dict is not None:
|
if hasattr(self, 'data_dict') and self.data_dict is not None:
|
||||||
# Данные из парсинга
|
# Данные из парсинга
|
||||||
data_source = self.data_dict
|
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:
|
elif hasattr(self, 'df') and self.df is not None and not self.df.empty:
|
||||||
# Данные из загрузки - преобразуем DataFrame обратно в словарь
|
# Данные из загрузки - преобразуем DataFrame обратно в словарь
|
||||||
data_source = self._df_to_data_dict()
|
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:
|
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 {}
|
return {}
|
||||||
|
|
||||||
# Фильтруем данные по запрошенным режимам и таблицам
|
# Фильтруем данные по запрошенным режимам и таблицам
|
||||||
@@ -55,18 +59,18 @@ class SvodkaCAParser(ParserPort):
|
|||||||
if mode in data_source:
|
if mode in data_source:
|
||||||
result_data[mode] = {}
|
result_data[mode] = {}
|
||||||
available_tables = list(data_source[mode].keys())
|
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 table_name, table_data in data_source[mode].items():
|
||||||
# Ищем таблицы по частичному совпадению
|
# Ищем таблицы по частичному совпадению
|
||||||
for requested_table in tables:
|
for requested_table in tables:
|
||||||
if requested_table in table_name:
|
if requested_table in table_name:
|
||||||
result_data[mode][table_name] = table_data
|
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 # Найдено совпадение, переходим к следующей таблице
|
break # Найдено совпадение, переходим к следующей таблице
|
||||||
else:
|
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
|
return result_data
|
||||||
|
|
||||||
def _df_to_data_dict(self):
|
def _df_to_data_dict(self):
|
||||||
@@ -91,7 +95,7 @@ class SvodkaCAParser(ParserPort):
|
|||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||||
"""Парсинг файла и возврат 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)
|
self.data_dict = self.parse_svodka_ca(file_path, params)
|
||||||
@@ -114,17 +118,17 @@ class SvodkaCAParser(ParserPort):
|
|||||||
if data_rows:
|
if data_rows:
|
||||||
df = pd.DataFrame(data_rows)
|
df = pd.DataFrame(data_rows)
|
||||||
self.df = df
|
self.df = df
|
||||||
print(f"🔍 DEBUG: Создан DataFrame с {len(data_rows)} записями")
|
logger.debug(f"🔍 Создан DataFrame с {len(data_rows)} записями")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
# Если данных нет, возвращаем пустой DataFrame
|
# Если данных нет, возвращаем пустой DataFrame
|
||||||
self.df = pd.DataFrame()
|
self.df = pd.DataFrame()
|
||||||
print(f"🔍 DEBUG: Возвращаем пустой DataFrame")
|
logger.debug(f"🔍 Возвращаем пустой DataFrame")
|
||||||
return self.df
|
return self.df
|
||||||
|
|
||||||
def parse_svodka_ca(self, file_path: str, params: dict) -> dict:
|
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)
|
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 = {
|
inclusion_list_fact = {
|
||||||
@@ -166,7 +170,7 @@ class SvodkaCAParser(ParserPort):
|
|||||||
}
|
}
|
||||||
|
|
||||||
df_ca_fact = self.parse_sheet(file_path, 'Факт', inclusion_list_fact)
|
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 = {
|
inclusion_list_normativ = {
|
||||||
@@ -185,7 +189,7 @@ class SvodkaCAParser(ParserPort):
|
|||||||
}
|
}
|
||||||
|
|
||||||
df_ca_normativ = self.parse_sheet(file_path, 'Норматив', inclusion_list_normativ)
|
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 в словарь по режимам и таблицам
|
# Преобразуем DataFrame в словарь по режимам и таблицам
|
||||||
data_dict = {}
|
data_dict = {}
|
||||||
@@ -211,9 +215,9 @@ class SvodkaCAParser(ParserPort):
|
|||||||
table_data = group_df.drop('table', axis=1)
|
table_data = group_df.drop('table', axis=1)
|
||||||
data_dict['normativ'][table_name] = table_data.to_dict('records')
|
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():
|
for mode, tables in data_dict.items():
|
||||||
print(f"🔍 DEBUG: Режим '{mode}' содержит таблицы: {list(tables.keys())}")
|
logger.debug(f"🔍 Режим '{mode}' содержит таблицы: {list(tables.keys())}")
|
||||||
|
|
||||||
return data_dict
|
return data_dict
|
||||||
|
|
||||||
@@ -368,7 +372,7 @@ class SvodkaCAParser(ParserPort):
|
|||||||
|
|
||||||
# Проверяем, что колонка 'name' существует
|
# Проверяем, что колонка 'name' существует
|
||||||
if 'name' not in df_cleaned.columns:
|
if 'name' not in df_cleaned.columns:
|
||||||
print(
|
logger.debug(
|
||||||
f"Внимание: колонка 'name' отсутствует в таблице для '{matched_key}'. Пропускаем добавление 'id'.")
|
f"Внимание: колонка 'name' отсутствует в таблице для '{matched_key}'. Пропускаем добавление 'id'.")
|
||||||
continue # или обработать по-другому
|
continue # или обработать по-другому
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import json
|
|||||||
import zipfile
|
import zipfile
|
||||||
import tempfile
|
import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
|
import logging
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from core.ports import ParserPort
|
from core.ports import ParserPort
|
||||||
from adapters.pconfig import SINGLE_OGS, replace_id_in_path, find_header_row, data_to_json
|
from adapters.pconfig import SINGLE_OGS, replace_id_in_path, find_header_row, data_to_json
|
||||||
|
|
||||||
|
# Настройка логгера для модуля
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SvodkaPMParser(ParserPort):
|
class SvodkaPMParser(ParserPort):
|
||||||
"""Парсер для сводок ПМ (план и факт)"""
|
"""Парсер для сводок ПМ (план и факт)"""
|
||||||
@@ -51,17 +55,17 @@ class SvodkaPMParser(ParserPort):
|
|||||||
# Разархивируем файл
|
# Разархивируем файл
|
||||||
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||||
zip_ref.extractall(temp_dir)
|
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):
|
for root, dirs, files in os.walk(temp_dir):
|
||||||
level = root.replace(temp_dir, '').count(os.sep)
|
level = root.replace(temp_dir, '').count(os.sep)
|
||||||
indent = ' ' * 2 * level
|
indent = ' ' * 2 * level
|
||||||
print(f"{indent}{os.path.basename(root)}/")
|
logger.debug(f"{indent}{os.path.basename(root)}/")
|
||||||
subindent = ' ' * 2 * (level + 1)
|
subindent = ' ' * 2 * (level + 1)
|
||||||
for file in files:
|
for file in files:
|
||||||
print(f"{subindent}{file}")
|
logger.debug(f"{subindent}{file}")
|
||||||
|
|
||||||
# Создаем словари для хранения данных как в оригинале
|
# Создаем словари для хранения данных как в оригинале
|
||||||
df_pm_facts = {} # Словарь с данными факта, ключ - ID ОГ
|
df_pm_facts = {} # Словарь с данными факта, ключ - ID ОГ
|
||||||
@@ -80,8 +84,8 @@ class SvodkaPMParser(ParserPort):
|
|||||||
elif 'plan' in file.lower() or 'план' in file.lower():
|
elif 'plan' in file.lower() or 'план' in file.lower():
|
||||||
plan_files.append(full_path)
|
plan_files.append(full_path)
|
||||||
|
|
||||||
print(f"📊 Найдено файлов факта: {len(fact_files)}")
|
logger.info(f"📊 Найдено файлов факта: {len(fact_files)}")
|
||||||
print(f"📊 Найдено файлов плана: {len(plan_files)}")
|
logger.info(f"📊 Найдено файлов плана: {len(plan_files)}")
|
||||||
|
|
||||||
# Обрабатываем найденные файлы
|
# Обрабатываем найденные файлы
|
||||||
for fact_file in fact_files:
|
for fact_file in fact_files:
|
||||||
@@ -91,9 +95,9 @@ class SvodkaPMParser(ParserPort):
|
|||||||
if 'svodka_fact_pm_' in filename:
|
if 'svodka_fact_pm_' in filename:
|
||||||
og_id = filename.replace('svodka_fact_pm_', '').replace('.xlsx', '').replace('.xlsm', '')
|
og_id = filename.replace('svodka_fact_pm_', '').replace('.xlsx', '').replace('.xlsm', '')
|
||||||
if og_id in SINGLE_OGS:
|
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, 'Сводка Нефтепереработка')
|
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:
|
for plan_file in plan_files:
|
||||||
# Извлекаем ID ОГ из имени файла
|
# Извлекаем ID ОГ из имени файла
|
||||||
@@ -102,9 +106,9 @@ class SvodkaPMParser(ParserPort):
|
|||||||
if 'svodka_plan_pm_' in filename:
|
if 'svodka_plan_pm_' in filename:
|
||||||
og_id = filename.replace('svodka_plan_pm_', '').replace('.xlsx', '').replace('.xlsm', '')
|
og_id = filename.replace('svodka_plan_pm_', '').replace('.xlsx', '').replace('.xlsm', '')
|
||||||
if og_id in SINGLE_OGS:
|
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, 'Сводка Нефтепереработка')
|
df_pm_plans[og_id] = self._parse_svodka_pm(plan_file, 'Сводка Нефтепереработка')
|
||||||
print(f"✅ План загружен для {og_id}")
|
logger.info(f"✅ План загружен для {og_id}")
|
||||||
|
|
||||||
# Инициализируем None для ОГ, для которых файлы не найдены
|
# Инициализируем None для ОГ, для которых файлы не найдены
|
||||||
for og_id in SINGLE_OGS:
|
for og_id in SINGLE_OGS:
|
||||||
@@ -123,14 +127,14 @@ class SvodkaPMParser(ParserPort):
|
|||||||
'df_pm_plans': df_pm_plans
|
'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
|
return result
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Удаляем временную директорию
|
# Удаляем временную директорию
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
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:
|
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):
|
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)} строками")
|
logger.debug(f"🔍 Ищем код '{code}' для ОГ '{og_id}' в DataFrame с {len(df_svodka)} строками")
|
||||||
print(f"🔍 DEBUG: Первая строка данных: {df_svodka.iloc[0].tolist()}")
|
logger.debug(f"🔍 Первая строка данных: {df_svodka.iloc[0].tolist()}")
|
||||||
print(f"🔍 DEBUG: Доступные индексы: {list(df_svodka.index)}")
|
logger.debug(f"🔍 Доступные индексы: {list(df_svodka.index)}")
|
||||||
print(f"🔍 DEBUG: Доступные столбцы: {list(df_svodka.columns)}")
|
logger.debug(f"🔍 Доступные столбцы: {list(df_svodka.columns)}")
|
||||||
|
|
||||||
# Проверяем, есть ли код в индексе
|
# Проверяем, есть ли код в индексе
|
||||||
if code not in df_svodka.index:
|
if code not in df_svodka.index:
|
||||||
print(f"⚠️ Код '{code}' не найден в индексе")
|
logger.warning(f"⚠️ Код '{code}' не найден в индексе")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Получаем позицию строки с кодом
|
# Получаем позицию строки с кодом
|
||||||
code_row_loc = df_svodka.index.get_loc(code)
|
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:
|
if search_value is None:
|
||||||
@@ -254,14 +258,14 @@ class SvodkaPMParser(ParserPort):
|
|||||||
if col_name == search_value:
|
if col_name == search_value:
|
||||||
target_positions.append(i)
|
target_positions.append(i)
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Найдены позиции для '{search_value}': {target_positions[:5]}...")
|
logger.debug(f"🔍 Найдены позиции для '{search_value}': {target_positions[:5]}...")
|
||||||
print(f"🔍 DEBUG: Позиции в первой строке: {target_positions[:5]}...")
|
logger.debug(f"🔍 Позиции в первой строке: {target_positions[:5]}...")
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Ищем столбцы с названием '{search_value}'")
|
logger.debug(f"🔍 Ищем столбцы с названием '{search_value}'")
|
||||||
print(f"🔍 DEBUG: Целевые позиции: {target_positions[:10]}...")
|
logger.debug(f"🔍 Целевые позиции: {target_positions[:10]}...")
|
||||||
|
|
||||||
if not target_positions:
|
if not target_positions:
|
||||||
print(f"⚠️ Позиции '{search_value}' не найдены")
|
logger.warning(f"⚠️ Позиции '{search_value}' не найдены")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Извлекаем значения из найденных позиций
|
# Извлекаем значения из найденных позиций
|
||||||
@@ -285,7 +289,7 @@ class SvodkaPMParser(ParserPort):
|
|||||||
|
|
||||||
# Преобразуем в числовой формат
|
# Преобразуем в числовой формат
|
||||||
numeric_values = pd.to_numeric(values, errors='coerce')
|
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:
|
try:
|
||||||
@@ -301,10 +305,10 @@ class SvodkaPMParser(ParserPort):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
manual_values.append(0)
|
manual_values.append(0)
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Ручное преобразование (первые 5): {manual_values[:5]}")
|
logger.debug(f"🔍 Ручное преобразование (первые 5): {manual_values[:5]}")
|
||||||
numeric_values = pd.Series(manual_values)
|
numeric_values = pd.Series(manual_values)
|
||||||
except Exception as e:
|
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])
|
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)
|
# Получаем данные из сохраненных словарей (через self.df)
|
||||||
if not hasattr(self, 'df') or self.df is None:
|
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}
|
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)
|
fact_df = df_pm_facts.get(og_id)
|
||||||
plan_df = df_pm_plans.get(og_id)
|
plan_df = df_pm_plans.get(og_id)
|
||||||
|
|
||||||
print(f"🔍 ===== НАЧАЛО ОБРАБОТКИ ОГ {og_id} =====")
|
logger.debug(f"🔍 ===== НАЧАЛО ОБРАБОТКИ ОГ {og_id} =====")
|
||||||
print(f"🔍 Коды: {codes}")
|
logger.debug(f"🔍 Коды: {codes}")
|
||||||
print(f"🔍 Столбцы: {columns}")
|
logger.debug(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}: факт={'✅' if fact_df is not None else '❌'}, план={'✅' if plan_df is not None else '❌'}")
|
||||||
|
|
||||||
# Определяем, какие столбцы из какого датафрейма брать
|
# Определяем, какие столбцы из какого датафрейма брать
|
||||||
for col in columns:
|
for col in columns:
|
||||||
@@ -360,24 +364,24 @@ class SvodkaPMParser(ParserPort):
|
|||||||
|
|
||||||
if col in ['ПП', 'БП']:
|
if col in ['ПП', 'БП']:
|
||||||
if plan_df is None:
|
if plan_df is None:
|
||||||
print(f"❌ Невозможно обработать '{col}': нет данных плана для {og_id}")
|
logger.warning(f"❌ Невозможно обработать '{col}': нет данных плана для {og_id}")
|
||||||
else:
|
else:
|
||||||
print(f"🔍 DEBUG: ===== ОБРАБАТЫВАЕМ '{col}' ИЗ ДАННЫХ ПЛАНА =====")
|
logger.debug(f"🔍 ===== ОБРАБАТЫВАЕМ '{col}' ИЗ ДАННЫХ ПЛАНА =====")
|
||||||
for code in codes:
|
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)
|
val = self._get_svodka_value(plan_df, og_id, code, col)
|
||||||
col_result[str(code)] = val
|
col_result[str(code)] = val
|
||||||
print(f"🔍 DEBUG: ===== ЗАВЕРШИЛИ ОБРАБОТКУ '{col}' =====")
|
logger.debug(f"🔍 ===== ЗАВЕРШИЛИ ОБРАБОТКУ '{col}' =====")
|
||||||
|
|
||||||
elif col in ['ТБ', 'СЭБ', 'НЭБ']:
|
elif col in ['ТБ', 'СЭБ', 'НЭБ']:
|
||||||
if fact_df is None:
|
if fact_df is None:
|
||||||
print(f"❌ Невозможно обработать '{col}': нет данных факта для {og_id}")
|
logger.warning(f"❌ Невозможно обработать '{col}': нет данных факта для {og_id}")
|
||||||
else:
|
else:
|
||||||
for code in codes:
|
for code in codes:
|
||||||
val = self._get_svodka_value(fact_df, og_id, code, col)
|
val = self._get_svodka_value(fact_df, og_id, code, col)
|
||||||
col_result[str(code)] = val
|
col_result[str(code)] = val
|
||||||
else:
|
else:
|
||||||
print(f"⚠️ Неизвестный столбец: '{col}'. Пропускаем.")
|
logger.warning(f"⚠️ Неизвестный столбец: '{col}'. Пропускаем.")
|
||||||
col_result = {str(code): None for code in codes}
|
col_result = {str(code): None for code in codes}
|
||||||
|
|
||||||
result[col] = col_result
|
result[col] = col_result
|
||||||
@@ -443,7 +447,7 @@ class SvodkaPMParser(ParserPort):
|
|||||||
data = self._get_svodka_og(og_id, codes, columns, search)
|
data = self._get_svodka_og(og_id, codes, columns, search)
|
||||||
total_result[og_id] = data
|
total_result[og_id] = data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при обработке {og_id}: {e}")
|
logger.error(f"❌ Ошибка при обработке {og_id}: {e}")
|
||||||
total_result[og_id] = None
|
total_result[og_id] = None
|
||||||
|
|
||||||
json_result = data_to_json(total_result)
|
json_result = data_to_json(total_result)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import logging
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Dict, List, Optional, Any
|
||||||
|
|
||||||
from core.ports import ParserPort
|
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 app.schemas.svodka_repair_ca import SvodkaRepairCARequest
|
||||||
from adapters.pconfig import SINGLE_OGS, find_header_row, get_og_by_name
|
from adapters.pconfig import SINGLE_OGS, find_header_row, get_og_by_name
|
||||||
|
|
||||||
|
# Настройка логгера для модуля
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SvodkaRepairCAParser(ParserPort):
|
class SvodkaRepairCAParser(ParserPort):
|
||||||
"""Парсер для сводок ремонта СА"""
|
"""Парсер для сводок ремонта СА"""
|
||||||
@@ -29,7 +33,7 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
|
|
||||||
def _get_repair_data_wrapper(self, params: dict):
|
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
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
validated_params = validate_params_with_schema(params, SvodkaRepairCARequest)
|
validated_params = validate_params_with_schema(params, SvodkaRepairCARequest)
|
||||||
@@ -39,21 +43,21 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
include_planned = validated_params.get("include_planned", True)
|
include_planned = validated_params.get("include_planned", True)
|
||||||
include_factual = validated_params.get("include_factual", True)
|
include_factual = validated_params.get("include_factual", True)
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Запрошенные ОГ: {og_ids}")
|
logger.debug(f"🔍 Запрошенные ОГ: {og_ids}")
|
||||||
print(f"🔍 DEBUG: Запрошенные типы ремонта: {repair_types}")
|
logger.debug(f"🔍 Запрошенные типы ремонта: {repair_types}")
|
||||||
print(f"🔍 DEBUG: Включать плановые: {include_planned}, фактические: {include_factual}")
|
logger.debug(f"🔍 Включать плановые: {include_planned}, фактические: {include_factual}")
|
||||||
|
|
||||||
# Проверяем, есть ли данные в data_dict (из парсинга) или в df (из загрузки)
|
# Проверяем, есть ли данные в data_dict (из парсинга) или в df (из загрузки)
|
||||||
if hasattr(self, 'data_dict') and self.data_dict is not None:
|
if hasattr(self, 'data_dict') and self.data_dict is not None:
|
||||||
# Данные из парсинга
|
# Данные из парсинга
|
||||||
data_source = self.data_dict
|
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:
|
elif hasattr(self, 'df') and self.df is not None and not self.df.empty:
|
||||||
# Данные из загрузки - преобразуем DataFrame обратно в словарь
|
# Данные из загрузки - преобразуем DataFrame обратно в словарь
|
||||||
data_source = self._df_to_data_dict()
|
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:
|
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 []
|
return []
|
||||||
|
|
||||||
# Группируем данные по ОГ (как в оригинале)
|
# Группируем данные по ОГ (как в оригинале)
|
||||||
@@ -86,8 +90,8 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
grouped_data[og_id].append(filtered_item)
|
grouped_data[og_id].append(filtered_item)
|
||||||
|
|
||||||
total_records = sum(len(v) for v in grouped_data.values())
|
total_records = sum(len(v) for v in grouped_data.values())
|
||||||
print(f"🔍 DEBUG: Отфильтровано {total_records} записей из {len(data_source)}")
|
logger.debug(f"🔍 Отфильтровано {total_records} записей из {len(data_source)}")
|
||||||
print(f"🔍 DEBUG: Группировано по {len(grouped_data)} ОГ: {list(grouped_data.keys())}")
|
logger.debug(f"🔍 Группировано по {len(grouped_data)} ОГ: {list(grouped_data.keys())}")
|
||||||
return grouped_data
|
return grouped_data
|
||||||
|
|
||||||
def _df_to_data_dict(self):
|
def _df_to_data_dict(self):
|
||||||
@@ -109,7 +113,7 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||||
"""Парсинг файла и возврат DataFrame"""
|
"""Парсинг файла и возврат DataFrame"""
|
||||||
print(f"🔍 DEBUG: SvodkaRepairCAParser.parse вызван с файлом: {file_path}")
|
logger.debug(f"🔍 SvodkaRepairCAParser.parse вызван с файлом: {file_path}")
|
||||||
|
|
||||||
# Определяем, это ZIP архив или одиночный файл
|
# Определяем, это ZIP архив или одиночный файл
|
||||||
if file_path.lower().endswith('.zip'):
|
if file_path.lower().endswith('.zip'):
|
||||||
@@ -133,17 +137,17 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
if data_rows:
|
if data_rows:
|
||||||
df = pd.DataFrame(data_rows)
|
df = pd.DataFrame(data_rows)
|
||||||
self.df = df
|
self.df = df
|
||||||
print(f"🔍 DEBUG: Создан DataFrame с {len(data_rows)} записями")
|
logger.debug(f"🔍 Создан DataFrame с {len(data_rows)} записями")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
# Если данных нет, возвращаем пустой DataFrame
|
# Если данных нет, возвращаем пустой DataFrame
|
||||||
self.df = pd.DataFrame()
|
self.df = pd.DataFrame()
|
||||||
print(f"🔍 DEBUG: Возвращаем пустой DataFrame")
|
logger.debug(f"🔍 Возвращаем пустой DataFrame")
|
||||||
return self.df
|
return self.df
|
||||||
|
|
||||||
def _parse_zip_archive(self, file_path: str, params: dict) -> List[Dict]:
|
def _parse_zip_archive(self, file_path: str, params: dict) -> List[Dict]:
|
||||||
"""Парсинг ZIP архива с файлами ремонта СА"""
|
"""Парсинг ZIP архива с файлами ремонта СА"""
|
||||||
print(f"🔍 DEBUG: Парсинг ZIP архива: {file_path}")
|
logger.info(f"🔍 Парсинг ZIP архива: {file_path}")
|
||||||
|
|
||||||
all_data = []
|
all_data = []
|
||||||
temp_dir = None
|
temp_dir = None
|
||||||
@@ -151,7 +155,7 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
try:
|
try:
|
||||||
# Создаем временную директорию
|
# Создаем временную директорию
|
||||||
temp_dir = tempfile.mkdtemp()
|
temp_dir = tempfile.mkdtemp()
|
||||||
print(f"📦 Архив разархивирован в: {temp_dir}")
|
logger.debug(f"📦 Архив разархивирован в: {temp_dir}")
|
||||||
|
|
||||||
# Разархивируем файл
|
# Разархивируем файл
|
||||||
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||||
@@ -164,30 +168,30 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
if file.lower().endswith(('.xlsx', '.xlsm', '.xls')):
|
if file.lower().endswith(('.xlsx', '.xlsm', '.xls')):
|
||||||
excel_files.append(os.path.join(root, file))
|
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:
|
for excel_file in excel_files:
|
||||||
print(f"📊 Обработка файла: {excel_file}")
|
logger.info(f"📊 Обработка файла: {excel_file}")
|
||||||
file_data = self._parse_single_file(excel_file, params)
|
file_data = self._parse_single_file(excel_file, params)
|
||||||
if file_data:
|
if file_data:
|
||||||
all_data.extend(file_data)
|
all_data.extend(file_data)
|
||||||
|
|
||||||
print(f"🎯 Всего обработано записей: {len(all_data)}")
|
logger.info(f"🎯 Всего обработано записей: {len(all_data)}")
|
||||||
return all_data
|
return all_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при обработке ZIP архива: {e}")
|
logger.error(f"❌ Ошибка при обработке ZIP архива: {e}")
|
||||||
return []
|
return []
|
||||||
finally:
|
finally:
|
||||||
# Удаляем временную директорию
|
# Удаляем временную директорию
|
||||||
if temp_dir:
|
if temp_dir:
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
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]:
|
def _parse_single_file(self, file_path: str, params: dict) -> List[Dict]:
|
||||||
"""Парсинг одиночного Excel файла"""
|
"""Парсинг одиночного Excel файла"""
|
||||||
print(f"🔍 DEBUG: Парсинг файла: {file_path}")
|
logger.debug(f"🔍 Парсинг файла: {file_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Получаем параметры
|
# Получаем параметры
|
||||||
@@ -198,10 +202,10 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
if header_num is None:
|
if header_num is None:
|
||||||
header_num = find_header_row(file_path, sheet_name, search_value="ОГ")
|
header_num = find_header_row(file_path, sheet_name, search_value="ОГ")
|
||||||
if header_num is None:
|
if header_num is None:
|
||||||
print(f"❌ Не найден заголовок в файле {file_path}")
|
logger.error(f"❌ Не найден заголовок в файле {file_path}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Заголовок найден в строке {header_num}")
|
logger.debug(f"🔍 Заголовок найден в строке {header_num}")
|
||||||
|
|
||||||
# Читаем Excel файл
|
# Читаем Excel файл
|
||||||
df = pd.read_excel(
|
df = pd.read_excel(
|
||||||
@@ -213,23 +217,23 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if df.empty:
|
if df.empty:
|
||||||
print(f"❌ Файл {file_path} пуст")
|
logger.error(f"❌ Файл {file_path} пуст")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if "ОГ" not in df.columns:
|
if "ОГ" not in df.columns:
|
||||||
print(f"⚠️ Предупреждение: Колонка 'ОГ' не найдена в файле {file_path}")
|
logger.warning(f"⚠️ Предупреждение: Колонка 'ОГ' не найдена в файле {file_path}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Обрабатываем данные
|
# Обрабатываем данные
|
||||||
return self._process_repair_data(df)
|
return self._process_repair_data(df)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при парсинге файла {file_path}: {e}")
|
logger.error(f"❌ Ошибка при парсинге файла {file_path}: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _process_repair_data(self, df: pd.DataFrame) -> List[Dict]:
|
def _process_repair_data(self, df: pd.DataFrame) -> List[Dict]:
|
||||||
"""Обработка данных о ремонте"""
|
"""Обработка данных о ремонте"""
|
||||||
print(f"🔍 DEBUG: Обработка данных с {len(df)} строками")
|
logger.debug(f"🔍 Обработка данных с {len(df)} строками")
|
||||||
|
|
||||||
# Шаг 1: Нормализация ОГ
|
# Шаг 1: Нормализация ОГ
|
||||||
def safe_replace(val):
|
def safe_replace(val):
|
||||||
@@ -254,7 +258,7 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
df = df[mask_og].copy()
|
df = df[mask_og].copy()
|
||||||
|
|
||||||
if df.empty:
|
if df.empty:
|
||||||
print(f"❌ Нет данных после фильтрации по ОГ")
|
logger.info(f"❌ Нет данных после фильтрации по ОГ")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Шаг 4: Удаление строк без "Вид простоя"
|
# Шаг 4: Удаление строк без "Вид простоя"
|
||||||
@@ -263,7 +267,7 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
mask_downtime = (downtime_clean != "") & (downtime_clean != "nan")
|
mask_downtime = (downtime_clean != "") & (downtime_clean != "nan")
|
||||||
df = df[mask_downtime].copy()
|
df = df[mask_downtime].copy()
|
||||||
else:
|
else:
|
||||||
print("⚠️ Предупреждение: Колонка 'Вид простоя' не найдена.")
|
logger.info("⚠️ Предупреждение: Колонка 'Вид простоя' не найдена.")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Шаг 5: Удаление ненужных колонок
|
# Шаг 5: Удаление ненужных колонок
|
||||||
@@ -278,7 +282,7 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
|
|
||||||
# Шаг 6: Переименование первых 8 колонок по порядку
|
# Шаг 6: Переименование первых 8 колонок по порядку
|
||||||
if df.shape[1] < 8:
|
if df.shape[1] < 8:
|
||||||
print(f"⚠️ Внимание: В DataFrame только {df.shape[1]} колонок, требуется минимум 8.")
|
logger.info(f"⚠️ Внимание: В DataFrame только {df.shape[1]} колонок, требуется минимум 8.")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
new_names = ["id", "name", "type", "start_date", "end_date", "plan", "fact", "downtime"]
|
new_names = ["id", "name", "type", "start_date", "end_date", "plan", "fact", "downtime"]
|
||||||
@@ -328,10 +332,10 @@ class SvodkaRepairCAParser(ParserPort):
|
|||||||
result_data.append(record)
|
result_data.append(record)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Ошибка при обработке строки: {e}")
|
logger.info(f"⚠️ Ошибка при обработке строки: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"✅ Обработано {len(result_data)} записей")
|
logger.info(f"✅ Обработано {len(result_data)} записей")
|
||||||
return result_data
|
return result_data
|
||||||
|
|
||||||
def _parse_date(self, value) -> Optional[str]:
|
def _parse_date(self, value) -> Optional[str]:
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import json
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Настройка логгера для модуля
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
OG_IDS = {
|
OG_IDS = {
|
||||||
"Комсомольский НПЗ": "KNPZ",
|
"Комсомольский НПЗ": "KNPZ",
|
||||||
@@ -163,7 +167,7 @@ def find_header_row(file, sheet, search_value="Итого", max_rows=50):
|
|||||||
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
||||||
for idx, row in df_temp.iterrows():
|
for idx, row in df_temp.iterrows():
|
||||||
if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
|
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=
|
return idx # 0-based index — то, что нужно для header=
|
||||||
|
|
||||||
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
||||||
|
|||||||
@@ -4,12 +4,16 @@
|
|||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import io
|
import io
|
||||||
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from minio import Minio # boto3
|
from minio import Minio # boto3
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from core.ports import StoragePort
|
from core.ports import StoragePort
|
||||||
|
|
||||||
|
# Настройка логгера для модуля
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MinIOStorageAdapter(StoragePort):
|
class MinIOStorageAdapter(StoragePort):
|
||||||
"""Адаптер для MinIO хранилища"""
|
"""Адаптер для MinIO хранилища"""
|
||||||
@@ -37,8 +41,8 @@ class MinIOStorageAdapter(StoragePort):
|
|||||||
# Проверяем bucket только при первом использовании
|
# Проверяем bucket только при первом использовании
|
||||||
self._ensure_bucket_exists()
|
self._ensure_bucket_exists()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Не удалось подключиться к MinIO: {e}")
|
logger.warning(f"⚠️ Не удалось подключиться к MinIO: {e}")
|
||||||
print("MinIO будет недоступен, но приложение продолжит работать")
|
logger.warning("MinIO будет недоступен, но приложение продолжит работать")
|
||||||
return None
|
return None
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
@@ -50,16 +54,16 @@ class MinIOStorageAdapter(StoragePort):
|
|||||||
try:
|
try:
|
||||||
if not self.client.bucket_exists(self._bucket_name):
|
if not self.client.bucket_exists(self._bucket_name):
|
||||||
self.client.make_bucket(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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при работе с bucket: {e}")
|
logger.error(f"❌ Ошибка при работе с bucket: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def save_dataframe(self, df: pd.DataFrame, object_id: str) -> bool:
|
def save_dataframe(self, df: pd.DataFrame, object_id: str) -> bool:
|
||||||
"""Сохранение DataFrame в MinIO"""
|
"""Сохранение DataFrame в MinIO"""
|
||||||
if self.client is None:
|
if self.client is None:
|
||||||
print("⚠️ MinIO недоступен, данные не сохранены")
|
logger.warning("⚠️ MinIO недоступен, данные не сохранены")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -78,16 +82,16 @@ class MinIOStorageAdapter(StoragePort):
|
|||||||
content_type='application/octet-stream'
|
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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при сохранении в MinIO: {e}")
|
logger.error(f"❌ Ошибка при сохранении в MinIO: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def load_dataframe(self, object_id: str) -> Optional[pd.DataFrame]:
|
def load_dataframe(self, object_id: str) -> Optional[pd.DataFrame]:
|
||||||
"""Загрузка DataFrame из MinIO"""
|
"""Загрузка DataFrame из MinIO"""
|
||||||
if self.client is None:
|
if self.client is None:
|
||||||
print("⚠️ MinIO недоступен, данные не загружены")
|
logger.warning("⚠️ MinIO недоступен, данные не загружены")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -102,7 +106,7 @@ class MinIOStorageAdapter(StoragePort):
|
|||||||
|
|
||||||
return df
|
return df
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при загрузке данных из MinIO: {e}")
|
logger.error(f"❌ Ошибка при загрузке данных из MinIO: {e}")
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
if 'response' in locals():
|
if 'response' in locals():
|
||||||
@@ -112,15 +116,15 @@ class MinIOStorageAdapter(StoragePort):
|
|||||||
def delete_object(self, object_id: str) -> bool:
|
def delete_object(self, object_id: str) -> bool:
|
||||||
"""Удаление объекта из MinIO"""
|
"""Удаление объекта из MinIO"""
|
||||||
if self.client is None:
|
if self.client is None:
|
||||||
print("⚠️ MinIO недоступен, объект не удален")
|
logger.warning("⚠️ MinIO недоступен, объект не удален")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.client.remove_object(self._bucket_name, object_id)
|
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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при удалении объекта из MinIO: {e}")
|
logger.error(f"❌ Ошибка при удалении объекта из MinIO: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def object_exists(self, object_id: str) -> bool:
|
def object_exists(self, object_id: str) -> bool:
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import os
|
import os
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
import logging
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
from fastapi import FastAPI, File, UploadFile, HTTPException, status
|
from fastapi import FastAPI, File, UploadFile, HTTPException, status
|
||||||
from fastapi.responses import JSONResponse
|
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.storage import MinIOStorageAdapter
|
||||||
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser
|
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser
|
||||||
|
|
||||||
@@ -154,7 +165,7 @@ async def get_available_ogs(parser_name: str):
|
|||||||
if available_ogs:
|
if available_ogs:
|
||||||
return {"parser": parser_name, "available_ogs": available_ogs}
|
return {"parser": parser_name, "available_ogs": available_ogs}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Ошибка при получении ОГ: {e}")
|
logger.error(f"⚠️ Ошибка при получении ОГ: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
@@ -1412,7 +1423,7 @@ async def get_oper_spravka_tech_pos_data(request: OperSpravkaTechPosRequest):
|
|||||||
if result.success:
|
if result.success:
|
||||||
# Извлекаем данные из результата
|
# Извлекаем данные из результата
|
||||||
value_data = result.data.get("value", []) if isinstance(result.data.get("value"), list) else []
|
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'}")
|
logger.debug(f"🔍 API возвращает данные: {type(value_data)}, длина: {len(value_data) if isinstance(value_data, (list, dict)) else 'N/A'}")
|
||||||
|
|
||||||
return OperSpravkaTechPosResponse(
|
return OperSpravkaTechPosResponse(
|
||||||
success=True,
|
success=True,
|
||||||
|
|||||||
@@ -3,11 +3,15 @@
|
|||||||
"""
|
"""
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from typing import Dict, Type
|
from typing import Dict, Type
|
||||||
|
|
||||||
from core.models import UploadRequest, UploadResult, DataRequest, DataResult
|
from core.models import UploadRequest, UploadResult, DataRequest, DataResult
|
||||||
from core.ports import ParserPort, StoragePort
|
from core.ports import ParserPort, StoragePort
|
||||||
|
|
||||||
|
# Настройка логгера для модуля
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Глобальный словарь парсеров
|
# Глобальный словарь парсеров
|
||||||
PARSERS: Dict[str, Type[ParserPort]] = {}
|
PARSERS: Dict[str, Type[ParserPort]] = {}
|
||||||
@@ -51,7 +55,7 @@ class ReportService:
|
|||||||
# Удаляем старый объект, если он существует и хранилище доступно
|
# Удаляем старый объект, если он существует и хранилище доступно
|
||||||
if self.storage.object_exists(object_id):
|
if self.storage.object_exists(object_id):
|
||||||
self.storage.delete_object(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):
|
if self.storage.save_dataframe(parse_result, object_id):
|
||||||
@@ -102,18 +106,18 @@ class ReportService:
|
|||||||
|
|
||||||
# Устанавливаем данные в парсер для использования в геттерах
|
# Устанавливаем данные в парсер для использования в геттерах
|
||||||
parser.df = loaded_data
|
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'):
|
if hasattr(loaded_data, 'shape'):
|
||||||
# Это DataFrame
|
# Это DataFrame
|
||||||
print(f"🔍 DEBUG: DataFrame shape: {loaded_data.shape}")
|
logger.debug(f"🔍 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 columns: {list(loaded_data.columns) if not loaded_data.empty else 'Empty'}")
|
||||||
elif isinstance(loaded_data, dict):
|
elif isinstance(loaded_data, dict):
|
||||||
# Это словарь (для парсера ПМ)
|
# Это словарь (для парсера ПМ)
|
||||||
print(f"🔍 DEBUG: Словарь с ключами: {list(loaded_data.keys())}")
|
logger.debug(f"🔍 Словарь с ключами: {list(loaded_data.keys())}")
|
||||||
else:
|
else:
|
||||||
print(f"🔍 DEBUG: Неизвестный тип данных: {type(loaded_data)}")
|
logger.debug(f"🔍 Неизвестный тип данных: {type(loaded_data)}")
|
||||||
|
|
||||||
# Получаем параметры запроса
|
# Получаем параметры запроса
|
||||||
get_params = request.get_params or {}
|
get_params = request.get_params or {}
|
||||||
@@ -158,7 +162,7 @@ class ReportService:
|
|||||||
available_getters = list(parser.getters.keys())
|
available_getters = list(parser.getters.keys())
|
||||||
if available_getters:
|
if available_getters:
|
||||||
getter_name = available_getters[0]
|
getter_name = available_getters[0]
|
||||||
print(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
||||||
else:
|
else:
|
||||||
return DataResult(
|
return DataResult(
|
||||||
success=False,
|
success=False,
|
||||||
@@ -172,7 +176,7 @@ class ReportService:
|
|||||||
available_getters = list(parser.getters.keys())
|
available_getters = list(parser.getters.keys())
|
||||||
if available_getters:
|
if available_getters:
|
||||||
getter_name = available_getters[0]
|
getter_name = available_getters[0]
|
||||||
print(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
||||||
else:
|
else:
|
||||||
return DataResult(
|
return DataResult(
|
||||||
success=False,
|
success=False,
|
||||||
@@ -189,7 +193,7 @@ class ReportService:
|
|||||||
available_getters = list(parser.getters.keys())
|
available_getters = list(parser.getters.keys())
|
||||||
if available_getters:
|
if available_getters:
|
||||||
getter_name = available_getters[0]
|
getter_name = available_getters[0]
|
||||||
print(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
||||||
else:
|
else:
|
||||||
return DataResult(
|
return DataResult(
|
||||||
success=False,
|
success=False,
|
||||||
|
|||||||
33
run_tests.py
Normal file
33
run_tests.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Скрипт для запуска тестов парсеров
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def run_tests():
|
||||||
|
"""Запуск тестов"""
|
||||||
|
print(" Запуск тестов парсеров...")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Переходим в директорию проекта
|
||||||
|
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# Запускаем pytest
|
||||||
|
cmd = [sys.executable, "-m", "pytest", "tests/", "-v", "--tb=short"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||||
|
print(result.stdout)
|
||||||
|
print(" Все тесты прошли успешно!")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(" Некоторые тесты не прошли:")
|
||||||
|
print(e.stdout)
|
||||||
|
print(e.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = run_tests()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
44
tests/README.md
Normal file
44
tests/README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Тесты для парсеров
|
||||||
|
|
||||||
|
Этот каталог содержит pytest тесты для всех парсеров и их геттеров.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
- est_parsers.py - Основные тесты для всех парсеров
|
||||||
|
- conftest.py - Конфигурация pytest
|
||||||
|
-
|
||||||
|
equirements.txt - Зависимости для тестов
|
||||||
|
- est_data/ - Тестовые данные
|
||||||
|
|
||||||
|
## Запуск тестов
|
||||||
|
|
||||||
|
`ash
|
||||||
|
# Установка зависимостей
|
||||||
|
pip install -r tests/requirements.txt
|
||||||
|
|
||||||
|
# Запуск всех тестов
|
||||||
|
pytest tests/
|
||||||
|
|
||||||
|
# Запуск конкретного теста
|
||||||
|
pytest tests/test_parsers.py::TestSvodkaPMParser
|
||||||
|
|
||||||
|
# Запуск с подробным выводом
|
||||||
|
pytest tests/ -v
|
||||||
|
|
||||||
|
# Запуск с покрытием кода
|
||||||
|
pytest tests/ --cov=python_parser
|
||||||
|
`
|
||||||
|
|
||||||
|
## Покрытие тестами
|
||||||
|
|
||||||
|
Тесты покрывают:
|
||||||
|
- Инициализацию всех парсеров
|
||||||
|
- Все геттеры каждого парсера
|
||||||
|
- Обработку валидных и невалидных параметров
|
||||||
|
- Интеграционные тесты
|
||||||
|
|
||||||
|
## Добавление новых тестов
|
||||||
|
|
||||||
|
При добавлении нового парсера:
|
||||||
|
1. Добавьте класс тестов в est_parsers.py
|
||||||
|
2. Создайте тесты для всех геттеров
|
||||||
28
tests/conftest.py
Normal file
28
tests/conftest.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
Конфигурация pytest для тестов парсеров
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем путь к проекту
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python_parser'))
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_data_dir():
|
||||||
|
"""Путь к директории с тестовыми данными"""
|
||||||
|
return os.path.join(os.path.dirname(__file__), 'test_data')
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_data():
|
||||||
|
"""Моковые данные для тестов"""
|
||||||
|
return {
|
||||||
|
'SNPZ': {
|
||||||
|
'data': 'test_data',
|
||||||
|
'records_count': 10
|
||||||
|
},
|
||||||
|
'KNPZ': {
|
||||||
|
'data': 'test_data_2',
|
||||||
|
'records_count': 5
|
||||||
|
}
|
||||||
|
}
|
||||||
4
tests/requirements.txt
Normal file
4
tests/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pytest>=7.0.0
|
||||||
|
pandas>=1.5.0
|
||||||
|
numpy>=1.20.0
|
||||||
|
openpyxl>=3.0.0
|
||||||
BIN
tests/test_data/monitoring.zip
Normal file
BIN
tests/test_data/monitoring.zip
Normal file
Binary file not shown.
BIN
tests/test_data/oper_spavka_tech_pos_SNPZ.zip
Normal file
BIN
tests/test_data/oper_spavka_tech_pos_SNPZ.zip
Normal file
Binary file not shown.
BIN
tests/test_data/pm_all.zip
Normal file
BIN
tests/test_data/pm_all.zip
Normal file
Binary file not shown.
BIN
tests/test_data/svodka_tar.zip
Normal file
BIN
tests/test_data/svodka_tar.zip
Normal file
Binary file not shown.
394
tests/test_parsers.py
Normal file
394
tests/test_parsers.py
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
"""
|
||||||
|
Тесты для всех парсеров и их геттеров
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import pandas as pd
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Добавляем путь к проекту
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python_parser'))
|
||||||
|
|
||||||
|
from adapters.parsers import (
|
||||||
|
SvodkaPMParser,
|
||||||
|
SvodkaCAParser,
|
||||||
|
MonitoringFuelParser,
|
||||||
|
MonitoringTarParser,
|
||||||
|
SvodkaRepairCAParser,
|
||||||
|
StatusesRepairCAParser,
|
||||||
|
OperSpravkaTechPosParser
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSvodkaPMParser:
|
||||||
|
"""Тесты для парсера Сводки ПМ"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = SvodkaPMParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'SNPZ': pd.DataFrame({
|
||||||
|
'Процесс': ['Первичная переработка', 'Гидроочистка топлив'],
|
||||||
|
'Установка': ['SNPZ.EAVT6', 'SNPZ.L24-6'],
|
||||||
|
'План, т': [100.0, 200.0],
|
||||||
|
'Факт, т': [95.0, 190.0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "Сводки ПМ"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 2
|
||||||
|
assert 'single_og' in self.parser.getters
|
||||||
|
assert 'total_ogs' in self.parser.getters
|
||||||
|
|
||||||
|
def test_single_og_getter(self):
|
||||||
|
"""Тест геттера single_og"""
|
||||||
|
params = {
|
||||||
|
'id': 'SNPZ',
|
||||||
|
'codes': [78, 79],
|
||||||
|
'columns': ['ПП', 'СЭБ']
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.parser.get_value('single_og', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, str) # Возвращает JSON строку
|
||||||
|
|
||||||
|
def test_total_ogs_getter(self):
|
||||||
|
"""Тест геттера total_ogs"""
|
||||||
|
params = {
|
||||||
|
'codes': [78, 79],
|
||||||
|
'columns': ['ПП', 'СЭБ']
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.parser.get_value('total_ogs', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, str) # Возвращает JSON строку
|
||||||
|
|
||||||
|
def test_getter_with_invalid_params(self):
|
||||||
|
"""Тест геттера с неверными параметрами"""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
self.parser.get_value('single_og', {'invalid': 'params'})
|
||||||
|
|
||||||
|
def test_getter_with_nonexistent_og(self):
|
||||||
|
"""Тест геттера с несуществующим ОГ"""
|
||||||
|
params = {
|
||||||
|
'id': 'NONEXISTENT',
|
||||||
|
'codes': [78, 79],
|
||||||
|
'columns': ['ПП', 'СЭБ']
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.parser.get_value('single_og', params)
|
||||||
|
# Должен вернуть пустой результат, но не упасть
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestSvodkaCAParser:
|
||||||
|
"""Тесты для парсера Сводки СА"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = SvodkaCAParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'plan': {
|
||||||
|
'ТиП': pd.DataFrame({
|
||||||
|
'ОГ': ['SNPZ', 'KNPZ'],
|
||||||
|
'Значение': [100.0, 200.0]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'fact': {
|
||||||
|
'ТиП': pd.DataFrame({
|
||||||
|
'ОГ': ['SNPZ', 'KNPZ'],
|
||||||
|
'Значение': [95.0, 190.0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "Сводки СА"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 1
|
||||||
|
assert 'get_ca_data' in self.parser.getters
|
||||||
|
|
||||||
|
def test_get_ca_data_getter(self):
|
||||||
|
"""Тест геттера get_ca_data"""
|
||||||
|
params = {
|
||||||
|
'modes': ['plan', 'fact'],
|
||||||
|
'tables': ['ТиП', 'Топливо']
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_ca_data', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, dict) # Возвращает словарь
|
||||||
|
|
||||||
|
|
||||||
|
class TestMonitoringFuelParser:
|
||||||
|
"""Тесты для парсера Мониторинга топлива"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = MonitoringFuelParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'SNPZ': pd.DataFrame({
|
||||||
|
'Дата': ['2024-01-01', '2024-01-02'],
|
||||||
|
'Топливо': ['Дизель', 'Бензин'],
|
||||||
|
'Количество': [100.0, 200.0],
|
||||||
|
'Объем': [50.0, 75.0] # Добавляем числовую колонку для агрегации
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "Мониторинг топлива"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
# Проверяем, что есть геттеры
|
||||||
|
assert len(self.parser.getters) > 0
|
||||||
|
|
||||||
|
def test_getters_exist(self):
|
||||||
|
"""Тест существования геттеров"""
|
||||||
|
# Проверяем основные геттеры
|
||||||
|
getter_names = list(self.parser.getters.keys())
|
||||||
|
assert len(getter_names) > 0
|
||||||
|
|
||||||
|
# Тестируем каждый геттер с правильными параметрами
|
||||||
|
for getter_name in getter_names:
|
||||||
|
if getter_name == 'total_by_columns':
|
||||||
|
params = {'columns': ['Количество', 'Объем']} # Используем числовые колонки
|
||||||
|
else:
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.parser.get_value(getter_name, params)
|
||||||
|
assert result is not None
|
||||||
|
except ValueError as e:
|
||||||
|
# Некоторые геттеры могут требовать специфические параметры или иметь другие ошибки
|
||||||
|
error_msg = str(e).lower()
|
||||||
|
assert any(keyword in error_msg for keyword in ["required", "missing", "отсутствуют", "обязательные", "ошибка выполнения"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestMonitoringTarParser:
|
||||||
|
"""Тесты для парсера Мониторинга ТЭР"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = MonitoringTarParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'total': [pd.DataFrame({
|
||||||
|
'Дата': ['2024-01-01'],
|
||||||
|
'Потребление': [100.0]
|
||||||
|
})],
|
||||||
|
'last_day': [pd.DataFrame({
|
||||||
|
'Дата': ['2024-01-02'],
|
||||||
|
'Потребление': [150.0]
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "monitoring_tar"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 2
|
||||||
|
assert 'get_tar_data' in self.parser.getters
|
||||||
|
assert 'get_tar_full_data' in self.parser.getters
|
||||||
|
|
||||||
|
def test_get_tar_data_getter(self):
|
||||||
|
"""Тест геттера get_tar_data"""
|
||||||
|
params = {'mode': 'total'}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_tar_data', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, str) # Возвращает JSON строку
|
||||||
|
|
||||||
|
def test_get_tar_full_data_getter(self):
|
||||||
|
"""Тест геттера get_tar_full_data"""
|
||||||
|
result = self.parser.get_value('get_tar_full_data', {})
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, str) # Возвращает JSON строку
|
||||||
|
|
||||||
|
|
||||||
|
class TestSvodkaRepairCAParser:
|
||||||
|
"""Тесты для парсера Сводки ремонта СА"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = SvodkaRepairCAParser()
|
||||||
|
# Создаем тестовые данные в правильном формате
|
||||||
|
self.test_data = [
|
||||||
|
{
|
||||||
|
'id': 'SNPZ',
|
||||||
|
'Тип_ремонта': 'Капитальный',
|
||||||
|
'Статус': 'Завершен',
|
||||||
|
'Дата': '2024-01-01'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'SNPZ',
|
||||||
|
'Тип_ремонта': 'Текущий',
|
||||||
|
'Статус': 'В работе',
|
||||||
|
'Дата': '2024-01-02'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "Сводки ремонта СА"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 1
|
||||||
|
assert 'get_repair_data' in self.parser.getters
|
||||||
|
|
||||||
|
def test_get_repair_data_getter(self):
|
||||||
|
"""Тест геттера get_repair_data"""
|
||||||
|
params = {
|
||||||
|
'og_ids': ['SNPZ'],
|
||||||
|
'repair_types': ['КР'],
|
||||||
|
'include_planned': True,
|
||||||
|
'include_factual': True
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_repair_data', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, dict) # Возвращает словарь
|
||||||
|
|
||||||
|
|
||||||
|
class TestStatusesRepairCAParser:
|
||||||
|
"""Тесты для парсера Статусов ремонта СА"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = StatusesRepairCAParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'SNPZ': pd.DataFrame({
|
||||||
|
'Статус': ['В работе', 'Завершен'],
|
||||||
|
'Процент': [50.0, 100.0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "Статусы ремонта СА"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 1
|
||||||
|
assert 'get_repair_statuses' in self.parser.getters
|
||||||
|
|
||||||
|
def test_get_repair_statuses_getter(self):
|
||||||
|
"""Тест геттера get_repair_statuses"""
|
||||||
|
params = {'ids': ['SNPZ']}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_repair_statuses', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, list) # Возвращает список
|
||||||
|
|
||||||
|
|
||||||
|
class TestOperSpravkaTechPosParser:
|
||||||
|
"""Тесты для парсера Операционных справок технологических позиций"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.parser = OperSpravkaTechPosParser()
|
||||||
|
# Создаем тестовые данные
|
||||||
|
self.test_data = {
|
||||||
|
'SNPZ': pd.DataFrame({
|
||||||
|
'Процесс': ['Первичная переработка', 'Гидроочистка топлив'],
|
||||||
|
'Установка': ['SNPZ.EAVT6', 'SNPZ.L24-6'],
|
||||||
|
'План, т': [100.0, 200.0],
|
||||||
|
'Факт, т': [95.0, 190.0],
|
||||||
|
'id': ['SNPZ.EAVT6', 'SNPZ.L24-6']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.parser.data_dict = self.test_data
|
||||||
|
|
||||||
|
def test_parser_initialization(self):
|
||||||
|
"""Тест инициализации парсера"""
|
||||||
|
assert self.parser.name == "oper_spravka_tech_pos"
|
||||||
|
assert hasattr(self.parser, 'getters')
|
||||||
|
assert len(self.parser.getters) == 1
|
||||||
|
assert 'get_tech_pos' in self.parser.getters
|
||||||
|
|
||||||
|
def test_get_tech_pos_getter(self):
|
||||||
|
"""Тест геттера get_tech_pos"""
|
||||||
|
params = {'id': 'SNPZ'}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_tech_pos', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, list) # Возвращает список словарей
|
||||||
|
|
||||||
|
def test_get_tech_pos_with_nonexistent_og(self):
|
||||||
|
"""Тест геттера с несуществующим ОГ"""
|
||||||
|
params = {'id': 'NONEXISTENT'}
|
||||||
|
|
||||||
|
result = self.parser.get_value('get_tech_pos', params)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, list)
|
||||||
|
assert len(result) == 0 # Пустой список для несуществующего ОГ
|
||||||
|
|
||||||
|
|
||||||
|
class TestParserIntegration:
|
||||||
|
"""Интеграционные тесты для всех парсеров"""
|
||||||
|
|
||||||
|
def test_all_parsers_have_getters(self):
|
||||||
|
"""Тест, что все парсеры имеют геттеры"""
|
||||||
|
parsers = [
|
||||||
|
SvodkaPMParser(),
|
||||||
|
SvodkaCAParser(),
|
||||||
|
MonitoringFuelParser(),
|
||||||
|
MonitoringTarParser(),
|
||||||
|
SvodkaRepairCAParser(),
|
||||||
|
StatusesRepairCAParser(),
|
||||||
|
OperSpravkaTechPosParser()
|
||||||
|
]
|
||||||
|
|
||||||
|
for parser in parsers:
|
||||||
|
assert hasattr(parser, 'getters')
|
||||||
|
assert len(parser.getters) > 0
|
||||||
|
assert hasattr(parser, 'name')
|
||||||
|
assert parser.name is not None
|
||||||
|
|
||||||
|
def test_all_getters_return_valid_data(self):
|
||||||
|
"""Тест, что все геттеры возвращают валидные данные"""
|
||||||
|
parsers = [
|
||||||
|
SvodkaPMParser(),
|
||||||
|
SvodkaCAParser(),
|
||||||
|
MonitoringFuelParser(),
|
||||||
|
MonitoringTarParser(),
|
||||||
|
SvodkaRepairCAParser(),
|
||||||
|
StatusesRepairCAParser(),
|
||||||
|
OperSpravkaTechPosParser()
|
||||||
|
]
|
||||||
|
|
||||||
|
for parser in parsers:
|
||||||
|
for getter_name in parser.getters.keys():
|
||||||
|
try:
|
||||||
|
result = parser.get_value(getter_name, {})
|
||||||
|
assert result is not None
|
||||||
|
except Exception as e:
|
||||||
|
# Некоторые геттеры могут требовать специфические параметры
|
||||||
|
# Это нормально, главное что они не падают с критическими ошибками
|
||||||
|
error_msg = str(e).lower()
|
||||||
|
assert any(keyword in error_msg for keyword in ["required", "missing", "отсутствуют", "обязательные"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
Reference in New Issue
Block a user