Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python [pydantic] - Date validation

i'd like to valid a json input for dates as pydantic class, next , to simply inject the file to Mongo .

Simple class with date type


class CustomerBase(BaseModel):
    birthdate: date = None

Using motor for working with Mongo

Db configuration :

from motor.motor_asyncio import AsyncIOMotorClient

DB = DB_CLIENT[CONF.get("databases", dict())["mongo"]["NAME"]]

8/03/2021 - Update:

I did the following debug test, first print the class to see how it saved and next try to inject it to Mongo.

so for input:

{ "birthdate": "2021-03-05"}

Routing:

@customers_router.post("/", response_model=dict)
async def add_customer(customer: CustomerBase):
    print(customer.dict())

>> {'birthdate': datetime.date(2021, 3, 5)}

    await DB.customer.insert_one(customer.dict())
    return {"test":1}

>> 
 File "./customers/routes.py", line 74, in add_customer
    await DB.customer.insert_one(customer.dict())
  File "/usr/local/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/usr/local/lib/python3.8/site-packages/pymongo/collection.py", line 698, in insert_one
    self._insert(document,
  File "/usr/local/lib/python3.8/site-packages/pymongo/collection.py", line 613, in _insert
    return self._insert_one(
  File "/usr/local/lib/python3.8/site-packages/pymongo/collection.py", line 602, in _insert_one
    self.__database.client._retryable_write(
  File "/usr/local/lib/python3.8/site-packages/pymongo/mongo_client.py", line 1498, in _retryable_write
    return self._retry_with_session(retryable, func, s, None)
  File "/usr/local/lib/python3.8/site-packages/pymongo/mongo_client.py", line 1384, in _retry_with_session
    return self._retry_internal(retryable, func, session, bulk)
  File "/usr/local/lib/python3.8/site-packages/pymongo/mongo_client.py", line 1416, in _retry_internal
    return func(session, sock_info, retryable)
  File "/usr/local/lib/python3.8/site-packages/pymongo/collection.py", line 590, in _insert_command
    result = sock_info.command(
  File "/usr/local/lib/python3.8/site-packages/pymongo/pool.py", line 699, in command
    self._raise_connection_failure(error)
  File "/usr/local/lib/python3.8/site-packages/pymongo/pool.py", line 683, in command
    return command(self, dbname, spec, slave_ok,
  File "/usr/local/lib/python3.8/site-packages/pymongo/network.py", line 120, in command
    request_id, msg, size, max_doc_size = message._op_msg(
  File "/usr/local/lib/python3.8/site-packages/pymongo/message.py", line 714, in _op_msg
    return _op_msg_uncompressed(
bson.errors.InvalidDocument: cannot encode object: datetime.date(2021, 3, 5), of type: <class 'datetime.date'>

Issues: 1.The date in class saved as birthday: datetime.date(2021, 3, 5) is that expected? 2.Surely the problem coming from : ''' DB.customer.insert_one(customer.dict()) ''' it does work when i change the date type to str in Class

Update 09/03/2022:

following Tom's suggestion: the decorator and parse_birthday function added. Now i able to document to Mongo but not able to read it.


class CustomerBase(BaseModel):
    birthdate: datetime.date

    @validator("birthdate", pre=True)
    def parse_birthdate(cls, value):
        return datetime.datetime.strptime(
            value,
            "%d/%m/%Y"
        ).date()

    def dict(self, *args, **kwargs) -> 'DictStrAny':
        for_mongo = kwargs.pop('for_mongo', False)
        d = super().dict(*args, **kwargs)
        if for_mongo:
            for k, v in d.items():
                if isinstance(v, datetime.date):
                    d[k] = datetime.datetime(
                        year=v.year,
                        month=v.month,
                        day=v.day,
                    )
        return d

class CustomerOnDB(CustomerBase):
    id_: str

assign data (working): input : {"birthdate": "01/11/1978"}

@customers_router.post("/", response_model=dict )
async def add_customer(customer: CustomerBase):

    customer_op = await DB.customer.insert_one(customer.dict(for_mongo=True))
    if customer_op.inserted_id:
        #customer_op.inserted_id -> is the str _id
        await _get_customer_or_404(customer_op.inserted_id)
        return { "id_": str(customer_op.inserted_id) }

When trying to read:

def validate_object_id(id_: str):
    try:
        _id = ObjectId(id_)
    except Exception:
        raise HTTPException(status_code=400)
    return _id


@customers_router.get(
    "/{id_}",
    response_model=CustomerOnDB
)
async def get_customer_by_id(id_: ObjectId = Depends(validate_object_id)):
    customer = await DB.customer.find_one({"_id": id_})
    if customer:
        customer["id_"] = str(customer["_id"])
        return customer
    else:
        raise HTTPException(status_code=404, detail="Customer not found")

Getting:


  File "/usr/local/lib/python3.8/site-packages/fastapi/routing.py", line 126, in serialize_response
    raise ValidationError(errors, field.type_)
pydantic.error_wrappers.ValidationError: 1 validation error for CustomerOnDB
response -> 0 -> birthdate
  strptime() argument 1 must be str, not datetime.datetime (type=type_error)

like image 573
AviC Avatar asked Sep 07 '25 15:09

AviC


1 Answers

I'm not sure what your question is as your CustomerBase works fine with

{ "birthdate": "2021-03-05"} this input.

If you want to parse %d/%m/%Y date, parse it using validator and pre parameter.

class CustomerBase(BaseModel):
    birthdate: date

    @validator("birthdate", pre=True)
    def parse_birthdate(cls, value):
        return datetime.datetime.strptime(
            value,
            "%d/%m/%Y"
        ).date()

EDIT: You added a comment mentioning something else that doesn't work as you expect. AFAIK mongo doesn't accept datetime.date. Just change it to datetime.datetime when dumping to dict or change type to datetime.

example

import datetime

from pydantic.main import BaseModel


class CustomerBase(BaseModel):
    birthdate: datetime.date

    def dict(self, *args, **kwargs) -> 'DictStrAny':
        d = super().dict(*args, **kwargs)
        for k, v in d.items():
            if isinstance(v, datetime.date):
                d[k] = datetime.datetime(
                    year=v.year,
                    month=v.month,
                    day=v.day,
                )
        return d

If you need both functionalities

import datetime

from pydantic import validator
from pydantic.main import BaseModel


class CustomerBase(BaseModel):
    birthdate: datetime.date

    @validator("birthdate", pre=True)
    def parse_birthdate(cls, value):
        return datetime.datetime.strptime(
            value,
            "%d/%m/%Y"
        ).date()

    def dict(self, *args, **kwargs) -> 'DictStrAny':
        for_mongo = kwargs.pop('for_mongo', False)
        d = super().dict(*args, **kwargs)
        if for_mongo:
            for k, v in d.items():
                if isinstance(v, datetime.date):
                    d[k] = datetime.datetime(
                        year=v.year,
                        month=v.month,
                        day=v.day,
                    )
        return d


>>> c = CustomerBase(**{"birthdate": "03/05/2021"})
>>> c.dict()
>>> {'birthdate': datetime.date(2021, 5, 3)}
>>> c.dict(for_mongo=True)
>>> {'birthdate': datetime.datetime(2021, 5, 3, 0, 0)}

like image 168
Tom Wojcik Avatar answered Sep 09 '25 10:09

Tom Wojcik