EasyFactory 1.4.2
EasyFactory is a wrapper above FactoryBoy to allow the generation of Factory class with mandatory parameter filed.
Requires Python: >=3.12

# EasyFactory

![pipeline](https://gitlab.com/Tagashy/easyfactory/badges/master/pipeline.svg)
![coverage](https://gitlab.com/Tagashy/easyfactory/badges/master/coverage.svg)
![release](https://gitlab.com/Tagashy/easyfactory/-/badges/release.svg)


## Description
EasyFactory is a wrapper above [FactoryBoy](https://factoryboy.readthedocs.io) to allow the generation of Factory class with mandatory parameter filed.

This way one can concentrate its test on only meaningful data without having to care about properties not impacting the tests.
The library also try to guess appropriate [Faker](https://faker.readthedocs.io) class to use based on parameter name

This library was inspired by [tiangolo's fastapi](https://fastapi.tiangolo.com/) usage of typing.

## Installation
`pip install EasyFactory --index-url https://gitlab.com/api/v4/projects/53133142/packages/pypi/simple`

## Usage

two usage scenarii are considered :

### FactoryBoy style

using FactoryBoy style one just have to herit from EasyFactory.

```python
from easyfactory import EasyFactory
from pydantic import BaseModel
from uuid import UUID
from datetime import datetime


class Model(BaseModel):
    id: UUID | None = None
    name: str
    level: int
    status: str | None
    created_at: datetime = datetime(2020, 12, 16)


class ModelFactory(EasyFactory):
    class Meta:
        model = Model


model = ModelFactory()
# Model(id=None, name='Randy Johnson', level=9837, status=None, created_at=datetime.datetime(2020, 12, 16, 0, 0))

```
#### Overwriting generated attributes

It is possible FactoryBoy attribute for property to override default value generated by EasyFactory. Also, as under the hood, a Factory model is generated one can specify value at Factory instantiation.

```python
from datetime import datetime
from uuid import UUID

import factory
from pydantic import BaseModel

from easyfactory import EasyFactory


class Model(BaseModel):
    id: UUID | None = None
    name: str
    level: int
    status: str | None
    created_at: datetime = datetime(2020, 12, 16)


class ModelFactory(EasyFactory):
    class Meta:
        model = Model

    status = "DONE"
    created_at = factory.Faker("past_datetime")


model = ModelFactory(level=5)
# Model(id=None, name='Mark Mckenzie', level=5, status='DONE', created_at=datetime.datetime(2023, 12, 5, 11, 9, 13))
```

### Type preserving style

while FactoryBoy style is great for large codebase it cannot be used to tell that a Factory class return a model instance and not an instance pf itself, EasyFactory provide a factory generation function that provide this utility:

```python
from easyfactory import make_factory_for
from pydantic import BaseModel
from uuid import UUID
from datetime import datetime


class Model(BaseModel):
    id: UUID | None = None
    name: str
    level: int
    status: str | None
    created_at: datetime = datetime(2020, 12, 16)


ModelFactory = make_factory_for(Model)  # the type of ModelFactory is `type[Model]`
```
it is also possible to provide override for the Factory and the Instance.

```python
from datetime import datetime
from uuid import UUID

import factory
from pydantic import BaseModel

from easyfactory import make_factory_for


class Model(BaseModel):
    id: UUID | None = None
    name: str
    level: int
    status: str | None
    created_at: datetime = datetime(2020, 12, 16)


ModelFactory = make_factory_for(Model, status="DONE", created_at=factory.Faker("past_datetime"))  # Factory override
model = ModelFactory(level=5)  # instance override
```
## Library support

Currently only pydantic's BaseModel and SqlAlchemy ORM (DeclarativeBase chimd) are supported.

### Pydantic
In case of pydantic, it is assumed that NO CYCLING relation exist within models. if one exist the library will crash (possibly with cryptic message)

### SqlAlchemy
EasyFactory handle SqlAlchemy model in the following way:

for properties, just generate a generator like it is done with pydantic.

#### Relationship
in the case of relation between models, EasyFactory will always generate a Sub Model for a relation even if it is optional as the library doesn't know how to determine if a relation is optional

##### One To One
EasyFactory will generate a factory for the child on the parent class and after instantiating the parent, set the child to parent property to the parent using the back_populates/backref of the relationship. 

if this is not clear consier the following python code:

```python
from typing import Annotated
from uuid import UUID, uuid4

from easyfactory import make_factory_for
from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy.orm import mapped_column, DeclarativeBase, Mapped, relationship

UUID_PK = Annotated[UUID, mapped_column(primary_key=True)]
PARENT_FK = Annotated[UUID, mapped_column(ForeignKey("parent.id"))]


class Base(DeclarativeBase):
    id: Mapped[UUID_PK] = mapped_column(default=uuid4)


class Child(Base):
    __tablename__ = "child"
    __table_args__ = (UniqueConstraint("parent_id"),)

    parent_id: Mapped[PARENT_FK]
    parent: Mapped["Parent"] = relationship(back_populates="child", single_parent=True)


class Parent(Base):
    __tablename__ = "parent"

    child: Mapped["Child"] = relationship(back_populates="parent", cascade="all, delete-orphan")


ParentFactory = make_factory_for(Parent)  # the type of ParentFactory is `type[Parent]`
parent = ParentFactory()  # parent is typed as `Parent` for the IDE so the following line have type helps from IDE.
assert parent.child.parent == parent
```

##### One To Many
EasyFactory will generate a factory for the childs on the parent class, generate one child and after instantiating the parent, set the child to parent property to the parent using the back_populates/backref of the relationship. 

if this is not clear consider the following python code:

```python
from typing import Annotated
from uuid import UUID, uuid4

from easyfactory import make_factory_for
from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy.orm import mapped_column, DeclarativeBase, Mapped, relationship

UUID_PK = Annotated[UUID, mapped_column(primary_key=True)]
PARENT_FK = Annotated[UUID, mapped_column(ForeignKey("parent.id"))]


class Base(DeclarativeBase):
    id: Mapped[UUID_PK] = mapped_column(default=uuid4)


class Child(Base):
    __tablename__ = "child"
    __table_args__ = (UniqueConstraint("parent_id"),)

    parent_id: Mapped[PARENT_FK]
    parent: Mapped["Parent"] = relationship(back_populates="childs")


class Parent(Base):
    __tablename__ = "parent"

    childs: Mapped[list["Child"]] = relationship(back_populates="parent", cascade="all, delete-orphan")


ParentFactory = make_factory_for(Parent)  # the type of ParentFactory is `type[Parent]`
parent = ParentFactory()  # parent is typed as `Parent` for the IDE so the following line have type helps from IDE.
assert len(parent.childs) == 1
assert parent.childs[0].parent == parent
```
##### Many To Many
In case of Many-to-Many relationship, EasyFactory don't generate ChildFactory, set the value to None and let the developper set what it wants.

## Support
Any help is welcome. you can either:
- [create an issue](https://gitlab.com/Tagashy/easyfactory/issues/new)
- look for TODO in the code and provide a MR with changes
- provide a MR for support of new class


## Authors and acknowledgment
Currently, solely developed by Tagashy but any help is welcomed and will be credited here.


## License
See the [LICENSE](LICENSE) file for licensing information as it pertains to
files in this repository.