This commit is contained in:
2025-09-01 20:54:31 +03:00
parent b98be22359
commit 79ab91c700
10 changed files with 351 additions and 53 deletions

20
.gitignore vendored
View File

@@ -1,5 +1,21 @@
# Python # Python
__pycache__
__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[cod]
*$py.class *$py.class
*.so *.so
@@ -152,4 +168,6 @@ htmlcov/
node_modules/ node_modules/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
__pycache__/

View File

@@ -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 схемами и парсерами. Теперь изменения в схемах автоматически отражаются в парсерах, что упрощает поддержку и развитие системы.

View File

@@ -3,6 +3,8 @@ import re
import zipfile import zipfile
from typing import Dict, Tuple from typing import Dict, Tuple
from core.ports import ParserPort 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 from adapters.pconfig import data_to_json
@@ -13,37 +15,40 @@ class MonitoringFuelParser(ParserPort):
def _register_default_getters(self): def _register_default_getters(self):
"""Регистрация геттеров по умолчанию""" """Регистрация геттеров по умолчанию"""
self.register_getter( # Используем схемы Pydantic как единый источник правды
name="total_by_columns", register_getter_from_schema(
parser_instance=self,
getter_name="total_by_columns",
method=self._get_total_by_columns, method=self._get_total_by_columns,
required_params=["columns"], schema_class=MonitoringFuelTotalRequest,
optional_params=[],
description="Агрегация данных по колонкам" description="Агрегация данных по колонкам"
) )
self.register_getter( register_getter_from_schema(
name="month_by_code", parser_instance=self,
getter_name="month_by_code",
method=self._get_month_by_code, method=self._get_month_by_code,
required_params=["month"], schema_class=MonitoringFuelMonthRequest,
optional_params=[],
description="Получение данных за конкретный месяц" description="Получение данных за конкретный месяц"
) )
def _get_total_by_columns(self, params: dict): def _get_total_by_columns(self, params: dict):
"""Агрегация по колонкам (обертка для совместимости)""" """Агрегация данных по колонкам"""
columns = params["columns"] # Валидируем параметры с помощью схемы Pydantic
if not columns: validated_params = validate_params_with_schema(params, MonitoringFuelTotalRequest)
raise ValueError("Отсутствуют идентификаторы столбцов")
columns = validated_params["columns"]
# TODO: Переделать под новую архитектуру # TODO: Переделать под новую архитектуру
df_means, _ = self.aggregate_by_columns(self.df, columns) df_means, _ = self.aggregate_by_columns(self.df, columns)
return df_means.to_dict(orient='index') return df_means.to_dict(orient='index')
def _get_month_by_code(self, params: dict): def _get_month_by_code(self, params: dict):
"""Получение данных за месяц (обертка для совместимости)""" """Получение данных за конкретный месяц"""
month = params["month"] # Валидируем параметры с помощью схемы Pydantic
if not month: validated_params = validate_params_with_schema(params, MonitoringFuelMonthRequest)
raise ValueError("Отсутствует идентификатор месяца")
month = validated_params["month"]
# TODO: Переделать под новую архитектуру # TODO: Переделать под новую архитектуру
df_month = self.get_month(self.df, month) df_month = self.get_month(self.df, month)

View File

@@ -2,6 +2,8 @@ import pandas as pd
import numpy as np import numpy as np
from core.ports import ParserPort 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 from adapters.pconfig import get_og_by_name
@@ -12,23 +14,22 @@ class SvodkaCAParser(ParserPort):
def _register_default_getters(self): def _register_default_getters(self):
"""Регистрация геттеров по умолчанию""" """Регистрация геттеров по умолчанию"""
self.register_getter( # Используем схемы Pydantic как единый источник правды
name="get_data", register_getter_from_schema(
parser_instance=self,
getter_name="get_data",
method=self._get_data_wrapper, method=self._get_data_wrapper,
required_params=["modes", "tables"], schema_class=SvodkaCARequest,
optional_params=[],
description="Получение данных по режимам и таблицам" description="Получение данных по режимам и таблицам"
) )
def _get_data_wrapper(self, params: dict): def _get_data_wrapper(self, params: dict):
"""Обертка для получения данных (для совместимости)""" """Получение данных по режимам и таблицам"""
modes = params["modes"] # Валидируем параметры с помощью схемы Pydantic
tables = params["tables"] validated_params = validate_params_with_schema(params, SvodkaCARequest)
if not isinstance(modes, list): modes = validated_params["modes"]
raise ValueError("Поле 'modes' должно быть списком") tables = validated_params["tables"]
if not isinstance(tables, list):
raise ValueError("Поле 'tables' должно быть списком")
# TODO: Переделать под новую архитектуру # TODO: Переделать под новую архитектуру
data_dict = {} data_dict = {}

View File

@@ -1,6 +1,8 @@
import pandas as pd import pandas as pd
from core.ports import ParserPort 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 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): def _register_default_getters(self):
"""Регистрация геттеров по умолчанию""" """Регистрация геттеров по умолчанию"""
self.register_getter( # Используем схемы Pydantic как единый источник правды
name="single_og", register_getter_from_schema(
parser_instance=self,
getter_name="single_og",
method=self._get_single_og, method=self._get_single_og,
required_params=["id", "codes", "columns"], schema_class=SvodkaPMSingleOGRequest,
optional_params=["search"],
description="Получение данных по одному ОГ" description="Получение данных по одному ОГ"
) )
self.register_getter( register_getter_from_schema(
name="total_ogs", parser_instance=self,
getter_name="total_ogs",
method=self._get_total_ogs, method=self._get_total_ogs,
required_params=["codes", "columns"], schema_class=SvodkaPMTotalOGsRequest,
optional_params=["search"],
description="Получение данных по всем ОГ" description="Получение данных по всем ОГ"
) )
def _get_single_og(self, params: dict): def _get_single_og(self, params: dict):
"""Получение данных по одному ОГ (обертка для совместимости)""" """Получение данных по одному ОГ"""
og_id = params["id"] # Валидируем параметры с помощью схемы Pydantic
codes = params["codes"] validated_params = validate_params_with_schema(params, SvodkaPMSingleOGRequest)
columns = params["columns"]
search = params.get("search")
if not isinstance(codes, list): og_id = validated_params["id"]
raise ValueError("Поле 'codes' должно быть списком") codes = validated_params["codes"]
if not isinstance(columns, list): columns = validated_params["columns"]
raise ValueError("Поле 'columns' должно быть списком") search = validated_params.get("search")
# Здесь нужно получить DataFrame из self.df, но пока используем старую логику # Здесь нужно получить DataFrame из self.df, но пока используем старую логику
# TODO: Переделать под новую архитектуру # TODO: Переделать под новую архитектуру
return self.get_svodka_og(self.df, og_id, codes, columns, search) return self.get_svodka_og(self.df, og_id, codes, columns, search)
def _get_total_ogs(self, params: dict): def _get_total_ogs(self, params: dict):
"""Получение данных по всем ОГ (обертка для совместимости)""" """Получение данных по всем ОГ"""
codes = params["codes"] # Валидируем параметры с помощью схемы Pydantic
columns = params["columns"] validated_params = validate_params_with_schema(params, SvodkaPMTotalOGsRequest)
search = params.get("search")
if not isinstance(codes, list): codes = validated_params["codes"]
raise ValueError("Поле 'codes' должно быть списком") columns = validated_params["columns"]
if not isinstance(columns, list): search = validated_params.get("search")
raise ValueError("Поле 'columns' должно быть списком")
# TODO: Переделать под новую архитектуру # TODO: Переделать под новую архитектуру
return self.get_svodka_total(self.df, codes, columns, search) return self.get_svodka_total(self.df, codes, columns, search)

View File

@@ -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)}")