steveflyer
steveflyer

Reputation: 33

Pydanyic V2: how to write more intelligent `created_at`, `updated_at` fields

I wanna write a nice TimestampMixin class for my project, I hope it can:

  1. created_at: autonow when creation, and never change since that
  2. updated_at: autonow when creation
  3. updated_at: auto update updated_at if it is not specified when update.

like:

foo = Foo(name='foo') # created_at, updated_at autofilled

time.sleep(3)

foo.name = 'bar' # `updated_at` auto updated!

data_dict = {"name": "bar", "created_at": 1717288789, "updated_at": 1717288801}
foo_from_orm = Foo.model_validate(**data_dict) # `created_at`: 1717288789, `updated_at`: 1717288801 

For now, I have no solution, I can only manually write a on_update function and manually call it everytime when I update the model.

Is there any better solution or any ideas on this issue?

from datetime import datetime, UTC

from pydantic import BaseModel, Field


now_factory = lambda: int(datetime.now(UTC).timestamp())

class TimestampMixin(BaseModel):
    created_at: int = Field(default_factory=now_factory)
    updated_at: int = Field(default_factory=now_factory)

    def on_update(self):
        self.updated_at = now_factory()

Upvotes: 1

Views: 149

Answers (2)

Rafael Marques
Rafael Marques

Reputation: 1872

Pydantic v2 answer:

You can use validate_assignment from ConfigDict to force model validation after updating your model. Notice that validate_assignment is False by default.

The only catch is that you need to turn off the validate_assignment before you update any field to prevent an infinite loop:

@pydantic.model_validator(mode="after")
@classmethod
def set_updated_at(cls, obj):
    obj.model_config["validate_assignment"] = False
    obj.updated_at = now_factory()
    obj.model_config["validate_assignment"] = True

    return obj

So in your case, here is a working example:

now_factory = lambda: int(datetime.now(UTC).timestamp())


class TimestampMixin(pydantic.BaseModel):
    model_config = pydantic.ConfigDict(
        validate_assignment=True,
    )

    created_at: int = pydantic.Field(
        default_factory=now_factory
    )

    updated_at: int | None = pydantic.Field(None)

    @pydantic.model_validator(mode="after")
    @classmethod
    def set_updated_at(cls, obj):
        obj.model_config["validate_assignment"] = False
        obj.updated_at = now_factory()
        obj.model_config["validate_assignment"] = True

        return obj


class User(TimestampMixin, pydantic.BaseModel):

    name: str | None = pydantic.Field(None)
    age: int | None = pydantic.Field(None)


if __name__ == "__main__":
    # create your object
    user = User()
    print(user.model_dump()) # {'created_at': 1717414213, 'updated_at': 1717414213, 'name': None, 'age': None}

    time.sleep(2)
    user.name = "John"
    print(user.model_dump()) # {'created_at': 1717414213, 'updated_at': 1717414215, 'name': 'John', 'age': None}

    time.sleep(2)
    user.age = 40
    print(user.model_dump()) # {'created_at': 1717414213, 'updated_at': 1717414217, 'name': 'John', 'age': 40}

Upvotes: 3

Kraigolas
Kraigolas

Reputation: 5560

Make a private _updated_at field, and access updated_at with a property:

@property
def updated_at(self):
    self._updated_at = now_factory()
    return self._updated_at

Upvotes: 0

Related Questions