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'}