Введите аннотацию для синтаксического анализа строки в файле JSON в диапазоне с использованием Pydantic в Python

Я создал класс Pydantic, предназначенный для анализа файлов JSON. Атрибут диапазон анализируется из строки вида "11-34" (или, точнее, из показанного регулярного выражения):

    RANGE_STRING_REGEX = r"^(?P<first>[1-6]+)(-(?P<last>[1-6]+))?$"

    class RandomTableEvent(BaseModel):
        name: str
        range: Annotated[str, Field(regex=RANGE_STRING_REGEX)]
    
        @validator("range", allow_reuse=True)
        def convert_range_string_to_range(cls, r) -> "range":
            match_groups = re.fullmatch(RANGE_STRING_REGEX, r).groupdict()
            first = int(match_groups["first"])
            last = int(match_groups["last"]) if match_groups["last"] else first
            return range(first, last + 1)

Сгенерированная схема работает и проверка проходит.

Однако аннотация типа для атрибута диапазон в классе, строго говоря, неверна, так как атрибут диапазон преобразуется из строки (аннотации типа) в объект range в функции валидатора.

Каким будет правильный способ аннотировать это и при этом поддерживать генерацию схемы? Есть ли другой способ справиться с этим неявным преобразованием типов (например, строки автоматически преобразуются в int в Pydantic - есть ли что-то подобное для пользовательских типов)?

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
0
16
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

range не поддерживается pydantic и использование его в качестве типа для поля вызовет ошибку при попытке создать схему JSON, но pydantic поддерживает Пользовательские типы данных:

You can also define your own custom data types. There are several ways to achieve it.

Classes with get_validators

You use a custom class with a classmethod __get_validators__. It will be called to get validators to parse and validate the input data.

Но этот пользовательский тип данных не может наследоваться от range, потому что он окончательный. Таким образом, вы можете создать собственный тип данных, который использует range внутри и предоставляет методы диапазона: он будет работать как range, но не будет range (isinstance(..., range) будет False).

В той же pydantic документации показано, как использовать __modify_schema__ метод для настройки схемы JSON пользовательского типа данных.

Полный пример:

import re
from typing import Any, Callable, Dict, Iterator, SupportsIndex, Union

from pydantic import BaseModel


class Range:
    _RANGE_STRING_REGEX = r"^(?P<first>[1-6]+)(-(?P<last>[1-6]+))?$"

    @classmethod
    def __get_validators__(cls) -> Iterator[Callable[[Any], Any]]:
        yield cls.validate

    @classmethod
    def validate(cls, v: Any) -> "Range":
        if not isinstance(v, str):
            raise ValueError("expected string")

        match = re.fullmatch(cls._RANGE_STRING_REGEX, v)
        if not match:
            raise ValueError("invalid string")

        match_groups = match.groupdict()
        first = int(match_groups["first"])
        last = int(match_groups["last"]) if match_groups["last"] else first

        return cls(range(first, last + 1))

    def __init__(self, r: range) -> None:
        self._range = r

    @classmethod
    def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
        # Customize the JSON schema as you want
        field_schema["pattern"] = cls._RANGE_STRING_REGEX
        field_schema["type"] = "string"

    # Implement the range methods and use self._range

    @property
    def start(self) -> int:
        return self._range.start

    @property
    def stop(self) -> int:
        return self._range.stop

    @property
    def step(self) -> int:
        return self._range.step

    def count(self, value: int) -> int:
        return self._range.count(value)

    def index(self, value: int) -> int:
        return self._range.index(value)

    def __len__(self) -> int:
        return self._range.__len__()

    def __contains__(self, o: object) -> bool:
        return self._range.__contains__(o)

    def __iter__(self) -> Iterator[int]:
        return self._range.__iter__()

    def __getitem__(self, key: Union[SupportsIndex, slice]) -> int:
        return self._range.__getitem__(key)

    def __reversed__(self) -> Iterator[int]:
        return self._range.__reversed__()

    def __repr__(self) -> str:
        return self._range.__repr__()


class RandomTableEvent(BaseModel):
    name: str
    range: Range


event = RandomTableEvent(name = "foo", range = "11-34")

print("event:", event)
print("event.range:", event.range)
print("schema:", event.schema_json(indent=2))
print("is instance of range:", isinstance(event.range, range))
print("event.range.start:", event.range.start)
print("event.range.stop:", event.range.stop)
print("event.range[0:5]", event.range[0:5])
print("last 3 elements:", list(event.range[-3:]))

Выход:

event: name='foo' range=range(11, 35)
event.range: range(11, 35)
schema: {
  "title": "RandomTableEvent",
  "type": "object",
  "properties": {
    "name": {
      "title": "Name",
      "type": "string"
    },
    "range": {
      "title": "Range",
      "pattern": "^(?P<first>[1-6]+)(-(?P<last>[1-6]+))?$",
      "type": "string"
    }
  },
  "required": [
    "name",
    "range"
  ]
}
is instance of range: False
event.range.start: 11
event.range.stop: 35
event.range[0:5] range(11, 16)
last 3 elements: [32, 33, 34]

Большое спасибо за это подробное решение. Обтекание диапазона проприетарным классом кажется немного неправильным, но я вижу, что это решает проблему очень элегантно.

djm 07.05.2022 16:09

Другие вопросы по теме