Полная реализация прарсера svodka_repair_ca
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
from .monitoring_fuel import MonitoringFuelParser
|
||||
from .svodka_ca import SvodkaCAParser
|
||||
from .svodka_pm import SvodkaPMParser
|
||||
from .svodka_repair_ca import SvodkaRepairCAParser
|
||||
|
||||
__all__ = [
|
||||
'MonitoringFuelParser',
|
||||
'SvodkaCAParser',
|
||||
'SvodkaPMParser'
|
||||
'SvodkaPMParser',
|
||||
'SvodkaRepairCAParser'
|
||||
]
|
||||
|
||||
377
python_parser/adapters/parsers/svodka_repair_ca.py
Normal file
377
python_parser/adapters/parsers/svodka_repair_ca.py
Normal file
@@ -0,0 +1,377 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import zipfile
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
from core.ports import ParserPort
|
||||
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
||||
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
|
||||
from adapters.pconfig import SINGLE_OGS, find_header_row, get_og_by_name
|
||||
|
||||
|
||||
class SvodkaRepairCAParser(ParserPort):
|
||||
"""Парсер для сводок ремонта СА"""
|
||||
|
||||
name = "Сводки ремонта СА"
|
||||
|
||||
def _register_default_getters(self):
|
||||
"""Регистрация геттеров по умолчанию"""
|
||||
register_getter_from_schema(
|
||||
parser_instance=self,
|
||||
getter_name="get_repair_data",
|
||||
method=self._get_repair_data_wrapper,
|
||||
schema_class=SvodkaRepairCARequest,
|
||||
description="Получение данных о ремонтных работах"
|
||||
)
|
||||
|
||||
def _get_repair_data_wrapper(self, params: dict):
|
||||
"""Получение данных о ремонтных работах"""
|
||||
print(f"🔍 DEBUG: _get_repair_data_wrapper вызван с параметрами: {params}")
|
||||
|
||||
# Валидируем параметры с помощью схемы Pydantic
|
||||
validated_params = validate_params_with_schema(params, SvodkaRepairCARequest)
|
||||
|
||||
og_ids = validated_params.get("og_ids")
|
||||
repair_types = validated_params.get("repair_types")
|
||||
include_planned = validated_params.get("include_planned", True)
|
||||
include_factual = validated_params.get("include_factual", True)
|
||||
|
||||
print(f"🔍 DEBUG: Запрошенные ОГ: {og_ids}")
|
||||
print(f"🔍 DEBUG: Запрошенные типы ремонта: {repair_types}")
|
||||
print(f"🔍 DEBUG: Включать плановые: {include_planned}, фактические: {include_factual}")
|
||||
|
||||
# Проверяем, есть ли данные в data_dict (из парсинга) или в df (из загрузки)
|
||||
if hasattr(self, 'data_dict') and self.data_dict is not None:
|
||||
# Данные из парсинга
|
||||
data_source = self.data_dict
|
||||
print(f"🔍 DEBUG: Используем data_dict с {len(data_source)} записями")
|
||||
elif hasattr(self, 'df') and self.df is not None and not self.df.empty:
|
||||
# Данные из загрузки - преобразуем DataFrame обратно в словарь
|
||||
data_source = self._df_to_data_dict()
|
||||
print(f"🔍 DEBUG: Используем df, преобразованный в data_dict с {len(data_source)} записями")
|
||||
else:
|
||||
print(f"🔍 DEBUG: Нет данных! data_dict={getattr(self, 'data_dict', 'None')}, df={getattr(self, 'df', 'None')}")
|
||||
return []
|
||||
|
||||
# Группируем данные по ОГ (как в оригинале)
|
||||
grouped_data = {}
|
||||
|
||||
for item in data_source:
|
||||
og_id = item.get('id')
|
||||
if not og_id:
|
||||
continue
|
||||
|
||||
# Проверяем фильтры
|
||||
if og_ids is not None and og_id not in og_ids:
|
||||
continue
|
||||
if repair_types is not None and item.get('type') not in repair_types:
|
||||
continue
|
||||
|
||||
# Фильтрация по плановым/фактическим данным
|
||||
filtered_item = item.copy()
|
||||
if not include_planned:
|
||||
filtered_item.pop('plan', None)
|
||||
if not include_factual:
|
||||
filtered_item.pop('fact', None)
|
||||
|
||||
# Убираем поле 'id' из записи, так как оно уже в ключе
|
||||
filtered_item.pop('id', None)
|
||||
|
||||
# Добавляем в группу по ОГ
|
||||
if og_id not in grouped_data:
|
||||
grouped_data[og_id] = []
|
||||
grouped_data[og_id].append(filtered_item)
|
||||
|
||||
total_records = sum(len(v) for v in grouped_data.values())
|
||||
print(f"🔍 DEBUG: Отфильтровано {total_records} записей из {len(data_source)}")
|
||||
print(f"🔍 DEBUG: Группировано по {len(grouped_data)} ОГ: {list(grouped_data.keys())}")
|
||||
return grouped_data
|
||||
|
||||
def _df_to_data_dict(self):
|
||||
"""Преобразование DataFrame обратно в словарь данных"""
|
||||
if not hasattr(self, 'df') or self.df is None or self.df.empty:
|
||||
return []
|
||||
|
||||
# Если df содержит данные в формате списка записей
|
||||
if 'data' in self.df.columns:
|
||||
# Извлекаем данные из колонки 'data'
|
||||
all_data = []
|
||||
for _, row in self.df.iterrows():
|
||||
data = row.get('data')
|
||||
if data and isinstance(data, list):
|
||||
all_data.extend(data)
|
||||
return all_data
|
||||
|
||||
return []
|
||||
|
||||
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||
"""Парсинг файла и возврат DataFrame"""
|
||||
print(f"🔍 DEBUG: SvodkaRepairCAParser.parse вызван с файлом: {file_path}")
|
||||
|
||||
# Определяем, это ZIP архив или одиночный файл
|
||||
if file_path.lower().endswith('.zip'):
|
||||
# Обрабатываем ZIP архив
|
||||
self.data_dict = self._parse_zip_archive(file_path, params)
|
||||
else:
|
||||
# Обрабатываем одиночный файл
|
||||
self.data_dict = self._parse_single_file(file_path, params)
|
||||
|
||||
# Преобразуем словарь в DataFrame для совместимости с services.py
|
||||
if self.data_dict:
|
||||
# Создаем DataFrame с информацией о загруженных данных
|
||||
data_rows = []
|
||||
for i, item in enumerate(self.data_dict):
|
||||
data_rows.append({
|
||||
'index': i,
|
||||
'data': [item], # Обертываем в список для совместимости
|
||||
'records_count': 1
|
||||
})
|
||||
|
||||
if data_rows:
|
||||
df = pd.DataFrame(data_rows)
|
||||
self.df = df
|
||||
print(f"🔍 DEBUG: Создан DataFrame с {len(data_rows)} записями")
|
||||
return df
|
||||
|
||||
# Если данных нет, возвращаем пустой DataFrame
|
||||
self.df = pd.DataFrame()
|
||||
print(f"🔍 DEBUG: Возвращаем пустой DataFrame")
|
||||
return self.df
|
||||
|
||||
def _parse_zip_archive(self, file_path: str, params: dict) -> List[Dict]:
|
||||
"""Парсинг ZIP архива с файлами ремонта СА"""
|
||||
print(f"🔍 DEBUG: Парсинг ZIP архива: {file_path}")
|
||||
|
||||
all_data = []
|
||||
temp_dir = None
|
||||
|
||||
try:
|
||||
# Создаем временную директорию
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
print(f"📦 Архив разархивирован в: {temp_dir}")
|
||||
|
||||
# Разархивируем файл
|
||||
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(temp_dir)
|
||||
|
||||
# Ищем Excel файлы в архиве
|
||||
excel_files = []
|
||||
for root, dirs, files in os.walk(temp_dir):
|
||||
for file in files:
|
||||
if file.lower().endswith(('.xlsx', '.xlsm', '.xls')):
|
||||
excel_files.append(os.path.join(root, file))
|
||||
|
||||
print(f"📊 Найдено Excel файлов: {len(excel_files)}")
|
||||
|
||||
# Обрабатываем каждый найденный файл
|
||||
for excel_file in excel_files:
|
||||
print(f"📊 Обработка файла: {excel_file}")
|
||||
file_data = self._parse_single_file(excel_file, params)
|
||||
if file_data:
|
||||
all_data.extend(file_data)
|
||||
|
||||
print(f"🎯 Всего обработано записей: {len(all_data)}")
|
||||
return all_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при обработке ZIP архива: {e}")
|
||||
return []
|
||||
finally:
|
||||
# Удаляем временную директорию
|
||||
if temp_dir:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
print(f"🗑️ Временная директория удалена: {temp_dir}")
|
||||
|
||||
def _parse_single_file(self, file_path: str, params: dict) -> List[Dict]:
|
||||
"""Парсинг одиночного Excel файла"""
|
||||
print(f"🔍 DEBUG: Парсинг файла: {file_path}")
|
||||
|
||||
try:
|
||||
# Получаем параметры
|
||||
sheet_name = params.get('sheet_name', 0) # По умолчанию первый лист
|
||||
header_num = params.get('header_num', None)
|
||||
|
||||
# Автоопределение header_num, если не передан
|
||||
if header_num is None:
|
||||
header_num = find_header_row(file_path, sheet_name, search_value="ОГ")
|
||||
if header_num is None:
|
||||
print(f"❌ Не найден заголовок в файле {file_path}")
|
||||
return []
|
||||
|
||||
print(f"🔍 DEBUG: Заголовок найден в строке {header_num}")
|
||||
|
||||
# Читаем Excel файл
|
||||
df = pd.read_excel(
|
||||
file_path,
|
||||
sheet_name=sheet_name,
|
||||
header=header_num,
|
||||
usecols=None,
|
||||
index_col=None
|
||||
)
|
||||
|
||||
if df.empty:
|
||||
print(f"❌ Файл {file_path} пуст")
|
||||
return []
|
||||
|
||||
if "ОГ" not in df.columns:
|
||||
print(f"⚠️ Предупреждение: Колонка 'ОГ' не найдена в файле {file_path}")
|
||||
return []
|
||||
|
||||
# Обрабатываем данные
|
||||
return self._process_repair_data(df)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при парсинге файла {file_path}: {e}")
|
||||
return []
|
||||
|
||||
def _process_repair_data(self, df: pd.DataFrame) -> List[Dict]:
|
||||
"""Обработка данных о ремонте"""
|
||||
print(f"🔍 DEBUG: Обработка данных с {len(df)} строками")
|
||||
|
||||
# Шаг 1: Нормализация ОГ
|
||||
def safe_replace(val):
|
||||
if pd.notna(val) and isinstance(val, str) and val.strip():
|
||||
cleaned_val = val.strip()
|
||||
result = get_og_by_name(cleaned_val)
|
||||
if result and pd.notna(result) and result != "" and result != "UNKNOWN":
|
||||
return result
|
||||
return val
|
||||
|
||||
df["ОГ"] = df["ОГ"].apply(safe_replace)
|
||||
|
||||
# Шаг 2: Приведение к NA и forward fill
|
||||
og_series = df["ОГ"].map(
|
||||
lambda x: pd.NA if (isinstance(x, str) and x.strip() == "") or pd.isna(x) else x
|
||||
)
|
||||
df["ОГ"] = og_series.ffill()
|
||||
|
||||
# Шаг 3: Фильтрация по валидным ОГ
|
||||
valid_og_values = set(SINGLE_OGS)
|
||||
mask_og = df["ОГ"].notna() & df["ОГ"].isin(valid_og_values)
|
||||
df = df[mask_og].copy()
|
||||
|
||||
if df.empty:
|
||||
print(f"❌ Нет данных после фильтрации по ОГ")
|
||||
return []
|
||||
|
||||
# Шаг 4: Удаление строк без "Вид простоя"
|
||||
if "Вид простоя" in df.columns:
|
||||
downtime_clean = df["Вид простоя"].astype(str).str.strip()
|
||||
mask_downtime = (downtime_clean != "") & (downtime_clean != "nan")
|
||||
df = df[mask_downtime].copy()
|
||||
else:
|
||||
print("⚠️ Предупреждение: Колонка 'Вид простоя' не найдена.")
|
||||
return []
|
||||
|
||||
# Шаг 5: Удаление ненужных колонок
|
||||
cols_to_drop = []
|
||||
for col in df.columns:
|
||||
if col.strip().lower() in ["п/п", "пп", "п.п.", "№"]:
|
||||
cols_to_drop.append(col)
|
||||
elif "НАЛИЧИЕ ПОДРЯДЧИКА" in col.upper() and "ОСНОВНЫЕ РАБОТЫ" in col.upper():
|
||||
cols_to_drop.append(col)
|
||||
|
||||
df.drop(columns=list(set(cols_to_drop)), inplace=True, errors='ignore')
|
||||
|
||||
# Шаг 6: Переименование первых 8 колонок по порядку
|
||||
if df.shape[1] < 8:
|
||||
print(f"⚠️ Внимание: В DataFrame только {df.shape[1]} колонок, требуется минимум 8.")
|
||||
return []
|
||||
|
||||
new_names = ["id", "name", "type", "start_date", "end_date", "plan", "fact", "downtime"]
|
||||
|
||||
# Сохраняем оставшиеся колонки (если больше 8)
|
||||
remaining_cols = df.columns[8:].tolist() # Все, что после 8-й
|
||||
renamed_cols = new_names + remaining_cols
|
||||
df.columns = renamed_cols
|
||||
|
||||
# меняем прочерки на null
|
||||
df = df.replace("-", None)
|
||||
|
||||
# Сброс индекса
|
||||
df.reset_index(drop=True, inplace=True)
|
||||
|
||||
# Шаг 7: Преобразование в список словарей
|
||||
result_data = []
|
||||
|
||||
for _, row in df.iterrows():
|
||||
try:
|
||||
# Извлекаем основные поля (теперь с правильными именами)
|
||||
og_id = row.get('id')
|
||||
name = row.get('name', '')
|
||||
repair_type = row.get('type', '')
|
||||
|
||||
# Обрабатываем даты
|
||||
start_date = self._parse_date(row.get('start_date'))
|
||||
end_date = self._parse_date(row.get('end_date'))
|
||||
|
||||
# Обрабатываем числовые значения
|
||||
plan = self._parse_numeric(row.get('plan'))
|
||||
fact = self._parse_numeric(row.get('fact'))
|
||||
downtime = self._parse_downtime(row.get('downtime'))
|
||||
|
||||
# Создаем запись
|
||||
record = {
|
||||
"id": og_id,
|
||||
"name": str(name) if pd.notna(name) else "",
|
||||
"type": str(repair_type) if pd.notna(repair_type) else "",
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"plan": plan,
|
||||
"fact": fact,
|
||||
"downtime": downtime
|
||||
}
|
||||
|
||||
result_data.append(record)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Ошибка при обработке строки: {e}")
|
||||
continue
|
||||
|
||||
print(f"✅ Обработано {len(result_data)} записей")
|
||||
return result_data
|
||||
|
||||
def _parse_date(self, value) -> Optional[str]:
|
||||
"""Парсинг даты"""
|
||||
if pd.isna(value) or value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
if isinstance(value, str):
|
||||
# Пытаемся преобразовать строку в дату
|
||||
date_obj = pd.to_datetime(value, errors='coerce')
|
||||
if pd.notna(date_obj):
|
||||
return date_obj.strftime('%Y-%m-%d %H:%M:%S')
|
||||
elif hasattr(value, 'strftime'):
|
||||
# Это уже объект даты
|
||||
return value.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _parse_numeric(self, value) -> Optional[float]:
|
||||
"""Парсинг числового значения"""
|
||||
if pd.isna(value) or value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
elif isinstance(value, str):
|
||||
# Заменяем запятую на точку для русских чисел
|
||||
cleaned = value.replace(',', '.').strip()
|
||||
return float(cleaned) if cleaned else None
|
||||
return None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def _parse_downtime(self, value) -> Optional[str]:
|
||||
"""Парсинг данных о простое"""
|
||||
if pd.isna(value) or value is None:
|
||||
return None
|
||||
|
||||
return str(value).strip() if str(value).strip() else None
|
||||
@@ -6,7 +6,7 @@ from fastapi import FastAPI, File, UploadFile, HTTPException, status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from adapters.storage import MinIOStorageAdapter
|
||||
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser
|
||||
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, SvodkaRepairCAParser
|
||||
|
||||
from core.models import UploadRequest, DataRequest
|
||||
from core.services import ReportService, PARSERS
|
||||
@@ -18,6 +18,7 @@ from app.schemas import (
|
||||
SvodkaCARequest,
|
||||
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest
|
||||
)
|
||||
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
|
||||
|
||||
|
||||
# Парсеры
|
||||
@@ -25,6 +26,7 @@ PARSERS.update({
|
||||
'svodka_pm': SvodkaPMParser,
|
||||
'svodka_ca': SvodkaCAParser,
|
||||
'monitoring_fuel': MonitoringFuelParser,
|
||||
'svodka_repair_ca': SvodkaRepairCAParser,
|
||||
# 'svodka_plan_sarnpz': SvodkaPlanSarnpzParser,
|
||||
})
|
||||
|
||||
@@ -80,22 +82,69 @@ async def root():
|
||||
description="Возвращает список идентификаторов всех доступных парсеров",
|
||||
response_model=Dict[str, List[str]],
|
||||
responses={
|
||||
200: {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"parsers": ["monitoring_fuel", "svodka_ca", "svodka_pm"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},)
|
||||
200: {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"parsers": ["monitoring_fuel", "svodka_ca", "svodka_pm"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},)
|
||||
async def get_available_parsers():
|
||||
"""Получение списка доступных парсеров"""
|
||||
parsers = list(PARSERS.keys())
|
||||
return {"parsers": parsers}
|
||||
|
||||
|
||||
@app.get("/parsers/{parser_name}/available_ogs", tags=["Общее"],
|
||||
summary="Доступные ОГ для парсера",
|
||||
description="Возвращает список доступных ОГ для указанного парсера",
|
||||
responses={
|
||||
200: {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"parser": "svodka_repair_ca",
|
||||
"available_ogs": ["KNPZ", "ANHK", "SNPZ", "BASH"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},)
|
||||
async def get_available_ogs(parser_name: str):
|
||||
"""Получение списка доступных ОГ для парсера"""
|
||||
if parser_name not in PARSERS:
|
||||
raise HTTPException(status_code=404, detail=f"Парсер '{parser_name}' не найден")
|
||||
|
||||
parser_class = PARSERS[parser_name]
|
||||
|
||||
# Для svodka_repair_ca возвращаем ОГ из загруженных данных
|
||||
if parser_name == "svodka_repair_ca":
|
||||
try:
|
||||
# Создаем экземпляр сервиса и загружаем данные из MinIO
|
||||
report_service = get_report_service()
|
||||
from core.models import DataRequest
|
||||
data_request = DataRequest(report_type=parser_name, get_params={})
|
||||
loaded_data = report_service.get_data(data_request)
|
||||
# Если данные загружены, извлекаем ОГ из них
|
||||
if loaded_data is not None and hasattr(loaded_data, 'data') and loaded_data.data is not None:
|
||||
# Для svodka_repair_ca данные возвращаются в формате словаря по ОГ
|
||||
data_value = loaded_data.data.get('value')
|
||||
if isinstance(data_value, dict):
|
||||
available_ogs = list(data_value.keys())
|
||||
return {"parser": parser_name, "available_ogs": available_ogs}
|
||||
except Exception as e:
|
||||
print(f"⚠️ Ошибка при получении ОГ: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Для других парсеров или если нет данных возвращаем статический список из pconfig
|
||||
from adapters.pconfig import SINGLE_OGS
|
||||
return {"parser": parser_name, "available_ogs": SINGLE_OGS}
|
||||
|
||||
|
||||
@app.get("/parsers/{parser_name}/getters", tags=["Общее"],
|
||||
summary="Информация о геттерах парсера",
|
||||
description="Возвращает информацию о доступных геттерах для указанного парсера",
|
||||
@@ -556,6 +605,131 @@ async def get_svodka_ca_data(
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/svodka_repair_ca/upload", tags=[SvodkaRepairCAParser.name],
|
||||
summary="Загрузка файла отчета сводки ремонта СА",
|
||||
response_model=UploadResponse,
|
||||
responses={
|
||||
400: {"model": UploadErrorResponse, "description": "Неверный формат файла"},
|
||||
500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}
|
||||
},)
|
||||
async def upload_svodka_repair_ca(
|
||||
file: UploadFile = File(..., description="Excel файл или ZIP архив сводки ремонта СА (.xlsx, .xlsm, .xls, .zip)")
|
||||
):
|
||||
"""
|
||||
Загрузка и обработка Excel файла или ZIP архива отчета сводки ремонта СА
|
||||
|
||||
**Поддерживаемые форматы:**
|
||||
- Excel (.xlsx, .xlsm, .xls)
|
||||
- ZIP архив (.zip)
|
||||
"""
|
||||
report_service = get_report_service()
|
||||
|
||||
try:
|
||||
# Проверяем тип файла
|
||||
if not file.filename.lower().endswith(('.xlsx', '.xlsm', '.xls', '.zip')):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content=UploadErrorResponse(
|
||||
message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls) или ZIP архивы (.zip)",
|
||||
error_code="INVALID_FILE_TYPE",
|
||||
details={
|
||||
"expected_formats": [".xlsx", ".xlsm", ".xls", ".zip"],
|
||||
"received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown"
|
||||
}
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
# Читаем содержимое файла
|
||||
file_content = await file.read()
|
||||
|
||||
# Создаем запрос
|
||||
request = UploadRequest(
|
||||
report_type='svodka_repair_ca',
|
||||
file_content=file_content,
|
||||
file_name=file.filename
|
||||
)
|
||||
|
||||
# Загружаем отчет
|
||||
result = report_service.upload_report(request)
|
||||
|
||||
if result.success:
|
||||
return UploadResponse(
|
||||
success=True,
|
||||
message=result.message,
|
||||
object_id=result.object_id
|
||||
)
|
||||
else:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content=UploadErrorResponse(
|
||||
message=result.message,
|
||||
error_code="ERR_UPLOAD"
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content=UploadErrorResponse(
|
||||
message=f"Внутренняя ошибка сервера: {str(e)}",
|
||||
error_code="INTERNAL_SERVER_ERROR"
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
|
||||
@app.post("/svodka_repair_ca/get_data", tags=[SvodkaRepairCAParser.name],
|
||||
summary="Получение данных из отчета сводки ремонта СА")
|
||||
async def get_svodka_repair_ca_data(
|
||||
request_data: SvodkaRepairCARequest
|
||||
):
|
||||
"""
|
||||
Получение данных из отчета сводки ремонта СА
|
||||
|
||||
### Структура параметров:
|
||||
- `og_ids`: **Массив ID ОГ** для фильтрации (опциональный)
|
||||
- `repair_types`: **Массив типов ремонта** - `КР`, `КП`, `ТР` (опциональный)
|
||||
- `include_planned`: **Включать плановые данные** (по умолчанию true)
|
||||
- `include_factual`: **Включать фактические данные** (по умолчанию true)
|
||||
|
||||
### Пример тела запроса:
|
||||
```json
|
||||
{
|
||||
"og_ids": ["SNPZ", "KNPZ"],
|
||||
"repair_types": ["КР", "КП"],
|
||||
"include_planned": true,
|
||||
"include_factual": true
|
||||
}
|
||||
```
|
||||
"""
|
||||
report_service = get_report_service()
|
||||
|
||||
try:
|
||||
# Создаем запрос
|
||||
request_dict = request_data.model_dump()
|
||||
request = DataRequest(
|
||||
report_type='svodka_repair_ca',
|
||||
get_params=request_dict
|
||||
)
|
||||
|
||||
# Получаем данные
|
||||
result = report_service.get_data(request)
|
||||
|
||||
if result.success:
|
||||
return {
|
||||
"success": True,
|
||||
"data": result.data
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=result.message)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
|
||||
# @app.post("/monitoring_fuel/upload", tags=[MonitoringFuelParser.name])
|
||||
# async def upload_monitoring_fuel(
|
||||
# file: UploadFile = File(...),
|
||||
|
||||
46
python_parser/app/schemas/svodka_repair_ca.py
Normal file
46
python_parser/app/schemas/svodka_repair_ca.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RepairType(str, Enum):
|
||||
"""Типы ремонтных работ"""
|
||||
KR = "КР" # Капитальный ремонт
|
||||
KP = "КП" # Капитальный ремонт
|
||||
TR = "ТР" # Текущий ремонт
|
||||
|
||||
|
||||
class SvodkaRepairCARequest(BaseModel):
|
||||
"""Запрос на получение данных сводки ремонта СА"""
|
||||
|
||||
og_ids: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="Список ID ОГ для фильтрации. Если не указан, возвращаются данные по всем ОГ",
|
||||
example=["SNPZ", "KNPZ", "BASH"]
|
||||
)
|
||||
|
||||
repair_types: Optional[List[RepairType]] = Field(
|
||||
default=None,
|
||||
description="Список типов ремонта для фильтрации. Если не указан, возвращаются все типы",
|
||||
example=[RepairType.KR, RepairType.KP]
|
||||
)
|
||||
|
||||
include_planned: bool = Field(
|
||||
default=True,
|
||||
description="Включать ли плановые данные"
|
||||
)
|
||||
|
||||
include_factual: bool = Field(
|
||||
default=True,
|
||||
description="Включать ли фактические данные"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"og_ids": ["SNPZ", "KNPZ"],
|
||||
"repair_types": ["КР", "КП"],
|
||||
"include_planned": True,
|
||||
"include_factual": True
|
||||
}
|
||||
}
|
||||
@@ -136,6 +136,9 @@ class ReportService:
|
||||
if request.report_type == 'svodka_ca':
|
||||
# Для svodka_ca используем геттер get_ca_data
|
||||
getter_name = 'get_ca_data'
|
||||
elif request.report_type == 'svodka_repair_ca':
|
||||
# Для svodka_repair_ca используем геттер get_repair_data
|
||||
getter_name = 'get_repair_data'
|
||||
elif request.report_type == 'monitoring_fuel':
|
||||
# Для monitoring_fuel определяем геттер из параметра mode
|
||||
getter_name = get_params.pop("mode", None)
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
import pandas as pd
|
||||
import io
|
||||
import zipfile
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, List
|
||||
import os
|
||||
|
||||
# Конфигурация страницы
|
||||
@@ -50,7 +50,12 @@ def get_server_info():
|
||||
def upload_file_to_api(endpoint: str, file_data: bytes, filename: str):
|
||||
"""Загрузка файла на API"""
|
||||
try:
|
||||
files = {"zip_file": (filename, file_data, "application/zip")}
|
||||
# Определяем правильное имя поля в зависимости от эндпоинта
|
||||
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:
|
||||
@@ -64,6 +69,20 @@ def make_api_request(endpoint: str, data: Dict[str, Any]):
|
||||
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():
|
||||
st.title("🚀 NIN Excel Parsers API - Демонстрация")
|
||||
st.markdown("---")
|
||||
@@ -96,10 +115,11 @@ def main():
|
||||
st.write(f"• {parser}")
|
||||
|
||||
# Основные вкладки - по одной на каждый парсер
|
||||
tab1, tab2, tab3 = st.tabs([
|
||||
tab1, tab2, tab3, tab4 = st.tabs([
|
||||
"📊 Сводки ПМ",
|
||||
"🏭 Сводки СА",
|
||||
"⛽ Мониторинг топлива"
|
||||
"⛽ Мониторинг топлива",
|
||||
"🔧 Ремонт СА"
|
||||
])
|
||||
|
||||
# Вкладка 1: Сводки ПМ - полный функционал
|
||||
@@ -371,6 +391,108 @@ def main():
|
||||
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', 'Неизвестная ошибка')}")
|
||||
|
||||
# Футер
|
||||
st.markdown("---")
|
||||
st.markdown("### 📚 Документация API")
|
||||
@@ -385,6 +507,7 @@ def main():
|
||||
- 📊 Парсинг сводок ПМ (план и факт)
|
||||
- 🏭 Парсинг сводок СА
|
||||
- ⛽ Мониторинг топлива
|
||||
- 🔧 Управление ремонтными работами СА
|
||||
|
||||
**Технологии:**
|
||||
- FastAPI
|
||||
|
||||
Reference in New Issue
Block a user