diff --git a/PARSER_DEVELOPMENT_GUIDE.md b/PARSER_DEVELOPMENT_GUIDE.md new file mode 100644 index 0000000..c9456c4 --- /dev/null +++ b/PARSER_DEVELOPMENT_GUIDE.md @@ -0,0 +1,1002 @@ +# 📚 Руководство по разработке парсеров + +Полное руководство по созданию новых парсеров для системы NIN Excel Parsers API. + +## 📋 Содержание + +1. [Архитектура системы](#архитектура-системы) +2. [Структура проекта](#структура-проекта) +3. [Создание нового парсера](#создание-нового-парсера) +4. [Регистрация парсера](#регистрация-парсера) +5. [Создание API эндпоинтов](#создание-api-эндпоинтов) +6. [Интеграция с Streamlit](#интеграция-с-streamlit) +7. [Тестирование](#тестирование) +8. [Лучшие практики](#лучшие-практики) +9. [Примеры](#примеры) + +--- + +## 🏗️ Архитектура системы + +### Hexagonal Architecture + +Система построена на принципах **Hexagonal Architecture** (Ports & Adapters): + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Streamlit UI │ │ FastAPI │ │ MinIO Storage │ +│ (Adapter) │◄──►│ (Application) │◄──►│ (Adapter) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ ParserPort │ + │ (Core) │ + └─────────────────┘ + ▲ + │ + ┌─────────────────┐ + │ Parser │ + │ (Adapter) │ + └─────────────────┘ +``` + +### Ключевые компоненты + +- **ParserPort** - базовый класс для всех парсеров +- **ReportService** - сервис для управления отчетами +- **MinIOStorageAdapter** - адаптер для хранения данных +- **FastAPI** - веб-фреймворк для API +- **Streamlit** - веб-интерфейс + +--- + +## 📁 Структура проекта + +``` +python_parser/ +├── adapters/ +│ ├── parsers/ # Парсеры (адаптеры) +│ │ ├── __init__.py +│ │ ├── monitoring_fuel.py +│ │ ├── monitoring_tar.py +│ │ ├── svodka_ca.py +│ │ ├── svodka_pm.py +│ │ ├── svodka_repair_ca.py +│ │ └── statuses_repair_ca.py +│ ├── pconfig.py # Конфигурация парсеров +│ └── storage.py # Адаптер хранилища +├── app/ +│ ├── main.py # FastAPI приложение +│ └── schemas/ # Pydantic схемы +│ ├── monitoring_fuel.py +│ ├── monitoring_tar.py +│ ├── svodka_ca.py +│ ├── svodka_pm.py +│ ├── svodka_repair_ca.py +│ └── statuses_repair_ca.py +├── core/ +│ ├── models.py # Модели данных +│ ├── ports.py # Базовые порты +│ ├── schema_utils.py # Утилиты схем +│ └── services.py # Сервисы +└── requirements.txt + +streamlit_app/ +├── streamlit_app.py # Streamlit интерфейс +└── requirements.txt +``` + +--- + +## 🔧 Создание нового парсера + +### 1. Создание Pydantic схем + +Создайте файл схемы в `python_parser/app/schemas/your_parser.py`: + +```python +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any +from enum import Enum + +class YourParserMode(str, Enum): + """Режимы работы парсера""" + MODE1 = "mode1" + MODE2 = "mode2" + +class YourParserRequest(BaseModel): + """Схема запроса для основного геттера""" + param1: Optional[str] = Field( + None, + description="Описание параметра 1", + example="example_value" + ) + param2: Optional[List[str]] = Field( + None, + description="Описание параметра 2", + example=["value1", "value2"] + ) + mode: Optional[YourParserMode] = Field( + None, + description="Режим работы парсера", + example="mode1" + ) + + class Config: + json_schema_extra = { + "example": { + "param1": "example_value", + "param2": ["value1", "value2"], + "mode": "mode1" + } + } + +class YourParserFullRequest(BaseModel): + """Схема запроса для получения всех данных""" + # Пустая схема - возвращает все данные без фильтрации + pass + + class Config: + json_schema_extra = { + "example": {} + } +``` + +### 2. Создание парсера + +Создайте файл парсера в `python_parser/adapters/parsers/your_parser.py`: + +```python +import pandas as pd +import os +import zipfile +import tempfile +from typing import Dict, Any, Optional +from core.ports import ParserPort +from adapters.pconfig import find_header_row, data_to_json +from app.schemas.your_parser import YourParserRequest, YourParserFullRequest + + +class YourParser(ParserPort): + """Парсер для вашего типа данных""" + + name = "your_parser" # Уникальное имя парсера + + def __init__(self): + super().__init__() + # Регистрируем геттеры + self.register_getter("get_data", YourParserRequest, self._get_data_wrapper) + self.register_getter("get_full_data", YourParserFullRequest, self._get_full_data_wrapper) + + # Данные парсера + self.data_dict = {} + + def parse(self, file_path: str, params: dict) -> Dict[str, Any]: + """Основной метод парсинга""" + print(f"🔍 DEBUG: YourParser.parse вызван с файлом: {file_path}") + + try: + # Проверяем тип файла (пример для ZIP-only парсера) + if not file_path.endswith('.zip'): + raise ValueError(f"Неподдерживаемый тип файла: {file_path}. Ожидается только ZIP архив.") + + # Обрабатываем ZIP архив + result = self._parse_zip_archive(file_path) + + # Сохраняем результат + self.data_dict = result + print(f"✅ Парсинг завершен. Получено {len(result)} записей") + return result + + except Exception as e: + print(f"❌ Ошибка при парсинге: {e}") + raise + + def _parse_zip_archive(self, zip_path: str) -> Dict[str, Any]: + """Парсинг ZIP архива""" + print(f"📦 Обработка ZIP архива: {zip_path}") + + with tempfile.TemporaryDirectory() as temp_dir: + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(temp_dir) + + # Ищем нужные файлы + target_files = [] + for root, dirs, files in os.walk(temp_dir): + for file in files: + if file.endswith(('.xlsx', '.xls')): + target_files.append(os.path.join(root, file)) + + if not target_files: + raise ValueError("В архиве не найдены поддерживаемые файлы") + + # Парсим все найденные файлы + all_data = {} + for file_path in target_files: + file_data = self._parse_single_file(file_path) + # Объединяем данные + all_data.update(file_data) + + return all_data + + def _parse_single_file(self, file_path: str) -> Dict[str, Any]: + """Парсинг одного файла""" + print(f"📁 Обработка файла: {file_path}") + + try: + # Читаем Excel файл + excel_file = pd.ExcelFile(file_path) + available_sheets = excel_file.sheet_names + + # Обрабатываем нужные листы + result_data = {} + for sheet_name in available_sheets: + if self._should_process_sheet(sheet_name): + sheet_data = self._parse_sheet(file_path, sheet_name) + result_data.update(sheet_data) + + return result_data + + except Exception as e: + print(f"❌ Ошибка при обработке файла {file_path}: {e}") + return {} + + def _should_process_sheet(self, sheet_name: str) -> bool: + """Определяет, нужно ли обрабатывать лист""" + # Логика фильтрации листов + return True # Или ваша логика + + def _parse_sheet(self, file_path: str, sheet_name: str) -> Dict[str, Any]: + """Парсинг конкретного листа""" + try: + # Находим заголовок + header_num = find_header_row(file_path, sheet_name, search_value="1") + if header_num is None: + print(f"❌ Не найден заголовок в листе {sheet_name}") + return {} + + # Читаем данные + df = pd.read_excel( + file_path, + sheet_name=sheet_name, + header=header_num, + index_col=None + ) + + # Обрабатываем данные + processed_data = self._process_dataframe(df, sheet_name) + + return processed_data + + except Exception as e: + print(f"❌ Ошибка при обработке листа {sheet_name}: {e}") + return {} + + def _process_dataframe(self, df: pd.DataFrame, sheet_name: str) -> Dict[str, Any]: + """Обработка DataFrame""" + # Ваша логика обработки данных + return {"sheet_name": sheet_name, "data": df.to_dict('records')} + + def _get_data_wrapper(self, params: dict) -> Dict[str, Any]: + """Обертка для основного геттера""" + print(f"🔍 DEBUG: _get_data_wrapper вызван с параметрами: {params}") + + # Валидируем параметры + validated_params = YourParserRequest(**params) + + # Получаем данные из парсера + data_source = self._get_data_source() + + if not data_source: + print("⚠️ Нет данных в парсере") + return {} + + # Фильтруем данные по параметрам + filtered_data = self._filter_data(data_source, validated_params) + + # Конвертируем в JSON + try: + result_json = data_to_json(filtered_data) + return result_json + except Exception as e: + print(f"❌ Ошибка при конвертации данных в JSON: {e}") + return {} + + def _get_full_data_wrapper(self, params: dict) -> Dict[str, Any]: + """Обертка для геттера всех данных""" + print(f"🔍 DEBUG: _get_full_data_wrapper вызван с параметрами: {params}") + + # Получаем данные из парсера + data_source = self._get_data_source() + + if not data_source: + print("⚠️ Нет данных в парсере") + return {} + + # Конвертируем все данные в JSON + try: + result_json = data_to_json(data_source) + return result_json + except Exception as e: + print(f"❌ Ошибка при конвертации данных в JSON: {e}") + return {} + + def _get_data_source(self) -> Dict[str, Any]: + """Получает источник данных""" + if hasattr(self, 'df') and self.df is not None: + # Данные загружены из MinIO + if isinstance(self.df, dict): + return self.df + else: + return {} + elif hasattr(self, 'data_dict') and self.data_dict: + # Данные из локального парсинга + return self.data_dict + else: + return {} + + def _filter_data(self, data_source: Dict[str, Any], params: YourParserRequest) -> Dict[str, Any]: + """Фильтрует данные по параметрам""" + # Ваша логика фильтрации + return data_source +``` + +--- + +## 📝 Регистрация парсера + +### 1. Регистрация в __init__.py + +Добавьте импорт в `python_parser/adapters/parsers/__init__.py`: + +```python +from .monitoring_fuel import MonitoringFuelParser +from .monitoring_tar import MonitoringTarParser +from .your_parser import YourParser # Добавить эту строку +from .svodka_ca import SvodkaCAParser +from .svodka_pm import SvodkaPMParser +from .svodka_repair_ca import SvodkaRepairCAParser +from .statuses_repair_ca import StatusesRepairCAParser + +__all__ = [ + 'MonitoringFuelParser', + 'MonitoringTarParser', + 'YourParser', # Добавить эту строку + 'SvodkaCAParser', + 'SvodkaPMParser', + 'SvodkaRepairCAParser', + 'StatusesRepairCAParser' +] +``` + +### 2. Регистрация в main.py + +Добавьте импорт и регистрацию в `python_parser/app/main.py`: + +```python +# Импорты +from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, YourParser, SvodkaRepairCAParser, StatusesRepairCAParser + +from app.schemas.your_parser import YourParserRequest, YourParserFullRequest + +# Регистрация парсера +PARSERS.update({ + 'svodka_pm': SvodkaPMParser, + 'svodka_ca': SvodkaCAParser, + 'monitoring_fuel': MonitoringFuelParser, + 'monitoring_tar': MonitoringTarParser, + 'your_parser': YourParser, # Добавить эту строку + 'svodka_repair_ca': SvodkaRepairCAParser, + 'statuses_repair_ca': StatusesRepairCAParser, +}) +``` + +### 3. Регистрация в services.py + +Добавьте логику выбора геттера в `python_parser/core/services.py`: + +```python +elif request.report_type == 'monitoring_tar': + # Для monitoring_tar используем геттер get_tar_data + getter_name = 'get_tar_data' +elif request.report_type == 'your_parser': # Добавить эту секцию + # Для your_parser используем геттер get_data + getter_name = 'get_data' +elif request.report_type == 'monitoring_fuel': +``` + +--- + +## 🌐 Создание API эндпоинтов + +Добавьте эндпоинты в `python_parser/app/main.py`: + +```python +# ====== YOUR PARSER ENDPOINTS ====== + +@app.post("/your_parser/upload", tags=[YourParser.name], + summary="Загрузка отчета вашего типа") +async def upload_your_parser( + file: UploadFile = File(...) +): + """Загрузка и обработка отчета вашего типа + + ### Поддерживаемые форматы: + - **ZIP архивы** с файлами (только ZIP) + + ### Структура данных: + - Описание структуры данных + - Какие данные извлекаются + - Формат возвращаемых данных + """ + report_service = get_report_service() + + try: + # Проверяем тип файла - только ZIP архивы + if not file.filename.endswith('.zip'): + raise HTTPException( + status_code=400, + detail="Неподдерживаемый тип файла. Ожидается только ZIP архив (.zip)" + ) + + # Читаем содержимое файла + file_content = await file.read() + + # Создаем запрос на загрузку + upload_request = UploadRequest( + report_type='your_parser', + file_content=file_content, + file_name=file.filename + ) + + # Загружаем отчет + result = report_service.upload_report(upload_request) + + if result.success: + return UploadResponse( + success=True, + message="Отчет успешно загружен и обработан", + report_id=result.object_id, + filename=file.filename + ).model_dump() + else: + return UploadErrorResponse( + success=False, + message=result.message, + error_code="ERR_UPLOAD", + details=None + ).model_dump() + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") + + +@app.post("/your_parser/get_data", tags=[YourParser.name], + summary="Получение данных из отчета") +async def get_your_parser_data( + request_data: YourParserRequest +): + """Получение данных из отчета + + ### Структура параметров: + - `param1`: **Описание параметра 1** (опциональный) + - `param2`: **Описание параметра 2** (опциональный) + - `mode`: **Режим работы** (опциональный) + + ### Пример тела запроса: + ```json + { + "param1": "example_value", + "param2": ["value1", "value2"], + "mode": "mode1" + } + ``` + """ + report_service = get_report_service() + + try: + # Создаем запрос + request_dict = request_data.model_dump() + request = DataRequest( + report_type='your_parser', + 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("/your_parser/get_full_data", tags=[YourParser.name], + summary="Получение всех данных из отчета") +async def get_your_parser_full_data(): + """Получение всех данных из отчета без фильтрации + + ### Возвращает: + - Все данные без фильтрации + - Полная структура данных + """ + report_service = get_report_service() + + try: + # Создаем запрос без параметров + request = DataRequest( + report_type='your_parser', + get_params={} + ) + + # Получаем данные через геттер get_full_data + result = report_service.get_data(request, getter_name='get_full_data') + + 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)}") +``` + +--- + +## 🎨 Интеграция с Streamlit + +### 1. Добавление новой вкладки + +В `streamlit_app/streamlit_app.py`: + +```python +# Обновите список вкладок +tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs([ + "📊 Сводки ПМ", + "🏭 Сводки СА", + "⛽ Мониторинг топлива", + "⚡ Мониторинг ТЭР", + "🔧 Ремонт СА", + "📋 Статусы ремонта СА", + "🆕 Ваш парсер" # Добавить новую вкладку +]) + +# Добавьте новую вкладку +with tab7: + st.header("🆕 Ваш парсер") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_file = st.file_uploader( + "Выберите ZIP архив для вашего парсера", + type=['zip'], + key="your_parser_upload" + ) + + if uploaded_file is not None: + if st.button("📤 Загрузить файл", key="your_parser_upload_btn"): + with st.spinner("Загружаем файл..."): + file_data = uploaded_file.read() + result, status_code = upload_file_to_api("/your_parser/upload", file_data, uploaded_file.name) + + if status_code == 200: + st.success("✅ Файл успешно загружен!") + st.json(result) + else: + st.error(f"❌ Ошибка загрузки: {result}") + + # Секция получения данных + st.subheader("📊 Получение данных") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("🔍 Фильтрованные данные") + + # Параметры запроса + param1 = st.text_input("Параметр 1:", key="your_parser_param1") + param2 = st.multiselect("Параметр 2:", ["value1", "value2", "value3"], key="your_parser_param2") + mode = st.selectbox("Режим:", ["mode1", "mode2"], key="your_parser_mode") + + if st.button("📊 Получить данные", key="your_parser_get_data_btn"): + with st.spinner("Получаем данные..."): + request_data = { + "param1": param1 if param1 else None, + "param2": param2 if param2 else None, + "mode": mode if mode else None + } + result, status_code = make_api_request("/your_parser/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.json(data) + else: + st.info("📋 Нет данных для отображения") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + + with col2: + st.subheader("📋 Все данные") + + if st.button("📊 Получить все данные", key="your_parser_get_full_data_btn"): + with st.spinner("Получаем все данные..."): + result, status_code = make_api_request("/your_parser/get_full_data", {}) + + if status_code == 200 and result.get("success"): + st.success("✅ Все данные успешно получены!") + + # Показываем данные + data = result.get("data", {}).get("value", {}) + if data: + st.subheader("📋 Результат:") + st.json(data) + else: + st.info("📋 Нет данных для отображения") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") +``` + +### 2. Обновление информации о проекте + +```python +# В секции "О проекте" +**Возможности:** +- 📊 Парсинг сводок ПМ (план и факт) +- 🏭 Парсинг сводок СА +- ⛽ Мониторинг топлива +- ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы) +- 🔧 Управление ремонтными работами СА +- 📋 Мониторинг статусов ремонта СА +- 🆕 Ваш новый парсер # Добавить эту строку +``` + +--- + +## 🧪 Тестирование + +### 1. Создание тестового скрипта + +Создайте `test_your_parser.py`: + +```python +#!/usr/bin/env python3 +""" +Тестовый скрипт для проверки парсера Your Parser +""" + +import requests +import os + +API_BASE_URL = "http://localhost:8000" + +def test_upload_file(): + """Тест загрузки файла""" + print("🔍 Тестируем загрузку файла...") + + # Используем тестовый файл + file_path = "path/to/your/test/file.xlsx" + + if not os.path.exists(file_path): + print(f"❌ Файл не найден: {file_path}") + return False + + print(f"📁 Найден файл: {file_path}") + + with open(file_path, 'rb') as f: + file_content = f.read() + + files = { + 'file': ('test_file.xlsx', file_content, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + } + + try: + response = requests.post(f"{API_BASE_URL}/your_parser/upload", files=files) + print(f"📊 Статус ответа: {response.status_code}") + print(f"📄 Ответ: {response.text}") + + if response.status_code == 200: + result = response.json() + if result.get("success"): + print("✅ Загрузка успешна!") + return True + else: + print("❌ Ошибка загрузки!") + return False + else: + print("❌ Ошибка HTTP!") + return False + + except Exception as e: + print(f"❌ Исключение: {e}") + return False + +def test_get_data(): + """Тест получения данных""" + print("\n🔍 Тестируем получение данных...") + + test_data = { + "param1": "test_value", + "param2": ["value1", "value2"], + "mode": "mode1" + } + + try: + response = requests.post(f"{API_BASE_URL}/your_parser/get_data", json=test_data) + print(f"📊 Статус ответа: {response.status_code}") + + if response.status_code == 200: + result = response.json() + if result.get("success"): + print("✅ Получение данных успешно!") + data = result.get("data", {}).get("value", {}) + print(f"📊 Получено данных: {len(data)} записей") + return True + else: + print("❌ Ошибка получения данных!") + print(f"📄 Ответ: {response.text}") + return False + else: + print("❌ Ошибка HTTP!") + print(f"📄 Ответ: {response.text}") + return False + + except Exception as e: + print(f"❌ Исключение: {e}") + return False + +def main(): + """Основная функция тестирования""" + print("🚀 Тестирование парсера Your Parser") + print("=" * 70) + + # Тест 1: Загрузка файла + upload_success = test_upload_file() + + if upload_success: + # Тест 2: Получение данных + get_data_success = test_get_data() + + print("\n" + "=" * 70) + print("📋 Результаты тестирования:") + print(f" ✅ Загрузка файла: {'ПРОЙДЕН' if upload_success else 'ПРОВАЛЕН'}") + print(f" ✅ Получение данных: {'ПРОЙДЕН' if get_data_success else 'ПРОВАЛЕН'}") + + if upload_success and get_data_success: + print("\n🎉 Все тесты пройдены! Парсер работает корректно!") + else: + print("\n❌ Некоторые тесты провалены.") + else: + print("\n❌ Загрузка файла провалена. Пропускаем остальные тесты.") + +if __name__ == "__main__": + main() +``` + +### 2. Запуск тестов + +```bash +# Запуск тестового скрипта +python test_your_parser.py + +# Проверка через curl +curl -X POST "http://localhost:8000/your_parser/upload" \ + -H "accept: application/json" \ + -H "Content-Type: multipart/form-data" \ + -F "file=@test_file.xlsx" + +curl -X POST "http://localhost:8000/your_parser/get_data" \ + -H "accept: application/json" \ + -H "Content-Type: application/json" \ + -d '{"param1": "test_value", "mode": "mode1"}' +``` + +--- + +## 📋 Лучшие практики + +### 1. Именование + +- **Парсер**: `YourParser` (PascalCase) +- **Файл парсера**: `your_parser.py` (snake_case) +- **Имя парсера**: `"your_parser"` (snake_case) +- **Геттеры**: `get_data`, `get_full_data` (snake_case) +- **Эндпоинты**: `/your_parser/upload`, `/your_parser/get_data` + +### 2. Структура данных + +```python +# Рекомендуемая структура возвращаемых данных +{ + "installation_id": { + "data_type1": [ + {"field1": "value1", "field2": "value2"}, + {"field1": "value3", "field2": "value4"} + ], + "data_type2": [ + {"field1": "value5", "field2": "value6"} + ] + } +} +``` + +### 3. Обработка ошибок + +```python +try: + # Ваш код + result = some_operation() + return result +except SpecificException as e: + print(f"❌ Специфическая ошибка: {e}") + return {} +except Exception as e: + print(f"❌ Общая ошибка: {e}") + raise +``` + +### 4. Логирование + +```python +# Используйте эмодзи для разных типов сообщений +print(f"🔍 DEBUG: Отладочная информация") +print(f"📁 INFO: Информационное сообщение") +print(f"✅ SUCCESS: Успешная операция") +print(f"⚠️ WARNING: Предупреждение") +print(f"❌ ERROR: Ошибка") +``` + +### 5. Валидация данных + +```python +# Всегда валидируйте входные параметры +validated_params = YourParserRequest(**params) + +# Проверяйте наличие данных +if not data_source: + print("⚠️ Нет данных в парсере") + return {} +``` + +### 6. Документация + +- Добавляйте docstrings ко всем методам +- Описывайте параметры в Pydantic схемах +- Добавляйте примеры в FastAPI эндпоинты +- Комментируйте сложную логику + +--- + +## 📚 Примеры + +### Пример 1: Простой парсер + +```python +class SimpleParser(ParserPort): + """Простой парсер для демонстрации""" + + name = "simple_parser" + + def __init__(self): + super().__init__() + self.register_getter("get_data", SimpleRequest, self._get_data_wrapper) + + def parse(self, file_path: str, params: dict) -> Dict[str, Any]: + """Парсинг простого Excel файла""" + df = pd.read_excel(file_path) + return {"data": df.to_dict('records')} + + def _get_data_wrapper(self, params: dict) -> Dict[str, Any]: + """Обертка для геттера""" + data_source = self._get_data_source() + return data_to_json(data_source) +``` + +### Пример 2: Парсер с множественными геттерами + +```python +class MultiGetterParser(ParserPort): + """Парсер с несколькими геттерами""" + + name = "multi_getter_parser" + + def __init__(self): + super().__init__() + # Регистрируем несколько геттеров + self.register_getter("get_summary", SummaryRequest, self._get_summary_wrapper) + self.register_getter("get_details", DetailsRequest, self._get_details_wrapper) + self.register_getter("get_statistics", StatisticsRequest, self._get_statistics_wrapper) + + def _get_summary_wrapper(self, params: dict) -> Dict[str, Any]: + """Геттер для получения сводки""" + # Логика получения сводки + pass + + def _get_details_wrapper(self, params: dict) -> Dict[str, Any]: + """Геттер для получения деталей""" + # Логика получения деталей + pass + + def _get_statistics_wrapper(self, params: dict) -> Dict[str, Any]: + """Геттер для получения статистики""" + # Логика получения статистики + pass +``` + +### Пример 3: Парсер с фильтрацией + +```python +class FilteredParser(ParserPort): + """Парсер с продвинутой фильтрацией""" + + def _filter_data(self, data_source: Dict[str, Any], params: FilterRequest) -> Dict[str, Any]: + """Фильтрация данных по параметрам""" + filtered_data = {} + + for key, value in data_source.items(): + # Фильтр по дате + if params.start_date and value.get('date') < params.start_date: + continue + + # Фильтр по типу + if params.type and value.get('type') != params.type: + continue + + # Фильтр по статусу + if params.status and value.get('status') not in params.status: + continue + + filtered_data[key] = value + + return filtered_data +``` + +--- + +## 🚀 Заключение + +Это руководство покрывает все аспекты создания новых парсеров в системе NIN Excel Parsers API. Следуйте этим инструкциям для создания качественных, поддерживаемых и тестируемых парсеров. + +### Чек-лист для нового парсера: + +- [ ] Создана Pydantic схема +- [ ] Создан класс парсера с геттерами +- [ ] Парсер зарегистрирован в `__init__.py` +- [ ] Парсер зарегистрирован в `main.py` +- [ ] Добавлена логика в `services.py` +- [ ] Созданы FastAPI эндпоинты +- [ ] Добавлена вкладка в Streamlit +- [ ] Создан тестовый скрипт +- [ ] Проведено тестирование +- [ ] Обновлена документация + +### Полезные ссылки: + +- [FastAPI Documentation](https://fastapi.tiangolo.com/) +- [Pydantic Documentation](https://pydantic-docs.helpmanual.io/) +- [Streamlit Documentation](https://docs.streamlit.io/) +- [Pandas Documentation](https://pandas.pydata.org/docs/) + +--- + +**Удачной разработки! 🚀** \ No newline at end of file diff --git a/monitoring_tar_correct.zip b/monitoring_tar_correct.zip new file mode 100644 index 0000000..7d9200b Binary files /dev/null and b/monitoring_tar_correct.zip differ diff --git a/monitoring_tar_test.zip b/monitoring_tar_test.zip new file mode 100644 index 0000000..7d9200b Binary files /dev/null and b/monitoring_tar_test.zip differ diff --git a/python_parser/adapters/monitoring_tar_test.zip b/python_parser/adapters/monitoring_tar_test.zip new file mode 100644 index 0000000..7d9200b Binary files /dev/null and b/python_parser/adapters/monitoring_tar_test.zip differ diff --git a/python_parser/adapters/parsers/__init__.py b/python_parser/adapters/parsers/__init__.py index eeb912e..0502ef8 100644 --- a/python_parser/adapters/parsers/__init__.py +++ b/python_parser/adapters/parsers/__init__.py @@ -1,4 +1,5 @@ from .monitoring_fuel import MonitoringFuelParser +from .monitoring_tar import MonitoringTarParser from .svodka_ca import SvodkaCAParser from .svodka_pm import SvodkaPMParser from .svodka_repair_ca import SvodkaRepairCAParser @@ -6,6 +7,7 @@ from .statuses_repair_ca import StatusesRepairCAParser __all__ = [ 'MonitoringFuelParser', + 'MonitoringTarParser', 'SvodkaCAParser', 'SvodkaPMParser', 'SvodkaRepairCAParser', diff --git a/python_parser/adapters/parsers/monitoring_tar.py b/python_parser/adapters/parsers/monitoring_tar.py new file mode 100644 index 0000000..d39134b --- /dev/null +++ b/python_parser/adapters/parsers/monitoring_tar.py @@ -0,0 +1,302 @@ +import os +import zipfile +import tempfile +import pandas as pd +from typing import Dict, Any, List +from core.ports import ParserPort +from adapters.pconfig import find_header_row, SNPZ_IDS, data_to_json + + +class MonitoringTarParser(ParserPort): + """Парсер для мониторинга ТЭР (топливно-энергетических ресурсов)""" + + name = "monitoring_tar" + + def __init__(self): + super().__init__() + self.data_dict = {} + self.df = None + + # Регистрируем геттеры + self.register_getter('get_tar_data', self._get_tar_data_wrapper, required_params=['mode']) + self.register_getter('get_tar_full_data', self._get_tar_full_data_wrapper, required_params=[]) + + def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame: + """Парсит ZIP архив с файлами мониторинга ТЭР""" + print(f"🔍 DEBUG: MonitoringTarParser.parse вызван с файлом: {file_path}") + + if not file_path.endswith('.zip'): + raise ValueError("MonitoringTarParser поддерживает только ZIP архивы") + + # Обрабатываем ZIP архив + result = self._parse_zip_archive(file_path) + + # Конвертируем результат в DataFrame для совместимости с ReportService + if result: + data_list = [] + for id, data in result.items(): + data_list.append({ + 'id': id, + 'data': data, + 'records_count': len(data.get('total', [])) + len(data.get('last_day', [])) + }) + + df = pd.DataFrame(data_list) + print(f"🔍 DEBUG: Создан DataFrame с {len(df)} записями") + return df + else: + print("🔍 DEBUG: Возвращаем пустой DataFrame") + return pd.DataFrame() + + def _parse_zip_archive(self, zip_path: str) -> Dict[str, Any]: + """Парсит ZIP архив с файлами мониторинга ТЭР""" + print(f"📦 Обработка ZIP архива: {zip_path}") + + with tempfile.TemporaryDirectory() as temp_dir: + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(temp_dir) + + # Ищем файлы мониторинга ТЭР + tar_files = [] + for root, dirs, files in os.walk(temp_dir): + for file in files: + # Поддерживаем файлы svodka_tar_*.xlsx (основные) и monitoring_*.xlsm (альтернативные) + if (file.startswith('svodka_tar_') and file.endswith('.xlsx')) or (file.startswith('monitoring_') and file.endswith('.xlsm')): + tar_files.append(os.path.join(root, file)) + + if not tar_files: + raise ValueError("В архиве не найдены файлы мониторинга ТЭР") + + print(f"📁 Найдено {len(tar_files)} файлов мониторинга ТЭР") + + # Обрабатываем каждый файл + all_data = {} + for file_path in tar_files: + print(f"📁 Обработка файла: {file_path}") + + # Извлекаем номер месяца из имени файла + filename = os.path.basename(file_path) + month_str = self._extract_month_from_filename(filename) + print(f"📅 Месяц: {month_str}") + + # Парсим файл + file_data = self._parse_single_file(file_path, month_str) + if file_data: + all_data.update(file_data) + + return all_data + + def _extract_month_from_filename(self, filename: str) -> str: + """Извлекает номер месяца из имени файла""" + # Для файлов типа svodka_tar_SNPZ_01.xlsx или monitoring_SNPZ_01.xlsm + parts = filename.split('_') + if len(parts) >= 3: + month_part = parts[-1].split('.')[0] # Убираем расширение + if month_part.isdigit(): + return month_part + return "01" # По умолчанию + + def _parse_single_file(self, file_path: str, month_str: str) -> Dict[str, Any]: + """Парсит один файл мониторинга ТЭР""" + try: + excel_file = pd.ExcelFile(file_path) + available_sheets = excel_file.sheet_names + except Exception as e: + print(f"❌ Не удалось открыть Excel-файл {file_path}: {e}") + return {} + + # Словарь для хранения данных: id -> {'total': [], 'last_day': []} + df_svodka_tar = {} + + # Определяем тип файла и обрабатываем соответственно + filename = os.path.basename(file_path) + + if filename.startswith('svodka_tar_'): + # Обрабатываем файлы svodka_tar_*.xlsx с SNPZ_IDS + for name, id in SNPZ_IDS.items(): + if name not in available_sheets: + print(f"🟡 Лист '{name}' отсутствует в файле {file_path}") + continue + + # Парсим оба типа строк + result = self._parse_monitoring_tar_single(file_path, name, month_str) + + # Инициализируем структуру для id + if id not in df_svodka_tar: + df_svodka_tar[id] = {'total': [], 'last_day': []} + + if isinstance(result['total'], pd.DataFrame) and not result['total'].empty: + df_svodka_tar[id]['total'].append(result['total']) + + if isinstance(result['last_day'], pd.DataFrame) and not result['last_day'].empty: + df_svodka_tar[id]['last_day'].append(result['last_day']) + + elif filename.startswith('monitoring_'): + # Обрабатываем файлы monitoring_*.xlsm с альтернативными листами + monitoring_sheets = { + 'Мониторинг потребления': 'SNPZ.MONITORING', + 'Исходные данные': 'SNPZ.SOURCE_DATA' + } + + for sheet_name, id in monitoring_sheets.items(): + if sheet_name not in available_sheets: + print(f"🟡 Лист '{sheet_name}' отсутствует в файле {file_path}") + continue + + # Парсим оба типа строк + result = self._parse_monitoring_tar_single(file_path, sheet_name, month_str) + + # Инициализируем структуру для id + if id not in df_svodka_tar: + df_svodka_tar[id] = {'total': [], 'last_day': []} + + if isinstance(result['total'], pd.DataFrame) and not result['total'].empty: + df_svodka_tar[id]['total'].append(result['total']) + + if isinstance(result['last_day'], pd.DataFrame) and not result['last_day'].empty: + df_svodka_tar[id]['last_day'].append(result['last_day']) + + # Агрегация: объединяем списки в DataFrame + for id, data in df_svodka_tar.items(): + if data['total']: + df_svodka_tar[id]['total'] = pd.concat(data['total'], ignore_index=True) + else: + df_svodka_tar[id]['total'] = pd.DataFrame() + + if data['last_day']: + df_svodka_tar[id]['last_day'] = pd.concat(data['last_day'], ignore_index=True) + else: + df_svodka_tar[id]['last_day'] = pd.DataFrame() + + print(f"✅ Агрегировано: {len(df_svodka_tar[id]['total'])} 'total' и " + f"{len(df_svodka_tar[id]['last_day'])} 'last_day' записей для id='{id}'") + + return df_svodka_tar + + def _parse_monitoring_tar_single(self, file: str, sheet: str, month_str: str) -> Dict[str, Any]: + """Парсит один файл и лист""" + try: + # Проверяем наличие листа + if sheet not in pd.ExcelFile(file).sheet_names: + print(f"🟡 Лист '{sheet}' не найден в файле {file}") + return {'total': None, 'last_day': None} + + # Определяем номер заголовка в зависимости от типа файла + filename = os.path.basename(file) + if filename.startswith('svodka_tar_'): + # Для файлов svodka_tar_*.xlsx ищем заголовок по значению "1" + header_num = find_header_row(file, sheet, search_value="1") + if header_num is None: + print(f"❌ Не найдена строка с заголовком '1' в файле {file}, лист '{sheet}'") + return {'total': None, 'last_day': None} + elif filename.startswith('monitoring_'): + # Для файлов monitoring_*.xlsm заголовок находится в строке 5 + header_num = 5 + else: + print(f"❌ Неизвестный тип файла: {filename}") + return {'total': None, 'last_day': None} + + print(f"🔍 DEBUG: Используем заголовок в строке {header_num} для листа '{sheet}'") + + # Читаем с двумя уровнями заголовков + df = pd.read_excel( + file, + sheet_name=sheet, + header=header_num, + index_col=None + ) + + # Убираем мультииндекс: оставляем первый уровень + df.columns = df.columns.get_level_values(0) + + # Удаляем строки, где все значения — NaN + df = df.dropna(how='all').reset_index(drop=True) + if df.empty: + print(f"🟡 Нет данных после очистки в файле {file}, лист '{sheet}'") + return {'total': None, 'last_day': None} + + # === 1. Обработка строки "Всего" === + first_col = df.columns[0] + mask_total = df[first_col].astype(str).str.strip() == "Всего" + df_total = df[mask_total].copy() + + if not df_total.empty: + # Заменяем "Всего" на номер месяца в первой колонке + df_total.loc[:, first_col] = df_total[first_col].astype(str).str.replace("Всего", month_str, regex=False) + df_total = df_total.reset_index(drop=True) + else: + df_total = pd.DataFrame() + + # === 2. Обработка последней строки (не пустая) === + # Берём последнюю строку из исходного df (не включая "Всего", если она внизу) + # Исключим строку "Всего" из "последней строки", если она есть + df_no_total = df[~mask_total].dropna(how='all') + if not df_no_total.empty: + df_last_day = df_no_total.tail(1).copy() + df_last_day = df_last_day.reset_index(drop=True) + else: + df_last_day = pd.DataFrame() + + return {'total': df_total, 'last_day': df_last_day} + + except Exception as e: + print(f"❌ Ошибка при обработке файла {file}, лист '{sheet}': {e}") + return {'total': None, 'last_day': None} + + def _get_tar_data_wrapper(self, params: Dict[str, Any] = None) -> str: + """Обертка для получения данных мониторинга ТЭР с фильтрацией по режиму""" + print(f"🔍 DEBUG: _get_tar_data_wrapper вызван с параметрами: {params}") + + # Получаем режим из параметров + mode = params.get('mode', 'total') if params else 'total' + + # Фильтруем данные по режиму + filtered_data = {} + if hasattr(self, 'df') and self.df is not None and not self.df.empty: + # Данные из MinIO + for _, row in self.df.iterrows(): + id = row['id'] + data = row['data'] + if isinstance(data, dict) and mode in data: + filtered_data[id] = data[mode] + else: + filtered_data[id] = pd.DataFrame() + elif hasattr(self, 'data_dict') and self.data_dict: + # Локальные данные + for id, data in self.data_dict.items(): + if isinstance(data, dict) and mode in data: + filtered_data[id] = data[mode] + else: + filtered_data[id] = pd.DataFrame() + + # Конвертируем в JSON + try: + result_json = data_to_json(filtered_data) + return result_json + except Exception as e: + print(f"❌ Ошибка при конвертации данных в JSON: {e}") + return "{}" + + def _get_tar_full_data_wrapper(self, params: Dict[str, Any] = None) -> str: + """Обертка для получения всех данных мониторинга ТЭР""" + print(f"🔍 DEBUG: _get_tar_full_data_wrapper вызван с параметрами: {params}") + + # Получаем все данные + full_data = {} + if hasattr(self, 'df') and self.df is not None and not self.df.empty: + # Данные из MinIO + for _, row in self.df.iterrows(): + id = row['id'] + data = row['data'] + full_data[id] = data + elif hasattr(self, 'data_dict') and self.data_dict: + # Локальные данные + full_data = self.data_dict + + # Конвертируем в JSON + try: + result_json = data_to_json(full_data) + return result_json + except Exception as e: + print(f"❌ Ошибка при конвертации данных в JSON: {e}") + return "{}" \ No newline at end of file diff --git a/python_parser/app/main.py b/python_parser/app/main.py index f4c1be7..8c63277 100644 --- a/python_parser/app/main.py +++ b/python_parser/app/main.py @@ -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, SvodkaRepairCAParser, StatusesRepairCAParser +from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser from core.models import UploadRequest, DataRequest from core.services import ReportService, PARSERS @@ -20,6 +20,7 @@ from app.schemas import ( ) from app.schemas.svodka_repair_ca import SvodkaRepairCARequest from app.schemas.statuses_repair_ca import StatusesRepairCARequest +from app.schemas.monitoring_tar import MonitoringTarRequest, MonitoringTarFullRequest # Парсеры @@ -27,6 +28,7 @@ PARSERS.update({ 'svodka_pm': SvodkaPMParser, 'svodka_ca': SvodkaCAParser, 'monitoring_fuel': MonitoringFuelParser, + 'monitoring_tar': MonitoringTarParser, 'svodka_repair_ca': SvodkaRepairCAParser, 'statuses_repair_ca': StatusesRepairCAParser, # 'svodka_plan_sarnpz': SvodkaPlanSarnpzParser, @@ -1163,5 +1165,149 @@ async def get_monitoring_fuel_month_by_code( raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") +# ====== MONITORING TAR ENDPOINTS ====== + +@app.post("/monitoring_tar/upload", tags=[MonitoringTarParser.name], + summary="Загрузка отчета мониторинга ТЭР") +async def upload_monitoring_tar( + file: UploadFile = File(...) +): + """Загрузка и обработка отчета мониторинга ТЭР (Топливно-энергетических ресурсов) + + ### Поддерживаемые форматы: + - **ZIP архивы** с файлами мониторинга ТЭР + + ### Структура данных: + - Обрабатывает ZIP архивы с файлами по месяцам (svodka_tar_SNPZ_01.xlsx - svodka_tar_SNPZ_12.xlsx) + - Извлекает данные по установкам (SNPZ_IDS) + - Возвращает два типа данных: 'total' (строки "Всего") и 'last_day' (последние строки) + """ + report_service = get_report_service() + + try: + # Проверяем тип файла - только ZIP архивы + if not file.filename.endswith('.zip'): + raise HTTPException( + status_code=400, + detail="Неподдерживаемый тип файла. Ожидается только ZIP архив (.zip)" + ) + + # Читаем содержимое файла + file_content = await file.read() + + # Создаем запрос на загрузку + upload_request = UploadRequest( + report_type='monitoring_tar', + file_content=file_content, + file_name=file.filename + ) + + # Загружаем отчет + result = report_service.upload_report(upload_request) + + if result.success: + return UploadResponse( + success=True, + message="Отчет успешно загружен и обработан", + report_id=result.object_id, + filename=file.filename + ).model_dump() + else: + return UploadErrorResponse( + success=False, + message=result.message, + error_code="ERR_UPLOAD", + details=None + ).model_dump() + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") + + +@app.post("/monitoring_tar/get_data", tags=[MonitoringTarParser.name], + summary="Получение данных из отчета мониторинга ТЭР") +async def get_monitoring_tar_data( + request_data: MonitoringTarRequest +): + """Получение данных из отчета мониторинга ТЭР + + ### Структура параметров: + - `mode`: **Режим получения данных** (опциональный) + - `"total"` - строки "Всего" (агрегированные данные) + - `"last_day"` - последние строки данных + - Если не указан, возвращаются все данные + + ### Пример тела запроса: + ```json + { + "mode": "total" + } + ``` + """ + report_service = get_report_service() + + try: + # Создаем запрос + request_dict = request_data.model_dump() + request = DataRequest( + report_type='monitoring_tar', + get_params=request_dict + ) + + # Получаем данные + result = report_service.get_data(request) + + if result.success: + return { + "success": True, + "data": result.data + } + else: + raise HTTPException(status_code=404, detail=result.message) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") + + +@app.post("/monitoring_tar/get_full_data", tags=[MonitoringTarParser.name], + summary="Получение всех данных из отчета мониторинга ТЭР") +async def get_monitoring_tar_full_data(): + """Получение всех данных из отчета мониторинга ТЭР без фильтрации + + ### Возвращает: + - Все данные по всем установкам + - И данные 'total', и данные 'last_day' + - Полная структура данных мониторинга ТЭР + """ + report_service = get_report_service() + + try: + # Создаем запрос без параметров + request = DataRequest( + report_type='monitoring_tar', + get_params={} + ) + + # Получаем данные + result = report_service.get_data(request) + + if result.success: + return { + "success": True, + "data": result.data + } + else: + raise HTTPException(status_code=404, detail=result.message) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") + + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/python_parser/app/schemas/monitoring_tar.py b/python_parser/app/schemas/monitoring_tar.py new file mode 100644 index 0000000..db9def3 --- /dev/null +++ b/python_parser/app/schemas/monitoring_tar.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel, Field +from typing import Optional, Literal +from enum import Enum + +class TarMode(str, Enum): + """Режимы получения данных мониторинга ТЭР""" + TOTAL = "total" + LAST_DAY = "last_day" + +class MonitoringTarRequest(BaseModel): + """Схема запроса для получения данных мониторинга ТЭР""" + mode: Optional[TarMode] = Field( + None, + description="Режим получения данных: 'total' (строки 'Всего') или 'last_day' (последние строки). Если не указан, возвращаются все данные", + example="total" + ) + + class Config: + json_schema_extra = { + "example": { + "mode": "total" + } + } + +class MonitoringTarFullRequest(BaseModel): + """Схема запроса для получения всех данных мониторинга ТЭР""" + # Пустая схема - возвращает все данные без фильтрации + pass + + class Config: + json_schema_extra = { + "example": {} + } \ No newline at end of file diff --git a/python_parser/core/services.py b/python_parser/core/services.py index b2171b5..e121d8e 100644 --- a/python_parser/core/services.py +++ b/python_parser/core/services.py @@ -142,6 +142,14 @@ class ReportService: elif request.report_type == 'statuses_repair_ca': # Для statuses_repair_ca используем геттер get_repair_statuses getter_name = 'get_repair_statuses' + elif request.report_type == 'monitoring_tar': + # Для monitoring_tar определяем геттер по параметрам + if 'mode' in get_params: + # Если есть параметр mode, используем get_tar_data + getter_name = 'get_tar_data' + else: + # Если нет параметра mode, используем get_tar_full_data + getter_name = 'get_tar_full_data' elif request.report_type == 'monitoring_fuel': # Для monitoring_fuel определяем геттер из параметра mode getter_name = get_params.pop("mode", None) diff --git a/streamlit_app/streamlit_app.py b/streamlit_app/streamlit_app.py index 07dbfe4..0bb2313 100644 --- a/streamlit_app/streamlit_app.py +++ b/streamlit_app/streamlit_app.py @@ -115,12 +115,13 @@ def main(): st.write(f"• {parser}") # Основные вкладки - по одной на каждый парсер - tab1, tab2, tab3, tab4, tab5 = st.tabs([ + tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([ "📊 Сводки ПМ", "🏭 Сводки СА", "⛽ Мониторинг топлива", "🔧 Ремонт СА", - "📋 Статусы ремонта СА" + "📋 Статусы ремонта СА", + "⚡ Мониторинг ТЭР" ]) # Вкладка 1: Сводки ПМ - полный функционал @@ -633,6 +634,112 @@ def main(): else: st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + # Вкладка 6: Мониторинг ТЭР + with tab6: + st.header("⚡ Мониторинг ТЭР (Топливно-энергетических ресурсов)") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_file = st.file_uploader( + "Выберите ZIP архив с файлами мониторинга ТЭР", + type=['zip'], + key="monitoring_tar_upload" + ) + + if uploaded_file is not None: + if st.button("📤 Загрузить файл", key="monitoring_tar_upload_btn"): + with st.spinner("Загружаем файл..."): + file_data = uploaded_file.read() + result, status_code = upload_file_to_api("/monitoring_tar/upload", file_data, uploaded_file.name) + + if status_code == 200: + st.success("✅ Файл успешно загружен!") + st.json(result) + else: + st.error(f"❌ Ошибка загрузки: {result}") + + # Секция получения данных + st.subheader("📊 Получение данных") + + # Выбор формата отображения + display_format = st.radio( + "Формат отображения:", + ["JSON", "Таблица"], + key="monitoring_tar_display_format", + horizontal=True + ) + + # Выбор режима данных + mode = st.selectbox( + "Выберите режим данных:", + ["all", "total", "last_day"], + help="total - строки 'Всего' (агрегированные данные), last_day - последние строки данных, all - все данные", + key="monitoring_tar_mode" + ) + + if st.button("📊 Получить данные", key="monitoring_tar_get_data_btn"): + with st.spinner("Получаем данные..."): + # Выбираем эндпоинт в зависимости от режима + if mode == "all": + # Используем полный эндпоинт + result, status_code = make_api_request("/monitoring_tar/get_full_data", {}) + else: + # Используем фильтрованный эндпоинт + request_data = {"mode": mode} + result, status_code = make_api_request("/monitoring_tar/get_data", request_data) + + if status_code == 200 and result.get("success"): + st.success("✅ Данные успешно получены!") + + # Показываем данные + data = result.get("data", {}).get("value", {}) + if data: + st.subheader("📋 Результат:") + + # # Отладочная информация + # st.write(f"🔍 Тип данных: {type(data)}") + # if isinstance(data, str): + # st.write(f"🔍 Длина строки: {len(data)}") + # st.write(f"🔍 Первые 200 символов: {data[:200]}...") + + # Парсим данные, если они пришли как строка + if isinstance(data, str): + try: + import json + data = json.loads(data) + st.write("✅ JSON успешно распарсен") + except json.JSONDecodeError as e: + st.error(f"❌ Ошибка при парсинге JSON данных: {e}") + st.write("Сырые данные:", data) + return + + if display_format == "JSON": + # Отображаем как JSON + st.json(data) + else: + # Отображаем как таблицы + if isinstance(data, dict): + # Показываем данные по установкам + for installation_id, installation_data in data.items(): + with st.expander(f"🏭 {installation_id}"): + if isinstance(installation_data, dict): + # Показываем структуру данных + for data_type, type_data in installation_data.items(): + st.write(f"**{data_type}:**") + if isinstance(type_data, list) and type_data: + df = pd.DataFrame(type_data) + st.dataframe(df) + else: + st.write("Нет данных") + else: + st.write("Нет данных") + else: + st.json(data) + else: + st.info("📋 Нет данных для отображения") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + # Футер st.markdown("---") st.markdown("### 📚 Документация API") @@ -647,6 +754,7 @@ def main(): - 📊 Парсинг сводок ПМ (план и факт) - 🏭 Парсинг сводок СА - ⛽ Мониторинг топлива + - ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы) - 🔧 Управление ремонтными работами СА - 📋 Мониторинг статусов ремонта СА diff --git a/test_repair_ca.zip b/test_repair_ca.zip new file mode 100644 index 0000000..9971f25 Binary files /dev/null and b/test_repair_ca.zip differ