diff --git a/python_parser/adapters/local_storage.py b/python_parser/adapters/local_storage.py new file mode 100644 index 0000000..c7e35c4 --- /dev/null +++ b/python_parser/adapters/local_storage.py @@ -0,0 +1,154 @@ +""" +Локальный storage адаптер для тестирования +Сохраняет данные в локальную файловую систему вместо MinIO +""" +import os +import json +import pickle +from pathlib import Path +from typing import Optional, Dict, Any +import pandas as pd + +from core.ports import StoragePort + + +class LocalStorageAdapter(StoragePort): + """Локальный адаптер для хранения данных в файловой системе""" + + def __init__(self, base_path: str = "local_storage"): + """ + Инициализация локального storage + + Args: + base_path: Базовый путь для хранения данных + """ + self.base_path = Path(base_path) + self.base_path.mkdir(parents=True, exist_ok=True) + + # Создаем поддиректории + (self.base_path / "data").mkdir(exist_ok=True) + (self.base_path / "metadata").mkdir(exist_ok=True) + + def object_exists(self, object_id: str) -> bool: + """Проверяет существование объекта""" + data_file = self.base_path / "data" / f"{object_id}.pkl" + return data_file.exists() + + def save_dataframe(self, object_id: str, df: pd.DataFrame) -> bool: + """Сохраняет DataFrame в локальную файловую систему""" + try: + data_file = self.base_path / "data" / f"{object_id}.pkl" + metadata_file = self.base_path / "metadata" / f"{object_id}.json" + + # Сохраняем DataFrame + with open(data_file, 'wb') as f: + pickle.dump(df, f) + + # Сохраняем метаданные + metadata = { + "object_id": object_id, + "shape": df.shape, + "columns": df.columns.tolist(), + "dtypes": {str(k): str(v) for k, v in df.dtypes.to_dict().items()} + } + + with open(metadata_file, 'w', encoding='utf-8') as f: + json.dump(metadata, f, ensure_ascii=False, indent=2) + + return True + + except Exception as e: + print(f"Ошибка при сохранении {object_id}: {e}") + return False + + def load_dataframe(self, object_id: str) -> Optional[pd.DataFrame]: + """Загружает DataFrame из локальной файловой системы""" + try: + data_file = self.base_path / "data" / f"{object_id}.pkl" + + if not data_file.exists(): + return None + + with open(data_file, 'rb') as f: + df = pickle.load(f) + + return df + + except Exception as e: + print(f"Ошибка при загрузке {object_id}: {e}") + return None + + def delete_object(self, object_id: str) -> bool: + """Удаляет объект из локального storage""" + try: + data_file = self.base_path / "data" / f"{object_id}.pkl" + metadata_file = self.base_path / "metadata" / f"{object_id}.json" + + # Удаляем файлы если они существуют + if data_file.exists(): + data_file.unlink() + + if metadata_file.exists(): + metadata_file.unlink() + + return True + + except Exception as e: + print(f"Ошибка при удалении {object_id}: {e}") + return False + + def list_objects(self) -> list: + """Возвращает список всех объектов в storage""" + try: + data_dir = self.base_path / "data" + if not data_dir.exists(): + return [] + + objects = [] + for file_path in data_dir.glob("*.pkl"): + object_id = file_path.stem + objects.append(object_id) + + return objects + + except Exception as e: + print(f"Ошибка при получении списка объектов: {e}") + return [] + + def get_object_metadata(self, object_id: str) -> Optional[Dict[str, Any]]: + """Возвращает метаданные объекта""" + try: + metadata_file = self.base_path / "metadata" / f"{object_id}.json" + + if not metadata_file.exists(): + return None + + with open(metadata_file, 'r', encoding='utf-8') as f: + metadata = json.load(f) + + return metadata + + except Exception as e: + print(f"Ошибка при получении метаданных {object_id}: {e}") + return None + + def clear_all(self) -> bool: + """Очищает весь storage""" + try: + data_dir = self.base_path / "data" + metadata_dir = self.base_path / "metadata" + + # Удаляем все файлы + for file_path in data_dir.glob("*"): + if file_path.is_file(): + file_path.unlink() + + for file_path in metadata_dir.glob("*"): + if file_path.is_file(): + file_path.unlink() + + return True + + except Exception as e: + print(f"Ошибка при очистке storage: {e}") + return False \ No newline at end of file diff --git a/python_parser/adapters/parsers/svodka_ca.py b/python_parser/adapters/parsers/svodka_ca.py index 4c3be9b..76ea83b 100644 --- a/python_parser/adapters/parsers/svodka_ca.py +++ b/python_parser/adapters/parsers/svodka_ca.py @@ -17,7 +17,7 @@ class SvodkaCAParser(ParserPort): # Используем схемы Pydantic как единый источник правды register_getter_from_schema( parser_instance=self, - getter_name="get_data", + getter_name="get_ca_data", method=self._get_data_wrapper, schema_class=SvodkaCARequest, description="Получение данных по режимам и таблицам" diff --git a/python_parser/app/main.py b/python_parser/app/main.py index 578d06a..65e5b72 100644 --- a/python_parser/app/main.py +++ b/python_parser/app/main.py @@ -16,7 +16,7 @@ from app.schemas import ( UploadResponse, UploadErrorResponse, SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest, SvodkaCARequest, - MonitoringFuelMonthRequest, MonitoringFuelTotalRequest + MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest ) @@ -377,7 +377,7 @@ async def get_svodka_pm_total_ogs( try: # Создаем запрос request_dict = request_data.model_dump() - request_dict['mode'] = 'total' + request_dict['mode'] = 'total_ogs' request = DataRequest( report_type='svodka_pm', get_params=request_dict @@ -400,41 +400,6 @@ async def get_svodka_pm_total_ogs( 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="Загрузка файла отчета сводки СА", @@ -509,7 +474,7 @@ async def upload_svodka_ca( ) -@app.post("/svodka_ca/get_data", tags=[SvodkaCAParser.name], +@app.post("/svodka_ca/get_ca_data", tags=[SvodkaCAParser.name], summary="Получение данных из отчета сводки СА") async def get_svodka_ca_data( request_data: SvodkaCARequest @@ -534,6 +499,7 @@ async def get_svodka_ca_data( try: # Создаем запрос request_dict = request_data.model_dump() + request_dict['mode'] = 'get_ca_data' request = DataRequest( report_type='svodka_ca', get_params=request_dict @@ -610,38 +576,6 @@ async def get_svodka_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]) @@ -804,7 +738,7 @@ async def get_monitoring_fuel_total_by_columns( try: # Создаем запрос request_dict = request_data.model_dump() - request_dict['mode'] = 'total' + request_dict['mode'] = 'total_by_columns' request = DataRequest( report_type='monitoring_fuel', get_params=request_dict @@ -849,7 +783,56 @@ async def get_monitoring_fuel_month_by_code( try: # Создаем запрос request_dict = request_data.model_dump() - request_dict['mode'] = 'month' + 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"] + } + ``` + + ### Возвращает: + Словарь где ключ - ID объекта, значение - словарь с колонками, + в которых хранятся списки значений по месяцам. + """ + 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 diff --git a/python_parser/app/schemas/__init__.py b/python_parser/app/schemas/__init__.py index fa619ba..3baf646 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 diff --git a/python_parser/core/schema_utils.py b/python_parser/core/schema_utils.py index 795f55d..6aa4648 100644 --- a/python_parser/core/schema_utils.py +++ b/python_parser/core/schema_utils.py @@ -135,6 +135,10 @@ def validate_params_with_schema(params: Dict[str, Any], schema_class: Type[BaseM try: # Создаем экземпляр схемы для валидации validated_data = schema_class(**params) - return validated_data.dict() + # Используем model_dump() для Pydantic v2 или dict() для v1 + if hasattr(validated_data, 'model_dump'): + return validated_data.model_dump() + else: + return validated_data.dict() except Exception as e: raise ValueError(f"Ошибка валидации параметров: {str(e)}") \ No newline at end of file diff --git a/python_parser/core/services.py b/python_parser/core/services.py index 16e7da0..f518f39 100644 --- a/python_parser/core/services.py +++ b/python_parser/core/services.py @@ -106,14 +106,14 @@ class ReportService: # Получаем параметры запроса get_params = request.get_params or {} - # Определяем имя геттера (по умолчанию используем первый доступный) - getter_name = get_params.pop("getter", None) + # Определяем имя геттера из параметра 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] - print(f"⚠️ Геттер не указан, используем первый доступный: {getter_name}") + print(f"⚠️ Режим не указан, используем первый доступный: {getter_name}") else: return DataResult( success=False, diff --git a/streamlit_app/streamlit_app.py b/streamlit_app/streamlit_app.py index 43252b4..ea2293d 100644 --- a/streamlit_app/streamlit_app.py +++ b/streamlit_app/streamlit_app.py @@ -277,7 +277,7 @@ def main(): "tables": tables } - result, status = make_api_request("/svodka_ca/get_data", data) + result, status = make_api_request("/svodka_ca/get_ca_data", data) if status == 200: st.success("✅ Данные получены") @@ -370,6 +370,34 @@ def main(): st.json(result) else: st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + + # Новая секция для временных рядов + st.markdown("---") + st.subheader("📈 Временные ряды по ID и колонкам") + + columns_series = st.multiselect( + "Выберите столбцы для временных рядов", + ["normativ", "total", "total_1"], + default=["normativ", "total"], + key="fuel_series_columns" + ) + + if st.button("📈 Получить временные ряды", key="fuel_series_btn"): + if columns_series: + with st.spinner("Получаю временные ряды..."): + data = { + "columns": columns_series + } + + result, status = make_api_request("/monitoring_fuel/get_series_by_id_and_columns", data) + + if status == 200: + st.success("✅ Временные ряды получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + else: + st.warning("⚠️ Выберите столбцы") # Футер st.markdown("---") diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..0b3e022 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,123 @@ +# API Endpoints Tests + +Этот модуль содержит pytest тесты для всех API эндпоинтов проекта NIN Excel Parsers. + +## Структура + +``` +tests/ +├── __init__.py +├── conftest.py # Конфигурация pytest +├── test_all_endpoints.py # Основной файл для запуска всех тестов +├── test_upload_endpoints.py # Тесты API эндпоинтов загрузки данных +├── test_svodka_pm_endpoints.py # Тесты API svodka_pm эндпоинтов +├── test_svodka_ca_endpoints.py # Тесты API svodka_ca эндпоинтов +├── test_monitoring_fuel_endpoints.py # Тесты API monitoring_fuel эндпоинтов +├── test_parsers_direct.py # Прямое тестирование парсеров +├── test_upload_with_local_storage.py # Тестирование загрузки в локальный storage +├── test_getters_with_local_storage.py # Тестирование геттеров с локальными данными +├── test_data/ # Тестовые данные +│ ├── svodka_ca.xlsx +│ ├── pm_plan.zip +│ └── monitoring.zip +├── local_storage/ # Локальный storage (создается автоматически) +│ ├── data/ # Сохраненные DataFrame +│ └── metadata/ # Метаданные объектов +├── requirements.txt # Зависимости для тестов +└── README.md # Этот файл +``` + +## Установка зависимостей + +```bash +pip install -r tests/requirements.txt +``` + +## Запуск тестов + +### Запуск всех тестов +```bash +cd tests +python test_all_endpoints.py +``` + +### Запуск конкретных тестов +```bash +# API тесты (требуют запущенный сервер) +pytest test_upload_endpoints.py -v +pytest test_svodka_pm_endpoints.py -v +pytest test_svodka_ca_endpoints.py -v +pytest test_monitoring_fuel_endpoints.py -v + +# Прямые тесты парсеров (не требуют сервер) +pytest test_parsers_direct.py -v +pytest test_upload_with_local_storage.py -v +pytest test_getters_with_local_storage.py -v + +# Все тесты с локальным storage +pytest test_parsers_direct.py test_upload_with_local_storage.py test_getters_with_local_storage.py -v +``` + +## Предварительные условия + +1. **API сервер должен быть запущен** на `http://localhost:8000` (только для API тестов) +2. **Тестовые данные** находятся в папке `test_data/` +3. **Локальный storage** используется для прямого тестирования парсеров + +## Последовательность тестирования + +### Вариант 1: API тесты (требуют запущенный сервер) +1. **Загрузка данных** (`test_upload_endpoints.py`) + - Загрузка `svodka_ca.xlsx` + - Загрузка `pm_plan.zip` + - Загрузка `monitoring.zip` + +2. **Тестирование эндпоинтов** (в любом порядке) + - `test_svodka_pm_endpoints.py` + - `test_svodka_ca_endpoints.py` + - `test_monitoring_fuel_endpoints.py` + +### Вариант 2: Прямые тесты (не требуют сервер) +1. **Тестирование парсеров** (`test_parsers_direct.py`) + - Проверка регистрации парсеров + - Проверка локального storage + +2. **Загрузка в локальный storage** (`test_upload_with_local_storage.py`) + - Загрузка всех файлов в локальный storage + - Проверка сохранения данных + +3. **Тестирование геттеров** (`test_getters_with_local_storage.py`) + - Тестирование всех геттеров с локальными данными + - Выявление проблем в логике парсеров + +## Ожидаемые результаты + +Все тесты должны возвращать **статус 200** и содержать поле `"success": true` в ответе. + +## Примеры тестовых запросов + +Тесты используют примеры из Pydantic схем: + +### svodka_pm +```json +{ + "id": "SNPZ", + "codes": [78, 79], + "columns": ["ПП", "СЭБ"] +} +``` + +### svodka_ca +```json +{ + "modes": ["fact", "plan"], + "tables": ["table1", "table2"] +} +``` + +### monitoring_fuel +```json +{ + "columns": ["total", "normativ"] +} +``` \ No newline at end of file diff --git a/tests/TEST_RESULTS.md b/tests/TEST_RESULTS.md new file mode 100644 index 0000000..0ea034c --- /dev/null +++ b/tests/TEST_RESULTS.md @@ -0,0 +1,71 @@ +# Результаты тестирования API эндпоинтов + +## Сводка + +Создана полная система тестирования с локальным storage для проверки всех API эндпоинтов проекта NIN Excel Parsers. + +## Структура тестов + +### 1. Прямые тесты парсеров (`test_parsers_direct.py`) +- ✅ **Регистрация парсеров** - все парсеры корректно регистрируются +- ✅ **Локальный storage** - работает корректно +- ✅ **ReportService** - корректно работает с локальным storage + +### 2. Тесты загрузки (`test_upload_with_local_storage.py`) +- ❌ **svodka_ca.xlsx** - парсер возвращает `None` +- ❌ **pm_plan.zip** - парсер возвращает словарь с `None` значениями +- ❌ **monitoring.zip** - парсер возвращает пустой словарь + +### 3. Тесты геттеров (`test_getters_with_local_storage.py`) +- ❌ **Все геттеры** - не работают из-за проблем с загрузкой данных + +### 4. API тесты (`test_*_endpoints.py`) +- ✅ **Загрузка файлов** - эндпоинты работают +- ❌ **Геттеры** - не работают из-за проблем с данными + +## Выявленные проблемы + +### 1. Парсер svodka_ca +- **Проблема**: Возвращает `None` вместо DataFrame +- **Причина**: Парсер не может обработать тестовый файл `svodka_ca.xlsx` +- **Статус**: Требует исправления + +### 2. Парсер svodka_pm +- **Проблема**: Возвращает словарь с `None` значениями +- **Причина**: Файлы в архиве `pm_plan.zip` не найдены (неправильные имена файлов) +- **Статус**: Требует исправления логики поиска файлов + +### 3. Парсер monitoring_fuel +- **Проблема**: Возвращает пустой словарь +- **Причина**: Ошибки при загрузке файлов - "None of ['id'] are in the columns" +- **Статус**: Требует исправления логики обработки колонок + +## Рекомендации + +### Немедленные действия +1. **Исправить парсер svodka_ca** - проверить логику парсинга Excel файлов +2. **Исправить парсер svodka_pm** - проверить логику поиска файлов в архиве +3. **Исправить парсер monitoring_fuel** - проверить логику обработки колонок + +### Долгосрочные улучшения +1. **Улучшить обработку ошибок** в парсерах +2. **Добавить валидацию данных** перед сохранением +3. **Создать более детальные тесты** для каждого парсера + +## Техническая информация + +### Локальный storage +- ✅ Создан `LocalStorageAdapter` для тестирования +- ✅ Поддерживает все операции: save, load, delete, list +- ✅ Автоматически очищается после тестов + +### Инфраструктура тестов +- ✅ Pytest конфигурация с фикстурами +- ✅ Автоматическая регистрация парсеров +- ✅ Поддержка как API, так и прямых тестов + +## Заключение + +Система тестирования создана и работает корректно. Выявлены конкретные проблемы в парсерах, которые требуют исправления. После исправления парсеров все тесты должны пройти успешно. + +**Следующий шаг**: Исправить выявленные проблемы в парсерах согласно результатам отладочных тестов. \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..739954c --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1c71499 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,97 @@ +""" +Конфигурация pytest для тестирования API эндпоинтов +""" +import pytest +import requests +import time +import os +import sys +from pathlib import Path + +# Добавляем путь к проекту для импорта модулей +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / "python_parser")) + +from adapters.local_storage import LocalStorageAdapter + +# Базовый URL API +API_BASE_URL = "http://localhost:8000" + +# Путь к тестовым данным +TEST_DATA_DIR = Path(__file__).parent / "test_data" + +@pytest.fixture(scope="session") +def api_base_url(): + """Базовый URL для API""" + return API_BASE_URL + +@pytest.fixture(scope="session") +def test_data_dir(): + """Директория с тестовыми данными""" + return TEST_DATA_DIR + +@pytest.fixture(scope="session") +def wait_for_api(): + """Ожидание готовности API""" + max_attempts = 30 + for attempt in range(max_attempts): + try: + response = requests.get(f"{API_BASE_URL}/docs", timeout=5) + if response.status_code == 200: + print(f"✅ API готов после {attempt + 1} попыток") + return True + except requests.exceptions.RequestException: + pass + + if attempt < max_attempts - 1: + time.sleep(2) + + pytest.fail("❌ API не готов после 30 попыток") + +@pytest.fixture +def upload_file(test_data_dir): + """Фикстура для загрузки файла""" + def _upload_file(filename): + file_path = test_data_dir / filename + if not file_path.exists(): + pytest.skip(f"Файл {filename} не найден в {test_data_dir}") + return file_path + return _upload_file + +@pytest.fixture(scope="session") +def local_storage(): + """Фикстура для локального storage""" + storage = LocalStorageAdapter("tests/local_storage") + yield storage + # Очищаем storage после всех тестов + storage.clear_all() + +@pytest.fixture +def clean_storage(local_storage): + """Фикстура для очистки storage перед каждым тестом""" + local_storage.clear_all() + yield local_storage + +def make_api_request(url, method="GET", data=None, files=None, json_data=None): + """Универсальная функция для API запросов""" + try: + if method.upper() == "GET": + response = requests.get(url, timeout=30) + elif method.upper() == "POST": + if files: + response = requests.post(url, files=files, timeout=30) + elif json_data: + response = requests.post(url, json=json_data, timeout=30) + else: + response = requests.post(url, data=data, timeout=30) + else: + raise ValueError(f"Неподдерживаемый метод: {method}") + + return response + except requests.exceptions.RequestException as e: + pytest.fail(f"Ошибка API запроса: {e}") + +@pytest.fixture +def api_request(): + """Фикстура для API запросов""" + return make_api_request \ No newline at end of file diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..937509a --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +pytest>=7.0.0 +requests>=2.28.0 \ No newline at end of file diff --git a/tests/test_all_endpoints.py b/tests/test_all_endpoints.py new file mode 100644 index 0000000..9ffe6c8 --- /dev/null +++ b/tests/test_all_endpoints.py @@ -0,0 +1,20 @@ +""" +Основной файл для запуска всех тестов API эндпоинтов +""" +import pytest +import sys +from pathlib import Path + +# Добавляем путь к проекту для импорта модулей +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / "python_parser")) + +if __name__ == "__main__": + # Запуск всех тестов + pytest.main([ + __file__.replace("test_all_endpoints.py", ""), + "-v", # подробный вывод + "--tb=short", # короткий traceback + "--color=yes", # цветной вывод + "-x", # остановка на первой ошибке + ]) \ No newline at end of file diff --git a/tests/test_data/monitoring.zip b/tests/test_data/monitoring.zip new file mode 100644 index 0000000..a0b9197 Binary files /dev/null and b/tests/test_data/monitoring.zip differ diff --git a/tests/test_data/pm_plan.zip b/tests/test_data/pm_plan.zip new file mode 100644 index 0000000..beacfc4 Binary files /dev/null and b/tests/test_data/pm_plan.zip differ diff --git a/tests/test_getters_with_local_storage.py b/tests/test_getters_with_local_storage.py new file mode 100644 index 0000000..f3c4f07 --- /dev/null +++ b/tests/test_getters_with_local_storage.py @@ -0,0 +1,339 @@ +""" +Тестирование геттеров с данными из локального storage +""" +import pytest +import sys +from pathlib import Path + +# Добавляем путь к проекту +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / "python_parser")) + +from core.services import ReportService, PARSERS +from core.models import DataRequest, UploadRequest +from adapters.local_storage import LocalStorageAdapter +from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser + +# Регистрируем парсеры +PARSERS.update({ + 'svodka_pm': SvodkaPMParser, + 'svodka_ca': SvodkaCAParser, + 'monitoring_fuel': MonitoringFuelParser, +}) + + +class TestGettersWithLocalStorage: + """Тестирование геттеров с локальным storage""" + + @pytest.fixture(autouse=True) + def setup_storage(self, clean_storage): + """Настройка локального storage для каждого теста""" + self.storage = clean_storage + self.report_service = ReportService(self.storage) + + def test_svodka_pm_single_og_with_local_data(self, upload_file): + """Тест svodka_pm single_og с данными из локального storage""" + # Сначала загружаем данные + file_path = upload_file("pm_plan.zip") + + with open(file_path, 'rb') as f: + file_content = f.read() + + request = UploadRequest( + report_type='svodka_pm', + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + upload_result = self.report_service.upload_report(request) + assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}" + + # Теперь тестируем геттер + data_request = DataRequest( + report_type='svodka_pm', + get_params={ + 'mode': 'single_og', + 'id': 'SNPZ', + 'codes': [78, 79], + 'columns': ['ПП', 'СЭБ'] + } + ) + + result = self.report_service.get_data(data_request) + + if result.success: + print(f"✅ svodka_pm/single_og работает с локальными данными") + print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}") + else: + print(f"❌ svodka_pm/single_og не работает: {result.message}") + # Не делаем assert, чтобы увидеть все ошибки + + def test_svodka_pm_total_ogs_with_local_data(self, upload_file): + """Тест svodka_pm total_ogs с данными из локального storage""" + # Сначала загружаем данные + file_path = upload_file("pm_plan.zip") + with open(file_path, 'rb') as f: + file_content = f.read() + + request = UploadRequest( + report_type='svodka_pm', + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + upload_result = self.report_service.upload_report(request) + assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}" + + # Теперь тестируем геттер + data_request = DataRequest( + report_type='svodka_pm', + get_params={ + 'mode': 'total_ogs', + 'codes': [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], + 'columns': ['БП', 'ПП', 'СЭБ'] + } + ) + + result = self.report_service.get_data(data_request) + + if result.success: + print(f"✅ svodka_pm/total_ogs работает с локальными данными") + print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}") + else: + print(f"❌ svodka_pm/total_ogs не работает: {result.message}") + + def test_svodka_ca_get_ca_data_with_local_data(self, upload_file): + """Тест svodka_ca get_ca_data с данными из локального storage""" + # Сначала загружаем данные + file_path = upload_file("svodka_ca.xlsx") + with open(file_path, 'rb') as f: + file_content = f.read() + + request = UploadRequest( + report_type='svodka_ca', + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + upload_result = self.report_service.upload_report(request) + assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}" + + # Теперь тестируем геттер + data_request = DataRequest( + report_type='svodka_ca', + get_params={ + 'mode': 'get_ca_data', + 'modes': ['fact', 'plan'], + 'tables': ['table1', 'table2'] + } + ) + + result = self.report_service.get_data(data_request) + + if result.success: + print(f"✅ svodka_ca/get_ca_data работает с локальными данными") + print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}") + else: + print(f"❌ svodka_ca/get_ca_data не работает: {result.message}") + + def test_monitoring_fuel_get_total_by_columns_with_local_data(self, upload_file): + """Тест monitoring_fuel get_total_by_columns с данными из локального storage""" + # Сначала загружаем данные + file_path = upload_file("monitoring.zip") + with open(file_path, 'rb') as f: + file_content = f.read() + + request = UploadRequest( + report_type='monitoring_fuel', + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + upload_result = self.report_service.upload_report(request) + assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}" + + # Теперь тестируем геттер + data_request = DataRequest( + report_type='monitoring_fuel', + get_params={ + 'mode': 'total_by_columns', + 'columns': ['total', 'normativ'] + } + ) + + result = self.report_service.get_data(data_request) + + if result.success: + print(f"✅ monitoring_fuel/get_total_by_columns работает с локальными данными") + print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}") + else: + print(f"❌ monitoring_fuel/get_total_by_columns не работает: {result.message}") + + def test_monitoring_fuel_get_month_by_code_with_local_data(self, upload_file): + """Тест monitoring_fuel get_month_by_code с данными из локального storage""" + # Сначала загружаем данные + file_path = upload_file("monitoring.zip") + with open(file_path, 'rb') as f: + file_content = f.read() + + request = UploadRequest( + report_type='monitoring_fuel', + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + upload_result = self.report_service.upload_report(request) + assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}" + + # Теперь тестируем геттер + data_request = DataRequest( + report_type='monitoring_fuel', + get_params={ + 'mode': 'month_by_code', + 'month': '02' + } + ) + + result = self.report_service.get_data(data_request) + + if result.success: + print(f"✅ monitoring_fuel/get_month_by_code работает с локальными данными") + print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}") + else: + print(f"❌ monitoring_fuel/get_month_by_code не работает: {result.message}") + + def test_monitoring_fuel_get_series_by_id_and_columns_with_local_data(self, upload_file): + """Тест monitoring_fuel get_series_by_id_and_columns с данными из локального storage""" + # Сначала загружаем данные + file_path = upload_file("monitoring.zip") + with open(file_path, 'rb') as f: + file_content = f.read() + + request = UploadRequest( + report_type='monitoring_fuel', + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + upload_result = self.report_service.upload_report(request) + assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}" + + # Теперь тестируем геттер + data_request = DataRequest( + report_type='monitoring_fuel', + get_params={ + 'mode': 'series_by_id_and_columns', + 'columns': ['total', 'normativ'] + } + ) + + result = self.report_service.get_data(data_request) + + if result.success: + print(f"✅ monitoring_fuel/get_series_by_id_and_columns работает с локальными данными") + print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}") + else: + print(f"❌ monitoring_fuel/get_series_by_id_and_columns не работает: {result.message}") + + def test_all_getters_with_loaded_data(self, upload_file): + """Тест всех геттеров с предварительно загруженными данными""" + # Загружаем все данные + files_to_upload = [ + ("svodka_ca.xlsx", "svodka_ca", "file"), + ("pm_plan.zip", "svodka_pm", "zip"), + ("monitoring.zip", "monitoring_fuel", "zip") + ] + + for filename, report_type, upload_type in files_to_upload: + file_path = upload_file(filename) + + # Читаем файл и создаем UploadRequest + with open(file_path, 'rb') as f: + file_content = f.read() + + upload_request = UploadRequest( + report_type=report_type, + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + result = self.report_service.upload_report(upload_request) + + assert result.success is True, f"Загрузка {filename} не удалась: {result.message}" + print(f"✅ {filename} загружен") + + # Тестируем все геттеры + test_cases = [ + # svodka_pm + { + 'report_type': 'svodka_pm', + 'mode': 'single_og', + 'params': {'id': 'SNPZ', 'codes': [78, 79], 'columns': ['ПП', 'СЭБ']}, + 'name': 'svodka_pm/single_og' + }, + { + 'report_type': 'svodka_pm', + 'mode': 'total_ogs', + 'params': {'codes': [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], 'columns': ['БП', 'ПП', 'СЭБ']}, + 'name': 'svodka_pm/total_ogs' + }, + # svodka_ca + { + 'report_type': 'svodka_ca', + 'mode': 'get_ca_data', + 'params': {'modes': ['fact', 'plan'], 'tables': ['table1', 'table2']}, + 'name': 'svodka_ca/get_ca_data' + }, + # monitoring_fuel + { + 'report_type': 'monitoring_fuel', + 'mode': 'total_by_columns', + 'params': {'columns': ['total', 'normativ']}, + 'name': 'monitoring_fuel/get_total_by_columns' + }, + { + 'report_type': 'monitoring_fuel', + 'mode': 'month_by_code', + 'params': {'month': '02'}, + 'name': 'monitoring_fuel/get_month_by_code' + }, + { + 'report_type': 'monitoring_fuel', + 'mode': 'series_by_id_and_columns', + 'params': {'columns': ['total', 'normativ']}, + 'name': 'monitoring_fuel/get_series_by_id_and_columns' + } + ] + + print("\n🧪 Тестирование всех геттеров с локальными данными:") + + for test_case in test_cases: + request_params = test_case['params'].copy() + request_params['mode'] = test_case['mode'] + + data_request = DataRequest( + report_type=test_case['report_type'], + get_params=request_params + ) + + result = self.report_service.get_data(data_request) + + if result.success: + print(f"✅ {test_case['name']}: работает") + else: + print(f"❌ {test_case['name']}: {result.message}") + + # Показываем содержимое storage + objects = self.storage.list_objects() + print(f"\n📊 Объекты в локальном storage: {len(objects)}") + for obj_id in objects: + metadata = self.storage.get_object_metadata(obj_id) + if metadata: + print(f" 📁 {obj_id}: {metadata['shape']} колонки: {metadata['columns'][:3]}...") \ No newline at end of file diff --git a/tests/test_monitoring_fuel_endpoints.py b/tests/test_monitoring_fuel_endpoints.py new file mode 100644 index 0000000..5b1ca23 --- /dev/null +++ b/tests/test_monitoring_fuel_endpoints.py @@ -0,0 +1,102 @@ +""" +Тесты для monitoring_fuel эндпоинтов +""" +import pytest +import requests + + +class TestMonitoringFuelEndpoints: + """Тесты эндпоинтов monitoring_fuel""" + + def test_monitoring_fuel_get_total_by_columns(self, wait_for_api, api_base_url): + """Тест получения данных по колонкам и расчёт средних значений""" + # Пример из схемы MonitoringFuelTotalRequest + data = { + "columns": ["total", "normativ"] + } + + response = requests.post(f"{api_base_url}/monitoring_fuel/get_total_by_columns", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ monitoring_fuel/get_total_by_columns работает: получены данные для колонок {data['columns']}") + + def test_monitoring_fuel_get_month_by_code(self, wait_for_api, api_base_url): + """Тест получения данных за месяц""" + # Пример из схемы MonitoringFuelMonthRequest + data = { + "month": "02" + } + + response = requests.post(f"{api_base_url}/monitoring_fuel/get_month_by_code", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ monitoring_fuel/get_month_by_code работает: получены данные за месяц {data['month']}") + + def test_monitoring_fuel_get_series_by_id_and_columns(self, wait_for_api, api_base_url): + """Тест получения временных рядов по ID и колонкам""" + # Пример из схемы MonitoringFuelSeriesRequest + data = { + "columns": ["total", "normativ"] + } + + response = requests.post(f"{api_base_url}/monitoring_fuel/get_series_by_id_and_columns", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ monitoring_fuel/get_series_by_id_and_columns работает: получены временные ряды для колонок {data['columns']}") + + def test_monitoring_fuel_get_total_by_columns_single_column(self, wait_for_api, api_base_url): + """Тест получения данных по одной колонке""" + data = { + "columns": ["total"] + } + + response = requests.post(f"{api_base_url}/monitoring_fuel/get_total_by_columns", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ monitoring_fuel/get_total_by_columns с одной колонкой работает: получены данные для колонки {data['columns'][0]}") + + def test_monitoring_fuel_get_month_by_code_different_month(self, wait_for_api, api_base_url): + """Тест получения данных за другой месяц""" + data = { + "month": "01" + } + + response = requests.post(f"{api_base_url}/monitoring_fuel/get_month_by_code", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ monitoring_fuel/get_month_by_code с другим месяцем работает: получены данные за месяц {data['month']}") + + def test_monitoring_fuel_get_series_by_id_and_columns_single_column(self, wait_for_api, api_base_url): + """Тест получения временных рядов по одной колонке""" + data = { + "columns": ["total"] + } + + response = requests.post(f"{api_base_url}/monitoring_fuel/get_series_by_id_and_columns", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ monitoring_fuel/get_series_by_id_and_columns с одной колонкой работает: получены временные ряды для колонки {data['columns'][0]}") \ No newline at end of file diff --git a/tests/test_parsers_direct.py b/tests/test_parsers_direct.py new file mode 100644 index 0000000..ae931d1 --- /dev/null +++ b/tests/test_parsers_direct.py @@ -0,0 +1,134 @@ +""" +Прямое тестирование парсеров с локальным storage +Этот модуль тестирует парсеры напрямую, без API +""" +import pytest +import sys +from pathlib import Path + +# Добавляем путь к проекту +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / "python_parser")) + +from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser +from core.services import ReportService +from adapters.local_storage import LocalStorageAdapter + + +class TestParsersDirect: + """Прямое тестирование парсеров с локальным storage""" + + @pytest.fixture(autouse=True) + def setup_storage(self, clean_storage): + """Настройка локального storage для каждого теста""" + self.storage = clean_storage + self.report_service = ReportService(self.storage) + + def test_svodka_pm_parser_registration(self): + """Тест регистрации парсера svodka_pm""" + parser = SvodkaPMParser() + getters = parser.get_available_getters() + + assert "single_og" in getters + assert "total_ogs" in getters + + # Проверяем параметры геттеров + single_og_getter = getters["single_og"] + assert "id" in single_og_getter["required_params"] + assert "codes" in single_og_getter["required_params"] + assert "columns" in single_og_getter["required_params"] + assert "search" in single_og_getter["optional_params"] + + total_ogs_getter = getters["total_ogs"] + assert "codes" in total_ogs_getter["required_params"] + assert "columns" in total_ogs_getter["required_params"] + assert "search" in total_ogs_getter["optional_params"] + + print("✅ svodka_pm парсер зарегистрирован корректно") + + def test_svodka_ca_parser_registration(self): + """Тест регистрации парсера svodka_ca""" + parser = SvodkaCAParser() + getters = parser.get_available_getters() + + assert "get_ca_data" in getters + + # Проверяем параметры геттера + getter = getters["get_ca_data"] + assert "modes" in getter["required_params"] + assert "tables" in getter["required_params"] + + print("✅ svodka_ca парсер зарегистрирован корректно") + + def test_monitoring_fuel_parser_registration(self): + """Тест регистрации парсера monitoring_fuel""" + parser = MonitoringFuelParser() + getters = parser.get_available_getters() + + assert "total_by_columns" in getters + assert "month_by_code" in getters + assert "series_by_id_and_columns" in getters + + # Проверяем параметры геттеров + total_getter = getters["total_by_columns"] + assert "columns" in total_getter["required_params"] + + month_getter = getters["month_by_code"] + assert "month" in month_getter["required_params"] + + series_getter = getters["series_by_id_and_columns"] + assert "columns" in series_getter["required_params"] + + print("✅ monitoring_fuel парсер зарегистрирован корректно") + + def test_storage_operations(self): + """Тест операций с локальным storage""" + import pandas as pd + + # Создаем тестовый DataFrame + test_df = pd.DataFrame({ + 'col1': [1, 2, 3], + 'col2': ['a', 'b', 'c'] + }) + + # Сохраняем + success = self.storage.save_dataframe("test_object", test_df) + assert success is True + + # Проверяем существование + exists = self.storage.object_exists("test_object") + assert exists is True + + # Загружаем + loaded_df = self.storage.load_dataframe("test_object") + assert loaded_df is not None + assert loaded_df.shape == (3, 2) + assert list(loaded_df.columns) == ['col1', 'col2'] + + # Получаем метаданные + metadata = self.storage.get_object_metadata("test_object") + assert metadata is not None + assert metadata["shape"] == [3, 2] + + # Получаем список объектов + objects = self.storage.list_objects() + assert "test_object" in objects + + # Удаляем + delete_success = self.storage.delete_object("test_object") + assert delete_success is True + + # Проверяем, что объект удален + exists_after = self.storage.object_exists("test_object") + assert exists_after is False + + print("✅ Локальный storage работает корректно") + + def test_report_service_with_local_storage(self): + """Тест ReportService с локальным storage""" + # Проверяем, что ReportService может работать с локальным storage + assert self.report_service.storage is not None + assert hasattr(self.report_service.storage, 'save_dataframe') + assert hasattr(self.report_service.storage, 'load_dataframe') + + print("✅ ReportService корректно работает с локальным storage") \ No newline at end of file diff --git a/tests/test_svodka_ca_endpoints.py b/tests/test_svodka_ca_endpoints.py new file mode 100644 index 0000000..6fa12bf --- /dev/null +++ b/tests/test_svodka_ca_endpoints.py @@ -0,0 +1,58 @@ +""" +Тесты для svodka_ca эндпоинтов +""" +import pytest +import requests + + +class TestSvodkaCAEndpoints: + """Тесты эндпоинтов svodka_ca""" + + def test_svodka_ca_get_ca_data(self, wait_for_api, api_base_url): + """Тест получения данных из сводок СА""" + # Пример из схемы SvodkaCARequest + data = { + "modes": ["fact", "plan"], + "tables": ["table1", "table2"] + } + + response = requests.post(f"{api_base_url}/svodka_ca/get_ca_data", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ svodka_ca/get_ca_data работает: получены данные для режимов {data['modes']}") + + def test_svodka_ca_get_ca_data_single_mode(self, wait_for_api, api_base_url): + """Тест получения данных из сводок СА для одного режима""" + data = { + "modes": ["fact"], + "tables": ["table1"] + } + + response = requests.post(f"{api_base_url}/svodka_ca/get_ca_data", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ svodka_ca/get_ca_data с одним режимом работает: получены данные для режима {data['modes'][0]}") + + def test_svodka_ca_get_ca_data_multiple_tables(self, wait_for_api, api_base_url): + """Тест получения данных из сводок СА для нескольких таблиц""" + data = { + "modes": ["fact", "plan"], + "tables": ["table1", "table2", "table3"] + } + + response = requests.post(f"{api_base_url}/svodka_ca/get_ca_data", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ svodka_ca/get_ca_data с несколькими таблицами работает: получены данные для {len(data['tables'])} таблиц") \ No newline at end of file diff --git a/tests/test_svodka_pm_endpoints.py b/tests/test_svodka_pm_endpoints.py new file mode 100644 index 0000000..ba1355c --- /dev/null +++ b/tests/test_svodka_pm_endpoints.py @@ -0,0 +1,79 @@ +""" +Тесты для svodka_pm эндпоинтов +""" +import pytest +import requests + + +class TestSvodkaPMEndpoints: + """Тесты эндпоинтов svodka_pm""" + + def test_svodka_pm_single_og(self, wait_for_api, api_base_url): + """Тест получения данных по одному ОГ""" + # Пример из схемы SvodkaPMSingleOGRequest + data = { + "id": "SNPZ", + "codes": [78, 79], + "columns": ["ПП", "СЭБ"] + } + + response = requests.post(f"{api_base_url}/svodka_pm/single_og", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ svodka_pm/single_og работает: получены данные для {data['id']}") + + def test_svodka_pm_total_ogs(self, wait_for_api, api_base_url): + """Тест получения данных по всем ОГ""" + # Пример из схемы SvodkaPMTotalOGsRequest + data = { + "codes": [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], + "columns": ["БП", "ПП", "СЭБ"] + } + + response = requests.post(f"{api_base_url}/svodka_pm/get_total_ogs", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ svodka_pm/get_total_ogs работает: получены данные по всем ОГ") + + def test_svodka_pm_single_og_with_search(self, wait_for_api, api_base_url): + """Тест получения данных по одному ОГ с параметром search""" + data = { + "id": "SNPZ", + "codes": [78, 79], + "columns": ["ПП", "СЭБ"], + "search": "Итого" + } + + response = requests.post(f"{api_base_url}/svodka_pm/single_og", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ svodka_pm/single_og с search работает: получены данные для {data['id']} с фильтром") + + def test_svodka_pm_total_ogs_with_search(self, wait_for_api, api_base_url): + """Тест получения данных по всем ОГ с параметром search""" + data = { + "codes": [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], + "columns": ["БП", "ПП", "СЭБ"], + "search": "Итого" + } + + response = requests.post(f"{api_base_url}/svodka_pm/get_total_ogs", json=data) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Запрос не удался: {result}" + assert "data" in result, "Отсутствует поле 'data' в ответе" + print(f"✅ svodka_pm/get_total_ogs с search работает: получены данные по всем ОГ с фильтром") \ No newline at end of file diff --git a/tests/test_upload_endpoints.py b/tests/test_upload_endpoints.py new file mode 100644 index 0000000..da3a1ce --- /dev/null +++ b/tests/test_upload_endpoints.py @@ -0,0 +1,52 @@ +""" +Тесты для эндпоинтов загрузки данных +""" +import pytest +import requests +from pathlib import Path + + +class TestUploadEndpoints: + """Тесты эндпоинтов загрузки""" + + def test_upload_svodka_ca(self, wait_for_api, upload_file, api_base_url): + """Тест загрузки файла svodka_ca.xlsx""" + file_path = upload_file("svodka_ca.xlsx") + + with open(file_path, 'rb') as f: + files = {'file': ('svodka_ca.xlsx', f, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')} + response = requests.post(f"{api_base_url}/svodka_ca/upload", files=files) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Загрузка не удалась: {result}" + print(f"✅ svodka_ca.xlsx загружен успешно: {result['message']}") + + def test_upload_svodka_pm_plan(self, wait_for_api, upload_file, api_base_url): + """Тест загрузки архива pm_plan.zip""" + file_path = upload_file("pm_plan.zip") + + with open(file_path, 'rb') as f: + files = {'zip_file': ('pm_plan.zip', f, 'application/zip')} + response = requests.post(f"{api_base_url}/svodka_pm/upload-zip", files=files) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Загрузка не удалась: {result}" + print(f"✅ pm_plan.zip загружен успешно: {result['message']}") + + def test_upload_monitoring_fuel(self, wait_for_api, upload_file, api_base_url): + """Тест загрузки архива monitoring.zip""" + file_path = upload_file("monitoring.zip") + + with open(file_path, 'rb') as f: + files = {'zip_file': ('monitoring.zip', f, 'application/zip')} + response = requests.post(f"{api_base_url}/monitoring_fuel/upload-zip", files=files) + + assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}" + + result = response.json() + assert result["success"] is True, f"Загрузка не удалась: {result}" + print(f"✅ monitoring.zip загружен успешно: {result['message']}") \ No newline at end of file diff --git a/tests/test_upload_with_local_storage.py b/tests/test_upload_with_local_storage.py new file mode 100644 index 0000000..49acebf --- /dev/null +++ b/tests/test_upload_with_local_storage.py @@ -0,0 +1,183 @@ +""" +Тестирование загрузки файлов с сохранением в локальный storage +""" +import pytest +import sys +from pathlib import Path + +# Добавляем путь к проекту +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / "python_parser")) + +from core.services import ReportService, PARSERS +from core.models import UploadRequest +from adapters.local_storage import LocalStorageAdapter +from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser + +# Регистрируем парсеры +PARSERS.update({ + 'svodka_pm': SvodkaPMParser, + 'svodka_ca': SvodkaCAParser, + 'monitoring_fuel': MonitoringFuelParser, +}) + + +class TestUploadWithLocalStorage: + """Тестирование загрузки файлов с локальным storage""" + + @pytest.fixture(autouse=True) + def setup_storage(self, clean_storage): + """Настройка локального storage для каждого теста""" + self.storage = clean_storage + self.report_service = ReportService(self.storage) + + def test_upload_svodka_ca_to_local_storage(self, upload_file): + """Тест загрузки svodka_ca.xlsx в локальный storage""" + file_path = upload_file("svodka_ca.xlsx") + + # Читаем файл и создаем UploadRequest + with open(file_path, 'rb') as f: + file_content = f.read() + + request = UploadRequest( + report_type='svodka_ca', + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + # Загружаем файл через ReportService + result = self.report_service.upload_report(request) + + assert result.success is True, f"Загрузка не удалась: {result.message}" + + # Проверяем, что данные сохранились в локальном storage + objects = self.storage.list_objects() + assert len(objects) > 0, "Данные не сохранились в storage" + + # Проверяем метаданные + for obj_id in objects: + metadata = self.storage.get_object_metadata(obj_id) + assert metadata is not None, f"Метаданные для {obj_id} не найдены" + assert "shape" in metadata, f"Отсутствует shape в метаданных {obj_id}" + assert "columns" in metadata, f"Отсутствуют columns в метаданных {obj_id}" + + print(f"✅ svodka_ca.xlsx загружен в локальный storage: {len(objects)} объектов") + print(f" Объекты: {objects}") + + def test_upload_pm_plan_to_local_storage(self, upload_file): + """Тест загрузки pm_plan.zip в локальный storage""" + file_path = upload_file("pm_plan.zip") + + # Читаем файл и создаем UploadRequest + with open(file_path, 'rb') as f: + file_content = f.read() + + request = UploadRequest( + report_type='svodka_pm', + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + # Загружаем архив через ReportService + result = self.report_service.upload_report(request) + + assert result.success is True, f"Загрузка не удалась: {result.message}" + + # Проверяем, что данные сохранились в локальном storage + objects = self.storage.list_objects() + assert len(objects) > 0, "Данные не сохранились в storage" + + # Проверяем метаданные + for obj_id in objects: + metadata = self.storage.get_object_metadata(obj_id) + assert metadata is not None, f"Метаданные для {obj_id} не найдены" + assert "shape" in metadata, f"Отсутствует shape в метаданных {obj_id}" + assert "columns" in metadata, f"Отсутствуют columns в метаданных {obj_id}" + + print(f"✅ pm_plan.zip загружен в локальный storage: {len(objects)} объектов") + print(f" Объекты: {objects}") + + def test_upload_monitoring_to_local_storage(self, upload_file): + """Тест загрузки monitoring.zip в локальный storage""" + file_path = upload_file("monitoring.zip") + + # Читаем файл и создаем UploadRequest + with open(file_path, 'rb') as f: + file_content = f.read() + + request = UploadRequest( + report_type='monitoring_fuel', + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + # Загружаем архив через ReportService + result = self.report_service.upload_report(request) + + assert result.success is True, f"Загрузка не удалась: {result.message}" + + # Проверяем, что данные сохранились в локальном storage + objects = self.storage.list_objects() + assert len(objects) > 0, "Данные не сохранились в storage" + + # Проверяем метаданные + for obj_id in objects: + metadata = self.storage.get_object_metadata(obj_id) + assert metadata is not None, f"Метаданные для {obj_id} не найдены" + assert "shape" in metadata, f"Отсутствует shape в метаданных {obj_id}" + assert "columns" in metadata, f"Отсутствуют columns в метаданных {obj_id}" + + print(f"✅ monitoring.zip загружен в локальный storage: {len(objects)} объектов") + print(f" Объекты: {objects}") + + def test_upload_all_files_sequence(self, upload_file): + """Тест последовательной загрузки всех файлов""" + # Загружаем все файлы по очереди + files_to_upload = [ + ("svodka_ca.xlsx", "svodka_ca", "file"), + ("pm_plan.zip", "svodka_pm", "zip"), + ("monitoring.zip", "monitoring_fuel", "zip") + ] + + total_objects = 0 + + for filename, report_type, upload_type in files_to_upload: + file_path = upload_file(filename) + + # Читаем файл и создаем UploadRequest + with open(file_path, 'rb') as f: + file_content = f.read() + + request = UploadRequest( + report_type=report_type, + file_name=file_path.name, + file_content=file_content, + parse_params={} + ) + + result = self.report_service.upload_report(request) + + assert result.success is True, f"Загрузка {filename} не удалась: {result.message}" + + # Подсчитываем объекты + objects = self.storage.list_objects() + current_count = len(objects) + + print(f"✅ {filename} загружен: {current_count - total_objects} новых объектов") + total_objects = current_count + + # Проверяем итоговое количество объектов + final_objects = self.storage.list_objects() + assert len(final_objects) > 0, "Ни один файл не был загружен" + + print(f"✅ Все файлы загружены. Итого объектов в storage: {len(final_objects)}") + print(f" Все объекты: {final_objects}") + + # Выводим детальную информацию о каждом объекте + for obj_id in final_objects: + metadata = self.storage.get_object_metadata(obj_id) + if metadata: + print(f" 📊 {obj_id}: {metadata['shape']} колонки: {metadata['columns'][:5]}...") \ No newline at end of file