import os import multiprocessing import uvicorn import logging from typing import Dict, List from fastapi import FastAPI, File, UploadFile, HTTPException, status from fastapi.responses import JSONResponse # Настройка логирования logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(name)s:%(lineno)d - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) # Настройка логгера для модуля logger = logging.getLogger(__name__) from adapters.storage import MinIOStorageAdapter from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser from core.models import UploadRequest, DataRequest from core.services import ReportService, PARSERS from app.schemas import ( ServerInfoResponse, UploadResponse, UploadErrorResponse, SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest, SvodkaCARequest, MonitoringFuelMonthRequest, MonitoringFuelTotalRequest ) from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse from app.schemas.svodka_repair_ca import SvodkaRepairCARequest from app.schemas.statuses_repair_ca import StatusesRepairCARequest from app.schemas.monitoring_tar import MonitoringTarRequest, MonitoringTarFullRequest # Парсеры PARSERS.update({ 'svodka_pm': SvodkaPMParser, 'svodka_ca': SvodkaCAParser, 'monitoring_fuel': MonitoringFuelParser, 'monitoring_tar': MonitoringTarParser, 'svodka_repair_ca': SvodkaRepairCAParser, 'statuses_repair_ca': StatusesRepairCAParser, 'oper_spravka_tech_pos': OperSpravkaTechPosParser, # 'svodka_plan_sarnpz': SvodkaPlanSarnpzParser, }) # Адаптеры storage_adapter = MinIOStorageAdapter() def get_report_service() -> ReportService: return ReportService(storage_adapter) tags_metadata = [ { "name": "Общее", "display_name": "Общее", }, { "name": SvodkaPMParser.name, "description": "✅ Ready", }, { "name": SvodkaCAParser.name, "description": "✅ Ready", "display_name": "Сводка ПМ", }, { "name": MonitoringFuelParser.name, "description": "✅ Ready", "display_name": "Мониторинг топлива", }, # { # "name": MonitoringFuelParser.name, # "description": "⚠️ WORK IN PROGRESS", # }, ] app = FastAPI( title="NIN Excel Parsers API", description="API для парсинга сводок и работы с данными экселей НиН", version="1.0.0", openapi_tags=tags_metadata, ) @app.get("/", tags=["Общее"]) async def root(): return {"message": "Svodka Parser API", "version": "1.0.0"} @app.get("/parsers", tags=["Общее"], summary="Список доступных парсеров", description="Возвращает список идентификаторов всех доступных парсеров", response_model=Dict[str, List[str]], responses={ 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] # Для парсеров с данными в MinIO возвращаем ОГ из загруженных данных if parser_name in ["svodka_repair_ca", "oper_spravka_tech_pos"]: 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 данные возвращаются в формате словаря по ОГ if parser_name == "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} # Для oper_spravka_tech_pos данные возвращаются в формате списка elif parser_name == "oper_spravka_tech_pos": # Данные уже в правильном формате, возвращаем их if isinstance(loaded_data.data, list) and loaded_data.data: # Извлекаем уникальные ОГ из данных available_ogs = [] for item in loaded_data.data: if isinstance(item, dict) and 'id' in item: available_ogs.append(item['id']) if available_ogs: return {"parser": parser_name, "available_ogs": available_ogs} except Exception as e: logger.error(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="Возвращает информацию о доступных геттерах для указанного парсера", responses={ 200: { "content": { "application/json": { "example": { "parser": "svodka_pm", "getters": { "single_og": { "required_params": ["id", "codes", "columns"], "optional_params": ["search"], "description": "Получение данных по одному ОГ" }, "total_ogs": { "required_params": ["codes", "columns"], "optional_params": ["search"], "description": "Получение данных по всем ОГ" } } } } } }, 404: { "description": "Парсер не найден" } }) async def get_parser_getters(parser_name: str): """Получение информации о геттерах парсера""" if parser_name not in PARSERS: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Парсер '{parser_name}' не найден" ) parser_class = PARSERS[parser_name] parser_instance = parser_class() getters_info = parser_instance.get_available_getters() return { "parser": parser_name, "getters": getters_info } @app.get("/server-info", tags=["Общее"], summary="Информация о сервере", response_model=ServerInfoResponse,) async def server_info(): return { "process_id": os.getpid(), "parent_id": os.getppid(), "cpu_cores": multiprocessing.cpu_count(), "memory_mb": os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') / (1024. ** 2) } # @app.get("/svodka_pm/schema", tags=[SvodkaPMParser.name]) # async def get_svodka_pm_schema(): # """Получение схемы параметров для парсера сводок ПМ факта и плана""" # parser = PARSERS['svodka_pm']() # return parser.get_schema() # @app.get("/svodka_ca/schema", tags=[SvodkaCAParser.name]) # async def get_svodka_ca_schema(): # """Получение схемы параметров для парсера сводки СА""" # parser = PARSERS['svodka_ca']() # return parser.get_schema() # @app.get("/monitoring_fuel/schema", tags=[MonitoringFuelParser.name]) # async def get_monitoring_fuel_schema(): # """Получение схемы параметров для парсера мониторинга топлива""" # parser = PARSERS['monitoring_fuel']() # return parser.get_schema() @app.post("/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], summary="Загрузка файлов сводок ПМ одним ZIP-архивом", response_model=UploadResponse, responses={ 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} },) async def upload_svodka_pm_zip( zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") ): """Загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве** **Шаблоны названий файлов:** - Факт: `svodka_fact_pm_.xlsm` - План: `svodka_plan_pm_.xlsx` """ report_service = get_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 = 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_pm/upload", tags=[SvodkaPMParser.name]) # async def upload_svodka_pm( # file: UploadFile = File(...) # ): # report_service = get_report_service() # """ # Загрузка отчета сводки факта СарНПЗ # - file: Excel файл для загрузки # """ # try: # # Проверяем тип файла # if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): # raise HTTPException( # status_code=status.HTTP_400_BAD_REQUEST, # detail="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)" # ) # # Читаем содержимое файла # file_content = await file.read() # # Создаем запрос # request = UploadRequest( # report_type='svodka_pm', # file_content=file_content, # file_name=file.filename # ) # # Загружаем отчет # result = report_service.upload_report(request) # # print(result) # if result.success: # return { # "success": True, # "message": result.message, # "object_id": result.object_id # } # else: # raise HTTPException(status_code=500, detail=result.message) # except HTTPException: # raise # except Exception as e: # raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") @app.post("/svodka_pm/get_single_og", tags=[SvodkaPMParser.name], summary="Получение данных по одному ОГ") async def get_svodka_pm_single_og( request_data: SvodkaPMSingleOGRequest ): """Получение данных из сводок ПМ (факта и плана) по одному ОГ ### Структура параметров: - `id`: **Идентификатор МА** для запрашиваемого ОГ (обязательный) - `codes`: **Массив кодов** выбираемых строк (обязательный) - `columns`: **Массив названий** выбираемых столбцов (обязательный) - `search`: **Опциональный параметр** для фильтрации ("Итого" или null) ### Пример тела запроса: ```json { "id": "SNPZ", "codes": [78, 79], "columns": ["ПП", "СЭБ"] } ``` """ report_service = get_report_service() """ Получение данных из отчета сводки факта СарНПЗ - id: ID ОГ - codes: коды выбираемых строк [78, 79] - columns: выбираемые колонки ["БП", "СЭБ"] - search: "Итого" не обязательный """ try: # Создаем запрос request_dict = request_data.model_dump() request_dict['mode'] = 'single_og' request = DataRequest( report_type='svodka_pm', 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("/svodka_pm/get_total_ogs", tags=[SvodkaPMParser.name], summary="Получение данных по всем ОГ") async def get_svodka_pm_total_ogs( request_data: SvodkaPMTotalOGsRequest ): """Получение данных из сводок ПМ (факта и плана) по всем ОГ ### Структура параметров: - `codes`: **Массив кодов** выбираемых строк (обязательный) - `columns`: **Массив названий** выбираемых столбцов (обязательный) - `search`: **Опциональный параметр** для фильтрации ("Итого" или null) ### Пример тела запроса: ```json { "codes": [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], "columns": ["БП", "ПП", "СЭБ"] } ``` """ report_service = get_report_service() """ Получение данных из отчета сводки факта СарНПЗ - codes: коды выбираемых строк [78, 79] - columns: выбираемые колонки ["БП", "СЭБ"] - search: "Итого" """ try: # Создаем запрос request_dict = request_data.model_dump() request_dict['mode'] = 'total_ogs' request = DataRequest( report_type='svodka_pm', 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("/svodka_pm/get_data", tags=[SvodkaPMParser.name]) async def get_svodka_pm_data( request_data: dict ): report_service = get_report_service() """ Получение данных из отчета сводки факта СарНПЗ - indicator_id: ID индикатора - code: Код для поиска - search_value: Опциональное значение для поиска """ try: # Создаем запрос request = DataRequest( report_type='svodka_pm', get_params=request_data ) # Получаем данные 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("/svodka_ca/upload", tags=[SvodkaCAParser.name], summary="Загрузка файла отчета сводки СА", response_model=UploadResponse, responses={ 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} },) async def upload_svodka_ca( file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)") ): """ Загрузка и обработка Excel файла отчета сводки СА **Поддерживаемые форматы:** - Excel (.xlsx, .xlsm, .xls) """ report_service = get_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 = 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_ca/get_data", tags=[SvodkaCAParser.name], summary="Получение данных из отчета сводки СА") async def get_svodka_ca_data( request_data: SvodkaCARequest ): """ Получение данных из отчета сводки СА по указанным режимам и таблицам ### Структура параметров: - `modes`: **Массив кодов** режимов - `plan`, `fact` или `normativ` (обязательный) - `tables`: **Массив названий** таблиц как есть (обязательный) ### Пример тела запроса: ```json { "modes": ["plan", "fact"], "tables": ["ТиП, %", "Топливо итого, тонн", "Топливо итого, %", "Потери итого, тонн"] } ``` """ report_service = get_report_service() try: # Создаем запрос request_dict = request_data.model_dump() request = DataRequest( report_type='svodka_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("/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("/statuses_repair_ca/upload", tags=[StatusesRepairCAParser.name], summary="Загрузка отчета статусов ремонта СА") async def upload_statuses_repair_ca( file: UploadFile = File(...) ): """ Загрузка отчета статусов ремонта СА ### Поддерживаемые форматы: - **Excel файлы**: `.xlsx`, `.xlsm`, `.xls` - **ZIP архивы**: `.zip` (содержащие Excel файлы) ### Пример использования: ```bash curl -X POST "http://localhost:8000/statuses_repair_ca/upload" \ -H "accept: application/json" \ -H "Content-Type: multipart/form-data" \ -F "file=@statuses_repair_ca.xlsx" ``` """ report_service = get_report_service() try: # Проверяем тип файла if not file.filename.endswith(('.xlsx', '.xlsm', '.xls', '.zip')): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls) или архивы (.zip)" ) # Читаем содержимое файла file_content = await file.read() # Создаем запрос на загрузку upload_request = UploadRequest( report_type='statuses_repair_ca', 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("/statuses_repair_ca/get_data", tags=[StatusesRepairCAParser.name], summary="Получение данных из отчета статусов ремонта СА") async def get_statuses_repair_ca_data( request_data: StatusesRepairCARequest ): """ Получение данных из отчета статусов ремонта СА ### Структура параметров: - `ids`: **Массив ID ОГ** для фильтрации (опциональный) - `keys`: **Массив ключей** для извлечения данных (опциональный) ### Пример тела запроса: ```json { "ids": ["SNPZ", "KNPZ", "ANHK"], "keys": [ ["Дата начала ремонта"], ["Готовность к КР", "Факт"], ["Заключение договоров на СМР", "Договор", "%"] ] } ``` """ report_service = get_report_service() try: # Создаем запрос request_dict = request_data.model_dump() request = DataRequest( report_type='statuses_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(...), # directory_path: str = None # ): # report_service = get_report_service() # """ # Загрузка отчета мониторинга топлива # - file: Excel файл для загрузки (или архив с файлами) # - directory_path: Путь к директории с файлами (опционально) # """ # try: # # Проверяем тип файла # if not file.filename.endswith(('.xlsx', '.xlsm', '.xls', '.zip')): # raise HTTPException( # status_code=status.HTTP_400_BAD_REQUEST, # detail="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls) или архивы (.zip)" # ) # # Читаем содержимое файла # file_content = await file.read() # # Создаем параметры для парсинга # parse_params = {} # if directory_path: # parse_params['directory_path'] = directory_path # # Создаем запрос # request = UploadRequest( # report_type='monitoring_fuel', # file_content=file_content, # file_name=file.filename, # parse_params=parse_params # ) # # Загружаем отчет # result = report_service.upload_report(request) # if result.success: # return { # "success": True, # "message": result.message, # "object_id": result.object_id # } # else: # raise HTTPException(status_code=500, detail=result.message) # except HTTPException: # raise # except Exception as e: # raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") @app.post("/monitoring_fuel/get_data", tags=[MonitoringFuelParser.name]) async def get_monitoring_fuel_data( request_data: dict ): report_service = get_report_service() """ Получение данных из отчета мониторинга топлива - column: Название колонки для агрегации (normativ, total, total_svod) """ try: # Создаем запрос request = DataRequest( report_type='monitoring_fuel', get_params=request_data ) # Получаем данные 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_directory", tags=[MonitoringFuelParser.name]) # async def upload_monitoring_fuel_directory( # request_data: dict # ): # report_service = get_report_service() # """ # Загрузка отчета мониторинга топлива из директории # - directory_path: Путь к директории с файлами monitoring_SNPZ_*.xlsm # """ # try: # import os # import glob # # Извлекаем directory_path из request_data # directory_path = request_data.get('directory_path') # if not directory_path: # raise HTTPException( # status_code=status.HTTP_400_BAD_REQUEST, # detail="Параметр 'directory_path' обязателен" # ) # # Проверяем существование директории # if not os.path.exists(directory_path): # raise HTTPException( # status_code=status.HTTP_400_BAD_REQUEST, # detail=f"Директория не найдена: {directory_path}" # ) # # Проверяем наличие файлов # file_pattern = os.path.join(directory_path, "monitoring_SNPZ_*.xlsm") # files = glob.glob(file_pattern) # if not files: # raise HTTPException( # status_code=status.HTTP_400_BAD_REQUEST, # detail=f"Не найдены файлы по паттерну {file_pattern}" # ) # # Создаем параметры для парсинга # parse_params = { # 'directory_path': directory_path, # 'sheet_name': 'Мониторинг потребления', # 'search_value': 'Установка' # } # # Создаем запрос (используем пустой файл, так как парсим директорию) # request = UploadRequest( # report_type='monitoring_fuel', # file_content=b'', # Пустой контент, так как парсим директорию # file_name='directory_upload', # parse_params=parse_params # ) # # Загружаем отчет # result = report_service.upload_report(request) # if result.success: # return { # "success": True, # "message": result.message, # "object_id": result.object_id, # "files_processed": len(files) # } # else: # raise HTTPException(status_code=500, detail=result.message) # except HTTPException: # raise # except Exception as e: # raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") @app.post("/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], summary="Загрузка файлов сводок мониторинга топлива одним ZIP-архивом", response_model=UploadResponse, responses={ 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} },) async def upload_monitoring_fuel_zip( zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") ): """Загрузка файлов сводок мониторинга топлива по всем ОГ в **одном ZIP-архиве** **Шаблоны названий файлов:** - `monitoring_SNPZ_{MM}.xlsm`, `MM` - номер месяца с ведущим 0 """ report_service = get_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 = 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("/monitoring_fuel/get_total_by_columns", tags=[MonitoringFuelParser.name], summary="Получение данных по колонкам и расчёт средних значений") async def get_monitoring_fuel_total_by_columns( request_data: MonitoringFuelTotalRequest ): """Получение данных из сводок мониторинга топлива по колонкам и расчёт средних значений ### Структура параметров: - `columns`: **Массив названий** выбираемых столбцов (обязательный) ### Пример тела запроса: ```json { "columns": ["total", "normativ"] } ``` """ report_service = get_report_service() try: # Создаем запрос request_dict = request_data.model_dump() request_dict['mode'] = 'total_by_columns' request = DataRequest( report_type='monitoring_fuel', 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/get_month_by_code", tags=[MonitoringFuelParser.name], summary="Получение данных за месяц") async def get_monitoring_fuel_month_by_code( request_data: MonitoringFuelMonthRequest ): """Получение данных из сводок мониторинга топлива за указанный номер месяца ### Структура параметров: - `month`: **Номер месяца строкой с ведущим 0** (обязательный) ### Пример тела запроса: ```json { "month": "02" } ``` """ report_service = get_report_service() try: # Создаем запрос request_dict = request_data.model_dump() request_dict['mode'] = 'month_by_code' request = DataRequest( report_type='monitoring_fuel', 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)}") # ====== 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)}") # ====== OPER SPRAVKA TECH POS ENDPOINTS ====== @app.post("/oper_spravka_tech_pos/upload", tags=[OperSpravkaTechPosParser.name], summary="Загрузка отчета операционной справки технологических позиций") async def upload_oper_spravka_tech_pos( 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="oper_spravka_tech_pos", file_name=file.filename, file_content=file_content, parse_params={} ) # Загружаем и обрабатываем отчет result = report_service.upload_report(upload_request) if result.success: return UploadResponse( success=True, message="Отчет успешно загружен и обработан", object_id=result.object_id ) else: return UploadErrorResponse( success=False, message=result.message, error_code="ERR_UPLOAD", details=None ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") @app.post("/oper_spravka_tech_pos/get_data", tags=[OperSpravkaTechPosParser.name], summary="Получение данных операционной справки технологических позиций", response_model=OperSpravkaTechPosResponse) async def get_oper_spravka_tech_pos_data(request: OperSpravkaTechPosRequest): """Получение данных операционной справки технологических позиций по ОГ ### Параметры: - **id** (str): ID ОГ (например, 'SNPZ', 'KNPZ') ### Возвращает: - Данные по технологическим позициям для указанного ОГ - Включает информацию о процессах, установках, плане и факте """ report_service = get_report_service() try: # Создаем запрос на получение данных data_request = DataRequest( report_type="oper_spravka_tech_pos", get_params={"id": request.id} ) # Получаем данные result = report_service.get_data(data_request) if result.success: # Извлекаем данные из результата value_data = result.data.get("value", []) if isinstance(result.data.get("value"), list) else [] logger.debug(f"🔍 API возвращает данные: {type(value_data)}, длина: {len(value_data) if isinstance(value_data, (list, dict)) else 'N/A'}") return OperSpravkaTechPosResponse( success=True, data=value_data, message="Данные успешно получены" ) else: return OperSpravkaTechPosResponse( success=False, data=None, message=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)