Metadata-Version: 2.4
Name: payforge
Version: 0.1.0
Summary: Pydantic-factry тестовых данных для генерация payload-ов
Project-URL: Homepage, https://github.com/TheGreatPepix/payforge
Project-URL: Source, https://github.com/TheGreatPepix/payforge
Author: Igor Safronov
License-Expression: MIT
License-File: LICENSE
Keywords: factory,fixtures,payload,pydantic,qa,testing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: pydantic>=2
Provides-Extra: dev
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# payforge

`payforge` – небольшая библиотека для QA-автотестов, которая помогает собирать тестовые payload-ы на базе Pydantic-моделей.

Идея простая: одна модель описывает структуру запроса. По ней можно собрать payload для API, точечно переопределить нужные поля, сломать данные для негативного теста и этой же моделью проверить ответ.

## Установка

```bash
pip install payforge
```

Из обязательных зависимостей – только `pydantic>=2`.

Генераторы случайных данных, например Faker, библиотека не подтягивает. Их можно подключить отдельно, если они нужны в проекте.

## Быстрый старт

```python
from payforge import DataFactory, FactoryModel
from pydantic import Field


class Config(FactoryModel):
    token: str = "default-token"
    timeout: int = 30


class Webhook(FactoryModel):
    list_id: str = Field("L-1", alias="list")   # API ждёт ключ "list"
    config: Config = Field(default_factory=Config)


class WebhookFactory(DataFactory):
    pass


f = WebhookFactory()

# Собрать валидный payload:
f.build(Webhook)
# {"list": "L-1", "config": {"token": "default-token", "timeout": 30}}

# Переопределить вложенное поле:
f.build(Webhook, **{"config.token": "secret"})
# {"list": "L-1", "config": {"token": "secret", "timeout": 30}}

# Собрать payload для негативного теста:
f.build_invalid(Webhook, field="config.timeout", value="not-a-number")

# Собрать несколько payload-ов:
f.build_many(Webhook, count=3, each=lambda i: {"config.token": f"tok-{i}"})
# [{...tok-0...}, {...tok-1...}, {...tok-2...}]

# Получить типизированный экземпляр модели:
wh = f.build_model(Webhook)

# Проверить ответ API той же моделью:
f.validate(Webhook, api_response)
# -> Webhook | SchemaValidationError
```

## Случайные значения

`payforge` специально не зависит от Faker или других генераторов данных. Так библиотека остаётся лёгкой, а проект сам решает, чем генерировать тестовые значения.

Например, Faker можно подключить через `default_factory`:

```python
from faker import Faker
from pydantic import Field

fake = Faker()


class User(FactoryModel):
    name: str = Field(default_factory=fake.name)
    email: str = Field(default_factory=fake.email)
```

## Как пользоваться

### Модель можно передать классом или строкой

Все основные методы принимают `entity`:

* класс Pydantic-модели, например `Webhook`;
* строковый ключ из `entities`, если хочется вызывать фабрику по имени.

Рекомендуемый вариант – передавать сам класс модели. Так не нужен реестр, а IDE лучше понимает типы, особенно при использовании `build_model()`.

```python
f.build(Webhook)
```

Если удобнее обращаться к моделям по строковым ключам, можно объявить `entities`:

```python
class WebhookFactory(DataFactory):
    entities = {
        "webhook": Webhook,
    }


WebhookFactory().build("webhook")
```

Если строковый ключ неизвестен или реестр не объявлен, будет выброшена ошибка `UnknownEntityError`.

## Валидация и негативные тесты

По умолчанию `build()` собирает payload и прогоняет его через модель.

```python
f.build(Webhook)
```

Это поведение соответствует `validate=True`.

Если в `overrides` передать значение неподходящего типа, Pydantic выбросит `ValidationError`.

```python
f.build(Webhook, **{"config.timeout": "not-a-number"})
# pydantic.ValidationError
```

Для негативных тестов можно отключить повторную валидацию:

```python
f.build(
    Webhook,
    validate=False,
    **{"config.timeout": "not-a-number"},
)
```

В этом режиме фабрика сначала собирает валидную базу из дефолтов модели, а потом аккуратно накладывает переопределения поверх неё. Соседние поля при этом не теряются.

Важно: для `validate=False` модель должна уметь создаваться без аргументов. То есть у всех обязательных полей должны быть значения по умолчанию или `default_factory`.

### `build_invalid`

`build_invalid()` – короткий способ собрать payload для негативного теста.

```python
f.build_invalid(
    Webhook,
    field="config.timeout",
    value="not-a-number",
)
```

Под капотом это валидная база плюс одно испорченное поле. Значение из `field` накладывается последним, поэтому оно перебивает остальные `overrides`, если есть конфликт.

## Несколько payload-ов

Метод `build_many()` собирает список payload-ов.

```python
f.build_many(Webhook, count=10)
```

Общие значения можно передать как обычные overrides – они применятся ко всем элементам:

```python
f.build_many(Webhook, count=3, list_id="SHARED")
```

Если каждому элементу нужны свои значения, используйте `each`:

```python
f.build_many(
    Webhook,
    count=3,
    each=lambda i: {"config.token": f"tok-{i}"},
)
```

Можно сочетать общий override и индивидуальные значения:

```python
f.build_many(
    Webhook,
    count=2,
    list_id="SHARED",
    each=lambda i: {"list": f"L{i}"},
)
```

Если одно и то же поле задано и в общих overrides, и в `each`, победит значение из `each`.

Особенности:

* `count=0` вернёт пустой список;
* отрицательный `count` выбросит `ValueError`;
* параметры `validate`, `exclude_none` и `by_alias` пробрасываются в `build()`.

## Вложенные поля и alias-ы

Поля можно переопределять по имени поля модели или по alias-у.

Для вложенных моделей используется dotted-path:

```python
f.build(Webhook, **{"config.token": "x"})
```

Так меняется только `config.token`, а остальные поля внутри `config` остаются на месте.

Сериализация по умолчанию идёт с `by_alias=True`, поэтому поле:

```python
list_id: str = Field("L-1", alias="list")
```

попадёт в payload как:

```python
{"list": "L-1"}
```

Это же учитывается и в негативных сценариях. Если переопределить поле по имени `list_id`, в итоговом payload всё равно будет ключ `list`, а не два разных ключа `list` и `list_id`.

## Валидация ответа API

Ответ API можно проверить той же моделью:

```python
result = f.validate(Webhook, api_response)
```

Если ответ подходит под схему, метод вернёт экземпляр модели.

Если данные не прошли валидацию, будет выброшена `SchemaValidationError`. В поле `.errors` лежит структурированный список ошибок Pydantic.

## Ошибки

`payforge` использует свои ошибки там, где нужно отделить ошибки фабрики от обычных ошибок Pydantic.

### `UnknownEntityError`

Возникает, если в фабрику передали строковый ключ, которого нет в `entities`, или если реестр `entities` не объявлен.

```python
f.build("unknown")
```

### `SchemaValidationError`

Возникает при проверке ответа API через `validate()`, если ответ не соответствует модели.

```python
try:
    f.validate(Webhook, api_response)
except SchemaValidationError as e:
    print(e.errors)
```
