← run

py-08-pydantic-orders

1.000
9/9 tests· lib-knowledge
Challenge · difficulty 4/5
# Order validation (pydantic v2)

Implement **`solution.py`** using **pydantic v2**
(`from pydantic import BaseModel, ...`).

Define a model `Order` and a parsing helper:

```python
from pydantic import BaseModel

class Order(BaseModel):
    id: int
    customer: str
    quantity: int
    unit_price: float
    # plus a derived `total`

def parse_orders(rows: list[dict]) -> list[Order]:
    ...
```

### `Order` field rules

- `id: int`
- `customer: str` — must be **non-empty** (after no special trimming required; an
  empty string `""` is invalid).
- `quantity: int` — must be **strictly greater than 0**.
- `unit_price: float` — must be **greater than or equal to 0**.
- `total: float` — a **derived/computed** value equal to `quantity * unit_price`.
  Callers should be able to read `order.total`. You may implement it as a
  `@computed_field` property or as a validated field that is always recomputed —
  but it must reflect `quantity * unit_price` and not be settable to an arbitrary
  inconsistent value.

Use pydantic's standard constraint mechanisms (e.g. `Field(gt=0)`,
`Field(ge=0)`, `Field(min_length=1)`, or `field_validator`).

### `parse_orders(rows)`

- Takes a list of dicts and returns a list of validated `Order` instances, one per
  input row, in order.
- If **any** row is invalid, it must raise pydantic's
  `pydantic.ValidationError` (do not catch and swallow it; do not return partial
  results in that case — letting the exception propagate from the first invalid
  row is fine).

Example:

```python
orders = parse_orders([
    {"id": 1, "customer": "Acme", "quantity": 3, "unit_price": 2.5},
])
orders[0].total           # 7.5

parse_orders([{"id": 2, "customer": "X", "quantity": 0, "unit_price": 1.0}])
# raises pydantic.ValidationError  (quantity must be > 0)
```
tests/test_orders.py
import pytest
from pydantic import ValidationError

from solution import Order, parse_orders


def test_valid_parse():
    orders = parse_orders([
        {"id": 1, "customer": "Acme", "quantity": 3, "unit_price": 2.5},
        {"id": 2, "customer": "Beta", "quantity": 1, "unit_price": 10.0},
    ])
    assert len(orders) == 2
    assert isinstance(orders[0], Order)
    assert orders[0].id == 1
    assert orders[0].customer == "Acme"


def test_computed_total():
    orders = parse_orders([
        {"id": 1, "customer": "Acme", "quantity": 3, "unit_price": 2.5},
    ])
    assert orders[0].total == pytest.approx(7.5)


def test_total_zero_price():
    orders = parse_orders([
        {"id": 1, "customer": "Acme", "quantity": 4, "unit_price": 0.0},
    ])
    assert orders[0].total == pytest.approx(0.0)


def test_invalid_quantity_zero_raises():
    with pytest.raises(ValidationError):
        parse_orders([{"id": 2, "customer": "X", "quantity": 0, "unit_price": 1.0}])


def test_invalid_negative_quantity_raises():
    with pytest.raises(ValidationError):
        parse_orders([{"id": 2, "customer": "X", "quantity": -1, "unit_price": 1.0}])


def test_invalid_negative_price_raises():
    with pytest.raises(ValidationError):
        parse_orders([{"id": 3, "customer": "Y", "quantity": 1, "unit_price": -0.01}])


def test_empty_customer_raises():
    with pytest.raises(ValidationError):
        parse_orders([{"id": 4, "customer": "", "quantity": 1, "unit_price": 1.0}])


def test_one_bad_row_in_batch_raises():
    rows = [
        {"id": 1, "customer": "Acme", "quantity": 3, "unit_price": 2.5},
        {"id": 2, "customer": "Beta", "quantity": 0, "unit_price": 1.0},  # bad
    ]
    with pytest.raises(ValidationError):
        parse_orders(rows)


def test_total_serialized_in_dump():
    o = parse_orders([{"id": 1, "customer": "Acme", "quantity": 2, "unit_price": 3.0}])[0]
    data = o.model_dump()
    assert data["total"] == pytest.approx(6.0)
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```python
from pydantic import BaseModel, Field, computed_field


class Order(BaseModel):
    id: int
    customer: str = Field(..., min_length=1)
    quantity: int = Field(..., gt=0)
    unit_price: float = Field(..., ge=0)

    @computed_field
    @property
    def total(self) -> float:
        return self.quantity * self.unit_price


def parse_orders(rows: list[dict]) -> list[Order]:
    return [Order(**row) for row in rows]
```

Test output (stdout)

.........                                                                [100%]
9 passed in 0.05s
Reasoning (chain-of-thought)
The user wants me to implement a solution using Pydantic v2 for order validation. Let me break down the requirements:

1. Create an `Order` class that inherits from `BaseModel`
2. Fields needed:
   - `id: int`
   - `customer: str` - must be non-empty (min_length=1)
   - `quantity: int` - must be > 0 (gt=0)
   - `unit_price: float` - must be >= 0 (ge=0)
   - `total: float` - computed field (quantity * unit_price)

3. Create a `parse_orders` function that:
   - Takes a list of dicts
   - Returns a list of validated `Order` instances
   - Raises `pydantic.ValidationError` if any row is invalid

4. For the `total` field, I should use `@computed_field` from Pydantic v2 to make it read-only and computed.

Let me write the solution:

```python
from pydantic import BaseModel, Field, computed_field

class Order(BaseModel):
    id: int
    customer: str = Field(..., min_length=1)
    quantity: int = Field(..., gt=0)
    unit_price: float = Field(..., ge=0)
    
    @computed_field
    @property
    def total(self) -> float:
        return self.quantity * self.unit_price

def parse_orders(rows: list[dict]) -> list[Order]:
    return [Order(**row) for row in rows]
```

Wait, I need to make sure the `total` field is properly computed. In Pydantic v2, `@computed_field` is the right approach. Let me verify this is correct.

Actually, looking at the requirements again, the `total` should be a derived/computed value. Using `@computed_field` is the cleanest approach in Pydantic v2.

Let me also make sure the validation constraints are correct:
- `customer: str` with `min_length=1` ensures non-empty strings


- `quantity: int` with `gt=0` ensures strictly greater than 0
- `unit_price: float` with `ge=0` ensures greater than or equal to 0

I'll finalize the implementation with these validation rules in place.