Python [pydantic] — проверка даты

я хотел бы проверить ввод json для дат в качестве класса pydantic, затем просто ввести файл в Mongo.

Простой класс с типом даты


class CustomerBase(BaseModel):
    birthdate: date = None

Использование мотора для работы с Mongo

Конфигурация БД:

from motor.motor_asyncio import AsyncIOMotorClient

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

03.08.2021 - Обновление:

Я провел следующий отладочный тест: сначала распечатал класс, чтобы посмотреть, как он сохранился, а затем попытался внедрить его в Mongo.

поэтому для ввода:

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

Маршрутизация:

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

Проблемы: 1. Дата в классе сохраняется как день рождения: datetime.date(2021, 3, 5) это ожидается? 2. Конечно, проблема исходит из: ''' DB.customer.insert_one (customer.dict()) ''' это работает, когда я меняю тип даты на str в классе

Обновление от 03.09.2022:

по предложению Тома: добавлены декоратор и функция parse_birthday. Теперь я могу документировать Mongo, но не могу его прочитать.


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

назначить данные (рабочие): ввод: {дата рождения: 11.01.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) }

При попытке прочитать:

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")

Получающий:


  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)


person AviC    schedule 05.03.2021    source источник
comment
Вы говорите, что хотите проверить dd/mm/yyyy и привести пример с yyyy-mm-dd. Так что это?   -  person Tom Wojcik    schedule 06.03.2021
comment
Ваше сообщение об ошибке ссылается на bson, который больше нигде не упоминается в вашем вопросе. Не могли бы вы привести минимально воспроизводимый пример, демонстрирующий вашу проблему?   -  person jwodder    schedule 06.03.2021
comment
@jwodder -u верно, ошибка исходит от bson, основной пост обновлен с более подробной информацией   -  person AviC    schedule 08.03.2021
comment
@TomWojcik - на самом деле он не работает с обоими, предпочитаю использовать формат дд/мм/гггг.   -  person AviC    schedule 08.03.2021
comment
Да, это так. Покажите, что не работает.   -  person Tom Wojcik    schedule 08.03.2021


Ответы (1)


Я не уверен, в чем заключается ваш вопрос, так как ваш CustomerBase отлично работает с

{ "birthdate": "2021-03-05"} этот ввод.

Если вы хотите проанализировать %d/%m/%Y дату, проанализируйте ее с помощью валидатора и pre параметр.

class CustomerBase(BaseModel):
    birthdate: date

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

РЕДАКТИРОВАТЬ: вы добавили комментарий, в котором упоминается что-то еще, что не работает, как вы ожидаете. Насколько я знаю, монго не принимает datetime.date. Просто измените его на datetime.datetime при сбросе в dict или измените тип на datetime.

пример

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

Если вам нужны обе функции

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

person Tom Wojcik    schedule 05.03.2021
comment
Спасибо, Том, так что, когда дело доходит до Mongo, похоже, мне нужна функция для datetime.date, преобразованная в datetime.datetime, а также нужен декоратор для обработки формата? есть ли способ включить обработку формата даты внутри def dict()? - person AviC; 08.03.2021
comment
validator запускается, когда вы хотите загрузить словарь в объект pydantic, и метод dict(), когда вы создаете ввод для Mongo (из объекта pydantic в dict). Так что нет, нет возможности объединить их в один метод. Но нет никакой проблемы в том, чтобы иметь оба. - person Tom Wojcik; 08.03.2021
comment
странно, в чем слабая сторона? Fastapi должен выполнить необходимую настройку для работы с Mongo? спрашивая, как часто требуется преформировать запрос БД на основе дат, это действительно усложняет задачу, теперь у меня проблема с response_model = CustomerBase и возвратом объект имеет другой тип данных (надеюсь, вы поняли мою точку зрения) - person AviC; 08.03.2021
comment
Нет, веб-фреймворк предназначен для веб-материалов, а pydantic — для типов. Вы должны изменить тип birthdate на datetime.datetime. - person Tom Wojcik; 08.03.2021
comment
Том, изменение типа дня рождения на datetime.datetime потребует от пользователя вставки дня рождения в сложном формате, это то, что вы имеете в виду? Или потребуется валидатор? - person AviC; 08.03.2021
comment
в parse_birthday не вызывайте date(), поэтому он возвращает дату со временем и меняет тип даты рождения на datetime.datetime. - person Tom Wojcik; 09.03.2021
comment
Том, не уверен, что ты имеешь в виду для date() в parse_birthday, поскольку для date() нет вызова. Я обновляю основной пост с изменениями и проблемой, поднятой при извлечении данных из Mongo. Кажется, такая головная боль для очень тривиальной вещи :( - person AviC; 09.03.2021
comment
Я открываю новый пост, так как предложенное решение не сработало для меня, спасибо, Том, ценю вашу помощь. - person AviC; 10.03.2021