Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

pydantic: Using property.getter decorator for a field with an alias

scroll all the way down for a tl;dr, I provide context which I think is important but is not directly relevant to the question asked

A bit of context

I'm in the making of an API for a webapp and some values are computed based on the values of others in a pydantic BaseModel. These are used for user validation, data serialization and definition of database (NoSQL) documents.

Specifically, I have nearly all resources inheriting from a OwnedResource class, which defines, amongst irrelevant other properties like creation/last-update dates:

  • object_key -- The key of the object using a nanoid of length 6 with a custom alphabet
  • owner_key -- This key references the user that owns that object -- a nanoid of length 10.
  • _key -- this one is where I'm bumping into some problems, and I'll explain why.

So arangodb -- the database I'm using -- imposes _key as the name of the property by which resources are identified.

Since, in my webapp, all resources are only accessed by the users who created them, they can be identified in URLs with just the object's key (eg. /subject/{object_key}). However, as _key must be unique, I intend to construct the value of this field using f"{owner_key}/{object_key}", to store the objects of every user in the database and potentially allow for cross-user resource sharing in the future.

The goal is to have the shortest per-user unique identifier, since the owner_key part of the full _key used to actually access and act upon the document stored in the database is always the same: the currently-logged-in user's _key.

My attempt

My thought was then to define the _key field as a @property-decorated function in the class. However, Pydantic does not seem to register those as model fields.

Moreover, the attribute must actually be named key and use an alias (with Field(... alias="_key"), as pydantic treats underscore-prefixed fields as internal and does not expose them.

Here is the definition of 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] Since Field(..., alias="...") cannot be used, I use this property of the Config subclass (see pydantic's documentation)

However, this does not work, as shown in the following example:

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

with InSubject defining properties proper to Subject, and Subject being an empty class inheriting from both InSubject and 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

When I perform a POST /subjects/, the following is printed in the console:

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

As you can see, _key or key are nowhere to be seen.

Please ask for details and clarification, I tried to make this as easy to understand as possible, but I'm not sure if this is clear enough.

tl;dr

A context-less and more generic example without insightful context:

With the following class:

from pydantic import BaseModel

class SomeClass(BaseModel):
    
    spam: str

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

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

I would like the following to be true:

a = SomeClass(spam="I like")
d = a.dict(by_alias=True)
d.get("_eggs") == "I like bacon"
like image 298
ewen-lbh Avatar asked Aug 05 '20 12:08

ewen-lbh


People also ask

What is __ root __ In Pydantic?

The root type can be any type supported by pydantic, and is specified by the type hint on the __root__ field. The root value can be passed to the model __init__ via the __root__ keyword argument, or as the first and only argument to parse_obj . Python 3.7 and above Python 3.9 and above.

What is Pydantic BaseModel?

1 — A simple syntax to define your data models You can define your data inside a class that inherits from the BaseModel class. Pydantic models are structures that ingest the data, parse it and make sure it conforms to the fields' constraints defined in it.

What is Pydantic in Python?

Pydantic is a useful library for data parsing and validation. It coerces input types to the declared type (using type hints), accumulates all the errors using ValidationError & it's also well documented making it easily discoverable.

What is Pydantic schema?

Pydantic allows auto creation of JSON Schemas from models: Python 3.7 and above.

What is @property decorator in Python?

Python @property decorator @property decorator is a built-in decorator in Python which is helpful in defining the properties effortlessly without manually calling the inbuilt function property (). Which is used to return the property attributes of a class from the stated getter, setter and deleter as parameters.

How to define the _Key Field in pydantic?

My thought was then to define the _key field as a @property -decorated function in the class. However, Pydantic does not seem to register those as model fields. Moreover, the attribute must actually be named key and use an alias (with Field (... alias="_key" ), as pydantic treats underscore-prefixed fields as internal and does not expose them.

What is the use of @property decorator in JavaScript?

Which is used to return the property attributes of a class from the stated getter, setter and deleter as parameters. Here, the @property decorator is used to define the property name in the class Portal, that has three methods (getter, setter, and deleter) with similar names i.e, name (), but they have different number of parameters.

Is there a validate_arguments decorator for pydantic?

The validate_arguments decorator is in beta, it has been added to pydantic in v1.5 on a provisional basis. It may change significantly in future releases and its interface will not be concrete until v2. Feedback from the community while it's still provisional would be extremely useful; either comment on #1205 or create a new issue.


2 Answers

You might be able to serialize your _key field using a pydantic validator with the always option set to True.

Using your example:

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"

So to serialize your _eggs field, instead of appending a string, you'd insert your serialization function there and return the output of that.

like image 124
Tim Estes Avatar answered Oct 22 '22 16:10

Tim Estes


Pydantic does not support serializing properties, there is an issue on GitHub requesting this feature.

Based on this comment by ludwig-weiss he suggests subclassing BaseModel and overriding the dict method to include the properties.

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
like image 24
Gabriel Cappelli Avatar answered Oct 22 '22 18:10

Gabriel Cappelli