pydantic: использование декоратора property.getter для поля с псевдонимом

прокрутите вниз, чтобы найти tl;dr, я даю контекст, который, на мой взгляд, важен, но не имеет прямого отношения к заданному вопросу

Немного контекста

Я занимаюсь созданием API для веб-приложения, и некоторые значения вычисляются на основе значений других в pydantic BaseModel. Они используются для проверки пользователем, сериализации данных и определения документов базы данных (NoSQL).

В частности, у меня есть почти все ресурсы, унаследованные от OwnedResource class, который определяет среди других нерелевантных свойств, таких как даты создания / последнего обновления:

  • object_key- Ключ объекта с использованием наноида длины 6 с произвольным алфавитом
  • owner_key - Этот ключ ссылается на пользователя, которому принадлежит этот объект - наноид длиной 10.
  • _key - здесь я столкнулся с некоторыми проблемами, и я объясню почему.

Итак, arangodb - база данных, которую я использую - накладывает _key как имя свойства, по которому идентифицируются ресурсы.

Поскольку в моем веб-приложении все ресурсы доступны только пользователям, которые их создали, их можно идентифицировать в URL-адресах только с помощью ключа объекта (например, /subject/{object_key}). Однако, как_key должно быть уникальным, я намереваюсь построить значение этого поля, используя f"{owner_key}/{object_key}", чтобы хранить объекты каждого пользователя в базе данных и потенциально допускать совместное использование ресурсов между пользователями в будущем.

Цель состоит в том, чтобы иметь самый короткий уникальный идентификатор для каждого пользователя, посколькуowner_key часть полного _key используется для фактического доступа к документу, хранящемуся в базе данных, и действий с ним, всегда одно и то же: текущий авторизованный пользователь _key.

Моя попытка

Тогда я задумал определить _key поле как @property-украшенная функция в классе. Однако Pydantic, похоже, не регистрирует их как поля модели.

Более того, атрибут должен быть назван key и используйте псевдоним (с Field(... alias="_key"), поскольку pydantic рассматривает поля с префиксом подчеркивания как внутренние и не раскрывает их.

Вот определение OwnedResource:

class OwnedResource(BaseModel):
    """
    Base model for resources owned by users
    """

    object_key: ObjectBareKey = nanoid.generate(ID_CHARSET, OBJECT_KEY_LEN)
    owner_key: UserKey
    updated_at: Optional[datetime] = None
    created_at: datetime = datetime.now()

    @property
    def key(self) -> ObjectKey:
        return objectkey(self.owner_key)

    class Config:
        fields = {"key": "_key"} # [1]

[1] Поскольку поле (..., alias="...") использовать нельзя, я использую это свойство подкласса Config (см. Документацию pydantic)

Однако это не работает, как показано в следующем примере:

@router.post("/subjects/")
def create_a_subject(subject: InSubject):
    print(subject.dict(by_alias=True))

с участием InSubject определение свойств, присущих Subject, а также Subject являясь пустым классом, унаследованным от обоих InSubject а также OwnedResource:

class InSubject(BaseModel):
    name: str
    color: Color
    weight: Union[PositiveFloat, Literal[0]] = 1.0
    goal: Primantissa # This is just a float constrained in a [0, 1] range
    room: str

class Subject(InSubject, OwnedResource):
    pass

Когда я выполняю POST /subjects/, в консоли печатается следующее:

{'name': 'string', 'color': Color('cyan', rgb=(0, 255, 255)), 'weight': 0, 'goal': 0.0, 'room': 'string'}

Как вы видете, _key или key нигде не видно.

Пожалуйста, попросите подробностей и разъяснений, я постарался сделать это как можно проще для понимания, но я не уверен, достаточно ли это ясно.

tl;dr

Бесконтекстный и более общий пример без проницательного контекста:

Со следующим классом:

from pydantic import BaseModel

class SomeClass(BaseModel):
    
    spam: str

    @property
    def eggs(self) -> str:
        return self.spam + " bacon"

    class Config:
        fields = {"eggs": "_eggs"}

Хочу, чтобы было правдой следующее:

a = SomeClass(spam="I like")
d = a.dict(by_alias=True)
d.get("_eggs") == "I like bacon"

3 ответа

Решение

Pydantic не поддерживает сериализацию свойств, на GitHub есть проблема с запросом этой функции.

Основываясь на этом комментарии Людвига-Вайса, он предлагает создать подкласс BaseModel и переопределитьdict метод для включения свойств.

class PropertyBaseModel(BaseModel):
    """
    Workaround for serializing properties with pydantic until
    https://github.com/samuelcolvin/pydantic/issues/935
    is solved
    """
    @classmethod
    def get_properties(cls):
        return [prop for prop in dir(cls) if isinstance(getattr(cls, prop), property) and prop not in ("__values__", "fields")]

    def dict(
        self,
        *,
        include: Union['AbstractSetIntStr', 'MappingIntStrAny'] = None,
        exclude: Union['AbstractSetIntStr', 'MappingIntStrAny'] = None,
        by_alias: bool = False,
        skip_defaults: bool = None,
        exclude_unset: bool = False,
        exclude_defaults: bool = False,
        exclude_none: bool = False,
    ) -> 'DictStrAny':
        attribs = super().dict(
            include=include,
            exclude=exclude,
            by_alias=by_alias,
            skip_defaults=skip_defaults,
            exclude_unset=exclude_unset,
            exclude_defaults=exclude_defaults,
            exclude_none=exclude_none
        )
        props = self.get_properties()
        # Include and exclude properties
        if include:
            props = [prop for prop in props if prop in include]
        if exclude:
            props = [prop for prop in props if prop not in exclude]

        # Update the attribute dict with the properties
        if props:
            attribs.update({prop: getattr(self, prop) for prop in props})

        return attribs

Возможно, вы сможете сериализовать свой _keyполе с помощью валидатора pydantic с alwaysопция установлена ​​в True.

Используя ваш пример:

      from typing import Optional
from pydantic import BaseModel, Field, validator


class SomeClass(BaseModel):

    spam: str
    eggs: Optional[str] = Field(alias="_eggs")

    @validator("eggs", always=True)
    def set_eggs(cls, v, values, **kwargs):
        """Set the eggs field based upon a spam value."""
        return v or values.get("spam") + " bacon"


a = SomeClass(spam="I like")
my_dictionary = a.dict(by_alias=True)
print(my_dictionary)
> {'spam': 'I like', '_eggs': 'I like bacon'}
print(my_dictionary.get("_eggs"))
> "I like bacon"

Итак, чтобы сериализовать ваш _eggsполе, вместо того, чтобы добавлять строку, вы бы вставили туда свою функцию сериализации и вернули бы ее результат.

Если вы сможете обновиться до новейшей версии Pydantic 2, что, честно говоря, может быть немного тяжелым испытанием, появится несколько действительно хороших новых функций, включая поддержку свойств, о которых вы говорите. Недавно я обновился и после некоторого рефакторинга остался доволен новой версией.

      from pydantic import BaseModel, computed_field
    
class SomeClass(BaseModel):
        
    spam: str
  
    @computed_field
    @property
    def eggs(self) -> str:
        return self.spam + " bacon" 

a = SomeClass(spam="I like")
a.model_dump()  # -> {'spam': 'I like', 'eggs': 'I like bacon'}
Другие вопросы по тегам