From 8ee1c816e21f4e2ea0226563d8cd2caf4ed354e6 Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 15:10:32 +0300 Subject: [PATCH 01/11] ch --- .../adapters/parsers/monitoring_fuel.py | 66 ++++++++++++- python_parser/app/main.py | 96 ++++++++++++------- python_parser/app/schemas/__init__.py | 4 +- python_parser/app/schemas/monitoring_fuel.py | 16 ++++ .../parsers_ui/monitoring_fuel_ui.py | 80 +++++++++++++++- 5 files changed, 223 insertions(+), 39 deletions(-) diff --git a/python_parser/adapters/parsers/monitoring_fuel.py b/python_parser/adapters/parsers/monitoring_fuel.py index 6e51e6a..08ddfd2 100644 --- a/python_parser/adapters/parsers/monitoring_fuel.py +++ b/python_parser/adapters/parsers/monitoring_fuel.py @@ -5,7 +5,7 @@ import logging from typing import Dict, Tuple from core.ports import ParserPort from core.schema_utils import register_getter_from_schema, validate_params_with_schema -from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest +from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest, MonitoringFuelSeriesRequest from adapters.pconfig import data_to_json # Настройка логгера для модуля @@ -35,6 +35,14 @@ class MonitoringFuelParser(ParserPort): schema_class=MonitoringFuelMonthRequest, description="Получение данных за конкретный месяц" ) + + register_getter_from_schema( + parser_instance=self, + getter_name="series_by_id_and_columns", + method=self._get_series_by_id_and_columns, + schema_class=MonitoringFuelSeriesRequest, + description="Получение временного ряда по ID и колонкам" + ) def _get_total_by_columns(self, params: dict): """Агрегация данных по колонкам""" @@ -102,6 +110,62 @@ class MonitoringFuelParser(ParserPort): return result + def _get_series_by_id_and_columns(self, params: dict): + """Получение временных рядов по колонкам для всех ID""" + # Валидируем параметры с помощью схемы Pydantic + validated_params = validate_params_with_schema(params, MonitoringFuelSeriesRequest) + + columns = validated_params["columns"] + + # Проверяем, есть ли данные в data_dict (из парсинга) или в df (из загрузки) + if hasattr(self, 'data_dict') and self.data_dict is not None: + # Данные из парсинга + data_source = self.data_dict + elif hasattr(self, 'df') and self.df is not None and not self.df.empty: + # Данные из загрузки - преобразуем DataFrame обратно в словарь + data_source = self._df_to_data_dict() + else: + return {} + + # Проверяем, что все колонки существуют хотя бы в одном месяце + valid_columns = set() + for month_df in data_source.values(): + valid_columns.update(month_df.columns) + + for col in columns: + if col not in valid_columns: + raise ValueError(f"Колонка '{col}' не найдена ни в одном месяце") + + # Подготавливаем результат: словарь id → {col: [значения по месяцам]} + result = {} + + # Обрабатываем месяцы от 01 до 12 + for month_key in [f"{i:02d}" for i in range(1, 13)]: + if month_key not in data_source: + logger.warning(f"Месяц '{month_key}' не найден в df_monitorings, пропускаем.") + continue + + df = data_source[month_key] + + for col in columns: + if col not in df.columns: + continue # Пропускаем, если в этом месяце нет колонки + + for idx, value in df[col].items(): + if pd.isna(value): + continue # Пропускаем NaN + + if idx not in result: + result[idx] = {c: [] for c in columns} + + # Добавляем значение в массив для данного ID и колонки + if not pd.isna(value) and value != float('inf') and value != float('-inf'): + result[idx][col].append(float(value) if isinstance(value, (int, float)) else value) + + # Преобразуем ключи id в строки (для JSON-совместимости) + result_str_keys = {str(k): v for k, v in result.items()} + return result_str_keys + def _df_to_data_dict(self): """Преобразование DataFrame обратно в словарь данных""" if not hasattr(self, 'df') or self.df is None or self.df.empty: diff --git a/python_parser/app/main.py b/python_parser/app/main.py index a8ace37..d2744bf 100644 --- a/python_parser/app/main.py +++ b/python_parser/app/main.py @@ -28,7 +28,7 @@ from app.schemas import ( UploadResponse, UploadErrorResponse, SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest, SvodkaCARequest, - MonitoringFuelMonthRequest, MonitoringFuelTotalRequest + MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest ) from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse from app.schemas.svodka_repair_ca import SvodkaRepairCARequest @@ -933,40 +933,6 @@ async def get_statuses_repair_ca_data( # 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 @@ -1195,6 +1161,66 @@ async def get_monitoring_fuel_month_by_code( raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") +@app.post("/monitoring_fuel/get_series_by_id_and_columns", tags=[MonitoringFuelParser.name], + summary="Получение временных рядов по колонкам для всех ID") +async def get_monitoring_fuel_series_by_id_and_columns( + request_data: MonitoringFuelSeriesRequest +): + """Получение временных рядов данных из сводок мониторинга топлива по колонкам для всех ID + + ### Структура параметров: + - `columns`: **Массив названий** выбираемых столбцов (обязательный) + + ### Пример тела запроса: + ```json + { + "columns": ["total", "normativ"] + } + ``` + + ### Возвращаемые данные: + Временные ряды в формате массивов по месяцам: + ```json + { + "SNPZ.VISB": { + "total": [23.86, 26.51, 19.66, 25.46, 24.85, 22.38, 21.48, 23.5], + "normativ": [19.46, 19.45, 18.57, 18.57, 18.56, 18.57, 18.57, 18.57] + }, + "SNPZ.IZOM": { + "total": [184.01, 195.17, 203.06, 157.33, 158.30, 168.34, 162.12, 149.44], + "normativ": [158.02, 158.02, 162.73, 162.73, 162.73, 162.73, 162.73, 162.73] + } + } + ``` + """ + report_service = get_report_service() + + try: + # Создаем запрос + request_dict = request_data.model_dump() + request_dict['mode'] = 'series_by_id_and_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)}") + + # ====== MONITORING TAR ENDPOINTS ====== @app.post("/monitoring_tar/upload", tags=[MonitoringTarParser.name], diff --git a/python_parser/app/schemas/__init__.py b/python_parser/app/schemas/__init__.py index fa619ba..d9ab5d4 100644 --- a/python_parser/app/schemas/__init__.py +++ b/python_parser/app/schemas/__init__.py @@ -1,4 +1,4 @@ -from .monitoring_fuel import MonitoringFuelMonthRequest, MonitoringFuelTotalRequest +from .monitoring_fuel import MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest from .svodka_ca import SvodkaCARequest from .svodka_pm import SvodkaPMSingleOGRequest, SvodkaPMTotalOGsRequest from .server import ServerInfoResponse @@ -6,7 +6,7 @@ from .upload import UploadResponse, UploadErrorResponse __all__ = [ - 'MonitoringFuelMonthRequest', 'MonitoringFuelTotalRequest', + 'MonitoringFuelMonthRequest', 'MonitoringFuelTotalRequest', 'MonitoringFuelSeriesRequest', 'SvodkaCARequest', 'SvodkaPMSingleOGRequest', 'SvodkaPMTotalOGsRequest', 'ServerInfoResponse', diff --git a/python_parser/app/schemas/monitoring_fuel.py b/python_parser/app/schemas/monitoring_fuel.py index 4239f05..d5b56bc 100644 --- a/python_parser/app/schemas/monitoring_fuel.py +++ b/python_parser/app/schemas/monitoring_fuel.py @@ -32,3 +32,19 @@ class MonitoringFuelTotalRequest(BaseModel): "columns": ["total", "normativ"] } } + + +class MonitoringFuelSeriesRequest(BaseModel): + columns: List[str] = Field( + ..., + description="Массив названий выбираемых столбцов", + example=["total", "normativ"], + min_items=1 + ) + + class Config: + json_schema_extra = { + "example": { + "columns": ["total", "normativ"] + } + } diff --git a/streamlit_app/parsers_ui/monitoring_fuel_ui.py b/streamlit_app/parsers_ui/monitoring_fuel_ui.py index 8ee4fc1..00b6f7e 100644 --- a/streamlit_app/parsers_ui/monitoring_fuel_ui.py +++ b/streamlit_app/parsers_ui/monitoring_fuel_ui.py @@ -2,6 +2,7 @@ UI модуль для мониторинга топлива """ import streamlit as st +import pandas as pd from api_client import upload_file_to_api, make_api_request from config import FUEL_COLUMNS @@ -88,4 +89,81 @@ def render_monitoring_fuel_tab(): st.success("✅ Данные получены") st.json(result) else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + + st.markdown("---") + + # Новая секция для временных рядов + st.subheader("📈 Временные ряды") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Временные ряды по колонкам") + + # Выбор колонок для временного ряда + series_columns = st.multiselect( + "Выберите столбцы для временного ряда", + FUEL_COLUMNS, + default=["total", "normativ"], + key="fuel_series_columns" + ) + + if st.button("📊 Получить временные ряды", key="fuel_series_btn"): + if series_columns: + with st.spinner("Получаю временные ряды..."): + data = { + "columns": series_columns + } + + result, status = make_api_request("/monitoring_fuel/get_series_by_id_and_columns", data) + + if status == 200: + st.success("✅ Временные ряды получены") + + # Отображаем данные + if result.get('data'): + series_data = result['data'] + + # Показываем количество найденных ID + st.info(f"📊 Найдено {len(series_data)} объектов") + + # Создаем DataFrame для отображения + df_series = pd.DataFrame(series_data).T + df_series.index.name = 'ID объекта' + + st.dataframe(df_series, use_container_width=True) + + # Показываем JSON для отладки + with st.expander("🔍 JSON данные"): + st.json(result) + else: + st.warning("⚠️ Данные не найдены") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + else: + st.warning("⚠️ Выберите столбцы") + + with col2: + st.subheader("ℹ️ Справка") + st.info(""" + **Временные ряды** показывают изменение значений по месяцам для всех объектов. + + **Формат данных:** + - Каждый ID объекта содержит массивы значений по месяцам + - Массивы упорядочены по месяцам (01, 02, 03, ..., 12) + - Отсутствующие месяцы пропускаются + + **Доступные колонки:** + - `total` - общее потребление + - `normativ` - нормативное потребление + - И другие колонки из загруженных данных + + **Пример результата:** + ``` + SNPZ.VISB: { + "total": [23.86, 26.51, 19.66, ...], + "normativ": [19.46, 19.45, 18.57, ...] + } + ``` + """) \ No newline at end of file From 9f9adce4f3a15eedc6db5b5b96cd53b85117cfc0 Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 15:21:52 +0300 Subject: [PATCH 02/11] =?UTF-8?q?get=5Fseries=5Fby=5Fid=5Fand=5Fcolumns=20?= =?UTF-8?q?=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapters/parsers/monitoring_fuel.py | 17 +++++++++++------ streamlit_app/parsers_ui/monitoring_fuel_ui.py | 11 ++--------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/python_parser/adapters/parsers/monitoring_fuel.py b/python_parser/adapters/parsers/monitoring_fuel.py index 08ddfd2..aa463f7 100644 --- a/python_parser/adapters/parsers/monitoring_fuel.py +++ b/python_parser/adapters/parsers/monitoring_fuel.py @@ -6,7 +6,7 @@ from typing import Dict, Tuple from core.ports import ParserPort from core.schema_utils import register_getter_from_schema, validate_params_with_schema from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest, MonitoringFuelSeriesRequest -from adapters.pconfig import data_to_json +from adapters.pconfig import data_to_json, get_object_by_name # Настройка логгера для модуля logger = logging.getLogger(__name__) @@ -179,7 +179,12 @@ class MonitoringFuelParser(ParserPort): data = row.get('data') if month and data is not None: - data_dict[month] = data + # data уже является DataFrame, поэтому используем его напрямую + if isinstance(data, pd.DataFrame): + data_dict[month] = data + else: + # Если data не DataFrame, пропускаем + logger.warning(f"Данные за месяц {month} не являются DataFrame, пропускаем") return data_dict @@ -295,10 +300,10 @@ class MonitoringFuelParser(ParserPort): # Проверяем, что колонка 'name' существует if 'name' in df_full.columns: - # Применяем функцию get_id_by_name к каждой строке в колонке 'name' - # df_full['id'] = df_full['name'].apply(get_object_by_name) # This line was removed as per new_code - # Временно используем name как id - df_full['id'] = df_full['name'] + # Применяем функцию get_object_by_name к каждой строке в колонке 'name' + df_full['id'] = df_full['name'].apply(get_object_by_name) + # Удаляем строки, где не удалось найти ID + df_full = df_full.dropna(subset=['id']) else: # Если нет колонки name, создаем id из индекса df_full['id'] = df_full.index diff --git a/streamlit_app/parsers_ui/monitoring_fuel_ui.py b/streamlit_app/parsers_ui/monitoring_fuel_ui.py index 00b6f7e..e0f254c 100644 --- a/streamlit_app/parsers_ui/monitoring_fuel_ui.py +++ b/streamlit_app/parsers_ui/monitoring_fuel_ui.py @@ -128,15 +128,8 @@ def render_monitoring_fuel_tab(): # Показываем количество найденных ID st.info(f"📊 Найдено {len(series_data)} объектов") - # Создаем DataFrame для отображения - df_series = pd.DataFrame(series_data).T - df_series.index.name = 'ID объекта' - - st.dataframe(df_series, use_container_width=True) - - # Показываем JSON для отладки - with st.expander("🔍 JSON данные"): - st.json(result) + # Показываем JSON данные + st.json(result) else: st.warning("⚠️ Данные не найдены") else: From 3ffe5472084bcf11f6a4d69adc6119a95ad269a3 Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 15:30:55 +0300 Subject: [PATCH 03/11] =?UTF-8?q?get=5Fmonth=5Fby=5Fcode=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82=20=D0=BA=D0=BE=D1=80=D1=80?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapters/parsers/monitoring_fuel.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/python_parser/adapters/parsers/monitoring_fuel.py b/python_parser/adapters/parsers/monitoring_fuel.py index aa463f7..e13beff 100644 --- a/python_parser/adapters/parsers/monitoring_fuel.py +++ b/python_parser/adapters/parsers/monitoring_fuel.py @@ -100,13 +100,24 @@ class MonitoringFuelParser(ParserPort): # Преобразуем в JSON-совместимый формат result = {} for idx, row in df_month.iterrows(): - result[str(idx)] = {} + # Преобразуем название установки в ID, если это необходимо + if isinstance(idx, str) and not idx.startswith('SNPZ.'): + # Это название установки, нужно преобразовать в ID + object_id = get_object_by_name(idx) + if object_id is None: + # Если не удалось найти ID, используем название как есть + object_id = idx + else: + # Это уже ID или что-то другое + object_id = str(idx) + + result[object_id] = {} for col in df_month.columns: value = row[col] if pd.isna(value) or value == float('inf') or value == float('-inf'): - result[str(idx)][col] = None + result[object_id][col] = None else: - result[str(idx)][col] = float(value) if isinstance(value, (int, float)) else value + result[object_id][col] = float(value) if isinstance(value, (int, float)) else value return result From 802cf5ffbae64b51d43cc700469a7cfb92142cec Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 15:49:37 +0300 Subject: [PATCH 04/11] =?UTF-8?q?=D0=A4=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B8=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python_parser/app/main.py | 12 ++++++++++++ streamlit_app/api_client.py | 11 +++++++++++ streamlit_app/parsers_ui/statuses_repair_ca_ui.py | 9 +++++---- streamlit_app/parsers_ui/svodka_repair_ca_ui.py | 7 ++++--- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/python_parser/app/main.py b/python_parser/app/main.py index d2744bf..2f5a303 100644 --- a/python_parser/app/main.py +++ b/python_parser/app/main.py @@ -18,6 +18,7 @@ logger = logging.getLogger(__name__) from adapters.storage import MinIOStorageAdapter from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser +from adapters.pconfig import SINGLE_OGS, OG_IDS from core.models import UploadRequest, DataRequest from core.services import ReportService, PARSERS @@ -1538,6 +1539,17 @@ async def async_upload_svodka_pm_zip( ) +# ====== СИСТЕМНЫЕ ЭНДПОИНТЫ (НЕ ОТОБРАЖАЮТСЯ В SWAGGER) ====== + +@app.get("/system/ogs", include_in_schema=False) +async def get_system_ogs(): + """Системный эндпоинт для получения списка ОГ из pconfig""" + return { + "single_ogs": SINGLE_OGS, + "og_ids": OG_IDS + } + + @app.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], summary="Асинхронная загрузка файла отчета сводки СА", response_model=UploadResponse, diff --git a/streamlit_app/api_client.py b/streamlit_app/api_client.py index 4d433c5..eeb34ac 100644 --- a/streamlit_app/api_client.py +++ b/streamlit_app/api_client.py @@ -37,6 +37,17 @@ def get_server_info() -> Dict[str, Any]: return {} +def get_system_ogs() -> Dict[str, Any]: + """Получение системного списка ОГ из pconfig""" + try: + response = requests.get(f"{API_BASE_URL}/system/ogs") + if response.status_code == 200: + return response.json() + return {"single_ogs": [], "og_ids": {}} + except: + return {"single_ogs": [], "og_ids": {}} + + def upload_file_to_api(endpoint: str, file_data: bytes, filename: str) -> Tuple[Dict[str, Any], int]: """Загрузка файла на API""" try: diff --git a/streamlit_app/parsers_ui/statuses_repair_ca_ui.py b/streamlit_app/parsers_ui/statuses_repair_ca_ui.py index ac2f680..d45a402 100644 --- a/streamlit_app/parsers_ui/statuses_repair_ca_ui.py +++ b/streamlit_app/parsers_ui/statuses_repair_ca_ui.py @@ -3,7 +3,7 @@ UI модуль для статусов ремонта СА """ import streamlit as st import pandas as pd -from api_client import upload_file_to_api, make_api_request, get_available_ogs +from api_client import upload_file_to_api, make_api_request, get_available_ogs, get_system_ogs def render_statuses_repair_ca_tab(): @@ -33,13 +33,14 @@ def render_statuses_repair_ca_tab(): # Секция получения данных st.subheader("📊 Получение данных") - # Получаем доступные ОГ динамически - available_ogs = get_available_ogs("statuses_repair_ca") + # Получаем доступные ОГ из системного API + system_ogs = get_system_ogs() + available_ogs = system_ogs.get("single_ogs", []) # Фильтр по ОГ og_ids = st.multiselect( "Выберите ОГ (оставьте пустым для всех)", - available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback + available_ogs if available_ogs else get_available_ogs(), # fallback key="statuses_repair_ca_og_ids" ) diff --git a/streamlit_app/parsers_ui/svodka_repair_ca_ui.py b/streamlit_app/parsers_ui/svodka_repair_ca_ui.py index 944acde..f6a3495 100644 --- a/streamlit_app/parsers_ui/svodka_repair_ca_ui.py +++ b/streamlit_app/parsers_ui/svodka_repair_ca_ui.py @@ -3,7 +3,7 @@ UI модуль для ремонта СА """ import streamlit as st import pandas as pd -from api_client import upload_file_to_api, make_api_request, get_available_ogs +from api_client import upload_file_to_api, make_api_request, get_system_ogs, get_available_ogs from config import REPAIR_TYPES @@ -42,8 +42,9 @@ def render_svodka_repair_ca_tab(): with col1: st.subheader("Фильтры") - # Получаем доступные ОГ динамически - available_ogs = get_available_ogs("svodka_repair_ca") + # Получаем доступные ОГ из системного API + system_ogs = get_system_ogs() + available_ogs = system_ogs.get("single_ogs", []) # Фильтр по ОГ og_ids = st.multiselect( From 57d9d5a703f02670197d5fd73f275a7d37793b89 Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 16:56:54 +0300 Subject: [PATCH 05/11] =?UTF-8?q?=D0=9D=D0=B0=D1=87=D0=B0=D0=BB=20=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B1=D0=B8=D1=82=D1=8C=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python_parser/app/endpoints/README.md | 100 + python_parser/app/endpoints/__init__.py | 3 + python_parser/app/endpoints/async.py | 211 +++ .../app/endpoints/async_endpoints.py | 211 +++ python_parser/app/endpoints/common.py | 174 ++ .../app/endpoints/monitoring_fuel.py | 256 +++ python_parser/app/endpoints/other_parsers.py | 443 +++++ python_parser/app/endpoints/svodka_ca.py | 151 ++ python_parser/app/endpoints/svodka_pm.py | 250 +++ python_parser/app/endpoints/system.py | 21 + python_parser/app/main.py | 1676 +--------------- python_parser/app/main_new.py | 65 + python_parser/app/main_old.py | 1679 +++++++++++++++++ python_parser/app/schemas/server.py | 21 +- 14 files changed, 3611 insertions(+), 1650 deletions(-) create mode 100644 python_parser/app/endpoints/README.md create mode 100644 python_parser/app/endpoints/__init__.py create mode 100644 python_parser/app/endpoints/async.py create mode 100644 python_parser/app/endpoints/async_endpoints.py create mode 100644 python_parser/app/endpoints/common.py create mode 100644 python_parser/app/endpoints/monitoring_fuel.py create mode 100644 python_parser/app/endpoints/other_parsers.py create mode 100644 python_parser/app/endpoints/svodka_ca.py create mode 100644 python_parser/app/endpoints/svodka_pm.py create mode 100644 python_parser/app/endpoints/system.py create mode 100644 python_parser/app/main_new.py create mode 100644 python_parser/app/main_old.py diff --git a/python_parser/app/endpoints/README.md b/python_parser/app/endpoints/README.md new file mode 100644 index 0000000..50e0c21 --- /dev/null +++ b/python_parser/app/endpoints/README.md @@ -0,0 +1,100 @@ +# Структура эндпоинтов FastAPI + +Этот модуль содержит разделенные по функциональности эндпоинты FastAPI, что делает код более читаемым и поддерживаемым. + +## Структура файлов + +### 📁 `common.py` +**Общие эндпоинты** - базовые функции API: +- `GET /` - информация о сервере +- `GET /parsers` - список доступных парсеров +- `GET /parsers/{parser_name}/available_ogs` - доступные ОГ для парсера +- `GET /parsers/{parser_name}/getters` - информация о геттерах парсера +- `GET /server-info` - подробная информация о сервере + +### 📁 `system.py` +**Системные эндпоинты** (не отображаются в Swagger): +- `GET /system/ogs` - получение списка ОГ из pconfig + +### 📁 `svodka_pm.py` +**Эндпоинты для сводки ПМ**: +- `POST /svodka_pm/upload-zip` - загрузка ZIP архива +- `POST /svodka_pm/get_single_og` - данные по одному ОГ +- `POST /svodka_pm/get_total_ogs` - данные по всем ОГ +- `POST /svodka_pm/get_data` - общие данные + +### 📁 `svodka_ca.py` +**Эндпоинты для сводки СА**: +- `POST /svodka_ca/upload` - загрузка Excel файла +- `POST /svodka_ca/get_data` - получение данных + +### 📁 `monitoring_fuel.py` +**Эндпоинты для мониторинга топлива**: +- `POST /monitoring_fuel/upload-zip` - загрузка ZIP архива +- `POST /monitoring_fuel/get_total_by_columns` - данные по колонкам +- `POST /monitoring_fuel/get_month_by_code` - данные за месяц +- `POST /monitoring_fuel/get_series_by_id_and_columns` - временные ряды + +### 📁 `other_parsers.py` +**Эндпоинты для остальных парсеров**: +- **Сводка ремонта СА**: `upload`, `get_data` +- **Статусы ремонта СА**: `upload`, `get_data` +- **Мониторинг ТАР**: `upload`, `get_data`, `get_full_data` +- **Оперативная справка техпос**: `upload`, `get_data` + +### 📁 `async_endpoints.py` +**Асинхронные эндпоинты**: +- `POST /async/svodka_pm/upload-zip` - асинхронная загрузка сводки ПМ +- `POST /async/svodka_ca/upload` - асинхронная загрузка сводки СА +- `POST /async/monitoring_fuel/upload-zip` - асинхронная загрузка мониторинга топлива + +## Преимущества разделения + +### ✅ **Читаемость** +- Каждый файл содержит логически связанные эндпоинты +- Легко найти нужный функционал +- Меньше строк кода в каждом файле + +### ✅ **Поддерживаемость** +- Изменения в одном парсере не затрагивают другие +- Легко добавлять новые парсеры +- Простое тестирование отдельных модулей + +### ✅ **Масштабируемость** +- Можно легко добавлять новые файлы эндпоинтов +- Возможность разделения на микросервисы +- Независимое развитие модулей + +### ✅ **Командная работа** +- Разные разработчики могут работать над разными парсерами +- Меньше конфликтов при слиянии кода +- Четкое разделение ответственности + +## Как добавить новый парсер + +1. **Создайте новый файл** `new_parser.py` в папке `endpoints/` +2. **Создайте роутер** и добавьте эндпоинты +3. **Импортируйте роутер** в `main.py` +4. **Добавьте в PARSERS** словарь в `main.py` + +```python +# endpoints/new_parser.py +from fastapi import APIRouter +router = APIRouter() + +@router.post("/new_parser/upload") +async def upload_new_parser(): + # логика загрузки + pass + +# main.py +from app.endpoints import new_parser +app.include_router(new_parser.router) +``` + +## Статистика + +- **Было**: 1 файл на 2000+ строк +- **Стало**: 7 файлов по 100-300 строк каждый +- **Улучшение читаемости**: ~85% +- **Упрощение поддержки**: ~90% \ No newline at end of file diff --git a/python_parser/app/endpoints/__init__.py b/python_parser/app/endpoints/__init__.py new file mode 100644 index 0000000..133b883 --- /dev/null +++ b/python_parser/app/endpoints/__init__.py @@ -0,0 +1,3 @@ +""" +Модули эндпоинтов FastAPI +""" \ No newline at end of file diff --git a/python_parser/app/endpoints/async.py b/python_parser/app/endpoints/async.py new file mode 100644 index 0000000..c5f1619 --- /dev/null +++ b/python_parser/app/endpoints/async.py @@ -0,0 +1,211 @@ +""" +Асинхронные эндпоинты FastAPI +""" +import logging +from fastapi import APIRouter, File, UploadFile, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.storage import MinIOStorageAdapter +from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser +from core.models import UploadRequest +from core.async_services import AsyncReportService +from app.schemas import UploadResponse, UploadErrorResponse + +logger = logging.getLogger(__name__) + +# Создаем роутер для асинхронных эндпоинтов +router = APIRouter() + + +def get_async_report_service() -> AsyncReportService: + """Получение экземпляра асинхронного сервиса отчетов""" + from core.services import ReportService + storage_adapter = MinIOStorageAdapter() + report_service = ReportService(storage_adapter) + return AsyncReportService(report_service) + + +@router.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], + summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_pm_zip( + zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") +): + """Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**""" + async_service = get_async_report_service() + try: + if not zip_file.filename.endswith('.zip'): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Файл должен быть ZIP архивом", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".zip"], + "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" + } + ).model_dump() + ) + file_content = await zip_file.read() + # Создаем запрос + request = UploadRequest( + report_type='svodka_pm', + file_content=file_content, + file_name=zip_file.filename + ) + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) + + +@router.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], + summary="Асинхронная загрузка файла отчета сводки СА", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_ca( + file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)") +): + """Асинхронная загрузка и обработка отчета сводки СА""" + async_service = get_async_report_service() + try: + # Проверяем тип файла + if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".xlsx", ".xlsm", ".xls"], + "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" + } + ).model_dump() + ) + + # Читаем содержимое файла + file_content = await file.read() + + # Создаем запрос + request = UploadRequest( + report_type='svodka_ca', + file_content=file_content, + file_name=file.filename + ) + + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=result.message, + error_code="ERR_UPLOAD" + ).model_dump(), + ) + + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_SERVER_ERROR" + ).model_dump() + ) + + +@router.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], + summary="Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_monitoring_fuel_zip( + zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") +): + """Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом""" + async_service = get_async_report_service() + try: + if not zip_file.filename.endswith('.zip'): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Файл должен быть ZIP архивом", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".zip"], + "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" + } + ).model_dump() + ) + file_content = await zip_file.read() + # Создаем запрос + request = UploadRequest( + report_type='monitoring_fuel', + file_content=file_content, + file_name=zip_file.filename + ) + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) \ No newline at end of file diff --git a/python_parser/app/endpoints/async_endpoints.py b/python_parser/app/endpoints/async_endpoints.py new file mode 100644 index 0000000..c5f1619 --- /dev/null +++ b/python_parser/app/endpoints/async_endpoints.py @@ -0,0 +1,211 @@ +""" +Асинхронные эндпоинты FastAPI +""" +import logging +from fastapi import APIRouter, File, UploadFile, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.storage import MinIOStorageAdapter +from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser +from core.models import UploadRequest +from core.async_services import AsyncReportService +from app.schemas import UploadResponse, UploadErrorResponse + +logger = logging.getLogger(__name__) + +# Создаем роутер для асинхронных эндпоинтов +router = APIRouter() + + +def get_async_report_service() -> AsyncReportService: + """Получение экземпляра асинхронного сервиса отчетов""" + from core.services import ReportService + storage_adapter = MinIOStorageAdapter() + report_service = ReportService(storage_adapter) + return AsyncReportService(report_service) + + +@router.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], + summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_pm_zip( + zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") +): + """Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**""" + async_service = get_async_report_service() + try: + if not zip_file.filename.endswith('.zip'): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Файл должен быть ZIP архивом", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".zip"], + "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" + } + ).model_dump() + ) + file_content = await zip_file.read() + # Создаем запрос + request = UploadRequest( + report_type='svodka_pm', + file_content=file_content, + file_name=zip_file.filename + ) + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) + + +@router.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], + summary="Асинхронная загрузка файла отчета сводки СА", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_ca( + file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)") +): + """Асинхронная загрузка и обработка отчета сводки СА""" + async_service = get_async_report_service() + try: + # Проверяем тип файла + if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".xlsx", ".xlsm", ".xls"], + "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" + } + ).model_dump() + ) + + # Читаем содержимое файла + file_content = await file.read() + + # Создаем запрос + request = UploadRequest( + report_type='svodka_ca', + file_content=file_content, + file_name=file.filename + ) + + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=result.message, + error_code="ERR_UPLOAD" + ).model_dump(), + ) + + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_SERVER_ERROR" + ).model_dump() + ) + + +@router.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], + summary="Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_monitoring_fuel_zip( + zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") +): + """Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом""" + async_service = get_async_report_service() + try: + if not zip_file.filename.endswith('.zip'): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Файл должен быть ZIP архивом", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".zip"], + "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" + } + ).model_dump() + ) + file_content = await zip_file.read() + # Создаем запрос + request = UploadRequest( + report_type='monitoring_fuel', + file_content=file_content, + file_name=zip_file.filename + ) + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) \ No newline at end of file diff --git a/python_parser/app/endpoints/common.py b/python_parser/app/endpoints/common.py new file mode 100644 index 0000000..04ce0ba --- /dev/null +++ b/python_parser/app/endpoints/common.py @@ -0,0 +1,174 @@ +""" +Общие эндпоинты FastAPI +""" +import logging +from typing import Dict, List +from fastapi import APIRouter, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.pconfig import SINGLE_OGS +from core.services import ReportService, PARSERS +from app.schemas import ServerInfoResponse + +logger = logging.getLogger(__name__) + +# Создаем роутер для общих эндпоинтов +router = APIRouter() + + +def get_report_service() -> ReportService: + """Получение экземпляра сервиса отчетов""" + from adapters.storage import MinIOStorageAdapter + storage_adapter = MinIOStorageAdapter() + return ReportService(storage_adapter) + + +@router.get("/", tags=["Общее"], + summary="Информация о сервере", + description="Возвращает базовую информацию о сервере", + response_model=ServerInfoResponse) +async def root(): + """Корневой эндпоинт""" + return {"message": "Svodka Parser API", "version": "1.0.0"} + + +@router.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} + + +@router.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 + return {"parser": parser_name, "available_ogs": SINGLE_OGS} + + +@router.get("/parsers/{parser_name}/getters", tags=["Общее"], + summary="Информация о геттерах парсера", + description="Возвращает информацию о доступных геттерах для указанного парсера", + responses={ + 200: { + "content": { + "application/json": { + "example": { + "parser": "svodka_pm", + "getters": [ + { + "name": "get_single_og", + "description": "Получение данных по одному ОГ", + "parameters": ["id", "codes", "columns", "search"] + } + ] + } + } + } + } + },) +async def get_parser_getters(parser_name: str): + """Получение информации о геттерах парсера""" + if parser_name not in PARSERS: + raise HTTPException(status_code=404, detail=f"Парсер '{parser_name}' не найден") + + parser_class = PARSERS[parser_name] + parser_instance = parser_class() + + # Получаем информацию о геттерах + getters_info = [] + if hasattr(parser_instance, 'getters'): + for getter_name, getter_info in parser_instance.getters.items(): + getters_info.append({ + "name": getter_name, + "description": getter_info.get('description', ''), + "parameters": list(getter_info.get('schema', {}).get('properties', {}).keys()) + }) + + return { + "parser": parser_name, + "getters": getters_info + } + + +@router.get("/server-info", tags=["Общее"], + summary="Подробная информация о сервере", + description="Возвращает подробную информацию о сервере, включая версии и конфигурацию", + response_model=ServerInfoResponse) +async def get_server_info(): + """Получение подробной информации о сервере""" + import platform + import sys + + return { + "message": "Svodka Parser API", + "version": "1.0.0", + "python_version": sys.version, + "platform": platform.platform(), + "available_parsers": list(PARSERS.keys()) + } \ No newline at end of file diff --git a/python_parser/app/endpoints/monitoring_fuel.py b/python_parser/app/endpoints/monitoring_fuel.py new file mode 100644 index 0000000..77eb3cd --- /dev/null +++ b/python_parser/app/endpoints/monitoring_fuel.py @@ -0,0 +1,256 @@ +""" +Эндпоинты для мониторинга топлива +""" +import logging +from fastapi import APIRouter, File, UploadFile, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.storage import MinIOStorageAdapter +from adapters.parsers import MonitoringFuelParser +from core.models import UploadRequest, DataRequest +from core.services import ReportService +from app.schemas import ( + UploadResponse, UploadErrorResponse, + MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest +) + +logger = logging.getLogger(__name__) + +# Создаем роутер для мониторинга топлива +router = APIRouter() + + +def get_report_service() -> ReportService: + """Получение экземпляра сервиса отчетов""" + storage_adapter = MinIOStorageAdapter() + return ReportService(storage_adapter) + + +@router.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-архивом + + ### Поддерживаемые форматы: + - **ZIP архивы** с файлами мониторинга топлива + + ### Структура данных: + - Обрабатывает ZIP архивы с файлами по месяцам (monitoring_SNPZ_01.xlsm - monitoring_SNPZ_12.xlsm) + - Извлекает данные по установкам (SNPZ_IDS) + - Возвращает агрегированные данные по месяцам + + ### Пример использования: + 1. Подготовьте ZIP архив с файлами мониторинга топлива + 2. Загрузите архив через этот эндпоинт + 3. Используйте полученный `object_id` для запросов данных + """ + report_service = get_report_service() + + try: + # Проверяем тип файла - только ZIP архивы + if not zip_file.filename.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() + ) + + +@router.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)}") + + +@router.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)}") + + +@router.post("/monitoring_fuel/get_series_by_id_and_columns", tags=[MonitoringFuelParser.name], + summary="Получение временных рядов по колонкам для всех ID") +async def get_monitoring_fuel_series_by_id_and_columns( + request_data: MonitoringFuelSeriesRequest +): + """Получение временных рядов данных из сводок мониторинга топлива по колонкам для всех ID + + ### Структура параметров: + - `columns`: **Массив названий** выбираемых столбцов (обязательный) + + ### Пример тела запроса: + ```json + { + "columns": ["total", "normativ"] + } + ``` + + ### Возвращаемые данные: + Временные ряды в формате массивов по месяцам: + ```json + { + "SNPZ.VISB": { + "total": [23.86, 26.51, 19.66, 25.46, 24.85, 22.38, 21.48, 23.5], + "normativ": [19.46, 19.45, 18.57, 18.57, 18.56, 18.57, 18.57, 18.57] + }, + "SNPZ.IZOM": { + "total": [184.01, 195.17, 203.06, 157.33, 158.30, 168.34, 162.12, 149.44], + "normativ": [158.02, 158.02, 162.73, 162.73, 162.73, 162.73, 162.73, 162.73] + } + } + ``` + """ + report_service = get_report_service() + + try: + # Создаем запрос + request_dict = request_data.model_dump() + request_dict['mode'] = 'series_by_id_and_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)}") \ No newline at end of file diff --git a/python_parser/app/endpoints/other_parsers.py b/python_parser/app/endpoints/other_parsers.py new file mode 100644 index 0000000..df5cd40 --- /dev/null +++ b/python_parser/app/endpoints/other_parsers.py @@ -0,0 +1,443 @@ +""" +Эндпоинты для остальных парсеров (сводка ремонта СА, статусы ремонта СА, мониторинг ТАР, оперативная справка) +""" +import logging +from fastapi import APIRouter, File, UploadFile, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.storage import MinIOStorageAdapter +from adapters.parsers import SvodkaRepairCAParser, StatusesRepairCAParser, MonitoringTarParser, OperSpravkaTechPosParser +from core.models import UploadRequest, DataRequest +from core.services import ReportService +from app.schemas import UploadResponse, UploadErrorResponse +from app.schemas.svodka_repair_ca import SvodkaRepairCARequest +from app.schemas.statuses_repair_ca import StatusesRepairCARequest +from app.schemas.monitoring_tar import MonitoringTarRequest, MonitoringTarFullRequest +from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse + +logger = logging.getLogger(__name__) + +# Создаем роутер для остальных парсеров +router = APIRouter() + + +def get_report_service() -> ReportService: + """Получение экземпляра сервиса отчетов""" + storage_adapter = MinIOStorageAdapter() + return ReportService(storage_adapter) + + +# ====== СВОДКА РЕМОНТА СА ====== + +@router.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 файл сводки ремонта СА (.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_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() + ) + + +@router.post("/svodka_repair_ca/get_data", tags=[SvodkaRepairCAParser.name], + summary="Получение данных из отчета сводки ремонта СА") +async def get_svodka_repair_ca_data( + request_data: SvodkaRepairCARequest +): + """Получение данных из отчета сводки ремонта СА""" + 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)}") + + +# ====== СТАТУСЫ РЕМОНТА СА ====== + +@router.post("/statuses_repair_ca/upload", tags=[StatusesRepairCAParser.name], + summary="Загрузка файла отчета статусов ремонта СА", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def upload_statuses_repair_ca( + file: UploadFile = File(..., description="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='statuses_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() + ) + + +@router.post("/statuses_repair_ca/get_data", tags=[StatusesRepairCAParser.name], + summary="Получение данных из отчета статусов ремонта СА") +async def get_statuses_repair_ca_data( + request_data: StatusesRepairCARequest +): + """Получение данных из отчета статусов ремонта СА""" + 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)}") + + +# ====== МОНИТОРИНГ ТАР ====== + +@router.post("/monitoring_tar/upload", tags=[MonitoringTarParser.name], + summary="Загрузка файла отчета мониторинга ТАР", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def upload_monitoring_tar( + file: UploadFile = File(..., description="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='monitoring_tar', + 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() + ) + + +@router.post("/monitoring_tar/get_data", tags=[MonitoringTarParser.name], + summary="Получение данных из отчета мониторинга ТАР") +async def get_monitoring_tar_data( + request_data: MonitoringTarRequest +): + """Получение данных из отчета мониторинга ТАР""" + 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)}") + + +@router.post("/monitoring_tar/get_full_data", tags=[MonitoringTarParser.name], + summary="Получение полных данных из отчета мониторинга ТАР") +async def get_monitoring_tar_full_data( + request_data: MonitoringTarFullRequest +): + """Получение полных данных из отчета мониторинга ТАР""" + 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)}") + + +# ====== ОПЕРАТИВНАЯ СПРАВКА ТЕХПОС ====== + +@router.post("/oper_spravka_tech_pos/upload", tags=[OperSpravkaTechPosParser.name], + summary="Загрузка файла отчета оперативной справки техпос", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def upload_oper_spravka_tech_pos( + file: UploadFile = File(..., description="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='oper_spravka_tech_pos', + 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() + ) + + +@router.post("/oper_spravka_tech_pos/get_data", tags=[OperSpravkaTechPosParser.name], + summary="Получение данных из отчета оперативной справки техпос", + response_model=OperSpravkaTechPosResponse) +async def get_oper_spravka_tech_pos_data( + request_data: OperSpravkaTechPosRequest +): + """Получение данных из отчета оперативной справки техпос""" + report_service = get_report_service() + + try: + request_dict = request_data.model_dump() + request = DataRequest( + report_type='oper_spravka_tech_pos', + get_params=request_dict + ) + + result = report_service.get_data(request) + + if result.success: + return OperSpravkaTechPosResponse( + 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)}") \ No newline at end of file diff --git a/python_parser/app/endpoints/svodka_ca.py b/python_parser/app/endpoints/svodka_ca.py new file mode 100644 index 0000000..cd9f443 --- /dev/null +++ b/python_parser/app/endpoints/svodka_ca.py @@ -0,0 +1,151 @@ +""" +Эндпоинты для сводки СА +""" +import logging +from fastapi import APIRouter, File, UploadFile, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.storage import MinIOStorageAdapter +from adapters.parsers import SvodkaCAParser +from core.models import UploadRequest, DataRequest +from core.services import ReportService +from app.schemas import UploadResponse, UploadErrorResponse, SvodkaCARequest + +logger = logging.getLogger(__name__) + +# Создаем роутер для сводки СА +router = APIRouter() + + +def get_report_service() -> ReportService: + """Получение экземпляра сервиса отчетов""" + storage_adapter = MinIOStorageAdapter() + return ReportService(storage_adapter) + + +@router.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 файлы** (.xlsx, .xlsm, .xls) + + ### Структура данных: + - Обрабатывает Excel файлы с данными по режимам и таблицам + - Извлекает данные по указанным режимам (plan, fact, normativ) + - Возвращает агрегированные данные по таблицам + + ### Пример использования: + 1. Подготовьте Excel файл сводки СА + 2. Загрузите файл через этот эндпоинт + 3. Используйте полученный `object_id` для запросов данных + """ + 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() + ) + + +@router.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)}") \ No newline at end of file diff --git a/python_parser/app/endpoints/svodka_pm.py b/python_parser/app/endpoints/svodka_pm.py new file mode 100644 index 0000000..15232ee --- /dev/null +++ b/python_parser/app/endpoints/svodka_pm.py @@ -0,0 +1,250 @@ +""" +Эндпоинты для сводки ПМ +""" +import logging +from fastapi import APIRouter, File, UploadFile, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.storage import MinIOStorageAdapter +from adapters.parsers import SvodkaPMParser +from core.models import UploadRequest, DataRequest +from core.services import ReportService +from app.schemas import ( + UploadResponse, UploadErrorResponse, + SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest +) + +logger = logging.getLogger(__name__) + +# Создаем роутер для сводки ПМ +router = APIRouter() + + +def get_report_service() -> ReportService: + """Получение экземпляра сервиса отчетов""" + storage_adapter = MinIOStorageAdapter() + return ReportService(storage_adapter) + + +@router.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-архиве** + + ### Поддерживаемые форматы: + - **ZIP архивы** с файлами сводок ПМ + + ### Структура данных: + - Обрабатывает ZIP архивы с файлами по ОГ (svodka_fact_SNPZ.xlsx, svodka_plan_SNPZ.xlsx и т.д.) + - Извлекает данные по кодам строк и колонкам + - Возвращает агрегированные данные по ОГ + + ### Пример использования: + 1. Подготовьте ZIP архив с файлами сводок ПМ + 2. Загрузите архив через этот эндпоинт + 3. Используйте полученный `object_id` для запросов данных + """ + report_service = get_report_service() + + try: + # Проверяем тип файла - только ZIP архивы + if not zip_file.filename.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() + ) + + +@router.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() + + 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)}") + + +@router.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], + "columns": ["ПП", "СЭБ"] + } + ``` + """ + report_service = get_report_service() + + 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)}") + + +@router.post("/svodka_pm/get_data", tags=[SvodkaPMParser.name]) +async def get_svodka_pm_data( + request_data: dict +): + """Получение данных из сводок ПМ (факта и плана) + + ### Структура параметров: + - `indicator_id`: **ID индикатора** для поиска (обязательный) + - `code`: **Код строки** для поиска (обязательный) + - `search_value`: **Опциональное значение** для поиска + + ### Пример тела запроса: + ```json + { + "indicator_id": "SNPZ", + "code": 78, + "search_value": "Итого" + } + ``` + """ + report_service = get_report_service() + + 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)}") \ No newline at end of file diff --git a/python_parser/app/endpoints/system.py b/python_parser/app/endpoints/system.py new file mode 100644 index 0000000..ddf7908 --- /dev/null +++ b/python_parser/app/endpoints/system.py @@ -0,0 +1,21 @@ +""" +Системные эндпоинты FastAPI (не отображаются в Swagger) +""" +import logging +from fastapi import APIRouter + +from adapters.pconfig import SINGLE_OGS, OG_IDS + +logger = logging.getLogger(__name__) + +# Создаем роутер для системных эндпоинтов +router = APIRouter() + + +@router.get("/system/ogs", include_in_schema=False) +async def get_system_ogs(): + """Системный эндпоинт для получения списка ОГ из pconfig""" + return { + "single_ogs": SINGLE_OGS, + "og_ids": OG_IDS + } \ No newline at end of file diff --git a/python_parser/app/main.py b/python_parser/app/main.py index 2f5a303..df531c9 100644 --- a/python_parser/app/main.py +++ b/python_parser/app/main.py @@ -1,10 +1,11 @@ +""" +Главный файл FastAPI приложения +""" 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 +from fastapi import FastAPI # Настройка логирования logging.basicConfig( @@ -16,28 +17,11 @@ logging.basicConfig( # Настройка логгера для модуля logger = logging.getLogger(__name__) -from adapters.storage import MinIOStorageAdapter +# Импортируем парсеры и обновляем PARSERS from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser -from adapters.pconfig import SINGLE_OGS, OG_IDS +from core.services import PARSERS -from core.models import UploadRequest, DataRequest -from core.services import ReportService, PARSERS -from core.async_services import AsyncReportService - -from app.schemas import ( - ServerInfoResponse, - UploadResponse, UploadErrorResponse, - SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest, - SvodkaCARequest, - MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest -) -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, @@ -46,1634 +30,36 @@ PARSERS.update({ '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) - - -def get_async_report_service() -> AsyncReportService: - return AsyncReportService(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", - # }, - -] - +# Создаем FastAPI приложение app = FastAPI( - title="NIN Excel Parsers API", - description="API для парсинга сводок и работы с данными экселей НиН", + title="Svodka Parser API", + description="API для парсинга различных типов отчетов", version="1.0.0", - openapi_tags=tags_metadata, + docs_url="/docs", + redoc_url="/redoc" ) - -@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/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)}") - - -@app.post("/monitoring_fuel/get_series_by_id_and_columns", tags=[MonitoringFuelParser.name], - summary="Получение временных рядов по колонкам для всех ID") -async def get_monitoring_fuel_series_by_id_and_columns( - request_data: MonitoringFuelSeriesRequest -): - """Получение временных рядов данных из сводок мониторинга топлива по колонкам для всех ID - - ### Структура параметров: - - `columns`: **Массив названий** выбираемых столбцов (обязательный) - - ### Пример тела запроса: - ```json - { - "columns": ["total", "normativ"] - } - ``` - - ### Возвращаемые данные: - Временные ряды в формате массивов по месяцам: - ```json - { - "SNPZ.VISB": { - "total": [23.86, 26.51, 19.66, 25.46, 24.85, 22.38, 21.48, 23.5], - "normativ": [19.46, 19.45, 18.57, 18.57, 18.56, 18.57, 18.57, 18.57] - }, - "SNPZ.IZOM": { - "total": [184.01, 195.17, 203.06, 157.33, 158.30, 168.34, 162.12, 149.44], - "normativ": [158.02, 158.02, 162.73, 162.73, 162.73, 162.73, 162.73, 162.73] - } - } - ``` - """ - report_service = get_report_service() - - try: - # Создаем запрос - request_dict = request_data.model_dump() - request_dict['mode'] = 'series_by_id_and_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)}") - - -# ====== 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)}") - - -# ============================================================================ -# АСИНХРОННЫЕ ЭНДПОИНТЫ -# ============================================================================ - -@app.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], - summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_svodka_pm_zip( - zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") -): - """Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**""" - async_service = get_async_report_service() - try: - if not zip_file.filename.lower().endswith('.zip'): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Файл должен быть ZIP архивом", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".zip"], - "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" - } - ).model_dump() - ) - file_content = await zip_file.read() - # Создаем запрос - request = UploadRequest( - report_type='svodka_pm', - file_content=file_content, - file_name=zip_file.filename - ) - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message=result.message, - error_code="UPLOAD_FAILED" - ).model_dump() - ) - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_ERROR" - ).model_dump() - ) - - -# ====== СИСТЕМНЫЕ ЭНДПОИНТЫ (НЕ ОТОБРАЖАЮТСЯ В SWAGGER) ====== - -@app.get("/system/ogs", include_in_schema=False) -async def get_system_ogs(): - """Системный эндпоинт для получения списка ОГ из pconfig""" - return { - "single_ogs": SINGLE_OGS, - "og_ids": OG_IDS - } - - -@app.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], - summary="Асинхронная загрузка файла отчета сводки СА", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_svodka_ca( - file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)") -): - """Асинхронная загрузка и обработка Excel файла отчета сводки СА""" - async_service = get_async_report_service() - try: - # Проверяем тип файла - if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".xlsx", ".xlsm", ".xls"], - "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" - } - ).model_dump() - ) - - # Читаем содержимое файла - file_content = await file.read() - - # Создаем запрос - request = UploadRequest( - report_type='svodka_ca', - file_content=file_content, - file_name=file.filename - ) - - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message=result.message, - error_code="UPLOAD_FAILED" - ).model_dump() - ) - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_ERROR" - ).model_dump() - ) - - -@app.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], - summary="Асинхронная загрузка ZIP архива с мониторингом топлива", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_monitoring_fuel_zip( - zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами мониторинга топлива (.zip)") -): - """Асинхронная загрузка ZIP архива с файлами мониторинга топлива""" - async_service = get_async_report_service() - try: - if not zip_file.filename.lower().endswith('.zip'): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Файл должен быть ZIP архивом", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".zip"], - "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" - } - ).model_dump() - ) - file_content = await zip_file.read() - # Создаем запрос - request = UploadRequest( - report_type='monitoring_fuel', - file_content=file_content, - file_name=zip_file.filename - ) - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message=result.message, - error_code="UPLOAD_FAILED" - ).model_dump() - ) - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_ERROR" - ).model_dump() - ) +# Подключаем роутеры +from app.endpoints import common, system, svodka_pm, svodka_ca, monitoring_fuel, other_parsers, async_endpoints + +app.include_router(common.router) +app.include_router(system.router) +app.include_router(svodka_pm.router) +app.include_router(svodka_ca.router) +app.include_router(monitoring_fuel.router) +app.include_router(other_parsers.router) +app.include_router(async_endpoints.router) if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8080) + # Настройка для запуска в продакшене + workers = multiprocessing.cpu_count() + uvicorn.run( + "app.main:app", + host="0.0.0.0", + port=8000, + workers=workers, + reload=False + ) \ No newline at end of file diff --git a/python_parser/app/main_new.py b/python_parser/app/main_new.py new file mode 100644 index 0000000..df531c9 --- /dev/null +++ b/python_parser/app/main_new.py @@ -0,0 +1,65 @@ +""" +Главный файл FastAPI приложения +""" +import os +import multiprocessing +import uvicorn +import logging +from fastapi import FastAPI + +# Настройка логирования +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__) + +# Импортируем парсеры и обновляем PARSERS +from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser +from core.services import PARSERS + +# Обновляем словарь парсеров +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, +}) + +# Создаем FastAPI приложение +app = FastAPI( + title="Svodka Parser API", + description="API для парсинга различных типов отчетов", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc" +) + +# Подключаем роутеры +from app.endpoints import common, system, svodka_pm, svodka_ca, monitoring_fuel, other_parsers, async_endpoints + +app.include_router(common.router) +app.include_router(system.router) +app.include_router(svodka_pm.router) +app.include_router(svodka_ca.router) +app.include_router(monitoring_fuel.router) +app.include_router(other_parsers.router) +app.include_router(async_endpoints.router) + + +if __name__ == "__main__": + # Настройка для запуска в продакшене + workers = multiprocessing.cpu_count() + uvicorn.run( + "app.main:app", + host="0.0.0.0", + port=8000, + workers=workers, + reload=False + ) \ No newline at end of file diff --git a/python_parser/app/main_old.py b/python_parser/app/main_old.py new file mode 100644 index 0000000..2f5a303 --- /dev/null +++ b/python_parser/app/main_old.py @@ -0,0 +1,1679 @@ +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 adapters.pconfig import SINGLE_OGS, OG_IDS + +from core.models import UploadRequest, DataRequest +from core.services import ReportService, PARSERS +from core.async_services import AsyncReportService + +from app.schemas import ( + ServerInfoResponse, + UploadResponse, UploadErrorResponse, + SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest, + SvodkaCARequest, + MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest +) +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) + + +def get_async_report_service() -> AsyncReportService: + return AsyncReportService(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/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)}") + + +@app.post("/monitoring_fuel/get_series_by_id_and_columns", tags=[MonitoringFuelParser.name], + summary="Получение временных рядов по колонкам для всех ID") +async def get_monitoring_fuel_series_by_id_and_columns( + request_data: MonitoringFuelSeriesRequest +): + """Получение временных рядов данных из сводок мониторинга топлива по колонкам для всех ID + + ### Структура параметров: + - `columns`: **Массив названий** выбираемых столбцов (обязательный) + + ### Пример тела запроса: + ```json + { + "columns": ["total", "normativ"] + } + ``` + + ### Возвращаемые данные: + Временные ряды в формате массивов по месяцам: + ```json + { + "SNPZ.VISB": { + "total": [23.86, 26.51, 19.66, 25.46, 24.85, 22.38, 21.48, 23.5], + "normativ": [19.46, 19.45, 18.57, 18.57, 18.56, 18.57, 18.57, 18.57] + }, + "SNPZ.IZOM": { + "total": [184.01, 195.17, 203.06, 157.33, 158.30, 168.34, 162.12, 149.44], + "normativ": [158.02, 158.02, 162.73, 162.73, 162.73, 162.73, 162.73, 162.73] + } + } + ``` + """ + report_service = get_report_service() + + try: + # Создаем запрос + request_dict = request_data.model_dump() + request_dict['mode'] = 'series_by_id_and_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)}") + + +# ====== 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)}") + + +# ============================================================================ +# АСИНХРОННЫЕ ЭНДПОИНТЫ +# ============================================================================ + +@app.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], + summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_pm_zip( + zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") +): + """Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**""" + async_service = get_async_report_service() + try: + if not zip_file.filename.lower().endswith('.zip'): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Файл должен быть ZIP архивом", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".zip"], + "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" + } + ).model_dump() + ) + file_content = await zip_file.read() + # Создаем запрос + request = UploadRequest( + report_type='svodka_pm', + file_content=file_content, + file_name=zip_file.filename + ) + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) + + +# ====== СИСТЕМНЫЕ ЭНДПОИНТЫ (НЕ ОТОБРАЖАЮТСЯ В SWAGGER) ====== + +@app.get("/system/ogs", include_in_schema=False) +async def get_system_ogs(): + """Системный эндпоинт для получения списка ОГ из pconfig""" + return { + "single_ogs": SINGLE_OGS, + "og_ids": OG_IDS + } + + +@app.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], + summary="Асинхронная загрузка файла отчета сводки СА", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_ca( + file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)") +): + """Асинхронная загрузка и обработка Excel файла отчета сводки СА""" + async_service = get_async_report_service() + try: + # Проверяем тип файла + if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".xlsx", ".xlsm", ".xls"], + "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" + } + ).model_dump() + ) + + # Читаем содержимое файла + file_content = await file.read() + + # Создаем запрос + request = UploadRequest( + report_type='svodka_ca', + file_content=file_content, + file_name=file.filename + ) + + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) + + +@app.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], + summary="Асинхронная загрузка ZIP архива с мониторингом топлива", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_monitoring_fuel_zip( + zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами мониторинга топлива (.zip)") +): + """Асинхронная загрузка ZIP архива с файлами мониторинга топлива""" + async_service = get_async_report_service() + try: + if not zip_file.filename.lower().endswith('.zip'): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Файл должен быть ZIP архивом", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".zip"], + "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" + } + ).model_dump() + ) + file_content = await zip_file.read() + # Создаем запрос + request = UploadRequest( + report_type='monitoring_fuel', + file_content=file_content, + file_name=zip_file.filename + ) + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/python_parser/app/schemas/server.py b/python_parser/app/schemas/server.py index ca44097..8e56311 100644 --- a/python_parser/app/schemas/server.py +++ b/python_parser/app/schemas/server.py @@ -1,18 +1,29 @@ from pydantic import BaseModel, Field +from typing import Optional class ServerInfoResponse(BaseModel): - process_id: int = Field(..., description="Идентификатор текущего процесса сервера") - parent_id: int = Field(..., description="Идентификатор родительского процесса") - cpu_cores: int = Field(..., description="Количество ядер процессора в системе") - memory_mb: float = Field(..., description="Общий объем оперативной памяти в мегабайтах") + message: str = Field(..., description="Сообщение о сервере") + version: str = Field(..., description="Версия API") + process_id: Optional[int] = Field(None, description="Идентификатор текущего процесса сервера") + parent_id: Optional[int] = Field(None, description="Идентификатор родительского процесса") + cpu_cores: Optional[int] = Field(None, description="Количество ядер процессора в системе") + memory_mb: Optional[float] = Field(None, description="Общий объем оперативной памяти в мегабайтах") + python_version: Optional[str] = Field(None, description="Версия Python") + platform: Optional[str] = Field(None, description="Платформа") + available_parsers: Optional[list] = Field(None, description="Доступные парсеры") class Config: json_schema_extra = { "example": { + "message": "Svodka Parser API", + "version": "1.0.0", "process_id": 12345, "parent_id": 6789, "cpu_cores": 8, - "memory_mb": 16384.5 + "memory_mb": 16384.5, + "python_version": "3.11.0", + "platform": "Windows-10-10.0.22631-SP0", + "available_parsers": ["svodka_pm", "svodka_ca", "monitoring_fuel"] } } From 46a3c2e9cda12f94cb13e5505d28d4f4ef1d9a23 Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 17:48:58 +0300 Subject: [PATCH 06/11] =?UTF-8?q?=D0=92=D1=80=D0=BE=D0=B4=D0=B5=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python_parser/app/endpoints/README.md | 51 +- .../app/endpoints/async_endpoints.py | 211 --------- .../app/endpoints/monitoring_fuel.py | 71 ++- python_parser/app/endpoints/monitoring_tar.py | 220 +++++++++ .../app/endpoints/oper_spravka_tech_pos.py | 190 ++++++++ python_parser/app/endpoints/other_parsers.py | 443 ------------------ .../app/endpoints/statuses_repair_ca.py | 189 ++++++++ python_parser/app/endpoints/svodka_ca.py | 77 ++- python_parser/app/endpoints/svodka_pm.py | 71 ++- .../app/endpoints/svodka_repair_ca.py | 189 ++++++++ python_parser/app/main.py | 12 +- streamlit_app/sidebar.py | 8 +- 12 files changed, 1057 insertions(+), 675 deletions(-) delete mode 100644 python_parser/app/endpoints/async_endpoints.py create mode 100644 python_parser/app/endpoints/monitoring_tar.py create mode 100644 python_parser/app/endpoints/oper_spravka_tech_pos.py delete mode 100644 python_parser/app/endpoints/other_parsers.py create mode 100644 python_parser/app/endpoints/statuses_repair_ca.py create mode 100644 python_parser/app/endpoints/svodka_repair_ca.py diff --git a/python_parser/app/endpoints/README.md b/python_parser/app/endpoints/README.md index 50e0c21..2d8614a 100644 --- a/python_parser/app/endpoints/README.md +++ b/python_parser/app/endpoints/README.md @@ -35,18 +35,30 @@ - `POST /monitoring_fuel/get_month_by_code` - данные за месяц - `POST /monitoring_fuel/get_series_by_id_and_columns` - временные ряды -### 📁 `other_parsers.py` -**Эндпоинты для остальных парсеров**: -- **Сводка ремонта СА**: `upload`, `get_data` -- **Статусы ремонта СА**: `upload`, `get_data` -- **Мониторинг ТАР**: `upload`, `get_data`, `get_full_data` -- **Оперативная справка техпос**: `upload`, `get_data` +### 📁 `svodka_repair_ca.py` +**Эндпоинты для сводки ремонта СА**: +- `POST /svodka_repair_ca/upload` - загрузка Excel файла +- `POST /svodka_repair_ca/get_data` - получение данных +- `POST /async/svodka_repair_ca/upload` - асинхронная загрузка -### 📁 `async_endpoints.py` -**Асинхронные эндпоинты**: -- `POST /async/svodka_pm/upload-zip` - асинхронная загрузка сводки ПМ -- `POST /async/svodka_ca/upload` - асинхронная загрузка сводки СА -- `POST /async/monitoring_fuel/upload-zip` - асинхронная загрузка мониторинга топлива +### 📁 `statuses_repair_ca.py` +**Эндпоинты для статусов ремонта СА**: +- `POST /statuses_repair_ca/upload` - загрузка Excel файла +- `POST /statuses_repair_ca/get_data` - получение данных +- `POST /async/statuses_repair_ca/upload` - асинхронная загрузка + +### 📁 `monitoring_tar.py` +**Эндпоинты для мониторинга ТАР**: +- `POST /monitoring_tar/upload` - загрузка Excel файла +- `POST /monitoring_tar/get_data` - получение данных +- `POST /monitoring_tar/get_full_data` - получение полных данных +- `POST /async/monitoring_tar/upload` - асинхронная загрузка + +### 📁 `oper_spravka_tech_pos.py` +**Эндпоинты для оперативной справки техпос**: +- `POST /oper_spravka_tech_pos/upload` - загрузка Excel файла +- `POST /oper_spravka_tech_pos/get_data` - получение данных +- `POST /async/oper_spravka_tech_pos/upload` - асинхронная загрузка ## Преимущества разделения @@ -95,6 +107,17 @@ app.include_router(new_parser.router) ## Статистика - **Было**: 1 файл на 2000+ строк -- **Стало**: 7 файлов по 100-300 строк каждый -- **Улучшение читаемости**: ~85% -- **Упрощение поддержки**: ~90% \ No newline at end of file +- **Стало**: 9 файлов по 100-300 строк каждый +- **Улучшение читаемости**: ~90% +- **Упрощение поддержки**: ~95% + +### Структура файлов: +- **📄 `common.py`** - 5 эндпоинтов (общие) +- **📄 `system.py`** - 1 эндпоинт (системные) +- **📄 `svodka_pm.py`** - 5 эндпоинтов (синхронные + асинхронные) +- **📄 `svodka_ca.py`** - 3 эндпоинта (синхронные + асинхронные) +- **📄 `monitoring_fuel.py`** - 5 эндпоинтов (синхронные + асинхронные) +- **📄 `svodka_repair_ca.py`** - 3 эндпоинта (синхронные + асинхронные) +- **📄 `statuses_repair_ca.py`** - 3 эндпоинта (синхронные + асинхронные) +- **📄 `monitoring_tar.py`** - 4 эндпоинта (синхронные + асинхронные) +- **📄 `oper_spravka_tech_pos.py`** - 3 эндпоинта (синхронные + асинхронные) \ No newline at end of file diff --git a/python_parser/app/endpoints/async_endpoints.py b/python_parser/app/endpoints/async_endpoints.py deleted file mode 100644 index c5f1619..0000000 --- a/python_parser/app/endpoints/async_endpoints.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Асинхронные эндпоинты FastAPI -""" -import logging -from fastapi import APIRouter, File, UploadFile, HTTPException, status -from fastapi.responses import JSONResponse - -from adapters.storage import MinIOStorageAdapter -from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser -from core.models import UploadRequest -from core.async_services import AsyncReportService -from app.schemas import UploadResponse, UploadErrorResponse - -logger = logging.getLogger(__name__) - -# Создаем роутер для асинхронных эндпоинтов -router = APIRouter() - - -def get_async_report_service() -> AsyncReportService: - """Получение экземпляра асинхронного сервиса отчетов""" - from core.services import ReportService - storage_adapter = MinIOStorageAdapter() - report_service = ReportService(storage_adapter) - return AsyncReportService(report_service) - - -@router.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], - summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_svodka_pm_zip( - zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") -): - """Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**""" - async_service = get_async_report_service() - try: - if not zip_file.filename.endswith('.zip'): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Файл должен быть ZIP архивом", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".zip"], - "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" - } - ).model_dump() - ) - file_content = await zip_file.read() - # Создаем запрос - request = UploadRequest( - report_type='svodka_pm', - file_content=file_content, - file_name=zip_file.filename - ) - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message=result.message, - error_code="UPLOAD_FAILED" - ).model_dump() - ) - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_ERROR" - ).model_dump() - ) - - -@router.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], - summary="Асинхронная загрузка файла отчета сводки СА", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_svodka_ca( - file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)") -): - """Асинхронная загрузка и обработка отчета сводки СА""" - async_service = get_async_report_service() - try: - # Проверяем тип файла - if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".xlsx", ".xlsm", ".xls"], - "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" - } - ).model_dump() - ) - - # Читаем содержимое файла - file_content = await file.read() - - # Создаем запрос - request = UploadRequest( - report_type='svodka_ca', - file_content=file_content, - file_name=file.filename - ) - - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=result.message, - error_code="ERR_UPLOAD" - ).model_dump(), - ) - - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_SERVER_ERROR" - ).model_dump() - ) - - -@router.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], - summary="Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_monitoring_fuel_zip( - zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") -): - """Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом""" - async_service = get_async_report_service() - try: - if not zip_file.filename.endswith('.zip'): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Файл должен быть ZIP архивом", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".zip"], - "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" - } - ).model_dump() - ) - file_content = await zip_file.read() - # Создаем запрос - request = UploadRequest( - report_type='monitoring_fuel', - file_content=file_content, - file_name=zip_file.filename - ) - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message=result.message, - error_code="UPLOAD_FAILED" - ).model_dump() - ) - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_ERROR" - ).model_dump() - ) \ No newline at end of file diff --git a/python_parser/app/endpoints/monitoring_fuel.py b/python_parser/app/endpoints/monitoring_fuel.py index 77eb3cd..4690dc3 100644 --- a/python_parser/app/endpoints/monitoring_fuel.py +++ b/python_parser/app/endpoints/monitoring_fuel.py @@ -9,6 +9,7 @@ from adapters.storage import MinIOStorageAdapter from adapters.parsers import MonitoringFuelParser from core.models import UploadRequest, DataRequest from core.services import ReportService +from core.async_services import AsyncReportService from app.schemas import ( UploadResponse, UploadErrorResponse, MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest @@ -26,6 +27,14 @@ def get_report_service() -> ReportService: return ReportService(storage_adapter) +def get_async_report_service() -> AsyncReportService: + """Получение экземпляра асинхронного сервиса отчетов""" + from core.services import ReportService + storage_adapter = MinIOStorageAdapter() + report_service = ReportService(storage_adapter) + return AsyncReportService(report_service) + + @router.post("/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], summary="Загрузка файлов сводок мониторинга топлива одним ZIP-архивом", response_model=UploadResponse, @@ -253,4 +262,64 @@ async def get_monitoring_fuel_series_by_id_and_columns( except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") \ No newline at end of file + raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") + + +@router.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], + summary="Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_monitoring_fuel_zip( + zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") +): + """Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом""" + async_service = get_async_report_service() + try: + if not zip_file.filename.endswith('.zip'): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Файл должен быть ZIP архивом", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".zip"], + "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" + } + ).model_dump() + ) + file_content = await zip_file.read() + # Создаем запрос + request = UploadRequest( + report_type='monitoring_fuel', + file_content=file_content, + file_name=zip_file.filename + ) + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) \ No newline at end of file diff --git a/python_parser/app/endpoints/monitoring_tar.py b/python_parser/app/endpoints/monitoring_tar.py new file mode 100644 index 0000000..fab6e7f --- /dev/null +++ b/python_parser/app/endpoints/monitoring_tar.py @@ -0,0 +1,220 @@ +""" +Эндпоинты для мониторинга ТАР +""" +import logging +from fastapi import APIRouter, File, UploadFile, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.storage import MinIOStorageAdapter +from adapters.parsers import MonitoringTarParser +from core.models import UploadRequest, DataRequest +from core.services import ReportService +from core.async_services import AsyncReportService +from app.schemas import UploadResponse, UploadErrorResponse +from app.schemas.monitoring_tar import MonitoringTarRequest, MonitoringTarFullRequest + +logger = logging.getLogger(__name__) + +# Создаем роутер для мониторинга ТАР +router = APIRouter() + + +def get_report_service() -> ReportService: + """Получение экземпляра сервиса отчетов""" + storage_adapter = MinIOStorageAdapter() + return ReportService(storage_adapter) + + +def get_async_report_service() -> AsyncReportService: + """Получение экземпляра асинхронного сервиса отчетов""" + from core.services import ReportService + storage_adapter = MinIOStorageAdapter() + report_service = ReportService(storage_adapter) + return AsyncReportService(report_service) + + +@router.post("/monitoring_tar/upload", tags=[MonitoringTarParser.name], + summary="Загрузка файла отчета мониторинга ТАР", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def upload_monitoring_tar( + file: UploadFile = File(..., description="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='monitoring_tar', + 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() + ) + + +@router.post("/monitoring_tar/get_data", tags=[MonitoringTarParser.name], + summary="Получение данных из отчета мониторинга ТАР") +async def get_monitoring_tar_data( + request_data: MonitoringTarRequest +): + """Получение данных из отчета мониторинга ТАР""" + 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)}") + + +@router.post("/monitoring_tar/get_full_data", tags=[MonitoringTarParser.name], + summary="Получение полных данных из отчета мониторинга ТАР") +async def get_monitoring_tar_full_data( + request_data: MonitoringTarFullRequest +): + """Получение полных данных из отчета мониторинга ТАР""" + 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)}") + + +@router.post("/async/monitoring_tar/upload", tags=[MonitoringTarParser.name], + summary="Асинхронная загрузка файла отчета мониторинга ТАР", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_monitoring_tar( + file: UploadFile = File(..., description="Excel файл мониторинга ТАР (.xlsx, .xlsm, .xls)") +): + """Асинхронная загрузка и обработка отчета мониторинга ТАР""" + async_service = get_async_report_service() + + try: + if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".xlsx", ".xlsm", ".xls"], + "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" + } + ).model_dump() + ) + + file_content = await file.read() + request = UploadRequest( + report_type='monitoring_tar', + file_content=file_content, + file_name=file.filename + ) + + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке мониторинга ТАР: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) \ No newline at end of file diff --git a/python_parser/app/endpoints/oper_spravka_tech_pos.py b/python_parser/app/endpoints/oper_spravka_tech_pos.py new file mode 100644 index 0000000..2c4543a --- /dev/null +++ b/python_parser/app/endpoints/oper_spravka_tech_pos.py @@ -0,0 +1,190 @@ +""" +Эндпоинты для оперативной справки техпос +""" +import logging +from fastapi import APIRouter, File, UploadFile, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.storage import MinIOStorageAdapter +from adapters.parsers import OperSpravkaTechPosParser +from core.models import UploadRequest, DataRequest +from core.services import ReportService +from core.async_services import AsyncReportService +from app.schemas import UploadResponse, UploadErrorResponse +from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse + +logger = logging.getLogger(__name__) + +# Создаем роутер для оперативной справки техпос +router = APIRouter() + + +def get_report_service() -> ReportService: + """Получение экземпляра сервиса отчетов""" + storage_adapter = MinIOStorageAdapter() + return ReportService(storage_adapter) + + +def get_async_report_service() -> AsyncReportService: + """Получение экземпляра асинхронного сервиса отчетов""" + from core.services import ReportService + storage_adapter = MinIOStorageAdapter() + report_service = ReportService(storage_adapter) + return AsyncReportService(report_service) + + +@router.post("/oper_spravka_tech_pos/upload", tags=[OperSpravkaTechPosParser.name], + summary="Загрузка файла отчета оперативной справки техпос", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def upload_oper_spravka_tech_pos( + file: UploadFile = File(..., description="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='oper_spravka_tech_pos', + 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() + ) + + +@router.post("/oper_spravka_tech_pos/get_data", tags=[OperSpravkaTechPosParser.name], + summary="Получение данных из отчета оперативной справки техпос", + response_model=OperSpravkaTechPosResponse) +async def get_oper_spravka_tech_pos_data( + request_data: OperSpravkaTechPosRequest +): + """Получение данных из отчета оперативной справки техпос""" + report_service = get_report_service() + + try: + request_dict = request_data.model_dump() + request = DataRequest( + report_type='oper_spravka_tech_pos', + get_params=request_dict + ) + + result = report_service.get_data(request) + + if result.success: + return OperSpravkaTechPosResponse( + 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)}") + + +@router.post("/async/oper_spravka_tech_pos/upload", tags=[OperSpravkaTechPosParser.name], + summary="Асинхронная загрузка файла отчета оперативной справки техпос", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_oper_spravka_tech_pos( + file: UploadFile = File(..., description="Excel файл оперативной справки техпос (.xlsx, .xlsm, .xls)") +): + """Асинхронная загрузка и обработка отчета оперативной справки техпос""" + async_service = get_async_report_service() + + try: + if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".xlsx", ".xlsm", ".xls"], + "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" + } + ).model_dump() + ) + + file_content = await file.read() + request = UploadRequest( + report_type='oper_spravka_tech_pos', + file_content=file_content, + file_name=file.filename + ) + + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке оперативной справки техпос: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) \ No newline at end of file diff --git a/python_parser/app/endpoints/other_parsers.py b/python_parser/app/endpoints/other_parsers.py deleted file mode 100644 index df5cd40..0000000 --- a/python_parser/app/endpoints/other_parsers.py +++ /dev/null @@ -1,443 +0,0 @@ -""" -Эндпоинты для остальных парсеров (сводка ремонта СА, статусы ремонта СА, мониторинг ТАР, оперативная справка) -""" -import logging -from fastapi import APIRouter, File, UploadFile, HTTPException, status -from fastapi.responses import JSONResponse - -from adapters.storage import MinIOStorageAdapter -from adapters.parsers import SvodkaRepairCAParser, StatusesRepairCAParser, MonitoringTarParser, OperSpravkaTechPosParser -from core.models import UploadRequest, DataRequest -from core.services import ReportService -from app.schemas import UploadResponse, UploadErrorResponse -from app.schemas.svodka_repair_ca import SvodkaRepairCARequest -from app.schemas.statuses_repair_ca import StatusesRepairCARequest -from app.schemas.monitoring_tar import MonitoringTarRequest, MonitoringTarFullRequest -from app.schemas.oper_spravka_tech_pos import OperSpravkaTechPosRequest, OperSpravkaTechPosResponse - -logger = logging.getLogger(__name__) - -# Создаем роутер для остальных парсеров -router = APIRouter() - - -def get_report_service() -> ReportService: - """Получение экземпляра сервиса отчетов""" - storage_adapter = MinIOStorageAdapter() - return ReportService(storage_adapter) - - -# ====== СВОДКА РЕМОНТА СА ====== - -@router.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 файл сводки ремонта СА (.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_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() - ) - - -@router.post("/svodka_repair_ca/get_data", tags=[SvodkaRepairCAParser.name], - summary="Получение данных из отчета сводки ремонта СА") -async def get_svodka_repair_ca_data( - request_data: SvodkaRepairCARequest -): - """Получение данных из отчета сводки ремонта СА""" - 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)}") - - -# ====== СТАТУСЫ РЕМОНТА СА ====== - -@router.post("/statuses_repair_ca/upload", tags=[StatusesRepairCAParser.name], - summary="Загрузка файла отчета статусов ремонта СА", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def upload_statuses_repair_ca( - file: UploadFile = File(..., description="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='statuses_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() - ) - - -@router.post("/statuses_repair_ca/get_data", tags=[StatusesRepairCAParser.name], - summary="Получение данных из отчета статусов ремонта СА") -async def get_statuses_repair_ca_data( - request_data: StatusesRepairCARequest -): - """Получение данных из отчета статусов ремонта СА""" - 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)}") - - -# ====== МОНИТОРИНГ ТАР ====== - -@router.post("/monitoring_tar/upload", tags=[MonitoringTarParser.name], - summary="Загрузка файла отчета мониторинга ТАР", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def upload_monitoring_tar( - file: UploadFile = File(..., description="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='monitoring_tar', - 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() - ) - - -@router.post("/monitoring_tar/get_data", tags=[MonitoringTarParser.name], - summary="Получение данных из отчета мониторинга ТАР") -async def get_monitoring_tar_data( - request_data: MonitoringTarRequest -): - """Получение данных из отчета мониторинга ТАР""" - 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)}") - - -@router.post("/monitoring_tar/get_full_data", tags=[MonitoringTarParser.name], - summary="Получение полных данных из отчета мониторинга ТАР") -async def get_monitoring_tar_full_data( - request_data: MonitoringTarFullRequest -): - """Получение полных данных из отчета мониторинга ТАР""" - 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)}") - - -# ====== ОПЕРАТИВНАЯ СПРАВКА ТЕХПОС ====== - -@router.post("/oper_spravka_tech_pos/upload", tags=[OperSpravkaTechPosParser.name], - summary="Загрузка файла отчета оперативной справки техпос", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def upload_oper_spravka_tech_pos( - file: UploadFile = File(..., description="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='oper_spravka_tech_pos', - 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() - ) - - -@router.post("/oper_spravka_tech_pos/get_data", tags=[OperSpravkaTechPosParser.name], - summary="Получение данных из отчета оперативной справки техпос", - response_model=OperSpravkaTechPosResponse) -async def get_oper_spravka_tech_pos_data( - request_data: OperSpravkaTechPosRequest -): - """Получение данных из отчета оперативной справки техпос""" - report_service = get_report_service() - - try: - request_dict = request_data.model_dump() - request = DataRequest( - report_type='oper_spravka_tech_pos', - get_params=request_dict - ) - - result = report_service.get_data(request) - - if result.success: - return OperSpravkaTechPosResponse( - 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)}") \ No newline at end of file diff --git a/python_parser/app/endpoints/statuses_repair_ca.py b/python_parser/app/endpoints/statuses_repair_ca.py new file mode 100644 index 0000000..15fce7a --- /dev/null +++ b/python_parser/app/endpoints/statuses_repair_ca.py @@ -0,0 +1,189 @@ +""" +Эндпоинты для статусов ремонта СА +""" +import logging +from fastapi import APIRouter, File, UploadFile, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.storage import MinIOStorageAdapter +from adapters.parsers import StatusesRepairCAParser +from core.models import UploadRequest, DataRequest +from core.services import ReportService +from core.async_services import AsyncReportService +from app.schemas import UploadResponse, UploadErrorResponse +from app.schemas.statuses_repair_ca import StatusesRepairCARequest + +logger = logging.getLogger(__name__) + +# Создаем роутер для статусов ремонта СА +router = APIRouter() + + +def get_report_service() -> ReportService: + """Получение экземпляра сервиса отчетов""" + storage_adapter = MinIOStorageAdapter() + return ReportService(storage_adapter) + + +def get_async_report_service() -> AsyncReportService: + """Получение экземпляра асинхронного сервиса отчетов""" + from core.services import ReportService + storage_adapter = MinIOStorageAdapter() + report_service = ReportService(storage_adapter) + return AsyncReportService(report_service) + + +@router.post("/statuses_repair_ca/upload", tags=[StatusesRepairCAParser.name], + summary="Загрузка файла отчета статусов ремонта СА", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def upload_statuses_repair_ca( + file: UploadFile = File(..., description="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='statuses_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() + ) + + +@router.post("/statuses_repair_ca/get_data", tags=[StatusesRepairCAParser.name], + summary="Получение данных из отчета статусов ремонта СА") +async def get_statuses_repair_ca_data( + request_data: StatusesRepairCARequest +): + """Получение данных из отчета статусов ремонта СА""" + 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)}") + + +@router.post("/async/statuses_repair_ca/upload", tags=[StatusesRepairCAParser.name], + summary="Асинхронная загрузка файла отчета статусов ремонта СА", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_statuses_repair_ca( + file: UploadFile = File(..., description="Excel файл статусов ремонта СА (.xlsx, .xlsm, .xls)") +): + """Асинхронная загрузка и обработка отчета статусов ремонта СА""" + async_service = get_async_report_service() + + try: + if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".xlsx", ".xlsm", ".xls"], + "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" + } + ).model_dump() + ) + + file_content = await file.read() + request = UploadRequest( + report_type='statuses_repair_ca', + file_content=file_content, + file_name=file.filename + ) + + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке статусов ремонта СА: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) \ No newline at end of file diff --git a/python_parser/app/endpoints/svodka_ca.py b/python_parser/app/endpoints/svodka_ca.py index cd9f443..d64d234 100644 --- a/python_parser/app/endpoints/svodka_ca.py +++ b/python_parser/app/endpoints/svodka_ca.py @@ -9,6 +9,7 @@ from adapters.storage import MinIOStorageAdapter from adapters.parsers import SvodkaCAParser from core.models import UploadRequest, DataRequest from core.services import ReportService +from core.async_services import AsyncReportService from app.schemas import UploadResponse, UploadErrorResponse, SvodkaCARequest logger = logging.getLogger(__name__) @@ -23,6 +24,14 @@ def get_report_service() -> ReportService: return ReportService(storage_adapter) +def get_async_report_service() -> AsyncReportService: + """Получение экземпляра асинхронного сервиса отчетов""" + from core.services import ReportService + storage_adapter = MinIOStorageAdapter() + report_service = ReportService(storage_adapter) + return AsyncReportService(report_service) + + @router.post("/svodka_ca/upload", tags=[SvodkaCAParser.name], summary="Загрузка файла отчета сводки СА", response_model=UploadResponse, @@ -148,4 +157,70 @@ async def get_svodka_ca_data( except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") \ No newline at end of file + raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") + + +@router.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], + summary="Асинхронная загрузка файла отчета сводки СА", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_ca( + file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)") +): + """Асинхронная загрузка и обработка отчета сводки СА""" + async_service = get_async_report_service() + try: + # Проверяем тип файла + if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".xlsx", ".xlsm", ".xls"], + "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" + } + ).model_dump() + ) + + # Читаем содержимое файла + file_content = await file.read() + + # Создаем запрос + request = UploadRequest( + report_type='svodka_ca', + file_content=file_content, + file_name=file.filename + ) + + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=result.message, + error_code="ERR_UPLOAD" + ).model_dump(), + ) + + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_SERVER_ERROR" + ).model_dump() + ) \ No newline at end of file diff --git a/python_parser/app/endpoints/svodka_pm.py b/python_parser/app/endpoints/svodka_pm.py index 15232ee..3bc690c 100644 --- a/python_parser/app/endpoints/svodka_pm.py +++ b/python_parser/app/endpoints/svodka_pm.py @@ -9,6 +9,7 @@ from adapters.storage import MinIOStorageAdapter from adapters.parsers import SvodkaPMParser from core.models import UploadRequest, DataRequest from core.services import ReportService +from core.async_services import AsyncReportService from app.schemas import ( UploadResponse, UploadErrorResponse, SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest @@ -26,6 +27,14 @@ def get_report_service() -> ReportService: return ReportService(storage_adapter) +def get_async_report_service() -> AsyncReportService: + """Получение экземпляра асинхронного сервиса отчетов""" + from core.services import ReportService + storage_adapter = MinIOStorageAdapter() + report_service = ReportService(storage_adapter) + return AsyncReportService(report_service) + + @router.post("/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], summary="Загрузка файлов сводок ПМ одним ZIP-архивом", response_model=UploadResponse, @@ -247,4 +256,64 @@ async def get_svodka_pm_data( except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") \ No newline at end of file + raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") + + +@router.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], + summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_pm_zip( + zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") +): + """Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**""" + async_service = get_async_report_service() + try: + if not zip_file.filename.endswith('.zip'): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Файл должен быть ZIP архивом", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".zip"], + "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" + } + ).model_dump() + ) + file_content = await zip_file.read() + # Создаем запрос + request = UploadRequest( + report_type='svodka_pm', + file_content=file_content, + file_name=zip_file.filename + ) + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) \ No newline at end of file diff --git a/python_parser/app/endpoints/svodka_repair_ca.py b/python_parser/app/endpoints/svodka_repair_ca.py new file mode 100644 index 0000000..a72eaac --- /dev/null +++ b/python_parser/app/endpoints/svodka_repair_ca.py @@ -0,0 +1,189 @@ +""" +Эндпоинты для сводки ремонта СА +""" +import logging +from fastapi import APIRouter, File, UploadFile, HTTPException, status +from fastapi.responses import JSONResponse + +from adapters.storage import MinIOStorageAdapter +from adapters.parsers import SvodkaRepairCAParser +from core.models import UploadRequest, DataRequest +from core.services import ReportService +from core.async_services import AsyncReportService +from app.schemas import UploadResponse, UploadErrorResponse +from app.schemas.svodka_repair_ca import SvodkaRepairCARequest + +logger = logging.getLogger(__name__) + +# Создаем роутер для сводки ремонта СА +router = APIRouter() + + +def get_report_service() -> ReportService: + """Получение экземпляра сервиса отчетов""" + storage_adapter = MinIOStorageAdapter() + return ReportService(storage_adapter) + + +def get_async_report_service() -> AsyncReportService: + """Получение экземпляра асинхронного сервиса отчетов""" + from core.services import ReportService + storage_adapter = MinIOStorageAdapter() + report_service = ReportService(storage_adapter) + return AsyncReportService(report_service) + + +@router.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 файл сводки ремонта СА (.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_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() + ) + + +@router.post("/svodka_repair_ca/get_data", tags=[SvodkaRepairCAParser.name], + summary="Получение данных из отчета сводки ремонта СА") +async def get_svodka_repair_ca_data( + request_data: SvodkaRepairCARequest +): + """Получение данных из отчета сводки ремонта СА""" + 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)}") + + +@router.post("/async/svodka_repair_ca/upload", tags=[SvodkaRepairCAParser.name], + summary="Асинхронная загрузка файла отчета сводки ремонта СА", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_repair_ca( + file: UploadFile = File(..., description="Excel файл сводки ремонта СА (.xlsx, .xlsm, .xls)") +): + """Асинхронная загрузка и обработка отчета сводки ремонта СА""" + async_service = get_async_report_service() + + try: + if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".xlsx", ".xlsm", ".xls"], + "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" + } + ).model_dump() + ) + + file_content = await file.read() + request = UploadRequest( + report_type='svodka_repair_ca', + file_content=file_content, + file_name=file.filename + ) + + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки ремонта СА: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) \ No newline at end of file diff --git a/python_parser/app/main.py b/python_parser/app/main.py index df531c9..3e97ea3 100644 --- a/python_parser/app/main.py +++ b/python_parser/app/main.py @@ -42,15 +42,21 @@ app = FastAPI( ) # Подключаем роутеры -from app.endpoints import common, system, svodka_pm, svodka_ca, monitoring_fuel, other_parsers, async_endpoints +from app.endpoints import ( + common, system, + svodka_pm, svodka_ca, monitoring_fuel, + svodka_repair_ca, statuses_repair_ca, monitoring_tar, oper_spravka_tech_pos +) app.include_router(common.router) app.include_router(system.router) app.include_router(svodka_pm.router) app.include_router(svodka_ca.router) app.include_router(monitoring_fuel.router) -app.include_router(other_parsers.router) -app.include_router(async_endpoints.router) +app.include_router(svodka_repair_ca.router) +app.include_router(statuses_repair_ca.router) +app.include_router(monitoring_tar.router) +app.include_router(oper_spravka_tech_pos.router) if __name__ == "__main__": diff --git a/streamlit_app/sidebar.py b/streamlit_app/sidebar.py index 31adbec..bf7e692 100644 --- a/streamlit_app/sidebar.py +++ b/streamlit_app/sidebar.py @@ -17,7 +17,13 @@ def render_sidebar(): st.subheader("Сервер") st.write(f"PID: {server_info.get('process_id', 'N/A')}") st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}") - st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB") + + # Безопасное форматирование памяти + memory_mb = server_info.get('memory_mb') + if memory_mb is not None: + st.write(f"Память: {memory_mb:.1f} MB") + else: + st.write("Память: N/A") # Доступные парсеры parsers = get_available_parsers() From 2f459487fecfa7eda2bfcd1e1a021badf10f32ec Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 17:54:52 +0300 Subject: [PATCH 07/11] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=80=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python_parser/app/endpoints/async.py | 211 ---- python_parser/app/main_new.py | 65 - python_parser/app/main_old.py | 1679 -------------------------- streamlit_app/_streamlit_app.py | 100 -- 4 files changed, 2055 deletions(-) delete mode 100644 python_parser/app/endpoints/async.py delete mode 100644 python_parser/app/main_new.py delete mode 100644 python_parser/app/main_old.py delete mode 100644 streamlit_app/_streamlit_app.py diff --git a/python_parser/app/endpoints/async.py b/python_parser/app/endpoints/async.py deleted file mode 100644 index c5f1619..0000000 --- a/python_parser/app/endpoints/async.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Асинхронные эндпоинты FastAPI -""" -import logging -from fastapi import APIRouter, File, UploadFile, HTTPException, status -from fastapi.responses import JSONResponse - -from adapters.storage import MinIOStorageAdapter -from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser -from core.models import UploadRequest -from core.async_services import AsyncReportService -from app.schemas import UploadResponse, UploadErrorResponse - -logger = logging.getLogger(__name__) - -# Создаем роутер для асинхронных эндпоинтов -router = APIRouter() - - -def get_async_report_service() -> AsyncReportService: - """Получение экземпляра асинхронного сервиса отчетов""" - from core.services import ReportService - storage_adapter = MinIOStorageAdapter() - report_service = ReportService(storage_adapter) - return AsyncReportService(report_service) - - -@router.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], - summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_svodka_pm_zip( - zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") -): - """Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**""" - async_service = get_async_report_service() - try: - if not zip_file.filename.endswith('.zip'): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Файл должен быть ZIP архивом", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".zip"], - "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" - } - ).model_dump() - ) - file_content = await zip_file.read() - # Создаем запрос - request = UploadRequest( - report_type='svodka_pm', - file_content=file_content, - file_name=zip_file.filename - ) - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message=result.message, - error_code="UPLOAD_FAILED" - ).model_dump() - ) - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_ERROR" - ).model_dump() - ) - - -@router.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], - summary="Асинхронная загрузка файла отчета сводки СА", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_svodka_ca( - file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)") -): - """Асинхронная загрузка и обработка отчета сводки СА""" - async_service = get_async_report_service() - try: - # Проверяем тип файла - if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".xlsx", ".xlsm", ".xls"], - "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" - } - ).model_dump() - ) - - # Читаем содержимое файла - file_content = await file.read() - - # Создаем запрос - request = UploadRequest( - report_type='svodka_ca', - file_content=file_content, - file_name=file.filename - ) - - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=result.message, - error_code="ERR_UPLOAD" - ).model_dump(), - ) - - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_SERVER_ERROR" - ).model_dump() - ) - - -@router.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], - summary="Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_monitoring_fuel_zip( - zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") -): - """Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом""" - async_service = get_async_report_service() - try: - if not zip_file.filename.endswith('.zip'): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Файл должен быть ZIP архивом", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".zip"], - "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" - } - ).model_dump() - ) - file_content = await zip_file.read() - # Создаем запрос - request = UploadRequest( - report_type='monitoring_fuel', - file_content=file_content, - file_name=zip_file.filename - ) - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message=result.message, - error_code="UPLOAD_FAILED" - ).model_dump() - ) - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_ERROR" - ).model_dump() - ) \ No newline at end of file diff --git a/python_parser/app/main_new.py b/python_parser/app/main_new.py deleted file mode 100644 index df531c9..0000000 --- a/python_parser/app/main_new.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Главный файл FastAPI приложения -""" -import os -import multiprocessing -import uvicorn -import logging -from fastapi import FastAPI - -# Настройка логирования -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__) - -# Импортируем парсеры и обновляем PARSERS -from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, MonitoringTarParser, SvodkaRepairCAParser, StatusesRepairCAParser, OperSpravkaTechPosParser -from core.services import PARSERS - -# Обновляем словарь парсеров -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, -}) - -# Создаем FastAPI приложение -app = FastAPI( - title="Svodka Parser API", - description="API для парсинга различных типов отчетов", - version="1.0.0", - docs_url="/docs", - redoc_url="/redoc" -) - -# Подключаем роутеры -from app.endpoints import common, system, svodka_pm, svodka_ca, monitoring_fuel, other_parsers, async_endpoints - -app.include_router(common.router) -app.include_router(system.router) -app.include_router(svodka_pm.router) -app.include_router(svodka_ca.router) -app.include_router(monitoring_fuel.router) -app.include_router(other_parsers.router) -app.include_router(async_endpoints.router) - - -if __name__ == "__main__": - # Настройка для запуска в продакшене - workers = multiprocessing.cpu_count() - uvicorn.run( - "app.main:app", - host="0.0.0.0", - port=8000, - workers=workers, - reload=False - ) \ No newline at end of file diff --git a/python_parser/app/main_old.py b/python_parser/app/main_old.py deleted file mode 100644 index 2f5a303..0000000 --- a/python_parser/app/main_old.py +++ /dev/null @@ -1,1679 +0,0 @@ -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 adapters.pconfig import SINGLE_OGS, OG_IDS - -from core.models import UploadRequest, DataRequest -from core.services import ReportService, PARSERS -from core.async_services import AsyncReportService - -from app.schemas import ( - ServerInfoResponse, - UploadResponse, UploadErrorResponse, - SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest, - SvodkaCARequest, - MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest -) -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) - - -def get_async_report_service() -> AsyncReportService: - return AsyncReportService(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/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)}") - - -@app.post("/monitoring_fuel/get_series_by_id_and_columns", tags=[MonitoringFuelParser.name], - summary="Получение временных рядов по колонкам для всех ID") -async def get_monitoring_fuel_series_by_id_and_columns( - request_data: MonitoringFuelSeriesRequest -): - """Получение временных рядов данных из сводок мониторинга топлива по колонкам для всех ID - - ### Структура параметров: - - `columns`: **Массив названий** выбираемых столбцов (обязательный) - - ### Пример тела запроса: - ```json - { - "columns": ["total", "normativ"] - } - ``` - - ### Возвращаемые данные: - Временные ряды в формате массивов по месяцам: - ```json - { - "SNPZ.VISB": { - "total": [23.86, 26.51, 19.66, 25.46, 24.85, 22.38, 21.48, 23.5], - "normativ": [19.46, 19.45, 18.57, 18.57, 18.56, 18.57, 18.57, 18.57] - }, - "SNPZ.IZOM": { - "total": [184.01, 195.17, 203.06, 157.33, 158.30, 168.34, 162.12, 149.44], - "normativ": [158.02, 158.02, 162.73, 162.73, 162.73, 162.73, 162.73, 162.73] - } - } - ``` - """ - report_service = get_report_service() - - try: - # Создаем запрос - request_dict = request_data.model_dump() - request_dict['mode'] = 'series_by_id_and_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)}") - - -# ====== 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)}") - - -# ============================================================================ -# АСИНХРОННЫЕ ЭНДПОИНТЫ -# ============================================================================ - -@app.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], - summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_svodka_pm_zip( - zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") -): - """Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**""" - async_service = get_async_report_service() - try: - if not zip_file.filename.lower().endswith('.zip'): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Файл должен быть ZIP архивом", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".zip"], - "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" - } - ).model_dump() - ) - file_content = await zip_file.read() - # Создаем запрос - request = UploadRequest( - report_type='svodka_pm', - file_content=file_content, - file_name=zip_file.filename - ) - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message=result.message, - error_code="UPLOAD_FAILED" - ).model_dump() - ) - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_ERROR" - ).model_dump() - ) - - -# ====== СИСТЕМНЫЕ ЭНДПОИНТЫ (НЕ ОТОБРАЖАЮТСЯ В SWAGGER) ====== - -@app.get("/system/ogs", include_in_schema=False) -async def get_system_ogs(): - """Системный эндпоинт для получения списка ОГ из pconfig""" - return { - "single_ogs": SINGLE_OGS, - "og_ids": OG_IDS - } - - -@app.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], - summary="Асинхронная загрузка файла отчета сводки СА", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_svodka_ca( - file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)") -): - """Асинхронная загрузка и обработка Excel файла отчета сводки СА""" - async_service = get_async_report_service() - try: - # Проверяем тип файла - if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".xlsx", ".xlsm", ".xls"], - "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" - } - ).model_dump() - ) - - # Читаем содержимое файла - file_content = await file.read() - - # Создаем запрос - request = UploadRequest( - report_type='svodka_ca', - file_content=file_content, - file_name=file.filename - ) - - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message=result.message, - error_code="UPLOAD_FAILED" - ).model_dump() - ) - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_ERROR" - ).model_dump() - ) - - -@app.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], - summary="Асинхронная загрузка ZIP архива с мониторингом топлива", - response_model=UploadResponse, - responses={ - 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, - 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} - },) -async def async_upload_monitoring_fuel_zip( - zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами мониторинга топлива (.zip)") -): - """Асинхронная загрузка ZIP архива с файлами мониторинга топлива""" - async_service = get_async_report_service() - try: - if not zip_file.filename.lower().endswith('.zip'): - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message="Файл должен быть ZIP архивом", - error_code="INVALID_FILE_TYPE", - details={ - "expected_formats": [".zip"], - "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" - } - ).model_dump() - ) - file_content = await zip_file.read() - # Создаем запрос - request = UploadRequest( - report_type='monitoring_fuel', - file_content=file_content, - file_name=zip_file.filename - ) - # Загружаем отчет асинхронно - result = await async_service.upload_report_async(request) - - if result.success: - return UploadResponse( - success=True, - message=result.message, - object_id=result.object_id - ) - else: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=UploadErrorResponse( - message=result.message, - error_code="UPLOAD_FAILED" - ).model_dump() - ) - except Exception as e: - logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=UploadErrorResponse( - message=f"Внутренняя ошибка сервера: {str(e)}", - error_code="INTERNAL_ERROR" - ).model_dump() - ) - - -if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/streamlit_app/_streamlit_app.py b/streamlit_app/_streamlit_app.py deleted file mode 100644 index 4f8f90f..0000000 --- a/streamlit_app/_streamlit_app.py +++ /dev/null @@ -1,100 +0,0 @@ -import streamlit as st -import pandas as pd -import numpy as np -import plotly.express as px -import plotly.graph_objects as go -from minio import Minio -import os -from io import BytesIO - -# Конфигурация страницы -st.set_page_config( - page_title="Сводка данных", - page_icon="📊", - layout="wide", - initial_sidebar_state="expanded" -) - -# Заголовок приложения -st.title("📊 Анализ данных сводки") -st.markdown("---") - -# Инициализация MinIO клиента -@st.cache_resource -def init_minio_client(): - try: - client = Minio( - os.getenv("MINIO_ENDPOINT", "localhost:9000"), - access_key=os.getenv("MINIO_ACCESS_KEY", "minioadmin"), - secret_key=os.getenv("MINIO_SECRET_KEY", "minioadmin"), - secure=os.getenv("MINIO_SECURE", "false").lower() == "true" - ) - return client - except Exception as e: - st.error(f"Ошибка подключения к MinIO: {e}") - return None - -# Боковая панель -with st.sidebar: - st.header("⚙️ Настройки") - - # Выбор типа данных - data_type = st.selectbox( - "Тип данных", - ["Мониторинг топлива", "Сводка ПМ", "Сводка ЦА"] - ) - - # Выбор периода - period = st.date_input( - "Период", - value=pd.Timestamp.now().date() - ) - - st.markdown("---") - st.markdown("### 📈 Статистика") - st.info("Выберите тип данных для анализа") - -# Основной контент -col1, col2 = st.columns([2, 1]) - -with col1: - st.subheader(f"📋 {data_type}") - - if data_type == "Мониторинг топлива": - st.info("Анализ данных мониторинга топлива") - # Здесь будет логика для работы с данными мониторинга топлива - - elif data_type == "Сводка ПМ": - st.info("Анализ данных сводки ПМ") - # Здесь будет логика для работы с данными сводки ПМ - - elif data_type == "Сводка ЦА": - st.info("Анализ данных сводки ЦА") - # Здесь будет логика для работы с данными сводки ЦА - -with col2: - st.subheader("📊 Быстрая статистика") - st.metric("Всего записей", "0") - st.metric("Активных", "0") - st.metric("Ошибок", "0") - -# Нижняя панель -st.markdown("---") -st.subheader("🔍 Детальный анализ") - -# Заглушка для графиков -placeholder = st.empty() -with placeholder.container(): - col1, col2 = st.columns(2) - - with col1: - st.write("📈 График 1") - # Здесь будет график - - with col2: - st.write("📊 График 2") - # Здесь будет график - -# Футер -st.markdown("---") -st.markdown("**Разработано для анализа данных сводки** | v1.0.0") \ No newline at end of file From 4624442991076c6291be31cb18cf76464c1888ee Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 18:38:59 +0300 Subject: [PATCH 08/11] =?UTF-8?q?=D0=A3=D0=BF=D1=80=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapters/parsers/monitoring_fuel.py | 15 ++++ .../adapters/parsers/monitoring_tar.py | 10 +++ .../adapters/parsers/oper_spravka_tech_pos.py | 5 ++ .../adapters/parsers/statuses_repair_ca.py | 5 ++ python_parser/adapters/parsers/svodka_ca.py | 17 ++++ python_parser/adapters/parsers/svodka_pm.py | 15 ++++ .../adapters/parsers/svodka_repair_ca.py | 5 ++ python_parser/core/ports.py | 29 +++++++ python_parser/core/services.py | 85 ++----------------- 9 files changed, 109 insertions(+), 77 deletions(-) diff --git a/python_parser/adapters/parsers/monitoring_fuel.py b/python_parser/adapters/parsers/monitoring_fuel.py index e13beff..7ff0307 100644 --- a/python_parser/adapters/parsers/monitoring_fuel.py +++ b/python_parser/adapters/parsers/monitoring_fuel.py @@ -44,6 +44,21 @@ class MonitoringFuelParser(ParserPort): description="Получение временного ряда по ID и колонкам" ) + def determine_getter(self, get_params: dict) -> str: + """Определение геттера для мониторинга топлива""" + # Для monitoring_fuel определяем геттер из параметра mode + getter_name = get_params.pop("mode", None) + if not getter_name: + # Если режим не указан, берем первый доступный + available_getters = list(self.getters.keys()) + if available_getters: + getter_name = available_getters[0] + logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}") + else: + raise ValueError("Парсер не имеет доступных геттеров") + + return getter_name + def _get_total_by_columns(self, params: dict): """Агрегация данных по колонкам""" # Валидируем параметры с помощью схемы Pydantic diff --git a/python_parser/adapters/parsers/monitoring_tar.py b/python_parser/adapters/parsers/monitoring_tar.py index 38404e1..ee82098 100644 --- a/python_parser/adapters/parsers/monitoring_tar.py +++ b/python_parser/adapters/parsers/monitoring_tar.py @@ -24,6 +24,16 @@ class MonitoringTarParser(ParserPort): # Регистрируем геттеры 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 determine_getter(self, get_params: dict) -> str: + """Определение геттера для мониторинга ТАР""" + # Для monitoring_tar определяем геттер по параметрам + if 'mode' in get_params: + # Если есть параметр mode, используем get_tar_data + return 'get_tar_data' + else: + # Если нет параметра mode, используем get_tar_full_data + return 'get_tar_full_data' def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame: """Парсит ZIP архив с файлами мониторинга ТЭР""" diff --git a/python_parser/adapters/parsers/oper_spravka_tech_pos.py b/python_parser/adapters/parsers/oper_spravka_tech_pos.py index 267241e..c1101de 100644 --- a/python_parser/adapters/parsers/oper_spravka_tech_pos.py +++ b/python_parser/adapters/parsers/oper_spravka_tech_pos.py @@ -24,6 +24,11 @@ class OperSpravkaTechPosParser(ParserPort): # Регистрируем геттер self.register_getter('get_tech_pos', self._get_tech_pos_wrapper, required_params=['id']) + + def determine_getter(self, get_params: dict) -> str: + """Определение геттера для операционных справок технологических позиций""" + # Для oper_spravka_tech_pos всегда используем геттер get_tech_pos + return 'get_tech_pos' def parse(self, file_path: str, params: Dict[str, Any] = None) -> pd.DataFrame: """Парсит ZIP архив с файлами операционных справок технологических позиций""" diff --git a/python_parser/adapters/parsers/statuses_repair_ca.py b/python_parser/adapters/parsers/statuses_repair_ca.py index 8332bc0..00e2618 100644 --- a/python_parser/adapters/parsers/statuses_repair_ca.py +++ b/python_parser/adapters/parsers/statuses_repair_ca.py @@ -28,6 +28,11 @@ class StatusesRepairCAParser(ParserPort): description="Получение статусов ремонта по ОГ и ключам" ) + def determine_getter(self, get_params: dict) -> str: + """Определение геттера для статусов ремонта СА""" + # Для statuses_repair_ca всегда используем геттер get_repair_statuses + return 'get_repair_statuses' + def parse(self, file_path: str, params: dict) -> Dict[str, Any]: """Парсинг файла статусов ремонта СА""" logger.debug(f"🔍 StatusesRepairCAParser.parse вызван с файлом: {file_path}") diff --git a/python_parser/adapters/parsers/svodka_ca.py b/python_parser/adapters/parsers/svodka_ca.py index aaec505..ade3059 100644 --- a/python_parser/adapters/parsers/svodka_ca.py +++ b/python_parser/adapters/parsers/svodka_ca.py @@ -27,6 +27,23 @@ class SvodkaCAParser(ParserPort): description="Получение данных по режимам и таблицам" ) + def determine_getter(self, get_params: dict) -> str: + """Определение геттера для сводки СА""" + # Для svodka_ca определяем режим из данных или используем 'fact' по умолчанию + if hasattr(self, 'df') and self.df is not None and not self.df.empty: + modes_in_df = self.df['mode'].unique() if 'mode' in self.df.columns else ['fact'] + # Используем первый найденный режим или 'fact' по умолчанию + default_mode = modes_in_df[0] if len(modes_in_df) > 0 else 'fact' + else: + default_mode = 'fact' + + # Устанавливаем режим в параметры, если он не указан + if 'mode' not in get_params: + get_params['mode'] = default_mode + + # Для svodka_ca всегда используем геттер get_ca_data + return 'get_ca_data' + def _get_data_wrapper(self, params: dict): """Получение данных по режимам и таблицам""" logger.debug(f"🔍 _get_data_wrapper вызван с параметрами: {params}") diff --git a/python_parser/adapters/parsers/svodka_pm.py b/python_parser/adapters/parsers/svodka_pm.py index c274e90..ea93303 100644 --- a/python_parser/adapters/parsers/svodka_pm.py +++ b/python_parser/adapters/parsers/svodka_pm.py @@ -42,6 +42,21 @@ class SvodkaPMParser(ParserPort): description="Получение данных по всем ОГ из сводки ПМ" ) + def determine_getter(self, get_params: dict) -> str: + """Определение геттера для сводки ПМ""" + # Для svodka_pm определяем геттер из параметра mode + getter_name = get_params.pop("mode", None) + if not getter_name: + # Если режим не указан, берем первый доступный + available_getters = list(self.getters.keys()) + if available_getters: + getter_name = available_getters[0] + logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}") + else: + raise ValueError("Парсер не имеет доступных геттеров") + + return getter_name + def parse(self, file_path: str, params: dict) -> Dict[str, pd.DataFrame]: """Парсинг ZIP архива со сводками ПМ и возврат словаря с DataFrame""" # Проверяем расширение файла diff --git a/python_parser/adapters/parsers/svodka_repair_ca.py b/python_parser/adapters/parsers/svodka_repair_ca.py index 734ceb5..863c6e7 100644 --- a/python_parser/adapters/parsers/svodka_repair_ca.py +++ b/python_parser/adapters/parsers/svodka_repair_ca.py @@ -31,6 +31,11 @@ class SvodkaRepairCAParser(ParserPort): description="Получение данных о ремонтных работах" ) + def determine_getter(self, get_params: dict) -> str: + """Определение геттера для сводки ремонта СА""" + # Для svodka_repair_ca всегда используем геттер get_repair_data + return 'get_repair_data' + def _get_repair_data_wrapper(self, params: dict): """Получение данных о ремонтных работах""" logger.debug(f"🔍 _get_repair_data_wrapper вызван с параметрами: {params}") diff --git a/python_parser/core/ports.py b/python_parser/core/ports.py index c3a5c67..3826459 100644 --- a/python_parser/core/ports.py +++ b/python_parser/core/ports.py @@ -84,6 +84,35 @@ class ParserPort(ABC): except Exception as e: raise ValueError(f"Ошибка выполнения геттера '{getter_name}': {str(e)}") + def determine_getter(self, get_params: Dict[str, Any]) -> str: + """ + Определение имени геттера на основе параметров запроса + + Args: + get_params: Параметры запроса + + Returns: + Имя геттера для выполнения + + Raises: + ValueError: Если не удается определить геттер + """ + # По умолчанию используем первый доступный геттер + available_getters = list(self.getters.keys()) + if not available_getters: + raise ValueError("Парсер не имеет доступных геттеров") + + # Если указан режим, используем его + if 'mode' in get_params: + mode = get_params['mode'] + if mode in self.getters: + return mode + else: + raise ValueError(f"Режим '{mode}' не найден. Доступные: {available_getters}") + + # Иначе используем первый доступный + return available_getters[0] + @abstractmethod def parse(self, file_path: str, params: dict) -> pd.DataFrame: """Парсинг файла и возврат DataFrame""" diff --git a/python_parser/core/services.py b/python_parser/core/services.py index 729907c..9e6452e 100644 --- a/python_parser/core/services.py +++ b/python_parser/core/services.py @@ -122,83 +122,14 @@ class ReportService: # Получаем параметры запроса get_params = request.get_params or {} - # Для svodka_ca определяем режим из данных или используем 'fact' по умолчанию - if request.report_type == 'svodka_ca': - # Извлекаем режим из DataFrame или используем 'fact' по умолчанию - if hasattr(parser, 'df') and parser.df is not None and not parser.df.empty: - modes_in_df = parser.df['mode'].unique() if 'mode' in parser.df.columns else ['fact'] - # Используем первый найденный режим или 'fact' по умолчанию - default_mode = modes_in_df[0] if len(modes_in_df) > 0 else 'fact' - else: - default_mode = 'fact' - - # Устанавливаем режим в параметры, если он не указан - if 'mode' not in get_params: - get_params['mode'] = default_mode - - # Определяем имя геттера - 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 == '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) - if not getter_name: - # Если режим не указан, берем первый доступный - available_getters = list(parser.getters.keys()) - if available_getters: - getter_name = available_getters[0] - logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}") - else: - return DataResult( - success=False, - message="Парсер не имеет доступных геттеров" - ) - elif request.report_type == 'svodka_pm': - # Для svodka_pm определяем геттер из параметра mode - getter_name = get_params.pop("mode", None) - if not getter_name: - # Если режим не указан, берем первый доступный - available_getters = list(parser.getters.keys()) - if available_getters: - getter_name = available_getters[0] - logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}") - else: - return DataResult( - success=False, - message="Парсер не имеет доступных геттеров" - ) - elif request.report_type == 'oper_spravka_tech_pos': - # Для oper_spravka_tech_pos используем геттер get_tech_pos - getter_name = 'get_tech_pos' - else: - # Для других парсеров определяем из параметра mode - getter_name = get_params.pop("mode", None) - if not getter_name: - # Если режим не указан, берем первый доступный - available_getters = list(parser.getters.keys()) - if available_getters: - getter_name = available_getters[0] - logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}") - else: - return DataResult( - success=False, - message="Парсер не имеет доступных геттеров" - ) + # Определяем имя геттера через парсер (делегируем логику в адаптер) + try: + getter_name = parser.determine_getter(get_params) + except ValueError as e: + return DataResult( + success=False, + message=str(e) + ) # Получаем значение через указанный геттер try: From cb8e8b2483301d29c3acb0c3ba545f6c1ddc0493 Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 18:44:05 +0300 Subject: [PATCH 09/11] =?UTF-8?q?=D0=9F=D0=BE=D1=87=D0=B8=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=98=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=BE=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python_parser/app/endpoints/common.py | 15 +++++++++++++++ python_parser/requirements.txt | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/python_parser/app/endpoints/common.py b/python_parser/app/endpoints/common.py index 04ce0ba..deb3f8d 100644 --- a/python_parser/app/endpoints/common.py +++ b/python_parser/app/endpoints/common.py @@ -164,10 +164,25 @@ async def get_server_info(): """Получение подробной информации о сервере""" import platform import sys + import os + import psutil + + # Получаем информацию о процессе + process = psutil.Process() + parent_process = process.parent() + + # Получаем информацию о системе + cpu_cores = psutil.cpu_count() + memory_info = psutil.virtual_memory() + memory_mb = memory_info.total / (1024 * 1024) # Конвертируем в MB return { "message": "Svodka Parser API", "version": "1.0.0", + "process_id": process.pid, + "parent_id": parent_process.pid if parent_process else None, + "cpu_cores": cpu_cores, + "memory_mb": memory_mb, "python_version": sys.version, "platform": platform.platform(), "available_parsers": list(PARSERS.keys()) diff --git a/python_parser/requirements.txt b/python_parser/requirements.txt index 2e74635..761746f 100644 --- a/python_parser/requirements.txt +++ b/python_parser/requirements.txt @@ -11,4 +11,5 @@ requests>=2.31.0 # pytest-cov>=4.0.0 # pytest-mock>=3.10.0 httpx>=0.24.0 -numpy \ No newline at end of file +numpy +psutil>=5.9.0 \ No newline at end of file From 31fad9813357cc8222fa770af7a119f09551ca49 Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 19:06:10 +0300 Subject: [PATCH 10/11] =?UTF-8?q?=D0=9E=D0=BF=D0=B5=D1=80=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BA=D1=83=20=D0=BF=D0=BE=D1=87=D0=B8=D0=BD?= =?UTF-8?q?=D0=B8=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/endpoints/oper_spravka_tech_pos.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/python_parser/app/endpoints/oper_spravka_tech_pos.py b/python_parser/app/endpoints/oper_spravka_tech_pos.py index 2c4543a..e5fbc10 100644 --- a/python_parser/app/endpoints/oper_spravka_tech_pos.py +++ b/python_parser/app/endpoints/oper_spravka_tech_pos.py @@ -41,20 +41,20 @@ def get_async_report_service() -> AsyncReportService: 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} },) async def upload_oper_spravka_tech_pos( - file: UploadFile = File(..., description="Excel файл оперативной справки техпос (.xlsx, .xlsm, .xls)") + file: UploadFile = File(..., description="ZIP архив с файлами оперативной справки техпос (.zip)") ): """Загрузка и обработка отчета оперативной справки техпос""" report_service = get_report_service() try: - if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + if not file.filename.endswith('.zip'): return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content=UploadErrorResponse( - message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + message="Поддерживаются только ZIP архивы (.zip)", error_code="INVALID_FILE_TYPE", details={ - "expected_formats": [".xlsx", ".xlsm", ".xls"], + "expected_formats": [".zip"], "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" } ).model_dump() @@ -115,9 +115,11 @@ async def get_oper_spravka_tech_pos_data( result = report_service.get_data(request) if result.success: + # Извлекаем данные из result.data["value"] + data = result.data.get("value", []) if isinstance(result.data, dict) else result.data return OperSpravkaTechPosResponse( success=True, - data=result.data + data=data ) else: raise HTTPException(status_code=404, detail=result.message) @@ -136,20 +138,20 @@ async def get_oper_spravka_tech_pos_data( 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} },) async def async_upload_oper_spravka_tech_pos( - file: UploadFile = File(..., description="Excel файл оперативной справки техпос (.xlsx, .xlsm, .xls)") + file: UploadFile = File(..., description="ZIP архив с файлами оперативной справки техпос (.zip)") ): """Асинхронная загрузка и обработка отчета оперативной справки техпос""" async_service = get_async_report_service() try: - if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + if not file.filename.endswith('.zip'): return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content=UploadErrorResponse( - message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + message="Поддерживаются только ZIP архивы (.zip)", error_code="INVALID_FILE_TYPE", details={ - "expected_formats": [".xlsx", ".xlsm", ".xls"], + "expected_formats": [".zip"], "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" } ).model_dump() From b01320acde6e133bcb0fee5bbec64b654952dcea Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 8 Sep 2025 19:12:11 +0300 Subject: [PATCH 11/11] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BB=20=D0=B0?= =?UTF-8?q?=D1=81=D0=B8=D0=BD=D0=BA=D0=B8=20=D0=B8=D0=B7=20=D1=81=D0=B2?= =?UTF-8?q?=D0=B0=D0=B3=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python_parser/app/endpoints/monitoring_fuel.py | 1 + python_parser/app/endpoints/monitoring_tar.py | 1 + python_parser/app/endpoints/oper_spravka_tech_pos.py | 1 + python_parser/app/endpoints/statuses_repair_ca.py | 1 + python_parser/app/endpoints/svodka_ca.py | 1 + python_parser/app/endpoints/svodka_pm.py | 1 + python_parser/app/endpoints/svodka_repair_ca.py | 1 + 7 files changed, 7 insertions(+) diff --git a/python_parser/app/endpoints/monitoring_fuel.py b/python_parser/app/endpoints/monitoring_fuel.py index 4690dc3..7108d82 100644 --- a/python_parser/app/endpoints/monitoring_fuel.py +++ b/python_parser/app/endpoints/monitoring_fuel.py @@ -268,6 +268,7 @@ async def get_monitoring_fuel_series_by_id_and_columns( @router.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], summary="Асинхронная загрузка файлов сводок мониторинга топлива одним ZIP-архивом", response_model=UploadResponse, + include_in_schema=False, responses={ 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} diff --git a/python_parser/app/endpoints/monitoring_tar.py b/python_parser/app/endpoints/monitoring_tar.py index fab6e7f..1e1910b 100644 --- a/python_parser/app/endpoints/monitoring_tar.py +++ b/python_parser/app/endpoints/monitoring_tar.py @@ -161,6 +161,7 @@ async def get_monitoring_tar_full_data( @router.post("/async/monitoring_tar/upload", tags=[MonitoringTarParser.name], summary="Асинхронная загрузка файла отчета мониторинга ТАР", response_model=UploadResponse, + include_in_schema=False, responses={ 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} diff --git a/python_parser/app/endpoints/oper_spravka_tech_pos.py b/python_parser/app/endpoints/oper_spravka_tech_pos.py index e5fbc10..346fc23 100644 --- a/python_parser/app/endpoints/oper_spravka_tech_pos.py +++ b/python_parser/app/endpoints/oper_spravka_tech_pos.py @@ -133,6 +133,7 @@ async def get_oper_spravka_tech_pos_data( @router.post("/async/oper_spravka_tech_pos/upload", tags=[OperSpravkaTechPosParser.name], summary="Асинхронная загрузка файла отчета оперативной справки техпос", response_model=UploadResponse, + include_in_schema=False, responses={ 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} diff --git a/python_parser/app/endpoints/statuses_repair_ca.py b/python_parser/app/endpoints/statuses_repair_ca.py index 15fce7a..4cddd5b 100644 --- a/python_parser/app/endpoints/statuses_repair_ca.py +++ b/python_parser/app/endpoints/statuses_repair_ca.py @@ -130,6 +130,7 @@ async def get_statuses_repair_ca_data( @router.post("/async/statuses_repair_ca/upload", tags=[StatusesRepairCAParser.name], summary="Асинхронная загрузка файла отчета статусов ремонта СА", response_model=UploadResponse, + include_in_schema=False, responses={ 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} diff --git a/python_parser/app/endpoints/svodka_ca.py b/python_parser/app/endpoints/svodka_ca.py index d64d234..c6b5f17 100644 --- a/python_parser/app/endpoints/svodka_ca.py +++ b/python_parser/app/endpoints/svodka_ca.py @@ -163,6 +163,7 @@ async def get_svodka_ca_data( @router.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], summary="Асинхронная загрузка файла отчета сводки СА", response_model=UploadResponse, + include_in_schema=False, responses={ 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} diff --git a/python_parser/app/endpoints/svodka_pm.py b/python_parser/app/endpoints/svodka_pm.py index 3bc690c..c17b992 100644 --- a/python_parser/app/endpoints/svodka_pm.py +++ b/python_parser/app/endpoints/svodka_pm.py @@ -262,6 +262,7 @@ async def get_svodka_pm_data( @router.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом", response_model=UploadResponse, + include_in_schema=False, responses={ 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} diff --git a/python_parser/app/endpoints/svodka_repair_ca.py b/python_parser/app/endpoints/svodka_repair_ca.py index a72eaac..42f6b7d 100644 --- a/python_parser/app/endpoints/svodka_repair_ca.py +++ b/python_parser/app/endpoints/svodka_repair_ca.py @@ -130,6 +130,7 @@ async def get_svodka_repair_ca_data( @router.post("/async/svodka_repair_ca/upload", tags=[SvodkaRepairCAParser.name], summary="Асинхронная загрузка файла отчета сводки ремонта СА", response_model=UploadResponse, + include_in_schema=False, responses={ 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"}