Pydantic-factories

Эта библиотека предлагает мощные возможности создания фиктивных данных для моделей и классов данных на основе pydantic. Можно использовать с другими библиотеками, использующими pydantic в качестве основы, например SQLModel и Beanie.

from datetime import date, datetime
from typing import List, Union

from pydantic import BaseModel, UUID4

from pydantic_factories import ModelFactory


class Person(BaseModel):
    id: UUID4
    name: str
    hobbies: List[str]
    age: Union[float, int]
    birthday: Union[datetime, date]


class PersonFactory(ModelFactory):
    __model__ = Person


result = PersonFactory.build()

Мы можем создать фиктивный объект данных, соответствующий определению модели класса Person. Это возможно из-за информации о типизации, доступной в модели pydantic и полях модели, которые используются в качестве источника истины для генерации данных. Фабрика анализирует информацию, хранящуюся в модели pydantic, и создает словарь kwargs, который передается методу init класса Person.

Возможности:

  • поддерживает как встроенные, так и [pydantic] типы.
  • поддерживает ограничения поля pydantic
  • поддерживает сложные типы полей
  • поддерживает настраиваемые поля модели

Методы

Класс ModelFactory предоставляет два метода сборки:

.build(**kwargs) — строит один экземпляр модели фабрики .batch(size: int, **kwargs) — построить список размером n экземпляров

result = PersonFactory.build()  # a single Person instance

result = PersonFactory.batch(size=5)  # list[Person, Person, Person, Person, Person]

Любые kwargs, которые вы передаете в .build, .batch или любой из методов, будут иметь приоритет над любыми значениями по умолчанию, определенными в самом фабричном классе.

По умолчанию при создании класса pydantic проверяются kwargs. Чтобы избежать проверки ввода, вы можете использовать параметр factory_use_construct.

result = PersonFactory.build(id=5)  # Raises a validation error

result = PersonFactory.build(
    factory_use_construct=True, id=5
)  # Build a Person with invalid id

Поддерживаются встроенные и сконструированные типы

from datetime import date, datetime
from enum import Enum
from pydantic import BaseModel, UUID4
from typing import Any, Dict, List, Union

from pydantic_factories import ModelFactory


class Species(str, Enum):
    CAT = "Cat"
    DOG = "Dog"
    PIG = "Pig"
    MONKEY = "Monkey"


class Pet(BaseModel):
    name: str
    sound: str
    species: Species


class Person(BaseModel):
    id: UUID4
    name: str
    hobbies: List[str]
    age: Union[float, int]
    birthday: Union[datetime, date]
    pets: List[Pet]
    assets: List[Dict[str, Dict[str, Any]]]


class PersonFactory(ModelFactory):
    __model__ = Person


result = PersonFactory.build()

Этот пример также будет работать, хотя для класса Pet не была определена фабрика, это не проблема — фабрика будет динамически сгенерирована для него на лету.

Сложная типизация под атрибутом assets немного сложнее, но фабрика сгенерирует объект python, соответствующий этой сигнатуре, поэтому пройдет проверку.

Обратите внимание: единственное, что фабрики не могут обработать, — это модели, ссылающиеся на самих себя, потому что это может привести к ошибкам рекурсии. В этом случае вам нужно будет обработать конкретное поле, установив для него значения по умолчанию.

Датаклассы

Эта библиотека работает с любым классом, который наследует класс pydantic BaseModel, включая GenericModel и классы из сторонних библиотек, а также с классами данных — как из стандартной библиотеки python, так и из классов данных pydantic. Фактически, вы можете использовать их взаимозаменяемо.

import dataclasses
from typing import Dict, List

import pydantic
from pydantic_factories import ModelFactory


@pydantic.dataclasses.dataclass
class MyPydanticDataClass:
    name: str


class MyFirstModel(pydantic.BaseModel):
    dataclass: MyPydanticDataClass


@dataclasses.dataclass()
class MyPythonDataClass:
    id: str
    complex_type: Dict[str, Dict[int, List[MyFirstModel]]]


class MySecondModel(pydantic.BaseModel):
    dataclasses: List[MyPythonDataClass]


class MyFactory(ModelFactory):
    __model__ = MySecondModel


result = MyFactory.build()

При создании фиктивных значений для полей с типом optional, если фабрика определена с __allow_none_Options__ = True, значение поля будет либо значением, либо None — в зависимости от случайного решения. Это работает, даже если необязательное типирование глубоко вложено, за исключением классов данных — типизация только поверхностно оценивается для классов данных, и поэтому всегда предполагается, что они требуют значения. Если вы хотите иметь значение None, в данном конкретном случае вы должны сделать это вручную, настроив обратный вызов Use для конкретного поля.

Конфигурирование фабрики

Конфигурация ModelFactory выполняется с использованием переменных класса:

__model__: обязательная переменная, определяющая модель фабрики. Он принимает любой класс, который расширяет BaseModel pydantic, включая классы из других библиотек. Если эта переменная не установлена, будет возбуждено исключение ConfigurationException.

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

__sync_persistence__: необязательная переменная, указывающая обработчик для синхронно сохраняемых данных. Если эта переменная не установлена, методы фабрики .create_sync и .create_batch_sync использовать нельзя.

__async_persistence__: необязательная переменная, определяющая обработчик асинхронно сохраняемых данных. Если эта переменная не задана, методы фабрики .create_async и .create_batch_async использовать нельзя.

__allow_none_optionals__: необязательная переменная, определяющая, должна ли фабрика случайным образом устанавливать значения None для необязательных полей или всегда устанавливать для них значения. Это True по умолчанию.

from faker import Faker
from pydantic_factories import ModelFactory

from app.models import Person
from .persistence import AsyncPersistenceHandler, SyncPersistenceHandler

Faker.seed(5)
my_faker = Faker("en-EN")


class PersonFactory(ModelFactory):
    __model__ = Person
    __faker__ = my_faker
    __sync_persistence__ = SyncPersistenceHandler
    __async_persistence__ = AsyncPersistenceHandler
    __allow_none_optionals__ = False
    ...

Для генерации детерменистических данных используйте метод ModelFactory.seed_random. Это передаст начальное значение как Faker, так и вызову случайного метода, гарантируя, что данные будут одинаковыми между вызовами. Особенно полезно для тестирования.

Определение атрибутов

from datetime import date, datetime
from enum import Enum
from pydantic import BaseModel, UUID4
from typing import Any, Dict, List, Union


class Species(str, Enum):
    CAT = "Cat"
    DOG = "Dog"


class Pet(BaseModel):
    name: str
    species: Species


class Person(BaseModel):
    id: UUID4
    name: str
    hobbies: List[str]
    age: Union[float, int]
    birthday: Union[datetime, date]
    pets: List[Pet]
    assets: List[Dict[str, Dict[str, Any]]]

Первый способ - непосредственно при создании

pet = Pet(name="Roxy", sound="woof woof", species=Species.DOG)


class PersonFactory(ModelFactory):
    __model__ = Person

    pets = [pet]

В этом случае, когда мы вызываем PersonFactory.build(), результат будет сгенерирован случайным образом, за исключением списка питомцев, который будет жестко запрограммированным значением по умолчанию, которое мы определили.

Поле Use

Другой вариант - определить фабрику для Pet, где мы ограничиваем выбор диапазоном, который нам нравится.

from enum import Enum
from pydantic_factories import ModelFactory, Use
from random import choice

from .models import Pet, Person


class Species(str, Enum):
    CAT = "Cat"
    DOG = "Dog"


class PetFactory(ModelFactory):
    __model__ = Pet

    name = Use(choice, ["Ralph", "Roxy"])
    species = Use(choice, list(Species))


class PersonFactory(ModelFactory):
    __model__ = Person

    pets = Use(PetFactory.batch, size=2)

или так (не используя Use):

class PetFactory(ModelFactory):
    __model__ = Pet

    name = lambda: choice(["Ralph", "Roxy"])
    species = lambda: choice(list(Species))

Поле PostGenerated

Позволяет создавать поля на основе уже сгенерированных значений других (не созданных таким способом) полей. В большинстве случаев этого шаблона лучше избегать.

from pydantic import BaseModel
from pydantic_factories import ModelFactory, PostGenerated
from random import randint


def add_timedelta(name: str, values: dict, *args, **kwds):
    delta = timedelta(days=randint(0, 12), seconds=randint(13, 13000))
    return values["from_dt"] + delta


class MyModel(BaseModel):
    from_dt: datetime
    to_dt: datetime


class MyFactory(ModelFactory):
    __model__ = MyModel

    to_dt = PostGenerated(add_timedelta)

Поле Ignore

Используется для обозначения данного атрибута как игнорируемого.

from typing import TypeVar

from odmantic import EmbeddedModel, Model
from pydantic_factories import ModelFactory, Ignore

T = TypeVar("T", Model, EmbeddedModel)


class OdmanticModelFactory(ModelFactory[T]):
    id = Ignore()

Поле Require

Указывает, что конкретный атрибут является обязательным kwarg. То есть, если kwarg со значением для этого конкретного атрибута не передается при вызове factory.build(), будет поднята ошибка MissingBuildKwargError.

Каков вариант использования для этого? Например, предположим, что у нас есть документ под названием Article, который мы храним в какой-либо БД и который представлен с использованием не-pydantic модели, скажем, документа с эластичной DSL. Затем нам нужно сохранить в нашем объекте pydantic ссылку на идентификатор этой статьи. Это значение не должно быть каким-то фиктивным значением, а должно быть фактическим идентификатором, переданным на фабрику. Таким образом, мы можем определить этот атрибут по мере необходимости:

from pydantic import BaseModel
from pydantic_factories import ModelFactory, Require
from uuid import UUID


class ArticleProxy(BaseModel):
    article_id: UUID
    ...


class ArticleProxyFactory(ModelFactory):
    __model__ = ArticleProxy

    article_id = Require()

Persistence

ModelFactory имеет четыре метода сохранения:

  • .create_sync(**kwargs) — синхронно создает и сохраняет один экземпляр модели фабрики.
  • .create_batch_sync(size: int, **kwargs) — синхронно создает и сохраняет список размером n экземпляров.
  • .create_async(**kwargs) — асинхронно создает и сохраняет один экземпляр модели фабрики.
  • .create_batch_async(size: int, **kwargs) — асинхронно создает и сохраняет список размером n экземпляров.

Чтобы использовать эти методы, вы должны сначала указать синнные и/или асинхронные обработчики для фабрики:

# persistence.py
from typing import TypeVar, List

from pydantic import BaseModel
from pydantic_factories import SyncPersistenceProtocol

T = TypeVar("T", bound=BaseModel)


class SyncPersistenceHandler(SyncPersistenceProtocol[T]):
    def save(self, data: T) -> T:
        ...  # do stuff

    def save_many(self, data: List[T]) -> List[T]:
        ...  # do stuff


class AsyncPersistenceHandler(AsyncPersistenceProtocol[T]):
    async def save(self, data: T) -> T:
        ...  # do stuff

    async def save_many(self, data: List[T]) -> List[T]:
        ...  # do stuff

Затем вы можете указать один или оба этих обработчика в вашей фабрике:

from pydantic_factories import ModelFactory

from app.models import Person
from .persistence import AsyncPersistenceHandler, SyncPersistenceHandler


class PersonFactory(ModelFactory):
    __model__ = Person
    __sync_persistence__ = SyncPersistenceHandler
    __async_persistence__ = AsyncPersistenceHandler

Или создайте свою собственную базовую фабрику и повторно используйте ее в различных фабриках:

from pydantic_factories import ModelFactory

from app.models import Person
from .persistence import AsyncPersistenceHandler, SyncPersistenceHandler


class BaseModelFactory(ModelFactory):
    __sync_persistence__ = SyncPersistenceHandler
    __async_persistence__ = AsyncPersistenceHandler


class PersonFactory(BaseModelFactory):
    __model__ = Person

Обратите внимание: вам не нужно определять какой-либо или оба обработчика сохранения. Если вы будете использовать только синхронное или асинхронное сохранение, вам нужно только определить соответствующий обработчик для использования этих методов.

Дополнительно - создание фабричных методов

Если вы предпочитаете императивное создание фабрики, вы можете сделать это с помощью метода ModelFactory.create_factory. Этот метод получает следующие аргументы:

  • model - модель для фабрики.
  • base - необязательный базовый класс фабрики. По умолчанию используется фабричный класс, для которого вызывается метод.
  • kwargs - словарь аргументов, соответствующих переменным класса, принятым ModelFactory. Вы также можете переопределить атрибут __model__ дочерней фабрики, чтобы указать используемую модель и kwargs

Пример

from datetime import date, datetime
from enum import Enum
from pydantic import BaseModel, UUID4
from typing import Any, Dict, List, TypeVar, Union, Generic, Optional
from pydantic_factories import ModelFactory


class Species(str, Enum):
    CAT = "Cat"
    DOG = "Dog"


class PetBase(BaseModel):
    name: str
    species: Species


class Pet(PetBase):
    id: UUID4


class PetCreate(PetBase):
    pass


class PetUpdate(PetBase):
    pass


class PersonBase(BaseModel):

    name: str
    hobbies: List[str]
    age: Union[float, int]
    birthday: Union[datetime, date]
    pets: List[Pet]
    assets: List[Dict[str, Dict[str, Any]]]


class PersonCreate(PersonBase):
    pass


class Person(PersonBase):
    id: UUID4


class PersonUpdate(PersonBase):
    pass


def test_factory():
    class PersonFactory(ModelFactory):
        __model__ = Person

    person = PersonFactory.build()

    assert person.pets != []


ModelType = TypeVar("ModelType", bound=BaseModel)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)


class BUILDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
    def __init__(
        self,
        model: ModelType = None,
        create_schema: Optional[CreateSchemaType] = None,
        update_schema: Optional[UpdateSchemaType] = None,
    ):
        self.model = model
        self.create_model = create_schema
        self.update_model = update_schema

    def build_object(self) -> ModelType:
        object_Factory = ModelFactory.create_factory(self.model)
        return object_Factory.build()

    def build_create_object(self) -> CreateSchemaType:
        object_Factory = ModelFactory.create_factory(self.create_model)
        return object_Factory.build()

    def build_update_object(self) -> UpdateSchemaType:
        object_Factory = ModelFactory.create_factory(self.update_model)
        return object_Factory.build()


class BUILDPet(BUILDBase[Pet, PetCreate, PetUpdate]):
    def build_object(self) -> Pet:
        object_Factory = ModelFactory.create_factory(self.model, name="Fido")
        return object_Factory.build()

    def build_create_object(self) -> PetCreate:
        object_Factory = ModelFactory.create_factory(self.create_model, name="Rover")
        return object_Factory.build()

    def build_update_object(self) -> PetUpdate:
        object_Factory = ModelFactory.create_factory(self.update_model, name="Spot")
        return object_Factory.build()


def test_factory_create():

    person_factory = BUILDBase(Person, PersonCreate, PersonUpdate)

    pet_factory = BUILDPet(Pet, PetCreate, PetUpdate)

    create_person = person_factory.build_create_object()
    update_person = person_factory.build_update_object()

    pet = pet_factory.build_object()
    create_pet = pet_factory.build_create_object()
    update_pet = pet_factory.build_update_object()

    assert create_person != None
    assert update_person != None

    assert pet.name == "Fido"
    assert create_pet.name == "Rover"
    assert update_pet.name == "Spot"

Дополнительно

Любой класс, производный от BaseModel pydantic, может использоваться как __model__ фабрики. Для большинства сторонних библиотек, например. SQLModel, эта библиотека будет работать как есть из коробки.

Есть API для [ormar]

Смотри еще: