diff --git a/dataconnect/service/default.py b/dataconnect/service/default.py index fc5ffa3..d26dcb3 100644 --- a/dataconnect/service/default.py +++ b/dataconnect/service/default.py @@ -8,7 +8,7 @@ import pandas as pd -from dataconnect.exceptions import ValidationError +from dataconnect.exceptions import ErrorDetail, ValidationError from dataconnect.models import ( Dataset, DatasetVersion, @@ -49,6 +49,27 @@ _VALID_DATETIME_FORMAT_TYPES: frozenset[str] = frozenset({"all", "date", "datetime"}) +def _coerce_int_field(field: str, field_value: int | None) -> int | None: + if field_value is None: + return None + + try: + return int(field_value) + except (ValueError, TypeError) as ex: + message = str(ex) if isinstance(ex, ValueError) else f"invalid literal for int() with base 10: {field_value}" + raise ValidationError( + error_code="INT_001", + message=message, + timestamp=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"), + details=[ + ErrorDetail( + field=field, + expected="Ensure you provide a numeric value for this field.", + ) + ], + ) from ex + + class DefaultDataConnectService(DataConnectService): """Concrete service injected with an abstract ``Transport``.""" @@ -112,7 +133,7 @@ def fetch_data(self, dataset_uuid: UUID, first_n_rows: int | None = None) -> pd. ticket = DatasetTicket( dataset_uuid=str(dataset_uuid), - limit=first_n_rows, + limit=_coerce_int_field("first_n_rows", first_n_rows), ) try: diff --git a/tests/test_fetch_data.py b/tests/test_fetch_data.py index 776c7d0..a431386 100644 --- a/tests/test_fetch_data.py +++ b/tests/test_fetch_data.py @@ -13,6 +13,7 @@ AuthorizationError, NotFoundError, ServerError, + ValidationError, ) from dataconnect.service.default import DefaultDataConnectService from dataconnect.transport.errors import ( @@ -107,6 +108,38 @@ def test_fetch_data_no_limit_sends_none_in_ticket() -> None: assert transport.last_ticket.limit is None +def test_fetch_data_raises_formatted_validation_error_for_non_numeric_first_n_rows() -> None: + dataset_uuid = UUID("073410b6-79be-3e7d-ae37-92f6e054013e") + transport = _FakeTransport(data_table=_make_ipc_table({"x": [1]})) + service = DefaultDataConnectService(transport) + + with pytest.raises(ValidationError) as exc_info: + service.fetch_data(dataset_uuid, first_n_rows="ten") # type: ignore[arg-type] + + error = str(exc_info.value) + assert "Error Code: [INT_001]" in error + assert "Message: invalid literal for int() with base 10: 'ten'" in error + assert "Field: first_n_rows" in error + assert "Expected: Ensure you provide a numeric value for this field." in error + assert transport.last_ticket is None + + +def test_fetch_data_raises_formatted_validation_error_for_dict_first_n_rows() -> None: + dataset_uuid = UUID("073410b6-79be-3e7d-ae37-92f6e054013e") + transport = _FakeTransport(data_table=_make_ipc_table({"x": [1]})) + service = DefaultDataConnectService(transport) + + with pytest.raises(ValidationError) as exc_info: + service.fetch_data(dataset_uuid, first_n_rows={}) # type: ignore[arg-type] + + error = str(exc_info.value) + assert "Error Code: [INT_001]" in error + assert "Message: invalid literal for int() with base 10: {}" in error + assert "Field: first_n_rows" in error + assert "Expected: Ensure you provide a numeric value for this field." in error + assert transport.last_ticket is None + + def test_fetch_data_returns_empty_dataframe_for_empty_table() -> None: dataset_uuid = UUID("073410b6-79be-3e7d-ae37-92f6e054013e") transport = _FakeTransport(data_table=_make_ipc_table({"col": pa.array([], type=pa.int32())}))