diff --git a/.gitignore b/.gitignore index 780feca..8722ece 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,21 @@ # Python +__pycache__ __pycache__/ +python_parser/__pycache__/ +python_parser/core/__pycache__/ +python_parser/adapters/__pycache__/ +python_parser/tests/__pycache__/ +python_parser/tests/test_core/__pycache__/ +python_parser/tests/test_adapters/__pycache__/ +python_parser/tests/test_app/__pycache__/ +python_parser/app/__pycache__/ +python_parser/app/schemas/__pycache__/ +python_parser/app/schemas/test_schemas/__pycache__/ +python_parser/app/schemas/test_schemas/test_core/__pycache__/ +python_parser/app/schemas/test_schemas/test_adapters/__pycache__/ +python_parser/app/schemas/test_schemas/test_app/__pycache__/ + + *.py[cod] *$py.class *.so @@ -152,4 +168,6 @@ htmlcov/ node_modules/ npm-debug.log* yarn-debug.log* -yarn-error.log* \ No newline at end of file +yarn-error.log* + +__pycache__/ diff --git a/python_parser/SCHEMA_INTEGRATION.md b/python_parser/SCHEMA_INTEGRATION.md new file mode 100644 index 0000000..73a4b39 --- /dev/null +++ b/python_parser/SCHEMA_INTEGRATION.md @@ -0,0 +1,135 @@ +# Интеграция схем Pydantic с парсерами + +## Обзор + +Этот документ описывает решение для устранения дублирования логики между схемами Pydantic и парсерами. Теперь схемы Pydantic являются единым источником правды для определения параметров парсеров. + +## Проблема + +Ранее в парсерах дублировалась информация о параметрах: + +```python +# В парсере +self.register_getter( + name="single_og", + method=self._get_single_og, + required_params=["id", "codes", "columns"], # Дублирование + optional_params=["search"], # Дублирование + description="Получение данных по одному ОГ" +) + +# В схеме +class SvodkaPMSingleOGRequest(BaseModel): + id: OGID = Field(...) # Обязательное поле + codes: List[int] = Field(...) # Обязательное поле + columns: List[str] = Field(...) # Обязательное поле + search: Optional[str] = Field(None) # Необязательное поле +``` + +## Решение + +### 1. Утилиты для работы со схемами + +Создан модуль `core/schema_utils.py` с функциями: + +- `get_required_fields_from_schema()` - извлекает обязательные поля +- `get_optional_fields_from_schema()` - извлекает необязательные поля +- `register_getter_from_schema()` - регистрирует геттер с использованием схемы +- `validate_params_with_schema()` - валидирует параметры с помощью схемы + +### 2. Обновленные парсеры + +Теперь парсеры используют схемы как единый источник правды: + +```python +def _register_default_getters(self): + """Регистрация геттеров по умолчанию""" + # Используем схемы Pydantic как единый источник правды + register_getter_from_schema( + parser_instance=self, + getter_name="single_og", + method=self._get_single_og, + schema_class=SvodkaPMSingleOGRequest, + description="Получение данных по одному ОГ" + ) +``` + +### 3. Валидация параметров + +Методы геттеров теперь автоматически валидируют параметры: + +```python +def _get_single_og(self, params: dict): + """Получение данных по одному ОГ""" + # Валидируем параметры с помощью схемы Pydantic + validated_params = validate_params_with_schema(params, SvodkaPMSingleOGRequest) + + og_id = validated_params["id"] + codes = validated_params["codes"] + columns = validated_params["columns"] + search = validated_params.get("search") + + # ... остальная логика +``` + +## Преимущества + +1. **Единый источник правды** - информация о параметрах хранится только в схемах Pydantic +2. **Автоматическая валидация** - параметры автоматически валидируются с помощью Pydantic +3. **Синхронизация** - изменения в схемах автоматически отражаются в парсерах +4. **Типобезопасность** - использование типов Pydantic обеспечивает типобезопасность +5. **Документация** - Swagger документация автоматически генерируется из схем + +## Совместимость + +Решение работает с: +- Pydantic v1 (через `__fields__`) +- Pydantic v2 (через `model_fields` и `is_required()`) + +## Использование + +### Для новых парсеров + +1. Создайте схему Pydantic с нужными полями +2. Используйте `register_getter_from_schema()` для регистрации геттера +3. Используйте `validate_params_with_schema()` в методах геттеров + +### Для существующих парсеров + +1. Убедитесь, что у вас есть соответствующая схема Pydantic +2. Замените ручную регистрацию геттеров на `register_getter_from_schema()` +3. Добавьте валидацию параметров в методы геттеров + +## Примеры + +### Схема с обязательными и необязательными полями + +```python +class ExampleRequest(BaseModel): + required_field: str = Field(..., description="Обязательное поле") + optional_field: Optional[str] = Field(None, description="Необязательное поле") +``` + +### Регистрация геттера + +```python +register_getter_from_schema( + parser_instance=self, + getter_name="example_getter", + method=self._example_method, + schema_class=ExampleRequest, + description="Пример геттера" +) +``` + +### Валидация в методе + +```python +def _example_method(self, params: dict): + validated_params = validate_params_with_schema(params, ExampleRequest) + # validated_params содержит валидированные данные +``` + +## Заключение + +Это решение устраняет дублирование кода и обеспечивает единообразие между API схемами и парсерами. Теперь изменения в схемах автоматически отражаются в парсерах, что упрощает поддержку и развитие системы. \ No newline at end of file diff --git a/python_parser/adapters/parsers/__pycache__/monitoring_fuel.cpython-313.pyc b/python_parser/adapters/parsers/__pycache__/monitoring_fuel.cpython-313.pyc index 1954ec4..c8ed95c 100644 Binary files a/python_parser/adapters/parsers/__pycache__/monitoring_fuel.cpython-313.pyc and b/python_parser/adapters/parsers/__pycache__/monitoring_fuel.cpython-313.pyc differ diff --git a/python_parser/adapters/parsers/__pycache__/svodka_ca.cpython-313.pyc b/python_parser/adapters/parsers/__pycache__/svodka_ca.cpython-313.pyc index 6be6eb5..9913883 100644 Binary files a/python_parser/adapters/parsers/__pycache__/svodka_ca.cpython-313.pyc and b/python_parser/adapters/parsers/__pycache__/svodka_ca.cpython-313.pyc differ diff --git a/python_parser/adapters/parsers/__pycache__/svodka_pm.cpython-313.pyc b/python_parser/adapters/parsers/__pycache__/svodka_pm.cpython-313.pyc index 09b5d5b..1fecb74 100644 Binary files a/python_parser/adapters/parsers/__pycache__/svodka_pm.cpython-313.pyc and b/python_parser/adapters/parsers/__pycache__/svodka_pm.cpython-313.pyc differ diff --git a/python_parser/adapters/parsers/monitoring_fuel.py b/python_parser/adapters/parsers/monitoring_fuel.py index 129a812..7f41328 100644 --- a/python_parser/adapters/parsers/monitoring_fuel.py +++ b/python_parser/adapters/parsers/monitoring_fuel.py @@ -3,6 +3,8 @@ import re import zipfile 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 adapters.pconfig import data_to_json @@ -13,37 +15,40 @@ class MonitoringFuelParser(ParserPort): def _register_default_getters(self): """Регистрация геттеров по умолчанию""" - self.register_getter( - name="total_by_columns", + # Используем схемы Pydantic как единый источник правды + register_getter_from_schema( + parser_instance=self, + getter_name="total_by_columns", method=self._get_total_by_columns, - required_params=["columns"], - optional_params=[], + schema_class=MonitoringFuelTotalRequest, description="Агрегация данных по колонкам" ) - self.register_getter( - name="month_by_code", + register_getter_from_schema( + parser_instance=self, + getter_name="month_by_code", method=self._get_month_by_code, - required_params=["month"], - optional_params=[], + schema_class=MonitoringFuelMonthRequest, description="Получение данных за конкретный месяц" ) def _get_total_by_columns(self, params: dict): - """Агрегация по колонкам (обертка для совместимости)""" - columns = params["columns"] - if not columns: - raise ValueError("Отсутствуют идентификаторы столбцов") + """Агрегация данных по колонкам""" + # Валидируем параметры с помощью схемы Pydantic + validated_params = validate_params_with_schema(params, MonitoringFuelTotalRequest) + + columns = validated_params["columns"] # TODO: Переделать под новую архитектуру df_means, _ = self.aggregate_by_columns(self.df, columns) return df_means.to_dict(orient='index') def _get_month_by_code(self, params: dict): - """Получение данных за месяц (обертка для совместимости)""" - month = params["month"] - if not month: - raise ValueError("Отсутствует идентификатор месяца") + """Получение данных за конкретный месяц""" + # Валидируем параметры с помощью схемы Pydantic + validated_params = validate_params_with_schema(params, MonitoringFuelMonthRequest) + + month = validated_params["month"] # TODO: Переделать под новую архитектуру df_month = self.get_month(self.df, month) diff --git a/python_parser/adapters/parsers/svodka_ca.py b/python_parser/adapters/parsers/svodka_ca.py index 289e25b..4c3be9b 100644 --- a/python_parser/adapters/parsers/svodka_ca.py +++ b/python_parser/adapters/parsers/svodka_ca.py @@ -2,6 +2,8 @@ import pandas as pd import numpy as np from core.ports import ParserPort +from core.schema_utils import register_getter_from_schema, validate_params_with_schema +from app.schemas.svodka_ca import SvodkaCARequest from adapters.pconfig import get_og_by_name @@ -12,23 +14,22 @@ class SvodkaCAParser(ParserPort): def _register_default_getters(self): """Регистрация геттеров по умолчанию""" - self.register_getter( - name="get_data", + # Используем схемы Pydantic как единый источник правды + register_getter_from_schema( + parser_instance=self, + getter_name="get_data", method=self._get_data_wrapper, - required_params=["modes", "tables"], - optional_params=[], + schema_class=SvodkaCARequest, description="Получение данных по режимам и таблицам" ) def _get_data_wrapper(self, params: dict): - """Обертка для получения данных (для совместимости)""" - modes = params["modes"] - tables = params["tables"] + """Получение данных по режимам и таблицам""" + # Валидируем параметры с помощью схемы Pydantic + validated_params = validate_params_with_schema(params, SvodkaCARequest) - if not isinstance(modes, list): - raise ValueError("Поле 'modes' должно быть списком") - if not isinstance(tables, list): - raise ValueError("Поле 'tables' должно быть списком") + modes = validated_params["modes"] + tables = validated_params["tables"] # TODO: Переделать под новую архитектуру data_dict = {} diff --git a/python_parser/adapters/parsers/svodka_pm.py b/python_parser/adapters/parsers/svodka_pm.py index 008c2f9..df473ca 100644 --- a/python_parser/adapters/parsers/svodka_pm.py +++ b/python_parser/adapters/parsers/svodka_pm.py @@ -1,6 +1,8 @@ import pandas as pd from core.ports import ParserPort +from core.schema_utils import register_getter_from_schema, validate_params_with_schema +from app.schemas.svodka_pm import SvodkaPMSingleOGRequest, SvodkaPMTotalOGsRequest from adapters.pconfig import OG_IDS, replace_id_in_path, data_to_json @@ -11,48 +13,45 @@ class SvodkaPMParser(ParserPort): def _register_default_getters(self): """Регистрация геттеров по умолчанию""" - self.register_getter( - name="single_og", + # Используем схемы Pydantic как единый источник правды + register_getter_from_schema( + parser_instance=self, + getter_name="single_og", method=self._get_single_og, - required_params=["id", "codes", "columns"], - optional_params=["search"], + schema_class=SvodkaPMSingleOGRequest, description="Получение данных по одному ОГ" ) - self.register_getter( - name="total_ogs", + register_getter_from_schema( + parser_instance=self, + getter_name="total_ogs", method=self._get_total_ogs, - required_params=["codes", "columns"], - optional_params=["search"], + schema_class=SvodkaPMTotalOGsRequest, description="Получение данных по всем ОГ" ) def _get_single_og(self, params: dict): - """Получение данных по одному ОГ (обертка для совместимости)""" - og_id = params["id"] - codes = params["codes"] - columns = params["columns"] - search = params.get("search") + """Получение данных по одному ОГ""" + # Валидируем параметры с помощью схемы Pydantic + validated_params = validate_params_with_schema(params, SvodkaPMSingleOGRequest) - if not isinstance(codes, list): - raise ValueError("Поле 'codes' должно быть списком") - if not isinstance(columns, list): - raise ValueError("Поле 'columns' должно быть списком") + og_id = validated_params["id"] + codes = validated_params["codes"] + columns = validated_params["columns"] + search = validated_params.get("search") # Здесь нужно получить DataFrame из self.df, но пока используем старую логику # TODO: Переделать под новую архитектуру return self.get_svodka_og(self.df, og_id, codes, columns, search) def _get_total_ogs(self, params: dict): - """Получение данных по всем ОГ (обертка для совместимости)""" - codes = params["codes"] - columns = params["columns"] - search = params.get("search") + """Получение данных по всем ОГ""" + # Валидируем параметры с помощью схемы Pydantic + validated_params = validate_params_with_schema(params, SvodkaPMTotalOGsRequest) - if not isinstance(codes, list): - raise ValueError("Поле 'codes' должно быть списком") - if not isinstance(columns, list): - raise ValueError("Поле 'columns' должно быть списком") + codes = validated_params["codes"] + columns = validated_params["columns"] + search = validated_params.get("search") # TODO: Переделать под новую архитектуру return self.get_svodka_total(self.df, codes, columns, search) diff --git a/python_parser/core/__pycache__/ports.cpython-313.pyc b/python_parser/core/__pycache__/ports.cpython-313.pyc index 062c066..6bf9520 100644 Binary files a/python_parser/core/__pycache__/ports.cpython-313.pyc and b/python_parser/core/__pycache__/ports.cpython-313.pyc differ diff --git a/python_parser/core/schema_utils.py b/python_parser/core/schema_utils.py new file mode 100644 index 0000000..795f55d --- /dev/null +++ b/python_parser/core/schema_utils.py @@ -0,0 +1,140 @@ +""" +Упрощенные утилиты для работы со схемами Pydantic +""" +from typing import List, Dict, Any, Type +from pydantic import BaseModel +import inspect + + +def get_required_fields_from_schema(schema_class: Type[BaseModel]) -> List[str]: + """ + Извлекает список обязательных полей из схемы Pydantic + + Args: + schema_class: Класс схемы Pydantic + + Returns: + Список имен обязательных полей + """ + required_fields = [] + + # Используем model_fields для Pydantic v2 или __fields__ для v1 + if hasattr(schema_class, 'model_fields'): + fields = schema_class.model_fields + else: + fields = schema_class.__fields__ + + for field_name, field_info in fields.items(): + # В Pydantic v2 есть метод is_required() + if hasattr(field_info, 'is_required'): + if field_info.is_required(): + required_fields.append(field_name) + elif hasattr(field_info, 'required'): + if field_info.required: + required_fields.append(field_name) + else: + # Fallback для старых версий - проверяем наличие default + has_default = False + + if hasattr(field_info, 'default'): + has_default = field_info.default is not ... + elif hasattr(field_info, 'default_factory'): + has_default = field_info.default_factory is not None + + if not has_default: + required_fields.append(field_name) + + return required_fields + + +def get_optional_fields_from_schema(schema_class: Type[BaseModel]) -> List[str]: + """ + Извлекает список необязательных полей из схемы Pydantic + + Args: + schema_class: Класс схемы Pydantic + + Returns: + Список имен необязательных полей + """ + optional_fields = [] + + # Используем model_fields для Pydantic v2 или __fields__ для v1 + if hasattr(schema_class, 'model_fields'): + fields = schema_class.model_fields + else: + fields = schema_class.__fields__ + + for field_name, field_info in fields.items(): + # В Pydantic v2 есть метод is_required() + if hasattr(field_info, 'is_required'): + if not field_info.is_required(): + optional_fields.append(field_name) + elif hasattr(field_info, 'required'): + if not field_info.required: + optional_fields.append(field_name) + else: + # Fallback для старых версий - проверяем наличие default + has_default = False + + if hasattr(field_info, 'default'): + has_default = field_info.default is not ... + elif hasattr(field_info, 'default_factory'): + has_default = field_info.default_factory is not None + + if has_default: + optional_fields.append(field_name) + + return optional_fields + + +def register_getter_from_schema(parser_instance, getter_name: str, method: callable, + schema_class: Type[BaseModel], description: str = ""): + """ + Регистрирует геттер в парсере, используя схему Pydantic для определения параметров + + Args: + parser_instance: Экземпляр парсера + getter_name: Имя геттера + method: Метод для выполнения + schema_class: Класс схемы Pydantic + description: Описание геттера (если не указано, берется из docstring метода) + """ + # Извлекаем параметры из схемы + required_params = get_required_fields_from_schema(schema_class) + optional_params = get_optional_fields_from_schema(schema_class) + + # Если описание не указано, берем из docstring метода + if not description: + description = inspect.getdoc(method) or "" + + # Регистрируем геттер + parser_instance.register_getter( + name=getter_name, + method=method, + required_params=required_params, + optional_params=optional_params, + description=description + ) + + +def validate_params_with_schema(params: Dict[str, Any], schema_class: Type[BaseModel]) -> Dict[str, Any]: + """ + Валидирует параметры с помощью схемы Pydantic + + Args: + params: Словарь параметров + schema_class: Класс схемы Pydantic + + Returns: + Валидированные параметры + + Raises: + ValidationError: Если параметры не прошли валидацию + """ + try: + # Создаем экземпляр схемы для валидации + validated_data = schema_class(**params) + return validated_data.dict() + except Exception as e: + raise ValueError(f"Ошибка валидации параметров: {str(e)}") \ No newline at end of file