Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/mxcp/sdk/validator/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ class ValidationError(ValueError):
class TypeConverter:
"""Utility class for type conversion and validation."""

@staticmethod
def _normalize_pandas_dataframe_nulls(value: pd.DataFrame) -> pd.DataFrame:
"""Convert pandas null sentinels into Python None for output handling."""
return value.astype(object).where(value.notna(), None)

@staticmethod
def _normalize_pandas_series_nulls(value: pd.Series) -> pd.Series:
"""Convert pandas null sentinels into Python None for output handling."""
return value.astype(object).where(value.notna(), None)

@staticmethod
def python_type_to_schema_type(python_type: str) -> str:
"""Map Python type names to schema types."""
Expand Down Expand Up @@ -238,13 +248,13 @@ def validate_output(value: Any, schema: TypeSchemaModel) -> None:
f"DataFrame rows must be objects, but schema expects array of {schema.items.type}"
)
# Convert DataFrame to list of dicts for validation
value = value.replace({pd.NaT: None}).to_dict("records")
value = TypeConverter._normalize_pandas_dataframe_nulls(value).to_dict("records")

# Handle Series - they validate as arrays
if isinstance(value, pd.Series):
if return_type != "array":
raise ValidationError(f"Expected {return_type}, got Series (which is array)")
value = value.replace({pd.NaT: None}).tolist()
value = TypeConverter._normalize_pandas_series_nulls(value).tolist()

if return_type == "string":
if (
Expand Down Expand Up @@ -355,10 +365,10 @@ def serialize_for_output(obj: Any) -> Any:
return [TypeConverter.serialize_for_output(item) for item in items]
elif isinstance(obj, pd.DataFrame):
# Convert DataFrame to list of dicts
return obj.replace({pd.NaT: None}).to_dict("records")
return TypeConverter._normalize_pandas_dataframe_nulls(obj).to_dict("records")
elif isinstance(obj, pd.Series):
# Convert Series to list
return obj.replace({pd.NaT: None}).tolist()
return TypeConverter._normalize_pandas_series_nulls(obj).tolist()
elif isinstance(obj, pd.Timestamp | datetime | date | time):
return obj.isoformat()
elif isinstance(obj, type(pd.NaT)):
Expand Down
39 changes: 39 additions & 0 deletions tests/sdk/validator/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,35 @@ def test_dataframe_output(self):
result = validator.validate_output(df)
assert result == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]

def test_dataframe_output_with_nat(self):
"""Test DataFrame output validation converts NaT values to None."""
schema = {
"output": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"created_at": {"type": "string", "format": "date-time"},
},
},
}
}

validator = TypeValidator.from_dict(schema)
df = pd.DataFrame(
[
{"id": 1, "created_at": pd.Timestamp("2024-01-01T00:00:00")},
{"id": 2, "created_at": pd.NaT},
]
)

result = validator.validate_output(df)
assert result == [
{"id": 1, "created_at": pd.Timestamp("2024-01-01T00:00:00")},
{"id": 2, "created_at": None},
]

def test_series_output(self):
"""Test Series output validation."""
schema = {"output": {"type": "array", "items": {"type": "number"}}}
Expand All @@ -266,6 +295,16 @@ def test_series_output(self):
result = validator.validate_output(series)
assert result == [1.0, 2.0, 3.0]

def test_series_output_with_nat(self):
"""Test Series output validation converts NaT values to None."""
schema = {"output": {"type": "array", "items": {"type": "string", "format": "date-time"}}}

validator = TypeValidator.from_dict(schema)
series = pd.Series([pd.Timestamp("2024-01-01T00:00:00"), pd.NaT])

result = validator.validate_output(series)
assert result == [pd.Timestamp("2024-01-01T00:00:00"), None]

def test_sensitive_field_masking(self):
"""Test sensitive field masking."""
schema = {
Expand Down
6 changes: 3 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading