2 Commits

Author SHA1 Message Date
00a01e99d7 Логгер работает 2025-09-04 18:24:53 +03:00
bbbfbbd508 Основа для логгера 2025-09-04 17:13:39 +03:00
11 changed files with 242 additions and 191 deletions

View File

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

View File

@@ -2,10 +2,14 @@ import os
import zipfile
import tempfile
import pandas as pd
import logging
from typing import Dict, Any, List
from core.ports import ParserPort
from adapters.pconfig import find_header_row, SNPZ_IDS, data_to_json
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
class MonitoringTarParser(ParserPort):
"""Парсер для мониторинга ТЭР (топливно-энергетических ресурсов)"""
@@ -23,7 +27,7 @@ class MonitoringTarParser(ParserPort):
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
print(f"🔍 DEBUG: MonitoringTarParser.parse вызван с файлом: {file_path}")
logger.debug(f"🔍 MonitoringTarParser.parse вызван с файлом: {file_path}")
if not file_path.endswith('.zip'):
raise ValueError("MonitoringTarParser поддерживает только ZIP архивы")
@@ -42,15 +46,15 @@ class MonitoringTarParser(ParserPort):
})
df = pd.DataFrame(data_list)
print(f"🔍 DEBUG: Создан DataFrame с {len(df)} записями")
logger.debug(f"🔍 Создан DataFrame с {len(df)} записями")
return df
else:
print("🔍 DEBUG: Возвращаем пустой DataFrame")
logger.debug("🔍 Возвращаем пустой DataFrame")
return pd.DataFrame()
def _parse_zip_archive(self, zip_path: str) -> Dict[str, Any]:
"""Парсит ZIP архив с файлами мониторинга ТЭР"""
print(f"📦 Обработка ZIP архива: {zip_path}")
logger.info(f"📦 Обработка ZIP архива: {zip_path}")
with tempfile.TemporaryDirectory() as temp_dir:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
@@ -67,17 +71,17 @@ class MonitoringTarParser(ParserPort):
if not tar_files:
raise ValueError("В архиве не найдены файлы мониторинга ТЭР")
print(f"📁 Найдено {len(tar_files)} файлов мониторинга ТЭР")
logger.info(f"📁 Найдено {len(tar_files)} файлов мониторинга ТЭР")
# Обрабатываем каждый файл
all_data = {}
for file_path in tar_files:
print(f"📁 Обработка файла: {file_path}")
logger.info(f"📁 Обработка файла: {file_path}")
# Извлекаем номер месяца из имени файла
filename = os.path.basename(file_path)
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)
@@ -102,7 +106,7 @@ class MonitoringTarParser(ParserPort):
excel_file = pd.ExcelFile(file_path)
available_sheets = excel_file.sheet_names
except Exception as e:
print(f"Не удалось открыть Excel-файл {file_path}: {e}")
logger.error(f"Не удалось открыть Excel-файл {file_path}: {e}")
return {}
# Словарь для хранения данных: id -> {'total': [], 'last_day': []}
@@ -115,7 +119,7 @@ class MonitoringTarParser(ParserPort):
# Обрабатываем файлы svodka_tar_*.xlsx с SNPZ_IDS
for name, id in SNPZ_IDS.items():
if name not in available_sheets:
print(f"🟡 Лист '{name}' отсутствует в файле {file_path}")
logger.warning(f"🟡 Лист '{name}' отсутствует в файле {file_path}")
continue
# Парсим оба типа строк
@@ -140,7 +144,7 @@ class MonitoringTarParser(ParserPort):
for sheet_name, id in monitoring_sheets.items():
if sheet_name not in available_sheets:
print(f"🟡 Лист '{sheet_name}' отсутствует в файле {file_path}")
logger.warning(f"🟡 Лист '{sheet_name}' отсутствует в файле {file_path}")
continue
# Парсим оба типа строк
@@ -168,7 +172,7 @@ class MonitoringTarParser(ParserPort):
else:
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}'")
return df_svodka_tar
@@ -178,7 +182,7 @@ class MonitoringTarParser(ParserPort):
try:
# Проверяем наличие листа
if sheet not in pd.ExcelFile(file).sheet_names:
print(f"🟡 Лист '{sheet}' не найден в файле {file}")
logger.warning(f"🟡 Лист '{sheet}' не найден в файле {file}")
return {'total': None, 'last_day': None}
# Определяем номер заголовка в зависимости от типа файла
@@ -187,16 +191,16 @@ class MonitoringTarParser(ParserPort):
# Для файлов svodka_tar_*.xlsx ищем заголовок по значению "1"
header_num = find_header_row(file, sheet, search_value="1")
if header_num is None:
print(f"Не найдена строка с заголовком '1' в файле {file}, лист '{sheet}'")
logger.error(f"Не найдена строка с заголовком '1' в файле {file}, лист '{sheet}'")
return {'total': None, 'last_day': None}
elif filename.startswith('monitoring_'):
# Для файлов monitoring_*.xlsm заголовок находится в строке 5
header_num = 5
else:
print(f"❌ Неизвестный тип файла: {filename}")
logger.error(f"❌ Неизвестный тип файла: {filename}")
return {'total': None, 'last_day': None}
print(f"🔍 DEBUG: Используем заголовок в строке {header_num} для листа '{sheet}'")
logger.debug(f"🔍 Используем заголовок в строке {header_num} для листа '{sheet}'")
# Читаем с двумя уровнями заголовков
df = pd.read_excel(
@@ -212,7 +216,7 @@ class MonitoringTarParser(ParserPort):
# Удаляем строки, где все значения — NaN
df = df.dropna(how='all').reset_index(drop=True)
if df.empty:
print(f"🟡 Нет данных после очистки в файле {file}, лист '{sheet}'")
logger.warning(f"🟡 Нет данных после очистки в файле {file}, лист '{sheet}'")
return {'total': None, 'last_day': None}
# === 1. Обработка строки "Всего" ===
@@ -240,12 +244,12 @@ class MonitoringTarParser(ParserPort):
return {'total': df_total, 'last_day': df_last_day}
except Exception as e:
print(f"❌ Ошибка при обработке файла {file}, лист '{sheet}': {e}")
logger.error(f"❌ Ошибка при обработке файла {file}, лист '{sheet}': {e}")
return {'total': None, 'last_day': None}
def _get_tar_data_wrapper(self, params: Dict[str, Any] = None) -> str:
"""Обертка для получения данных мониторинга ТЭР с фильтрацией по режиму"""
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'
@@ -274,12 +278,12 @@ class MonitoringTarParser(ParserPort):
result_json = data_to_json(filtered_data)
return result_json
except Exception as e:
print(f"❌ Ошибка при конвертации данных в JSON: {e}")
logger.error(f"❌ Ошибка при конвертации данных в JSON: {e}")
return "{}"
def _get_tar_full_data_wrapper(self, params: Dict[str, Any] = None) -> str:
"""Обертка для получения всех данных мониторинга ТЭР"""
print(f"🔍 DEBUG: _get_tar_full_data_wrapper вызван с параметрами: {params}")
logger.debug(f"🔍 _get_tar_full_data_wrapper вызван с параметрами: {params}")
# Получаем все данные
full_data = {}
@@ -298,5 +302,5 @@ class MonitoringTarParser(ParserPort):
result_json = data_to_json(full_data)
return result_json
except Exception as e:
print(f"❌ Ошибка при конвертации данных в JSON: {e}")
logger.error(f"❌ Ошибка при конвертации данных в JSON: {e}")
return "{}"

View File

@@ -2,11 +2,15 @@ import os
import tempfile
import zipfile
import pandas as pd
import logging
from typing import Dict, Any, List
from datetime import datetime
from core.ports import ParserPort
from adapters.pconfig import find_header_row, get_object_by_name, data_to_json
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
class OperSpravkaTechPosParser(ParserPort):
"""Парсер для операционных справок технологических позиций"""
@@ -23,7 +27,7 @@ class OperSpravkaTechPosParser(ParserPort):
def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame:
"""Парсит ZIP архив с файлами операционных справок технологических позиций"""
print(f"🔍 DEBUG: OperSpravkaTechPosParser.parse вызван с файлом: {file_path}")
logger.debug(f"🔍 OperSpravkaTechPosParser.parse вызван с файлом: {file_path}")
if not file_path.endswith('.zip'):
raise ValueError("OperSpravkaTechPosParser поддерживает только ZIP архивы")
@@ -44,15 +48,15 @@ class OperSpravkaTechPosParser(ParserPort):
})
df = pd.DataFrame(data_list)
print(f"🔍 DEBUG: Создан DataFrame с {len(df)} записями")
logger.debug(f"🔍 Создан DataFrame с {len(df)} записями")
return df
else:
print("🔍 DEBUG: Возвращаем пустой DataFrame")
logger.debug("🔍 Возвращаем пустой DataFrame")
return pd.DataFrame()
def _parse_zip_archive(self, zip_path: str) -> Dict[str, pd.DataFrame]:
"""Парсит ZIP архив с файлами операционных справок"""
print(f"📦 Обработка ZIP архива: {zip_path}")
logger.info(f"📦 Обработка ZIP архива: {zip_path}")
with tempfile.TemporaryDirectory() as temp_dir:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
@@ -69,17 +73,17 @@ class OperSpravkaTechPosParser(ParserPort):
if not tech_pos_files:
raise ValueError("В архиве не найдены файлы операционных справок технологических позиций")
print(f"📁 Найдено {len(tech_pos_files)} файлов операционных справок")
logger.info(f"📁 Найдено {len(tech_pos_files)} файлов операционных справок")
# Обрабатываем каждый файл
all_data = {}
for file_path in tech_pos_files:
print(f"📁 Обработка файла: {file_path}")
logger.info(f"📁 Обработка файла: {file_path}")
# Извлекаем ID ОГ из имени файла
filename = os.path.basename(file_path)
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)
@@ -102,11 +106,11 @@ class OperSpravkaTechPosParser(ParserPort):
try:
# Находим актуальный лист
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)
print(f"📋 Заголовок найден в строке {header_row}")
logger.debug(f"📋 Заголовок найден в строке {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)
return {og_id: df}
else:
print(f"⚠️ Нет данных в файле {file_path}")
logger.warning(f"⚠️ Нет данных в файле {file_path}")
return {}
except Exception as e:
print(f"❌ Ошибка при обработке файла {file_path}: {e}")
logger.error(f"❌ Ошибка при обработке файла {file_path}: {e}")
return {}
def _find_actual_sheet_num(self, file_path: str) -> str:
@@ -160,7 +164,7 @@ class OperSpravkaTechPosParser(ParserPort):
except:
continue
except Exception as e:
print(f"⚠️ Ошибка при поиске актуального листа: {e}")
logger.warning(f"⚠️ Ошибка при поиске актуального листа: {e}")
return actual_sheet
@@ -173,12 +177,12 @@ class OperSpravkaTechPosParser(ParserPort):
# Ищем строку с искомым значением
for idx, row in df_temp.iterrows():
if row.astype(str).str.contains(search_value, case=False, regex=False).any():
print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
logger.debug(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
return idx + 1 # возвращаем индекс строки (0-based), который будет использован как `header=`
raise ValueError(f"Не найдена строка с заголовком '{search_value}'.")
except Exception as e:
print(f"❌ Ошибка при поиске заголовка: {e}")
logger.error(f"❌ Ошибка при поиске заголовка: {e}")
return 0
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)
)
print(f"🔍 DEBUG: Прочитано {len(df_temp)} строк из Excel")
print(f"🔍 DEBUG: Колонки: {list(df_temp.columns)}")
logger.debug(f"🔍 Прочитано {len(df_temp)} строк из Excel")
logger.debug(f"🔍 Колонки: {list(df_temp.columns)}")
# Фильтруем по валидным процессам
df_cleaned = df_temp[
@@ -202,11 +206,11 @@ class OperSpravkaTechPosParser(ParserPort):
df_temp['Процесс'].notna()
].copy()
print(f"🔍 DEBUG: После фильтрации осталось {len(df_cleaned)} строк")
logger.debug(f"🔍 После фильтрации осталось {len(df_cleaned)} строк")
if df_cleaned.empty:
print("⚠️ Нет данных после фильтрации по процессам")
print(f"🔍 DEBUG: Доступные процессы в данных: {df_temp['Процесс'].unique()}")
logger.warning("⚠️ Нет данных после фильтрации по процессам")
logger.debug(f"🔍 Доступные процессы в данных: {df_temp['Процесс'].unique()}")
return pd.DataFrame()
df_cleaned['Процесс'] = df_cleaned['Процесс'].astype(str).str.strip()
@@ -214,68 +218,68 @@ class OperSpravkaTechPosParser(ParserPort):
# Добавляем ID установки
if 'Установка' in df_cleaned.columns:
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:
print("⚠️ Колонка 'Установка' не найдена")
logger.warning("⚠️ Колонка 'Установка' не найдена")
print(f"✅ Получено {len(df_cleaned)} записей")
logger.info(f"✅ Получено {len(df_cleaned)} записей")
return df_cleaned
except Exception as e:
print(f"❌ Ошибка при парсинге данных: {e}")
logger.error(f"❌ Ошибка при парсинге данных: {e}")
return pd.DataFrame()
def _get_tech_pos_wrapper(self, params: Dict[str, Any] = None) -> str:
"""Обертка для получения данных технологических позиций"""
print(f"🔍 DEBUG: _get_tech_pos_wrapper вызван с параметрами: {params}")
logger.debug(f"🔍 _get_tech_pos_wrapper вызван с параметрами: {params}")
# Получаем ID ОГ из параметров
og_id = params.get('id') if params else None
if not og_id:
print("Не указан ID ОГ")
logger.error("Не указан ID ОГ")
return "{}"
# Получаем данные
tech_pos_data = {}
if hasattr(self, 'df') and self.df is not None and not self.df.empty:
# Данные из MinIO
print(f"🔍 DEBUG: Ищем данные для ОГ '{og_id}' в DataFrame с {len(self.df)} записями")
logger.debug(f"🔍 Ищем данные для ОГ '{og_id}' в DataFrame с {len(self.df)} записями")
available_ogs = self.df['id'].tolist()
print(f"🔍 DEBUG: Доступные ОГ в данных: {available_ogs}")
logger.debug(f"🔍 Доступные ОГ в данных: {available_ogs}")
for _, row in self.df.iterrows():
if row['id'] == og_id:
tech_pos_data = row['data']
print(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
logger.info(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
break
else:
print(f"❌ Данные для ОГ '{og_id}' не найдены")
logger.warning(f"❌ Данные для ОГ '{og_id}' не найдены")
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())
print(f"🔍 DEBUG: Доступные ОГ в data_dict: {available_ogs}")
logger.debug(f"🔍 Доступные ОГ в data_dict: {available_ogs}")
if og_id in self.data_dict:
tech_pos_data = self.data_dict[og_id].to_dict(orient='records')
print(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
logger.info(f"✅ Найдены данные для ОГ '{og_id}': {len(tech_pos_data)} записей")
else:
print(f"❌ Данные для ОГ '{og_id}' не найдены в data_dict")
logger.warning(f"❌ Данные для ОГ '{og_id}' не найдены в data_dict")
# Конвертируем в список записей
try:
if isinstance(tech_pos_data, pd.DataFrame):
# Если это DataFrame, конвертируем в список словарей
result_list = tech_pos_data.to_dict(orient='records')
print(f"🔍 DEBUG: Конвертировано в список: {len(result_list)} записей")
logger.debug(f"🔍 Конвертировано в список: {len(result_list)} записей")
return result_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
else:
print(f"🔍 DEBUG: Неожиданный тип данных: {type(tech_pos_data)}")
logger.warning(f"🔍 Неожиданный тип данных: {type(tech_pos_data)}")
return []
except Exception as e:
print(f"❌ Ошибка при конвертации данных: {e}")
logger.error(f"❌ Ошибка при конвертации данных: {e}")
return []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,21 @@
import os
import multiprocessing
import uvicorn
import logging
from typing import Dict, List
from fastapi import FastAPI, File, UploadFile, HTTPException, status
from fastapi.responses import JSONResponse
# Настройка логирования
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(name)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Настройка логгера для модуля
logger = logging.getLogger(__name__)
from adapters.storage import MinIOStorageAdapter
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser
@@ -154,7 +165,7 @@ async def get_available_ogs(parser_name: str):
if available_ogs:
return {"parser": parser_name, "available_ogs": available_ogs}
except Exception as e:
print(f"⚠️ Ошибка при получении ОГ: {e}")
logger.error(f"⚠️ Ошибка при получении ОГ: {e}")
import traceback
traceback.print_exc()
@@ -1412,7 +1423,7 @@ async def get_oper_spravka_tech_pos_data(request: OperSpravkaTechPosRequest):
if result.success:
# Извлекаем данные из результата
value_data = result.data.get("value", []) if isinstance(result.data.get("value"), list) else []
print(f"🔍 DEBUG: API возвращает данные: {type(value_data)}, длина: {len(value_data) if isinstance(value_data, (list, dict)) else 'N/A'}")
logger.debug(f"🔍 API возвращает данные: {type(value_data)}, длина: {len(value_data) if isinstance(value_data, (list, dict)) else 'N/A'}")
return OperSpravkaTechPosResponse(
success=True,

View File

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