11 Commits

Author SHA1 Message Date
55626490dd fix: цвета вкладок 2025-09-04 22:59:47 +03:00
1bfe3c0cd8 Работает!!! 2025-09-04 22:53:01 +03:00
36f37ffacb Работает все, кроме отображения задач 2025-09-04 22:42:31 +03:00
6a1f685ee3 fix errors 2025-09-04 21:44:48 +03:00
2fcee9f065 streamlit рефактор 2025-09-04 21:33:13 +03:00
f54a36ab22 Merge branch 'create-tests' 2025-09-04 18:57:16 +03:00
2555fd80e0 pytests 2025-09-04 18:56:36 +03:00
847441842c Merge branch 'logging-upgrade' 2025-09-04 18:25:31 +03:00
00a01e99d7 Логгер работает 2025-09-04 18:24:53 +03:00
bbbfbbd508 Основа для логгера 2025-09-04 17:13:39 +03:00
0f3340c899 Merge branch 'add-new-parsers' 2025-09-04 15:54:15 +03:00
36 changed files with 2400 additions and 1016 deletions

View File

@@ -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 с именем месяца

View File

@@ -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 "{}"

View File

@@ -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 []

View File

@@ -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]:

View File

@@ -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:

View File

@@ -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)

View File

@@ -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]:

View File

@@ -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} строках.")

View File

@@ -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:

View File

@@ -1,15 +1,27 @@
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
from core.models import UploadRequest, DataRequest from core.models import UploadRequest, DataRequest
from core.services import ReportService, PARSERS from core.services import ReportService, PARSERS
from core.async_services import AsyncReportService
from app.schemas import ( from app.schemas import (
ServerInfoResponse, ServerInfoResponse,
@@ -44,6 +56,10 @@ def get_report_service() -> ReportService:
return ReportService(storage_adapter) return ReportService(storage_adapter)
def get_async_report_service() -> AsyncReportService:
return AsyncReportService(ReportService(storage_adapter))
tags_metadata = [ tags_metadata = [
{ {
"name": "Общее", "name": "Общее",
@@ -154,7 +170,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 +1428,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,
@@ -1432,5 +1448,194 @@ async def get_oper_spravka_tech_pos_data(request: OperSpravkaTechPosRequest):
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
# ============================================================================
# АСИНХРОННЫЕ ЭНДПОИНТЫ
# ============================================================================
@app.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name],
summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def async_upload_svodka_pm_zip(
zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)")
):
"""Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**"""
async_service = get_async_report_service()
try:
if not zip_file.filename.lower().endswith('.zip'):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Файл должен быть ZIP архивом",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".zip"],
"received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown"
}
).model_dump()
)
file_content = await zip_file.read()
# Создаем запрос
request = UploadRequest(
report_type='svodka_pm',
file_content=file_content,
file_name=zip_file.filename
)
# Загружаем отчет асинхронно
result = await async_service.upload_report_async(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message=result.message,
error_code="UPLOAD_FAILED"
).model_dump()
)
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_ERROR"
).model_dump()
)
@app.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name],
summary="Асинхронная загрузка файла отчета сводки СА",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def async_upload_svodka_ca(
file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)")
):
"""Асинхронная загрузка и обработка Excel файла отчета сводки СА"""
async_service = get_async_report_service()
try:
# Проверяем тип файла
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".xlsx", ".xlsm", ".xls"],
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
}
).model_dump()
)
# Читаем содержимое файла
file_content = await file.read()
# Создаем запрос
request = UploadRequest(
report_type='svodka_ca',
file_content=file_content,
file_name=file.filename
)
# Загружаем отчет асинхронно
result = await async_service.upload_report_async(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message=result.message,
error_code="UPLOAD_FAILED"
).model_dump()
)
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_ERROR"
).model_dump()
)
@app.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name],
summary="Асинхронная загрузка ZIP архива с мониторингом топлива",
response_model=UploadResponse,
responses={
400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"},
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
},)
async def async_upload_monitoring_fuel_zip(
zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами мониторинга топлива (.zip)")
):
"""Асинхронная загрузка ZIP архива с файлами мониторинга топлива"""
async_service = get_async_report_service()
try:
if not zip_file.filename.lower().endswith('.zip'):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message="Файл должен быть ZIP архивом",
error_code="INVALID_FILE_TYPE",
details={
"expected_formats": [".zip"],
"received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown"
}
).model_dump()
)
file_content = await zip_file.read()
# Создаем запрос
request = UploadRequest(
report_type='monitoring_fuel',
file_content=file_content,
file_name=zip_file.filename
)
# Загружаем отчет асинхронно
result = await async_service.upload_report_async(request)
if result.success:
return UploadResponse(
success=True,
message=result.message,
object_id=result.object_id
)
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=UploadErrorResponse(
message=result.message,
error_code="UPLOAD_FAILED"
).model_dump()
)
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=UploadErrorResponse(
message=f"Внутренняя ошибка сервера: {str(e)}",
error_code="INTERNAL_ERROR"
).model_dump()
)
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080) uvicorn.run(app, host="0.0.0.0", port=8080)

View File

@@ -0,0 +1,72 @@
"""
Асинхронные сервисы для работы с отчетами
"""
import asyncio
import tempfile
import os
import logging
from concurrent.futures import ThreadPoolExecutor
from typing import Optional
from .services import ReportService
from .models import UploadRequest, UploadResult, DataRequest, DataResult
from .ports import StoragePort
logger = logging.getLogger(__name__)
class AsyncReportService:
"""Асинхронный сервис для работы с отчетами"""
def __init__(self, report_service: ReportService):
self.report_service = report_service
self.executor = ThreadPoolExecutor(max_workers=4)
async def upload_report_async(self, request: UploadRequest) -> UploadResult:
"""Асинхронная загрузка отчета"""
try:
# Запускаем синхронную обработку в отдельном потоке
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
self.executor,
self._process_upload_sync,
request
)
return result
except Exception as e:
logger.error(f"Ошибка при асинхронной загрузке отчета: {str(e)}")
return UploadResult(
success=False,
message=f"Ошибка при асинхронной загрузке отчета: {str(e)}"
)
def _process_upload_sync(self, request: UploadRequest) -> UploadResult:
"""Синхронная обработка загрузки (выполняется в отдельном потоке)"""
return self.report_service.upload_report(request)
async def get_data_async(self, request: DataRequest) -> DataResult:
"""Асинхронное получение данных"""
try:
# Запускаем синхронную обработку в отдельном потоке
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
self.executor,
self._process_get_data_sync,
request
)
return result
except Exception as e:
logger.error(f"Ошибка при асинхронном получении данных: {str(e)}")
return DataResult(
success=False,
message=f"Ошибка при асинхронном получении данных: {str(e)}"
)
def _process_get_data_sync(self, request: DataRequest) -> DataResult:
"""Синхронное получение данных (выполняется в отдельном потоке)"""
return self.report_service.get_data(request)
def __del__(self):
"""Очистка ресурсов"""
if hasattr(self, 'executor'):
self.executor.shutdown(wait=False)

View File

@@ -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
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""
Скрипт для запуска тестов парсеров
"""
import subprocess
import sys
import os
def run_tests():
"""Запуск тестов"""
print(" Запуск тестов парсеров...")
print("=" * 50)
# Переходим в директорию проекта
os.chdir(os.path.dirname(os.path.abspath(__file__)))
# Запускаем pytest
cmd = [sys.executable, "-m", "pytest", "tests/", "-v", "--tb=short"]
try:
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
print(result.stdout)
print(" Все тесты прошли успешно!")
return True
except subprocess.CalledProcessError as e:
print(" Некоторые тесты не прошли:")
print(e.stdout)
print(e.stderr)
return False
if __name__ == "__main__":
success = run_tests()
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,76 @@
"""
Модуль для работы с API
"""
import requests
from typing import Dict, Any, List, Tuple
from config import API_BASE_URL, API_PUBLIC_URL
def check_api_health() -> bool:
"""Проверка доступности API"""
try:
response = requests.get(f"{API_BASE_URL}/", timeout=5)
return response.status_code == 200
except:
return False
def get_available_parsers() -> List[str]:
"""Получение списка доступных парсеров"""
try:
response = requests.get(f"{API_BASE_URL}/parsers")
if response.status_code == 200:
return response.json()["parsers"]
return []
except:
return []
def get_server_info() -> Dict[str, Any]:
"""Получение информации о сервере"""
try:
response = requests.get(f"{API_BASE_URL}/server-info")
if response.status_code == 200:
return response.json()
return {}
except:
return {}
def upload_file_to_api(endpoint: str, file_data: bytes, filename: str) -> Tuple[Dict[str, Any], int]:
"""Загрузка файла на API"""
try:
# Определяем правильное имя поля в зависимости от эндпоинта
if "zip" in endpoint:
files = {"zip_file": (filename, file_data, "application/zip")}
else:
files = {"file": (filename, file_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
response = requests.post(f"{API_BASE_URL}{endpoint}", files=files)
return response.json(), response.status_code
except Exception as e:
return {"error": str(e)}, 500
def make_api_request(endpoint: str, data: Dict[str, Any]) -> Tuple[Dict[str, Any], int]:
"""Выполнение API запроса"""
try:
response = requests.post(f"{API_BASE_URL}{endpoint}", json=data)
return response.json(), response.status_code
except Exception as e:
return {"error": str(e)}, 500
def get_available_ogs(parser_name: str) -> List[str]:
"""Получение доступных ОГ для парсера"""
try:
response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/available_ogs")
if response.status_code == 200:
data = response.json()
return data.get("available_ogs", [])
else:
print(f"⚠️ Ошибка получения ОГ: {response.status_code}")
return []
except Exception as e:
print(f"⚠️ Ошибка при запросе ОГ: {e}")
return []

View File

@@ -0,0 +1,160 @@
"""
Страница асинхронной загрузки файлов
"""
import streamlit as st
import asyncio
import threading
import time
import json
import os
from api_client import upload_file_to_api
from config import PARSER_TABS
# Глобальное хранилище задач (в реальном приложении лучше использовать Redis или БД)
TASKS_STORAGE = {}
def upload_file_async_background(endpoint, file_data, filename, task_id):
"""Асинхронная загрузка файла в фоновом режиме"""
global TASKS_STORAGE
try:
# Обновляем статус на "running"
TASKS_STORAGE[task_id] = {
'status': 'running',
'filename': filename,
'endpoint': endpoint,
'started_at': time.time(),
'progress': 0
}
# Имитируем асинхронную работу
time.sleep(1) # Небольшая задержка для демонстрации
# Выполняем загрузку
result, status = upload_file_to_api(endpoint, file_data, filename)
# Сохраняем результат в глобальном хранилище
TASKS_STORAGE[task_id] = {
'status': 'completed' if status == 200 else 'failed',
'result': result,
'status_code': status,
'filename': filename,
'endpoint': endpoint,
'started_at': TASKS_STORAGE.get(task_id, {}).get('started_at', time.time()),
'completed_at': time.time(),
'progress': 100
}
except Exception as e:
# Сохраняем ошибку
TASKS_STORAGE[task_id] = {
'status': 'failed',
'error': str(e),
'filename': filename,
'endpoint': endpoint,
'started_at': TASKS_STORAGE.get(task_id, {}).get('started_at', time.time()),
'completed_at': time.time(),
'progress': 0
}
def render_async_upload_page():
"""Рендер страницы асинхронной загрузки"""
st.title("🚀 Асинхронная загрузка файлов")
st.markdown("---")
st.info("""
**Асинхронная загрузка** позволяет загружать файлы без блокировки интерфейса.
После загрузки файл будет обработан в фоновом режиме, а вы сможете отслеживать прогресс на странице "Управление задачами".
""")
# Выбор парсера
st.subheader("📋 Выбор парсера")
# Создаем словарь парсеров с их асинхронными эндпоинтами
parser_endpoints = {
"Сводки ПМ": "/async/svodka_pm/upload-zip",
"Сводки СА": "/async/svodka_ca/upload",
"Мониторинг топлива": "/async/monitoring_fuel/upload-zip",
"Ремонт СА": "/svodka_repair_ca/upload", # Пока синхронный
"Статусы ремонта СА": "/statuses_repair_ca/upload", # Пока синхронный
"Мониторинг ТЭР": "/monitoring_tar/upload", # Пока синхронный
"Операционные справки": "/oper_spravka_tech_pos/upload" # Пока синхронный
}
selected_parser = st.selectbox(
"Выберите тип парсера для загрузки:",
list(parser_endpoints.keys()),
key="async_parser_select"
)
st.markdown("---")
# Загрузка файла
st.subheader("📤 Загрузка файла")
uploaded_file = st.file_uploader(
f"Выберите ZIP архив для парсера '{selected_parser}'",
type=['zip'],
key="async_file_upload"
)
if uploaded_file is not None:
st.success(f"✅ Файл выбран: {uploaded_file.name}")
st.info(f"📊 Размер файла: {uploaded_file.size / 1024 / 1024:.2f} MB")
if st.button("🚀 Загрузить асинхронно", key="async_upload_btn", use_container_width=True):
# Создаем уникальный ID задачи
task_id = f"task_{int(time.time())}_{uploaded_file.name}"
# Показываем сообщение о создании задачи
st.success("✅ Задача загрузки создана!")
st.info(f"ID задачи: `{task_id}`")
st.info("📋 Перейдите на страницу 'Управление задачами' для отслеживания прогресса")
# Запускаем загрузку в фоновом потоке
endpoint = parser_endpoints[selected_parser]
file_data = uploaded_file.read()
# Создаем поток для асинхронной загрузки
thread = threading.Thread(
target=upload_file_async_background,
args=(endpoint, file_data, uploaded_file.name, task_id)
)
thread.daemon = True
thread.start()
# Автоматически переключаемся на страницу задач
st.session_state.sidebar_tasks_clicked = True
st.rerun()
st.markdown("---")
# Информация о поддерживаемых форматах
with st.expander(" Поддерживаемые форматы файлов"):
st.markdown("""
**Поддерживаемые форматы:**
- 📦 ZIP архивы с Excel файлами
- 📊 Excel файлы (.xlsx, .xls)
- 📋 CSV файлы (для некоторых парсеров)
**Ограничения:**
- Максимальный размер файла: 100 MB
- Количество файлов в архиве: до 50
- Поддерживаемые кодировки: UTF-8, Windows-1251
""")
# Статистика загрузок
st.subheader("📈 Статистика загрузок")
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Всего загружено", "0", "0")
with col2:
st.metric("В обработке", "0", "0")
with col3:
st.metric("Завершено", "0", "0")

58
streamlit_app/config.py Normal file
View File

@@ -0,0 +1,58 @@
"""
Конфигурация приложения
"""
import streamlit as st
import os
# Конфигурация API
API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker
API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя
# Конфигурация страницы
def setup_page_config():
"""Настройка конфигурации страницы Streamlit"""
st.set_page_config(
page_title="NIN Excel Parsers API Demo",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded"
)
# Константы для парсеров
PARSER_TABS = [
"📊 Сводки ПМ",
"🏭 Сводки СА",
"⛽ Мониторинг топлива",
"🔧 Ремонт СА",
"📋 Статусы ремонта СА",
"⚡ Мониторинг ТЭР",
"🏭 Операционные справки"
]
# Константы для ОГ
DEFAULT_OGS = [
"SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV",
"NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK",
"NVNPO", "KLNPZ", "PurNP", "YANOS"
]
# Константы для кодов строк ПМ
PM_CODES = [78, 79, 394, 395, 396, 397, 81, 82, 83, 84]
# Константы для столбцов ПМ
PM_COLUMNS = ["БП", "ПП", "СЭБ", "Факт", "План"]
# Константы для режимов СА
CA_MODES = ["plan", "fact", "normativ"]
# Константы для таблиц СА
CA_TABLES = ["ТиП", "Топливо", "Потери"]
# Константы для столбцов мониторинга топлива
FUEL_COLUMNS = ["normativ", "total", "total_1"]
# Константы для типов ремонта
REPAIR_TYPES = ["КР", "КП", "ТР"]
# Константы для режимов мониторинга ТЭР
TAR_MODES = ["all", "total", "last_day"]

View File

@@ -0,0 +1,3 @@
"""
UI модули для парсеров
"""

View File

@@ -0,0 +1,91 @@
"""
UI модуль для мониторинга топлива
"""
import streamlit as st
from api_client import upload_file_to_api, make_api_request
from config import FUEL_COLUMNS
def render_monitoring_fuel_tab():
"""Рендер вкладки мониторинга топлива"""
st.header("⛽ Мониторинг топлива - Полный функционал")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_fuel = st.file_uploader(
"Выберите ZIP архив с мониторингом топлива",
type=['zip'],
key="fuel_upload"
)
if uploaded_fuel is not None:
if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"):
with st.spinner("Загружаю файл..."):
result, status = upload_file_to_api(
"/monitoring_fuel/upload-zip",
uploaded_fuel.read(),
uploaded_fuel.name
)
if status == 200:
st.success(f"{result.get('message', 'Файл загружен')}")
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
st.markdown("---")
# Секция получения данных
st.subheader("🔍 Получение данных")
col1, col2 = st.columns(2)
with col1:
st.subheader("Агрегация по колонкам")
columns_fuel = st.multiselect(
"Выберите столбцы",
FUEL_COLUMNS,
default=["normativ", "total"],
key="fuel_columns"
)
if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"):
if columns_fuel:
with st.spinner("Получаю данные..."):
data = {
"columns": columns_fuel
}
result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите столбцы")
with col2:
st.subheader("Данные за месяц")
month = st.selectbox(
"Выберите месяц",
[f"{i:02d}" for i in range(1, 13)],
key="fuel_month"
)
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
with st.spinner("Получаю данные..."):
data = {
"month": month
}
result, status = make_api_request("/monitoring_fuel/get_month_by_code", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")

View File

@@ -0,0 +1,108 @@
"""
UI модуль для мониторинга ТЭР
"""
import streamlit as st
import pandas as pd
import json
from api_client import upload_file_to_api, make_api_request
from config import TAR_MODES
def render_monitoring_tar_tab():
"""Рендер вкладки мониторинга ТЭР"""
st.header("⚡ Мониторинг ТЭР (Топливно-энергетических ресурсов)")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_file = st.file_uploader(
"Выберите ZIP архив с файлами мониторинга ТЭР",
type=['zip'],
key="monitoring_tar_upload"
)
if uploaded_file is not None:
if st.button("📤 Загрузить файл", key="monitoring_tar_upload_btn"):
with st.spinner("Загружаем файл..."):
file_data = uploaded_file.read()
result, status_code = upload_file_to_api("/monitoring_tar/upload", file_data, uploaded_file.name)
if status_code == 200:
st.success("✅ Файл успешно загружен!")
st.json(result)
else:
st.error(f"❌ Ошибка загрузки: {result}")
# Секция получения данных
st.subheader("📊 Получение данных")
# Выбор формата отображения
display_format = st.radio(
"Формат отображения:",
["JSON", "Таблица"],
key="monitoring_tar_display_format",
horizontal=True
)
# Выбор режима данных
mode = st.selectbox(
"Выберите режим данных:",
TAR_MODES,
help="total - строки 'Всего' (агрегированные данные), last_day - последние строки данных, all - все данные",
key="monitoring_tar_mode"
)
if st.button("📊 Получить данные", key="monitoring_tar_get_data_btn"):
with st.spinner("Получаем данные..."):
# Выбираем эндпоинт в зависимости от режима
if mode == "all":
# Используем полный эндпоинт
result, status_code = make_api_request("/monitoring_tar/get_full_data", {})
else:
# Используем фильтрованный эндпоинт
request_data = {"mode": mode}
result, status_code = make_api_request("/monitoring_tar/get_data", request_data)
if status_code == 200 and result.get("success"):
st.success("✅ Данные успешно получены!")
# Показываем данные
data = result.get("data", {}).get("value", {})
if data:
st.subheader("📋 Результат:")
# Парсим данные, если они пришли как строка
if isinstance(data, str):
try:
data = json.loads(data)
st.write("✅ JSON успешно распарсен")
except json.JSONDecodeError as e:
st.error(f"❌ Ошибка при парсинге JSON данных: {e}")
st.write("Сырые данные:", data)
return
if display_format == "JSON":
# Отображаем как JSON
st.json(data)
else:
# Отображаем как таблицы
if isinstance(data, dict):
# Показываем данные по установкам
for installation_id, installation_data in data.items():
with st.expander(f"🏭 {installation_id}"):
if isinstance(installation_data, dict):
# Показываем структуру данных
for data_type, type_data in installation_data.items():
st.write(f"**{data_type}:**")
if isinstance(type_data, list) and type_data:
df = pd.DataFrame(type_data)
st.dataframe(df)
else:
st.write("Нет данных")
else:
st.write("Нет данных")
else:
st.json(data)
else:
st.info("📋 Нет данных для отображения")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")

View File

@@ -0,0 +1,84 @@
"""
UI модуль для операционных справок технологических позиций
"""
import streamlit as st
import pandas as pd
from api_client import upload_file_to_api, make_api_request, get_available_ogs
def render_oper_spravka_tech_pos_tab():
"""Рендер вкладки операционных справок технологических позиций"""
st.header("🏭 Операционные справки технологических позиций")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_file = st.file_uploader(
"Выберите ZIP архив с файлами операционных справок",
type=['zip'],
key="oper_spravka_tech_pos_upload"
)
if uploaded_file is not None:
if st.button("📤 Загрузить файл", key="oper_spravka_tech_pos_upload_btn"):
with st.spinner("Загружаем файл..."):
file_data = uploaded_file.read()
result, status_code = upload_file_to_api("/oper_spravka_tech_pos/upload", file_data, uploaded_file.name)
if status_code == 200:
st.success("✅ Файл успешно загружен!")
st.json(result)
else:
st.error(f"❌ Ошибка загрузки: {result}")
st.markdown("---")
# Секция получения данных
st.subheader("📊 Получение данных")
# Выбор формата отображения
display_format = st.radio(
"Формат отображения:",
["JSON", "Таблица"],
key="oper_spravka_tech_pos_display_format",
horizontal=True
)
# Получаем доступные ОГ динамически
available_ogs = get_available_ogs("oper_spravka_tech_pos")
# Выбор ОГ
og_id = st.selectbox(
"Выберите ОГ:",
available_ogs if available_ogs else ["SNPZ", "KNPZ", "ANHK", "BASH", "UNH", "NOV"],
key="oper_spravka_tech_pos_og_id"
)
if st.button("📊 Получить данные", key="oper_spravka_tech_pos_get_data_btn"):
with st.spinner("Получаем данные..."):
request_data = {"id": og_id}
result, status_code = make_api_request("/oper_spravka_tech_pos/get_data", request_data)
if status_code == 200 and result.get("success"):
st.success("✅ Данные успешно получены!")
# Показываем данные
data = result.get("data", [])
if data and len(data) > 0:
st.subheader("📋 Результат:")
if display_format == "JSON":
# Отображаем как JSON
st.json(data)
else:
# Отображаем как таблицу
if isinstance(data, list) and data:
df = pd.DataFrame(data)
st.dataframe(df, use_container_width=True)
else:
st.write("Нет данных")
else:
st.info("📋 Нет данных для отображения")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")

View File

@@ -0,0 +1,146 @@
"""
UI модуль для статусов ремонта СА
"""
import streamlit as st
import pandas as pd
from api_client import upload_file_to_api, make_api_request, get_available_ogs
def render_statuses_repair_ca_tab():
"""Рендер вкладки статусов ремонта СА"""
st.header("📋 Статусы ремонта СА")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_file = st.file_uploader(
"Выберите файл статусов ремонта СА",
type=['xlsx', 'xlsm', 'xls', 'zip'],
key="statuses_repair_ca_upload"
)
if uploaded_file is not None:
if st.button("📤 Загрузить файл", key="statuses_repair_ca_upload_btn"):
with st.spinner("Загружаем файл..."):
file_data = uploaded_file.read()
result, status_code = upload_file_to_api("/statuses_repair_ca/upload", file_data, uploaded_file.name)
if status_code == 200:
st.success("✅ Файл успешно загружен!")
st.json(result)
else:
st.error(f"❌ Ошибка загрузки: {result}")
# Секция получения данных
st.subheader("📊 Получение данных")
# Получаем доступные ОГ динамически
available_ogs = get_available_ogs("statuses_repair_ca")
# Фильтр по ОГ
og_ids = st.multiselect(
"Выберите ОГ (оставьте пустым для всех)",
available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback
key="statuses_repair_ca_og_ids"
)
# Предустановленные ключи для извлечения
st.subheader("🔑 Ключи для извлечения данных")
# Основные ключи
include_basic_keys = st.checkbox("Основные данные", value=True, key="statuses_basic_keys")
include_readiness_keys = st.checkbox("Готовность к КР", value=True, key="statuses_readiness_keys")
include_contract_keys = st.checkbox("Заключение договоров", value=True, key="statuses_contract_keys")
include_supply_keys = st.checkbox("Поставка МТР", value=True, key="statuses_supply_keys")
# Формируем ключи на основе выбора
keys = []
if include_basic_keys:
keys.append(["Дата начала ремонта"])
keys.append(["Отставание / опережение подготовки к КР", "Отставание / опережение"])
keys.append(["Отставание / опережение подготовки к КР", "Динамика за прошедшую неделю"])
if include_readiness_keys:
keys.append(["Готовность к КР", "Факт"])
if include_contract_keys:
keys.append(["Заключение договоров на СМР", "Договор", "%"])
if include_supply_keys:
keys.append(["Поставка МТР", "На складе, позиций", "%"])
# Кнопка получения данных
if st.button("📊 Получить данные", key="statuses_repair_ca_get_data_btn"):
if not keys:
st.warning("⚠️ Выберите хотя бы одну группу ключей для извлечения")
else:
with st.spinner("Получаем данные..."):
request_data = {
"ids": og_ids if og_ids else None,
"keys": keys
}
result, status_code = make_api_request("/statuses_repair_ca/get_data", request_data)
if status_code == 200 and result.get("success"):
st.success("✅ Данные успешно получены!")
data = result.get("data", {}).get("value", [])
if data:
# Отображаем данные в виде таблицы
if isinstance(data, list) and len(data) > 0:
# Преобразуем в DataFrame для лучшего отображения
df_data = []
for item in data:
row = {
"ID": item.get("id", ""),
"Название": item.get("name", ""),
}
# Добавляем основные поля
if "Дата начала ремонта" in item:
row["Дата начала ремонта"] = item["Дата начала ремонта"]
# Добавляем готовность к КР
if "Готовность к КР" in item:
readiness = item["Готовность к КР"]
if isinstance(readiness, dict) and "Факт" in readiness:
row["Готовность к КР (Факт)"] = readiness["Факт"]
# Добавляем отставание/опережение
if "Отставание / опережение подготовки к КР" in item:
delay = item["Отставание / опережение подготовки к КР"]
if isinstance(delay, dict):
if "Отставание / опережение" in delay:
row["Отставание/опережение"] = delay["Отставание / опережение"]
if "Динамика за прошедшую неделю" in delay:
row["Динамика за неделю"] = delay["Динамика за прошедшую неделю"]
# Добавляем договоры
if "Заключение договоров на СМР" in item:
contracts = item["Заключение договоров на СМР"]
if isinstance(contracts, dict) and "Договор" in contracts:
contract = contracts["Договор"]
if isinstance(contract, dict) and "%" in contract:
row["Договоры (%)"] = contract["%"]
# Добавляем поставки МТР
if "Поставка МТР" in item:
supply = item["Поставка МТР"]
if isinstance(supply, dict) and "На складе, позиций" in supply:
warehouse = supply["На складе, позиций"]
if isinstance(warehouse, dict) and "%" in warehouse:
row["МТР на складе (%)"] = warehouse["%"]
df_data.append(row)
if df_data:
df = pd.DataFrame(df_data)
st.dataframe(df, use_container_width=True)
else:
st.info("📋 Нет данных для отображения")
else:
st.json(result)
else:
st.info("📋 Нет данных для отображения")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")

View File

@@ -0,0 +1,80 @@
"""
UI модуль для парсера сводок СА
"""
import streamlit as st
import requests
from api_client import make_api_request, API_BASE_URL
from config import CA_MODES, CA_TABLES
def render_svodka_ca_tab():
"""Рендер вкладки сводок СА"""
st.header("🏭 Сводки СА - Полный функционал")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_ca = st.file_uploader(
"Выберите Excel файл сводки СА",
type=['xlsx', 'xlsm', 'xls'],
key="ca_upload"
)
if uploaded_ca is not None:
if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"):
with st.spinner("Загружаю файл..."):
try:
files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files)
result = response.json()
if response.status_code == 200:
st.success(f"{result.get('message', 'Файл загружен')}")
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
except Exception as e:
st.error(f"❌ Ошибка: {str(e)}")
st.markdown("---")
# Секция получения данных
st.subheader("🔍 Получение данных")
col1, col2 = st.columns(2)
with col1:
st.subheader("Параметры запроса")
modes = st.multiselect(
"Выберите режимы",
CA_MODES,
default=["plan", "fact"],
key="ca_modes"
)
tables = st.multiselect(
"Выберите таблицы",
CA_TABLES,
default=["ТиП", "Топливо"],
key="ca_tables"
)
with col2:
st.subheader("Результат")
if st.button("🔍 Получить данные СА", key="ca_btn"):
if modes and tables:
with st.spinner("Получаю данные..."):
data = {
"modes": modes,
"tables": tables
}
result, status = make_api_request("/svodka_ca/get_data", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите режимы и таблицы")

View File

@@ -0,0 +1,118 @@
"""
UI модуль для парсера сводок ПМ
"""
import streamlit as st
from api_client import upload_file_to_api, make_api_request
from config import PM_CODES, PM_COLUMNS, DEFAULT_OGS
def render_svodka_pm_tab():
"""Рендер вкладки сводок ПМ"""
st.header("📊 Сводки ПМ - Полный функционал")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_pm = st.file_uploader(
"Выберите ZIP архив со сводками ПМ",
type=['zip'],
key="pm_upload"
)
if uploaded_pm is not None:
if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"):
with st.spinner("Загружаю файл..."):
result, status = upload_file_to_api(
"/svodka_pm/upload-zip",
uploaded_pm.read(),
uploaded_pm.name
)
if status == 200:
st.success(f"{result.get('message', 'Файл загружен')}")
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
st.markdown("---")
# Секция получения данных
st.subheader("🔍 Получение данных")
col1, col2 = st.columns(2)
with col1:
st.subheader("Данные по одному ОГ")
og_id = st.selectbox(
"Выберите ОГ",
DEFAULT_OGS,
key="pm_single_og"
)
codes = st.multiselect(
"Выберите коды строк",
PM_CODES,
default=[78, 79],
key="pm_single_codes"
)
columns = st.multiselect(
"Выберите столбцы",
PM_COLUMNS,
default=["БП", "ПП"],
key="pm_single_columns"
)
if st.button("🔍 Получить данные по ОГ", key="pm_single_btn"):
if codes and columns:
with st.spinner("Получаю данные..."):
data = {
"id": og_id,
"codes": codes,
"columns": columns
}
result, status = make_api_request("/svodka_pm/get_single_og", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите коды и столбцы")
with col2:
st.subheader("Данные по всем ОГ")
codes_total = st.multiselect(
"Выберите коды строк",
PM_CODES,
default=[78, 79, 394, 395],
key="pm_total_codes"
)
columns_total = st.multiselect(
"Выберите столбцы",
PM_COLUMNS,
default=["БП", "ПП", "СЭБ"],
key="pm_total_columns"
)
if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"):
if codes_total and columns_total:
with st.spinner("Получаю данные..."):
data = {
"codes": codes_total,
"columns": columns_total
}
result, status = make_api_request("/svodka_pm/get_total_ogs", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите коды и столбцы")

View File

@@ -0,0 +1,110 @@
"""
UI модуль для ремонта СА
"""
import streamlit as st
import pandas as pd
from api_client import upload_file_to_api, make_api_request, get_available_ogs
from config import REPAIR_TYPES
def render_svodka_repair_ca_tab():
"""Рендер вкладки ремонта СА"""
st.header("🔧 Ремонт СА - Управление ремонтными работами")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_file = st.file_uploader(
"Выберите Excel файл или ZIP архив с данными о ремонте СА",
type=['xlsx', 'xlsm', 'xls', 'zip'],
key="repair_ca_upload"
)
if uploaded_file is not None:
if st.button("📤 Загрузить файл", key="repair_ca_upload_btn"):
with st.spinner("Загружаю файл..."):
file_data = uploaded_file.read()
result, status = upload_file_to_api("/svodka_repair_ca/upload", file_data, uploaded_file.name)
if status == 200:
st.success("✅ Файл успешно загружен")
st.json(result)
else:
st.error(f"❌ Ошибка загрузки: {result.get('message', 'Неизвестная ошибка')}")
st.markdown("---")
# Секция получения данных
st.subheader("🔍 Получение данных")
col1, col2 = st.columns(2)
with col1:
st.subheader("Фильтры")
# Получаем доступные ОГ динамически
available_ogs = get_available_ogs("svodka_repair_ca")
# Фильтр по ОГ
og_ids = st.multiselect(
"Выберите ОГ (оставьте пустым для всех)",
available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback
key="repair_ca_og_ids"
)
# Фильтр по типам ремонта
repair_types = st.multiselect(
"Выберите типы ремонта (оставьте пустым для всех)",
REPAIR_TYPES,
key="repair_ca_types"
)
# Включение плановых/фактических данных
include_planned = st.checkbox("Включать плановые данные", value=True, key="repair_ca_planned")
include_factual = st.checkbox("Включать фактические данные", value=True, key="repair_ca_factual")
with col2:
st.subheader("Действия")
if st.button("🔍 Получить данные о ремонте", key="repair_ca_get_btn"):
with st.spinner("Получаю данные..."):
data = {
"include_planned": include_planned,
"include_factual": include_factual
}
# Добавляем фильтры только если они выбраны
if og_ids:
data["og_ids"] = og_ids
if repair_types:
data["repair_types"] = repair_types
result, status = make_api_request("/svodka_repair_ca/get_data", data)
if status == 200:
st.success("✅ Данные получены")
# Отображаем данные в виде таблицы, если возможно
if result.get("data") and isinstance(result["data"], list):
df_data = []
for item in result["data"]:
df_data.append({
"ID ОГ": item.get("id", ""),
"Наименование": item.get("name", ""),
"Тип ремонта": item.get("type", ""),
"Дата начала": item.get("start_date", ""),
"Дата окончания": item.get("end_date", ""),
"План": item.get("plan", ""),
"Факт": item.get("fact", ""),
"Простой": item.get("downtime", "")
})
if df_data:
df = pd.DataFrame(df_data)
st.dataframe(df, use_container_width=True)
else:
st.info("📋 Нет данных для отображения")
else:
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")

76
streamlit_app/sidebar.py Normal file
View File

@@ -0,0 +1,76 @@
"""
Модуль для сайдбара
"""
import streamlit as st
from api_client import get_server_info, get_available_parsers
from config import API_PUBLIC_URL
def render_sidebar():
"""Рендер боковой панели"""
with st.sidebar:
st.header(" Информация1")
# Информация о сервере
server_info = get_server_info()
if server_info:
st.subheader("Сервер")
st.write(f"PID: {server_info.get('process_id', 'N/A')}")
st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}")
st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB")
# Доступные парсеры
parsers = get_available_parsers()
if parsers:
st.subheader("Доступные парсеры")
for parser in parsers:
st.write(f"{parser}")
# Навигация по страницам
st.markdown("---")
st.subheader("🧭 Навигация")
# Определяем активную страницу
active_page = st.session_state.get("active_page", 0)
# Кнопка для страницы синхронных парсеров
if st.button("📊 Синхронные парсеры", key="sidebar_sync_btn", use_container_width=True, type="primary" if active_page == 0 else "secondary"):
st.session_state.sidebar_sync_clicked = True
st.rerun()
# Кнопка для страницы асинхронной загрузки
if st.button("🚀 Асинхронная загрузка", key="sidebar_async_btn", use_container_width=True, type="primary" if active_page == 1 else "secondary"):
st.session_state.sidebar_async_clicked = True
st.rerun()
# Кнопка для страницы управления задачами
if st.button("📋 Управление задачами", key="sidebar_tasks_btn", use_container_width=True, type="primary" if active_page == 2 else "secondary"):
st.session_state.sidebar_tasks_clicked = True
st.rerun()
def render_footer():
"""Рендер футера"""
st.markdown("---")
st.markdown("### 📚 Документация API")
st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs")
# Информация о проекте
with st.expander(" О проекте"):
st.markdown("""
**NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов.
**Возможности:**
- 📊 Парсинг сводок ПМ (план и факт)
- 🏭 Парсинг сводок СА
- ⛽ Мониторинг топлива
- ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы)
- 🔧 Управление ремонтными работами СА
- 📋 Мониторинг статусов ремонта СА
**Технологии:**
- FastAPI
- Pandas
- MinIO (S3-совместимое хранилище)
- Streamlit (веб-интерфейс)
""")

View File

@@ -1,847 +1,61 @@
import streamlit as st import streamlit as st
import requests from config import setup_page_config, API_PUBLIC_URL
import json from api_client import check_api_health
import pandas as pd from sidebar import render_sidebar, render_footer
import io from sync_parsers_page import render_sync_parsers_page
import zipfile from async_upload_page import render_async_upload_page
from typing import Dict, Any, List from tasks_page import render_tasks_page
import os
# Конфигурация страницы # Конфигурация страницы
st.set_page_config( setup_page_config()
page_title="NIN Excel Parsers API Demo",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded"
)
# Конфигурация API
API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker
API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя
def check_api_health():
"""Проверка доступности API"""
try:
response = requests.get(f"{API_BASE_URL}/", timeout=5)
return response.status_code == 200
except:
return False
def get_available_parsers():
"""Получение списка доступных парсеров"""
try:
response = requests.get(f"{API_BASE_URL}/parsers")
if response.status_code == 200:
return response.json()["parsers"]
return []
except:
return []
def get_server_info():
"""Получение информации о сервере"""
try:
response = requests.get(f"{API_BASE_URL}/server-info")
if response.status_code == 200:
return response.json()
return {}
except:
return {}
def upload_file_to_api(endpoint: str, file_data: bytes, filename: str):
"""Загрузка файла на API"""
try:
# Определяем правильное имя поля в зависимости от эндпоинта
if "zip" in endpoint:
files = {"zip_file": (filename, file_data, "application/zip")}
else:
files = {"file": (filename, file_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
response = requests.post(f"{API_BASE_URL}{endpoint}", files=files)
return response.json(), response.status_code
except Exception as e:
return {"error": str(e)}, 500
def make_api_request(endpoint: str, data: Dict[str, Any]):
"""Выполнение API запроса"""
try:
response = requests.post(f"{API_BASE_URL}{endpoint}", json=data)
return response.json(), response.status_code
except Exception as e:
return {"error": str(e)}, 500
def get_available_ogs(parser_name: str) -> List[str]:
"""Получение доступных ОГ для парсера"""
try:
response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/available_ogs")
if response.status_code == 200:
data = response.json()
return data.get("available_ogs", [])
else:
print(f"⚠️ Ошибка получения ОГ: {response.status_code}")
return []
except Exception as e:
print(f"⚠️ Ошибка при запросе ОГ: {e}")
return []
def main(): def main():
st.title("🚀 NIN Excel Parsers API - Демонстрация") # Определяем активную страницу для заголовка
active_page = st.session_state.get("active_page", 0)
page_titles = {
0: "Синхронные парсеры",
1: "Асинхронная загрузка",
2: "Управление задачами"
}
st.title(f"🚀 NIN Excel Parsers API - {page_titles.get(active_page, 'Демонстрация')}")
st.markdown("---") st.markdown("---")
# Проверка доступности API # Проверка доступности API
if not check_api_health(): if not check_api_health():
st.error(f"❌ API недоступен по адресу {API_BASE_URL}") st.error(f"❌ API недоступен по адресу {API_PUBLIC_URL}")
st.info("Убедитесь, что FastAPI сервер запущен") st.info("Убедитесь, что FastAPI сервер запущен")
return return
st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}") st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}")
# Боковая панель с информацией # Обрабатываем клики по кнопкам в сайдбаре ПЕРЕД рендером
with st.sidebar: if st.session_state.get("sidebar_sync_clicked", False):
st.header(" Информация") st.session_state.sidebar_sync_clicked = False
st.session_state.active_page = 0
elif st.session_state.get("sidebar_async_clicked", False):
st.session_state.sidebar_async_clicked = False
st.session_state.active_page = 1
elif st.session_state.get("sidebar_tasks_clicked", False):
st.session_state.sidebar_tasks_clicked = False
st.session_state.active_page = 2
# Информация о сервере # Определяем активную страницу
server_info = get_server_info() active_page = st.session_state.get("active_page", 0)
if server_info:
st.subheader("Сервер")
st.write(f"PID: {server_info.get('process_id', 'N/A')}")
st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}")
st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB")
# Доступные парсеры # Боковая панель с информацией и навигацией
parsers = get_available_parsers() render_sidebar()
if parsers:
st.subheader("Доступные парсеры")
for parser in parsers:
st.write(f"{parser}")
# Основные вкладки - по одной на каждый парсер # Рендерим соответствующую страницу
tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs([ if active_page == 0:
"📊 Сводки ПМ", render_sync_parsers_page()
"🏭 Сводки СА", elif active_page == 1:
"⛽ Мониторинг топлива", render_async_upload_page()
"🔧 Ремонт СА",
"📋 Статусы ремонта СА",
"⚡ Мониторинг ТЭР",
"🏭 Операционные справки"
])
# Вкладка 1: Сводки ПМ - полный функционал
with tab1:
st.header("📊 Сводки ПМ - Полный функционал")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_pm = st.file_uploader(
"Выберите ZIP архив со сводками ПМ",
type=['zip'],
key="pm_upload"
)
if uploaded_pm is not None:
if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"):
with st.spinner("Загружаю файл..."):
result, status = upload_file_to_api(
"/svodka_pm/upload-zip",
uploaded_pm.read(),
uploaded_pm.name
)
if status == 200:
st.success(f"{result.get('message', 'Файл загружен')}")
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
else: else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") render_tasks_page()
st.markdown("---")
# Секция получения данных
st.subheader("🔍 Получение данных")
col1, col2 = st.columns(2)
with col1:
st.subheader("Данные по одному ОГ")
og_id = st.selectbox(
"Выберите ОГ",
["SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV",
"NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK",
"NVNPO", "KLNPZ", "PurNP", "YANOS"],
key="pm_single_og"
)
codes = st.multiselect(
"Выберите коды строк",
[78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
default=[78, 79],
key="pm_single_codes"
)
columns = st.multiselect(
"Выберите столбцы",
["БП", "ПП", "СЭБ", "Факт", "План"],
default=["БП", "ПП"],
key="pm_single_columns"
)
if st.button("🔍 Получить данные по ОГ", key="pm_single_btn"):
if codes and columns:
with st.spinner("Получаю данные..."):
data = {
"id": og_id,
"codes": codes,
"columns": columns
}
result, status = make_api_request("/svodka_pm/get_single_og", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите коды и столбцы")
with col2:
st.subheader("Данные по всем ОГ")
codes_total = st.multiselect(
"Выберите коды строк",
[78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
default=[78, 79, 394, 395],
key="pm_total_codes"
)
columns_total = st.multiselect(
"Выберите столбцы",
["БП", "ПП", "СЭБ", "Факт", "План"],
default=["БП", "ПП", "СЭБ"],
key="pm_total_columns"
)
if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"):
if codes_total and columns_total:
with st.spinner("Получаю данные..."):
data = {
"codes": codes_total,
"columns": columns_total
}
result, status = make_api_request("/svodka_pm/get_total_ogs", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите коды и столбцы")
# Вкладка 2: Сводки СА - полный функционал
with tab2:
st.header("🏭 Сводки СА - Полный функционал")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_ca = st.file_uploader(
"Выберите Excel файл сводки СА",
type=['xlsx', 'xlsm', 'xls'],
key="ca_upload"
)
if uploaded_ca is not None:
if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"):
with st.spinner("Загружаю файл..."):
try:
files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files)
result = response.json()
if response.status_code == 200:
st.success(f"{result.get('message', 'Файл загружен')}")
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
except Exception as e:
st.error(f"❌ Ошибка: {str(e)}")
st.markdown("---")
# Секция получения данных
st.subheader("🔍 Получение данных")
col1, col2 = st.columns(2)
with col1:
st.subheader("Параметры запроса")
modes = st.multiselect(
"Выберите режимы",
["plan", "fact", "normativ"],
default=["plan", "fact"],
key="ca_modes"
)
tables = st.multiselect(
"Выберите таблицы",
["ТиП", "Топливо", "Потери"],
default=["ТиП", "Топливо"],
key="ca_tables"
)
with col2:
st.subheader("Результат")
if st.button("🔍 Получить данные СА", key="ca_btn"):
if modes and tables:
with st.spinner("Получаю данные..."):
data = {
"modes": modes,
"tables": tables
}
result, status = make_api_request("/svodka_ca/get_data", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите режимы и таблицы")
# Вкладка 3: Мониторинг топлива - полный функционал
with tab3:
st.header("⛽ Мониторинг топлива - Полный функционал")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_fuel = st.file_uploader(
"Выберите ZIP архив с мониторингом топлива",
type=['zip'],
key="fuel_upload"
)
if uploaded_fuel is not None:
if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"):
with st.spinner("Загружаю файл..."):
result, status = upload_file_to_api(
"/monitoring_fuel/upload-zip",
uploaded_fuel.read(),
uploaded_fuel.name
)
if status == 200:
st.success(f"{result.get('message', 'Файл загружен')}")
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
st.markdown("---")
# Секция получения данных
st.subheader("🔍 Получение данных")
col1, col2 = st.columns(2)
with col1:
st.subheader("Агрегация по колонкам")
columns_fuel = st.multiselect(
"Выберите столбцы",
["normativ", "total", "total_1"],
default=["normativ", "total"],
key="fuel_columns"
)
if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"):
if columns_fuel:
with st.spinner("Получаю данные..."):
data = {
"columns": columns_fuel
}
result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите столбцы")
with col2:
st.subheader("Данные за месяц")
month = st.selectbox(
"Выберите месяц",
[f"{i:02d}" for i in range(1, 13)],
key="fuel_month"
)
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
with st.spinner("Получаю данные..."):
data = {
"month": month
}
result, status = make_api_request("/monitoring_fuel/get_month_by_code", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
# Вкладка 4: Ремонт СА
with tab4:
st.header("🔧 Ремонт СА - Управление ремонтными работами")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_file = st.file_uploader(
"Выберите Excel файл или ZIP архив с данными о ремонте СА",
type=['xlsx', 'xlsm', 'xls', 'zip'],
key="repair_ca_upload"
)
if uploaded_file is not None:
if st.button("📤 Загрузить файл", key="repair_ca_upload_btn"):
with st.spinner("Загружаю файл..."):
file_data = uploaded_file.read()
result, status = upload_file_to_api("/svodka_repair_ca/upload", file_data, uploaded_file.name)
if status == 200:
st.success("✅ Файл успешно загружен")
st.json(result)
else:
st.error(f"❌ Ошибка загрузки: {result.get('message', 'Неизвестная ошибка')}")
st.markdown("---")
# Секция получения данных
st.subheader("🔍 Получение данных")
col1, col2 = st.columns(2)
with col1:
st.subheader("Фильтры")
# Получаем доступные ОГ динамически
available_ogs = get_available_ogs("svodka_repair_ca")
# Фильтр по ОГ
og_ids = st.multiselect(
"Выберите ОГ (оставьте пустым для всех)",
available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback
key="repair_ca_og_ids"
)
# Фильтр по типам ремонта
repair_types = st.multiselect(
"Выберите типы ремонта (оставьте пустым для всех)",
["КР", "КП", "ТР"],
key="repair_ca_types"
)
# Включение плановых/фактических данных
include_planned = st.checkbox("Включать плановые данные", value=True, key="repair_ca_planned")
include_factual = st.checkbox("Включать фактические данные", value=True, key="repair_ca_factual")
with col2:
st.subheader("Действия")
if st.button("🔍 Получить данные о ремонте", key="repair_ca_get_btn"):
with st.spinner("Получаю данные..."):
data = {
"include_planned": include_planned,
"include_factual": include_factual
}
# Добавляем фильтры только если они выбраны
if og_ids:
data["og_ids"] = og_ids
if repair_types:
data["repair_types"] = repair_types
result, status = make_api_request("/svodka_repair_ca/get_data", data)
if status == 200:
st.success("✅ Данные получены")
# Отображаем данные в виде таблицы, если возможно
if result.get("data") and isinstance(result["data"], list):
df_data = []
for item in result["data"]:
df_data.append({
"ID ОГ": item.get("id", ""),
"Наименование": item.get("name", ""),
"Тип ремонта": item.get("type", ""),
"Дата начала": item.get("start_date", ""),
"Дата окончания": item.get("end_date", ""),
"План": item.get("plan", ""),
"Факт": item.get("fact", ""),
"Простой": item.get("downtime", "")
})
if df_data:
df = pd.DataFrame(df_data)
st.dataframe(df, use_container_width=True)
else:
st.info("📋 Нет данных для отображения")
else:
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
# Вкладка 5: Статусы ремонта СА
with tab5:
st.header("📋 Статусы ремонта СА")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_file = st.file_uploader(
"Выберите файл статусов ремонта СА",
type=['xlsx', 'xlsm', 'xls', 'zip'],
key="statuses_repair_ca_upload"
)
if uploaded_file is not None:
if st.button("📤 Загрузить файл", key="statuses_repair_ca_upload_btn"):
with st.spinner("Загружаем файл..."):
file_data = uploaded_file.read()
result, status_code = upload_file_to_api("/statuses_repair_ca/upload", file_data, uploaded_file.name)
if status_code == 200:
st.success("✅ Файл успешно загружен!")
st.json(result)
else:
st.error(f"❌ Ошибка загрузки: {result}")
# Секция получения данных
st.subheader("📊 Получение данных")
# Получаем доступные ОГ динамически
available_ogs = get_available_ogs("statuses_repair_ca")
# Фильтр по ОГ
og_ids = st.multiselect(
"Выберите ОГ (оставьте пустым для всех)",
available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback
key="statuses_repair_ca_og_ids"
)
# Предустановленные ключи для извлечения
st.subheader("🔑 Ключи для извлечения данных")
# Основные ключи
include_basic_keys = st.checkbox("Основные данные", value=True, key="statuses_basic_keys")
include_readiness_keys = st.checkbox("Готовность к КР", value=True, key="statuses_readiness_keys")
include_contract_keys = st.checkbox("Заключение договоров", value=True, key="statuses_contract_keys")
include_supply_keys = st.checkbox("Поставка МТР", value=True, key="statuses_supply_keys")
# Формируем ключи на основе выбора
keys = []
if include_basic_keys:
keys.append(["Дата начала ремонта"])
keys.append(["Отставание / опережение подготовки к КР", "Отставание / опережение"])
keys.append(["Отставание / опережение подготовки к КР", "Динамика за прошедшую неделю"])
if include_readiness_keys:
keys.append(["Готовность к КР", "Факт"])
if include_contract_keys:
keys.append(["Заключение договоров на СМР", "Договор", "%"])
if include_supply_keys:
keys.append(["Поставка МТР", "На складе, позиций", "%"])
# Кнопка получения данных
if st.button("📊 Получить данные", key="statuses_repair_ca_get_data_btn"):
if not keys:
st.warning("⚠️ Выберите хотя бы одну группу ключей для извлечения")
else:
with st.spinner("Получаем данные..."):
request_data = {
"ids": og_ids if og_ids else None,
"keys": keys
}
result, status_code = make_api_request("/statuses_repair_ca/get_data", request_data)
if status_code == 200 and result.get("success"):
st.success("✅ Данные успешно получены!")
data = result.get("data", {}).get("value", [])
if data:
# Отображаем данные в виде таблицы
if isinstance(data, list) and len(data) > 0:
# Преобразуем в DataFrame для лучшего отображения
df_data = []
for item in data:
row = {
"ID": item.get("id", ""),
"Название": item.get("name", ""),
}
# Добавляем основные поля
if "Дата начала ремонта" in item:
row["Дата начала ремонта"] = item["Дата начала ремонта"]
# Добавляем готовность к КР
if "Готовность к КР" in item:
readiness = item["Готовность к КР"]
if isinstance(readiness, dict) and "Факт" in readiness:
row["Готовность к КР (Факт)"] = readiness["Факт"]
# Добавляем отставание/опережение
if "Отставание / опережение подготовки к КР" in item:
delay = item["Отставание / опережение подготовки к КР"]
if isinstance(delay, dict):
if "Отставание / опережение" in delay:
row["Отставание/опережение"] = delay["Отставание / опережение"]
if "Динамика за прошедшую неделю" in delay:
row["Динамика за неделю"] = delay["Динамика за прошедшую неделю"]
# Добавляем договоры
if "Заключение договоров на СМР" in item:
contracts = item["Заключение договоров на СМР"]
if isinstance(contracts, dict) and "Договор" in contracts:
contract = contracts["Договор"]
if isinstance(contract, dict) and "%" in contract:
row["Договоры (%)"] = contract["%"]
# Добавляем поставки МТР
if "Поставка МТР" in item:
supply = item["Поставка МТР"]
if isinstance(supply, dict) and "На складе, позиций" in supply:
warehouse = supply["На складе, позиций"]
if isinstance(warehouse, dict) and "%" in warehouse:
row["МТР на складе (%)"] = warehouse["%"]
df_data.append(row)
if df_data:
df = pd.DataFrame(df_data)
st.dataframe(df, use_container_width=True)
else:
st.info("📋 Нет данных для отображения")
else:
st.json(result)
else:
st.info("📋 Нет данных для отображения")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
# Вкладка 6: Мониторинг ТЭР
with tab6:
st.header("⚡ Мониторинг ТЭР (Топливно-энергетических ресурсов)")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_file = st.file_uploader(
"Выберите ZIP архив с файлами мониторинга ТЭР",
type=['zip'],
key="monitoring_tar_upload"
)
if uploaded_file is not None:
if st.button("📤 Загрузить файл", key="monitoring_tar_upload_btn"):
with st.spinner("Загружаем файл..."):
file_data = uploaded_file.read()
result, status_code = upload_file_to_api("/monitoring_tar/upload", file_data, uploaded_file.name)
if status_code == 200:
st.success("✅ Файл успешно загружен!")
st.json(result)
else:
st.error(f"❌ Ошибка загрузки: {result}")
# Секция получения данных
st.subheader("📊 Получение данных")
# Выбор формата отображения
display_format = st.radio(
"Формат отображения:",
["JSON", "Таблица"],
key="monitoring_tar_display_format",
horizontal=True
)
# Выбор режима данных
mode = st.selectbox(
"Выберите режим данных:",
["all", "total", "last_day"],
help="total - строки 'Всего' (агрегированные данные), last_day - последние строки данных, all - все данные",
key="monitoring_tar_mode"
)
if st.button("📊 Получить данные", key="monitoring_tar_get_data_btn"):
with st.spinner("Получаем данные..."):
# Выбираем эндпоинт в зависимости от режима
if mode == "all":
# Используем полный эндпоинт
result, status_code = make_api_request("/monitoring_tar/get_full_data", {})
else:
# Используем фильтрованный эндпоинт
request_data = {"mode": mode}
result, status_code = make_api_request("/monitoring_tar/get_data", request_data)
if status_code == 200 and result.get("success"):
st.success("✅ Данные успешно получены!")
# Показываем данные
data = result.get("data", {}).get("value", {})
if data:
st.subheader("📋 Результат:")
# # Отладочная информация
# st.write(f"🔍 Тип данных: {type(data)}")
# if isinstance(data, str):
# st.write(f"🔍 Длина строки: {len(data)}")
# st.write(f"🔍 Первые 200 символов: {data[:200]}...")
# Парсим данные, если они пришли как строка
if isinstance(data, str):
try:
import json
data = json.loads(data)
st.write("✅ JSON успешно распарсен")
except json.JSONDecodeError as e:
st.error(f"❌ Ошибка при парсинге JSON данных: {e}")
st.write("Сырые данные:", data)
return
if display_format == "JSON":
# Отображаем как JSON
st.json(data)
else:
# Отображаем как таблицы
if isinstance(data, dict):
# Показываем данные по установкам
for installation_id, installation_data in data.items():
with st.expander(f"🏭 {installation_id}"):
if isinstance(installation_data, dict):
# Показываем структуру данных
for data_type, type_data in installation_data.items():
st.write(f"**{data_type}:**")
if isinstance(type_data, list) and type_data:
df = pd.DataFrame(type_data)
st.dataframe(df)
else:
st.write("Нет данных")
else:
st.write("Нет данных")
else:
st.json(data)
else:
st.info("📋 Нет данных для отображения")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
# Вкладка 7: Операционные справки технологических позиций
with tab7:
st.header("🏭 Операционные справки технологических позиций")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_file = st.file_uploader(
"Выберите ZIP архив с файлами операционных справок",
type=['zip'],
key="oper_spravka_tech_pos_upload"
)
if uploaded_file is not None:
if st.button("📤 Загрузить файл", key="oper_spravka_tech_pos_upload_btn"):
with st.spinner("Загружаем файл..."):
file_data = uploaded_file.read()
result, status_code = upload_file_to_api("/oper_spravka_tech_pos/upload", file_data, uploaded_file.name)
if status_code == 200:
st.success("✅ Файл успешно загружен!")
st.json(result)
else:
st.error(f"❌ Ошибка загрузки: {result}")
st.markdown("---")
# Секция получения данных
st.subheader("📊 Получение данных")
# Выбор формата отображения
display_format = st.radio(
"Формат отображения:",
["JSON", "Таблица"],
key="oper_spravka_tech_pos_display_format",
horizontal=True
)
# Получаем доступные ОГ динамически
available_ogs = get_available_ogs("oper_spravka_tech_pos")
# Выбор ОГ
og_id = st.selectbox(
"Выберите ОГ:",
available_ogs if available_ogs else ["SNPZ", "KNPZ", "ANHK", "BASH", "UNH", "NOV"],
key="oper_spravka_tech_pos_og_id"
)
if st.button("📊 Получить данные", key="oper_spravka_tech_pos_get_data_btn"):
with st.spinner("Получаем данные..."):
request_data = {"id": og_id}
result, status_code = make_api_request("/oper_spravka_tech_pos/get_data", request_data)
if status_code == 200 and result.get("success"):
st.success("✅ Данные успешно получены!")
# Показываем данные
data = result.get("data", [])
if data and len(data) > 0:
st.subheader("📋 Результат:")
if display_format == "JSON":
# Отображаем как JSON
st.json(data)
else:
# Отображаем как таблицу
if isinstance(data, list) and data:
df = pd.DataFrame(data)
st.dataframe(df, use_container_width=True)
else:
st.write("Нет данных")
else:
st.info("📋 Нет данных для отображения")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
# Футер # Футер
st.markdown("---") render_footer()
st.markdown("### 📚 Документация API")
st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs")
# Информация о проекте
with st.expander(" О проекте"):
st.markdown("""
**NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов.
**Возможности:**
- 📊 Парсинг сводок ПМ (план и факт)
- 🏭 Парсинг сводок СА
- ⛽ Мониторинг топлива
- ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы)
- 🔧 Управление ремонтными работами СА
- 📋 Мониторинг статусов ремонта СА
**Технологии:**
- FastAPI
- Pandas
- MinIO (S3-совместимое хранилище)
- Streamlit (веб-интерфейс)
""")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -0,0 +1,54 @@
"""
Страница синхронных парсеров
"""
import streamlit as st
from parsers_ui.svodka_pm_ui import render_svodka_pm_tab
from parsers_ui.svodka_ca_ui import render_svodka_ca_tab
from parsers_ui.monitoring_fuel_ui import render_monitoring_fuel_tab
from parsers_ui.svodka_repair_ca_ui import render_svodka_repair_ca_tab
from parsers_ui.statuses_repair_ca_ui import render_statuses_repair_ca_tab
from parsers_ui.monitoring_tar_ui import render_monitoring_tar_tab
from parsers_ui.oper_spravka_tech_pos_ui import render_oper_spravka_tech_pos_tab
from config import PARSER_TABS
def render_sync_parsers_page():
"""Рендер страницы синхронных парсеров"""
st.title("📊 Синхронные парсеры")
st.markdown("---")
st.info("""
**Синхронные парсеры** обрабатывают файлы сразу после загрузки.
Интерфейс будет заблокирован до завершения обработки.
""")
# Основные вкладки - по одной на каждый парсер
tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs(PARSER_TABS)
# Вкладка 1: Сводки ПМ - полный функционал
with tab1:
render_svodka_pm_tab()
# Вкладка 2: Сводки СА - полный функционал
with tab2:
render_svodka_ca_tab()
# Вкладка 3: Мониторинг топлива - полный функционал
with tab3:
render_monitoring_fuel_tab()
# Вкладка 4: Ремонт СА
with tab4:
render_svodka_repair_ca_tab()
# Вкладка 5: Статусы ремонта СА
with tab5:
render_statuses_repair_ca_tab()
# Вкладка 6: Мониторинг ТЭР
with tab6:
render_monitoring_tar_tab()
# Вкладка 7: Операционные справки технологических позиций
with tab7:
render_oper_spravka_tech_pos_tab()

186
streamlit_app/tasks_page.py Normal file
View File

@@ -0,0 +1,186 @@
"""
Страница управления задачами загрузки
"""
import streamlit as st
from datetime import datetime
import time
from async_upload_page import TASKS_STORAGE
def render_tasks_page():
"""Рендер страницы управления задачами"""
st.title("📋 Управление задачами загрузки")
st.markdown("---")
# Кнопки управления
col1, col2, col3, col4 = st.columns([1, 1, 1, 2])
with col1:
if st.button("🔄 Обновить", key="refresh_tasks_btn", use_container_width=True):
st.rerun()
with col2:
if st.button("🗑️ Очистить завершенные", key="clear_completed_btn", use_container_width=True):
# Удаляем завершенные и неудачные задачи
tasks_to_remove = []
for task_id, task in TASKS_STORAGE.items():
if task.get('status') in ['completed', 'failed']:
tasks_to_remove.append(task_id)
for task_id in tasks_to_remove:
del TASKS_STORAGE[task_id]
st.success(f"✅ Удалено {len(tasks_to_remove)} завершенных задач")
st.rerun()
with col3:
auto_refresh = st.checkbox("🔄 Автообновление", key="auto_refresh_checkbox")
if auto_refresh:
time.sleep(2)
st.rerun()
with col4:
st.caption("Последнее обновление: " + datetime.now().strftime("%H:%M:%S"))
st.markdown("---")
# Статистика задач
st.subheader("📊 Статистика задач")
# Получаем задачи из глобального хранилища
tasks = TASKS_STORAGE
# Подсчитываем статистику
total_tasks = len(tasks)
pending_tasks = len([t for t in tasks.values() if t.get('status') == 'pending'])
running_tasks = len([t for t in tasks.values() if t.get('status') == 'running'])
completed_tasks = len([t for t in tasks.values() if t.get('status') == 'completed'])
failed_tasks = len([t for t in tasks.values() if t.get('status') == 'failed'])
col1, col2, col3, col4, col5 = st.columns(5)
with col1:
st.metric("Всего", total_tasks, f"+{total_tasks}")
with col2:
st.metric("Ожидают", pending_tasks, f"+{pending_tasks}")
with col3:
st.metric("Выполняются", running_tasks, f"+{running_tasks}")
with col4:
st.metric("Завершены", completed_tasks, f"+{completed_tasks}")
with col5:
st.metric("Ошибки", failed_tasks, f"+{failed_tasks}")
st.markdown("---")
# Список задач
st.subheader("📋 Список задач")
# Получаем задачи из глобального хранилища
tasks = TASKS_STORAGE
if tasks:
# Показываем задачи
for task_id, task in tasks.items():
status_emoji = {
'pending': '🟡',
'running': '🔵',
'completed': '🟢',
'failed': '🔴'
}.get(task.get('status', 'pending'), '')
with st.expander(f"{status_emoji} {task.get('filename', 'Unknown')} - {task.get('status', 'unknown').upper()}", expanded=True):
col1, col2 = st.columns([3, 1])
with col1:
st.write(f"**ID:** `{task_id}`")
st.write(f"**Статус:** {status_emoji} {task.get('status', 'unknown').upper()}")
st.write(f"**Файл:** {task.get('filename', 'Unknown')}")
st.write(f"**Эндпоинт:** {task.get('endpoint', 'Unknown')}")
# Показываем прогресс для выполняющихся задач
if task.get('status') == 'running':
progress = task.get('progress', 0)
st.write(f"**Прогресс:** {progress}%")
st.progress(progress / 100)
# Показываем время выполнения
if task.get('started_at'):
started_time = datetime.fromtimestamp(task['started_at']).strftime("%Y-%m-%d %H:%M:%S")
st.write(f"**Начата:** {started_time}")
if task.get('completed_at'):
completed_time = datetime.fromtimestamp(task['completed_at']).strftime("%Y-%m-%d %H:%M:%S")
st.write(f"**Завершена:** {completed_time}")
# Показываем длительность
if task.get('started_at'):
duration = task['completed_at'] - task['started_at']
st.write(f"**Длительность:** {duration:.1f} сек")
if task.get('result'):
result = task['result']
if task.get('status') == 'completed':
st.success(f"{result.get('message', 'Задача выполнена')}")
if result.get('object_id'):
st.info(f"ID объекта: {result['object_id']}")
else:
st.error(f"{result.get('message', 'Ошибка выполнения')}")
if task.get('error'):
st.error(f"❌ Ошибка: {task['error']}")
with col2:
if task.get('status') in ['pending', 'running']:
if st.button("❌ Отменить", key=f"cancel_{task_id}_btn", use_container_width=True):
st.info("Функция отмены будет реализована в следующих версиях")
else:
if st.button("🗑️ Удалить", key=f"delete_{task_id}_btn", use_container_width=True):
# Удаляем задачу из глобального хранилища
if task_id in TASKS_STORAGE:
del TASKS_STORAGE[task_id]
st.rerun()
else:
# Пустое состояние
st.info("""
**Нет активных задач**
Загрузите файл на странице "Асинхронная загрузка", чтобы создать новую задачу.
Здесь вы сможете отслеживать прогресс обработки и управлять задачами.
""")
# Кнопка для создания тестовой задачи
if st.button("🧪 Создать тестовую задачу", key="create_test_task_btn"):
test_task_id = f"test_task_{int(time.time())}"
TASKS_STORAGE[test_task_id] = {
'status': 'completed',
'filename': 'test_file.zip',
'endpoint': '/test/upload',
'result': {'message': 'Тестовая задача выполнена', 'object_id': 'test-123'},
'started_at': time.time() - 5, # 5 секунд назад
'completed_at': time.time(),
'progress': 100
}
st.rerun()
st.markdown("---")
# Информация о статусах задач
with st.expander(" Статусы задач"):
st.markdown("""
**Статусы задач:**
- 🟡 **Ожидает** - задача создана и ожидает выполнения
- 🔵 **Выполняется** - задача обрабатывается
- 🟢 **Завершена** - задача успешно выполнена
- 🔴 **Ошибка** - произошла ошибка при выполнении
- ⚫ **Отменена** - задача была отменена пользователем
**Действия:**
- ❌ **Отменить** - отменить выполнение задачи
- 🔄 **Обновить** - обновить статус задачи
- 📊 **Детали** - просмотреть подробную информацию
""")

44
tests/README.md Normal file
View File

@@ -0,0 +1,44 @@
# Тесты для парсеров
Этот каталог содержит pytest тесты для всех парсеров и их геттеров.
## Структура
- est_parsers.py - Основные тесты для всех парсеров
- conftest.py - Конфигурация pytest
-
equirements.txt - Зависимости для тестов
- est_data/ - Тестовые данные
## Запуск тестов
`ash
# Установка зависимостей
pip install -r tests/requirements.txt
# Запуск всех тестов
pytest tests/
# Запуск конкретного теста
pytest tests/test_parsers.py::TestSvodkaPMParser
# Запуск с подробным выводом
pytest tests/ -v
# Запуск с покрытием кода
pytest tests/ --cov=python_parser
`
## Покрытие тестами
Тесты покрывают:
- Инициализацию всех парсеров
- Все геттеры каждого парсера
- Обработку валидных и невалидных параметров
- Интеграционные тесты
## Добавление новых тестов
При добавлении нового парсера:
1. Добавьте класс тестов в est_parsers.py
2. Создайте тесты для всех геттеров

28
tests/conftest.py Normal file
View File

@@ -0,0 +1,28 @@
"""
Конфигурация pytest для тестов парсеров
"""
import pytest
import sys
import os
# Добавляем путь к проекту
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python_parser'))
@pytest.fixture(scope="session")
def test_data_dir():
"""Путь к директории с тестовыми данными"""
return os.path.join(os.path.dirname(__file__), 'test_data')
@pytest.fixture
def mock_data():
"""Моковые данные для тестов"""
return {
'SNPZ': {
'data': 'test_data',
'records_count': 10
},
'KNPZ': {
'data': 'test_data_2',
'records_count': 5
}
}

4
tests/requirements.txt Normal file
View File

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

Binary file not shown.

Binary file not shown.

BIN
tests/test_data/pm_all.zip Normal file

Binary file not shown.

Binary file not shown.

394
tests/test_parsers.py Normal file
View File

@@ -0,0 +1,394 @@
"""
Тесты для всех парсеров и их геттеров
"""
import pytest
import pandas as pd
import tempfile
import os
from unittest.mock import Mock, patch
import sys
# Добавляем путь к проекту
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python_parser'))
from adapters.parsers import (
SvodkaPMParser,
SvodkaCAParser,
MonitoringFuelParser,
MonitoringTarParser,
SvodkaRepairCAParser,
StatusesRepairCAParser,
OperSpravkaTechPosParser
)
class TestSvodkaPMParser:
"""Тесты для парсера Сводки ПМ"""
def setup_method(self):
"""Настройка перед каждым тестом"""
self.parser = SvodkaPMParser()
# Создаем тестовые данные
self.test_data = {
'SNPZ': pd.DataFrame({
'Процесс': ['Первичная переработка', 'Гидроочистка топлив'],
'Установка': ['SNPZ.EAVT6', 'SNPZ.L24-6'],
'План, т': [100.0, 200.0],
'Факт, т': [95.0, 190.0]
})
}
self.parser.data_dict = self.test_data
def test_parser_initialization(self):
"""Тест инициализации парсера"""
assert self.parser.name == "Сводки ПМ"
assert hasattr(self.parser, 'getters')
assert len(self.parser.getters) == 2
assert 'single_og' in self.parser.getters
assert 'total_ogs' in self.parser.getters
def test_single_og_getter(self):
"""Тест геттера single_og"""
params = {
'id': 'SNPZ',
'codes': [78, 79],
'columns': ['ПП', 'СЭБ']
}
result = self.parser.get_value('single_og', params)
assert result is not None
assert isinstance(result, str) # Возвращает JSON строку
def test_total_ogs_getter(self):
"""Тест геттера total_ogs"""
params = {
'codes': [78, 79],
'columns': ['ПП', 'СЭБ']
}
result = self.parser.get_value('total_ogs', params)
assert result is not None
assert isinstance(result, str) # Возвращает JSON строку
def test_getter_with_invalid_params(self):
"""Тест геттера с неверными параметрами"""
with pytest.raises(ValueError):
self.parser.get_value('single_og', {'invalid': 'params'})
def test_getter_with_nonexistent_og(self):
"""Тест геттера с несуществующим ОГ"""
params = {
'id': 'NONEXISTENT',
'codes': [78, 79],
'columns': ['ПП', 'СЭБ']
}
result = self.parser.get_value('single_og', params)
# Должен вернуть пустой результат, но не упасть
assert result is not None
class TestSvodkaCAParser:
"""Тесты для парсера Сводки СА"""
def setup_method(self):
"""Настройка перед каждым тестом"""
self.parser = SvodkaCAParser()
# Создаем тестовые данные
self.test_data = {
'plan': {
'ТиП': pd.DataFrame({
'ОГ': ['SNPZ', 'KNPZ'],
'Значение': [100.0, 200.0]
})
},
'fact': {
'ТиП': pd.DataFrame({
'ОГ': ['SNPZ', 'KNPZ'],
'Значение': [95.0, 190.0]
})
}
}
self.parser.data_dict = self.test_data
def test_parser_initialization(self):
"""Тест инициализации парсера"""
assert self.parser.name == "Сводки СА"
assert hasattr(self.parser, 'getters')
assert len(self.parser.getters) == 1
assert 'get_ca_data' in self.parser.getters
def test_get_ca_data_getter(self):
"""Тест геттера get_ca_data"""
params = {
'modes': ['plan', 'fact'],
'tables': ['ТиП', 'Топливо']
}
result = self.parser.get_value('get_ca_data', params)
assert result is not None
assert isinstance(result, dict) # Возвращает словарь
class TestMonitoringFuelParser:
"""Тесты для парсера Мониторинга топлива"""
def setup_method(self):
"""Настройка перед каждым тестом"""
self.parser = MonitoringFuelParser()
# Создаем тестовые данные
self.test_data = {
'SNPZ': pd.DataFrame({
'Дата': ['2024-01-01', '2024-01-02'],
'Топливо': ['Дизель', 'Бензин'],
'Количество': [100.0, 200.0],
'Объем': [50.0, 75.0] # Добавляем числовую колонку для агрегации
})
}
self.parser.data_dict = self.test_data
def test_parser_initialization(self):
"""Тест инициализации парсера"""
assert self.parser.name == "Мониторинг топлива"
assert hasattr(self.parser, 'getters')
# Проверяем, что есть геттеры
assert len(self.parser.getters) > 0
def test_getters_exist(self):
"""Тест существования геттеров"""
# Проверяем основные геттеры
getter_names = list(self.parser.getters.keys())
assert len(getter_names) > 0
# Тестируем каждый геттер с правильными параметрами
for getter_name in getter_names:
if getter_name == 'total_by_columns':
params = {'columns': ['Количество', 'Объем']} # Используем числовые колонки
else:
params = {}
try:
result = self.parser.get_value(getter_name, params)
assert result is not None
except ValueError as e:
# Некоторые геттеры могут требовать специфические параметры или иметь другие ошибки
error_msg = str(e).lower()
assert any(keyword in error_msg for keyword in ["required", "missing", "отсутствуют", "обязательные", "ошибка выполнения"])
class TestMonitoringTarParser:
"""Тесты для парсера Мониторинга ТЭР"""
def setup_method(self):
"""Настройка перед каждым тестом"""
self.parser = MonitoringTarParser()
# Создаем тестовые данные
self.test_data = {
'total': [pd.DataFrame({
'Дата': ['2024-01-01'],
'Потребление': [100.0]
})],
'last_day': [pd.DataFrame({
'Дата': ['2024-01-02'],
'Потребление': [150.0]
})]
}
self.parser.data_dict = self.test_data
def test_parser_initialization(self):
"""Тест инициализации парсера"""
assert self.parser.name == "monitoring_tar"
assert hasattr(self.parser, 'getters')
assert len(self.parser.getters) == 2
assert 'get_tar_data' in self.parser.getters
assert 'get_tar_full_data' in self.parser.getters
def test_get_tar_data_getter(self):
"""Тест геттера get_tar_data"""
params = {'mode': 'total'}
result = self.parser.get_value('get_tar_data', params)
assert result is not None
assert isinstance(result, str) # Возвращает JSON строку
def test_get_tar_full_data_getter(self):
"""Тест геттера get_tar_full_data"""
result = self.parser.get_value('get_tar_full_data', {})
assert result is not None
assert isinstance(result, str) # Возвращает JSON строку
class TestSvodkaRepairCAParser:
"""Тесты для парсера Сводки ремонта СА"""
def setup_method(self):
"""Настройка перед каждым тестом"""
self.parser = SvodkaRepairCAParser()
# Создаем тестовые данные в правильном формате
self.test_data = [
{
'id': 'SNPZ',
'Тип_ремонта': 'Капитальный',
'Статус': 'Завершен',
'Дата': '2024-01-01'
},
{
'id': 'SNPZ',
'Тип_ремонта': 'Текущий',
'Статус': 'В работе',
'Дата': '2024-01-02'
}
]
self.parser.data_dict = self.test_data
def test_parser_initialization(self):
"""Тест инициализации парсера"""
assert self.parser.name == "Сводки ремонта СА"
assert hasattr(self.parser, 'getters')
assert len(self.parser.getters) == 1
assert 'get_repair_data' in self.parser.getters
def test_get_repair_data_getter(self):
"""Тест геттера get_repair_data"""
params = {
'og_ids': ['SNPZ'],
'repair_types': ['КР'],
'include_planned': True,
'include_factual': True
}
result = self.parser.get_value('get_repair_data', params)
assert result is not None
assert isinstance(result, dict) # Возвращает словарь
class TestStatusesRepairCAParser:
"""Тесты для парсера Статусов ремонта СА"""
def setup_method(self):
"""Настройка перед каждым тестом"""
self.parser = StatusesRepairCAParser()
# Создаем тестовые данные
self.test_data = {
'SNPZ': pd.DataFrame({
'Статус': ['В работе', 'Завершен'],
'Процент': [50.0, 100.0]
})
}
self.parser.data_dict = self.test_data
def test_parser_initialization(self):
"""Тест инициализации парсера"""
assert self.parser.name == "Статусы ремонта СА"
assert hasattr(self.parser, 'getters')
assert len(self.parser.getters) == 1
assert 'get_repair_statuses' in self.parser.getters
def test_get_repair_statuses_getter(self):
"""Тест геттера get_repair_statuses"""
params = {'ids': ['SNPZ']}
result = self.parser.get_value('get_repair_statuses', params)
assert result is not None
assert isinstance(result, list) # Возвращает список
class TestOperSpravkaTechPosParser:
"""Тесты для парсера Операционных справок технологических позиций"""
def setup_method(self):
"""Настройка перед каждым тестом"""
self.parser = OperSpravkaTechPosParser()
# Создаем тестовые данные
self.test_data = {
'SNPZ': pd.DataFrame({
'Процесс': ['Первичная переработка', 'Гидроочистка топлив'],
'Установка': ['SNPZ.EAVT6', 'SNPZ.L24-6'],
'План, т': [100.0, 200.0],
'Факт, т': [95.0, 190.0],
'id': ['SNPZ.EAVT6', 'SNPZ.L24-6']
})
}
self.parser.data_dict = self.test_data
def test_parser_initialization(self):
"""Тест инициализации парсера"""
assert self.parser.name == "oper_spravka_tech_pos"
assert hasattr(self.parser, 'getters')
assert len(self.parser.getters) == 1
assert 'get_tech_pos' in self.parser.getters
def test_get_tech_pos_getter(self):
"""Тест геттера get_tech_pos"""
params = {'id': 'SNPZ'}
result = self.parser.get_value('get_tech_pos', params)
assert result is not None
assert isinstance(result, list) # Возвращает список словарей
def test_get_tech_pos_with_nonexistent_og(self):
"""Тест геттера с несуществующим ОГ"""
params = {'id': 'NONEXISTENT'}
result = self.parser.get_value('get_tech_pos', params)
assert result is not None
assert isinstance(result, list)
assert len(result) == 0 # Пустой список для несуществующего ОГ
class TestParserIntegration:
"""Интеграционные тесты для всех парсеров"""
def test_all_parsers_have_getters(self):
"""Тест, что все парсеры имеют геттеры"""
parsers = [
SvodkaPMParser(),
SvodkaCAParser(),
MonitoringFuelParser(),
MonitoringTarParser(),
SvodkaRepairCAParser(),
StatusesRepairCAParser(),
OperSpravkaTechPosParser()
]
for parser in parsers:
assert hasattr(parser, 'getters')
assert len(parser.getters) > 0
assert hasattr(parser, 'name')
assert parser.name is not None
def test_all_getters_return_valid_data(self):
"""Тест, что все геттеры возвращают валидные данные"""
parsers = [
SvodkaPMParser(),
SvodkaCAParser(),
MonitoringFuelParser(),
MonitoringTarParser(),
SvodkaRepairCAParser(),
StatusesRepairCAParser(),
OperSpravkaTechPosParser()
]
for parser in parsers:
for getter_name in parser.getters.keys():
try:
result = parser.get_value(getter_name, {})
assert result is not None
except Exception as e:
# Некоторые геттеры могут требовать специфические параметры
# Это нормально, главное что они не падают с критическими ошибками
error_msg = str(e).lower()
assert any(keyword in error_msg for keyword in ["required", "missing", "отсутствуют", "обязательные"])
if __name__ == "__main__":
pytest.main([__file__])