-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add the ability to do nested validation and saving for our REST APIs
- Loading branch information
Showing
4 changed files
with
251 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import pytest | ||
from kiln_ai.datamodel.basemodel import KilnParentedModel, KilnParentModel | ||
from pydantic import Field, ValidationError | ||
|
||
|
||
class ModelC(KilnParentedModel): | ||
code: str = Field(..., pattern=r"^[A-Z]{3}$") | ||
|
||
@classmethod | ||
def relationship_name(cls) -> str: | ||
return "cs" | ||
|
||
@classmethod | ||
def parent_type(cls): | ||
return ModelB | ||
|
||
|
||
class ModelB(KilnParentedModel, KilnParentModel, parent_of={"cs": ModelC}): | ||
value: int = Field(..., ge=0) | ||
|
||
@classmethod | ||
def relationship_name(cls) -> str: | ||
return "bs" | ||
|
||
@classmethod | ||
def parent_type(cls): | ||
return ModelA | ||
|
||
|
||
# Define the hierarchy | ||
class ModelA(KilnParentModel, parent_of={"bs": ModelB}): | ||
name: str = Field(..., min_length=3) | ||
|
||
|
||
def test_validation_error_in_c_level(): | ||
data = { | ||
"name": "Root", | ||
"bs": [ | ||
{ | ||
"value": 10, | ||
"cs": [ | ||
{"code": "ABC"}, | ||
{"code": "DEF"}, | ||
{"code": "invalid"}, # This should cause a validation error | ||
], | ||
} | ||
], | ||
} | ||
|
||
with pytest.raises(ValidationError) as exc_info: | ||
ModelA.validate_and_save_with_subrelations(data) | ||
|
||
assert "String should match pattern" in str(exc_info.value) | ||
|
||
|
||
def test_persist_three_level_hierarchy(tmp_path): | ||
# Set up temporary paths | ||
root_path = tmp_path / "model_a.kiln" | ||
|
||
data = { | ||
"name": "Root", | ||
"bs": [ | ||
{"value": 10, "cs": [{"code": "ABC"}, {"code": "DEF"}]}, | ||
{"value": 20, "cs": [{"code": "XYZ"}]}, | ||
], | ||
} | ||
|
||
instance = ModelA.validate_and_save_with_subrelations(data, path=root_path) | ||
|
||
assert isinstance(instance, ModelA) | ||
assert instance.name == "Root" | ||
assert instance.path == root_path | ||
assert len(instance.bs()) == 2 | ||
|
||
# Load the instance back from the file to double-check | ||
instance = ModelA.load_from_file(root_path) | ||
|
||
bs = instance.bs() | ||
assert len(bs) == 2 | ||
|
||
# Check for the existence of both expected B models | ||
b_values = [b.value for b in bs] | ||
assert 10 in b_values | ||
assert 20 in b_values | ||
|
||
# Find the B models by their values | ||
b10 = next(b for b in bs if b.value == 10) | ||
b20 = next(b for b in bs if b.value == 20) | ||
|
||
assert len(b10.cs()) == 2 | ||
assert len(b20.cs()) == 1 | ||
|
||
# Check C models for b10 | ||
c_codes_b10 = [c.code for c in b10.cs()] | ||
assert "ABC" in c_codes_b10 | ||
assert "DEF" in c_codes_b10 | ||
|
||
# Check C model for b20 | ||
c_codes_b20 = [c.code for c in b20.cs()] | ||
assert "XYZ" in c_codes_b20 | ||
|
||
# Check that all objects have their parent set correctly | ||
assert all(b.parent == instance for b in bs) | ||
assert all(c.parent.id == b10.id for c in b10.cs()) | ||
assert all(c.parent.id == b20.id for c in b20.cs()) | ||
|
||
|
||
def test_persist_model_a_without_children(tmp_path): | ||
# Set up temporary path | ||
root_path = tmp_path / "model_a_no_children.kiln" | ||
|
||
data = {"name": "RootNoChildren"} | ||
|
||
instance = ModelA.validate_and_save_with_subrelations(data, path=root_path) | ||
|
||
assert isinstance(instance, ModelA) | ||
assert instance.name == "RootNoChildren" | ||
assert instance.path == root_path | ||
assert len(instance.bs()) == 0 | ||
|
||
# Verify that the file was created | ||
assert root_path.exists() | ||
|
||
# Load the instance back from the file to double-check | ||
loaded_instance = ModelA.load_from_file(root_path) | ||
assert loaded_instance.name == "RootNoChildren" | ||
assert len(loaded_instance.bs()) == 0 | ||
|
||
|
||
def test_validate_without_saving(tmp_path): | ||
data = { | ||
"name": "ValidateOnly", | ||
"bs": [ | ||
{"value": 30, "cs": [{"code": "GHI"}, {"code": "JKL"}]}, | ||
{"value": 40, "cs": [{"code": "MNO"}]}, | ||
], | ||
} | ||
|
||
# Validate the data without saving | ||
ModelA._validate_nested(data, save=False) | ||
|
||
data = { | ||
"name": "ValidateOnly", | ||
"bs": [ | ||
{"value": 30, "cs": [{"code": "GHI"}, {"code": "JKL"}]}, | ||
{"value": 40, "cs": [{"code": 123}]}, | ||
], | ||
} | ||
|
||
with pytest.raises(ValidationError): | ||
ModelA._validate_nested(data, save=False) |