128 lines
4 KiB
Python
128 lines
4 KiB
Python
from datetime import datetime
|
|
from typing import Any, Dict, List
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from great_ai import GreatAI, Trace
|
|
from great_ai.helper.freeze_arguments import freeze
|
|
from great_ai.persistence.mongodb_driver import MongoDbDriver
|
|
from pydantic import BaseModel, Field, field_validator
|
|
from pymongo import MongoClient
|
|
from pymongo.synchronous.collection import Collection
|
|
|
|
|
|
class Item(BaseModel):
|
|
value: int = Field(alias="number")
|
|
|
|
@field_validator("value")
|
|
@classmethod
|
|
def increment(cls, value: int) -> int:
|
|
return value + 1
|
|
|
|
|
|
class Payload(BaseModel):
|
|
item: Item
|
|
others: List[Item]
|
|
|
|
|
|
def test_freezing_preserves_validated_models() -> None:
|
|
original = Payload(item=Item(number=1), others=[Item(number=2)])
|
|
frozen = freeze(original)
|
|
assert isinstance(frozen, Payload)
|
|
assert isinstance(frozen.item, Item)
|
|
assert frozen.item.value == 2
|
|
assert frozen.others[0].value == 3
|
|
assert frozen.model_dump() == original.model_dump()
|
|
assert hash(frozen) == hash(freeze(original))
|
|
assert type(original) is Payload
|
|
|
|
|
|
@pytest.mark.parametrize("is_async", [False, True])
|
|
def test_http_preserves_nested_models(isolated_context: None, is_async: bool) -> None:
|
|
def predict(data: Payload) -> int:
|
|
assert isinstance(data.item, Item)
|
|
assert all(isinstance(item, Item) for item in data.others)
|
|
return data.item.value + sum(item.value for item in data.others)
|
|
|
|
async def predict_async(data: Payload) -> int:
|
|
return predict(data)
|
|
|
|
service = GreatAI.create(predict_async if is_async else predict)
|
|
with TestClient(service.app) as client:
|
|
response = client.post(
|
|
"/predict",
|
|
json={"data": {"item": {"number": 1}, "others": [{"number": 2}]}},
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["output"] == 5
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_after_async_prediction(isolated_context: None) -> None:
|
|
@GreatAI.create
|
|
async def predict(value: int) -> int:
|
|
return value + 1
|
|
|
|
assert (await predict(1)).output == 2
|
|
assert [trace.output for trace in predict.process_batch([1, 2], concurrency=1)] == [
|
|
2,
|
|
3,
|
|
]
|
|
assert (await predict(1)).output == 2
|
|
|
|
|
|
def test_flattening_preserves_reserved_trace_fields() -> None:
|
|
trace = Trace(
|
|
trace_id="actual-id",
|
|
created=datetime(2026, 1, 1).isoformat(),
|
|
original_execution_time_ms=0,
|
|
models=[],
|
|
tags=["online"],
|
|
output="prediction",
|
|
logged_values={
|
|
"trace_id": "input-id",
|
|
"created": "input-created",
|
|
"output": 123,
|
|
"tags": ["input"],
|
|
"models": "input",
|
|
"_id": "input-id",
|
|
"feature": 5,
|
|
},
|
|
)
|
|
flat = trace.to_flat_dict()
|
|
for name, value in trace.model_dump().items():
|
|
assert flat[name] == value
|
|
assert "_id" not in flat
|
|
assert flat["feature"] == 5
|
|
assert Trace.model_validate(flat) == trace
|
|
|
|
|
|
def test_mongo_update_uses_valid_replacement() -> None:
|
|
trace: Trace = Trace(
|
|
trace_id="id",
|
|
created="2026-01-01",
|
|
original_execution_time_ms=0,
|
|
logged_values={},
|
|
models=[],
|
|
tags=[],
|
|
feedback="correct",
|
|
)
|
|
driver = MongoDbDriver.__new__(MongoDbDriver)
|
|
driver.mongo_connection_string = "mongodb://127.0.0.1:1"
|
|
driver.mongo_database = "test"
|
|
client: MongoClient[Dict[str, Any]] = MongoClient(
|
|
driver.mongo_connection_string, connect=False
|
|
)
|
|
with (
|
|
patch("great_ai.persistence.mongodb_driver.MongoClient") as factory,
|
|
patch.object(
|
|
Collection, "_update_retryable", return_value={"n": 1, "nModified": 1}
|
|
) as update,
|
|
):
|
|
factory.__getitem__.return_value.return_value = client
|
|
driver.update("id", trace)
|
|
assert update.call_args.args[0] == {"_id": "id"}
|
|
document = update.call_args.args[1]
|
|
assert document["_id"] == "id"
|
|
assert Trace.model_validate(document) == trace
|