Skip to content
Merged
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
26 changes: 3 additions & 23 deletions dataconnect/service/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@
_ACTION_LIST_DATASETS = "datasets.list"
_ACTION_LIST_DATASET_VERSIONS = "dataset_versions.list"

# Accepted values for the ``format_type`` filter passed to
# :meth:`DefaultDataConnectService.get_datetime_formats`.
_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:
Expand Down Expand Up @@ -325,13 +321,8 @@ def get_datetime_formats(
) -> DatetimeFormatsResult:
"""Return the supported datetime formats filtered by ``format_type``.

The ``format_type`` argument is normalised (stripped + lower-cased) and
validated client-side against the accepted set ``{"all", "date",
"datetime"}``. Invalid values raise :class:`ValidationError` *before*
any transport call is made.

The server filters the list according to ``format_type``; the service
then classifies each returned format as ``"date"`` or ``"datetime"``
The server validates and filters the list according to ``format_type``;
the service then classifies each returned format as ``"date"`` or ``"datetime"``
(based on whether the format contains a time component ``"HH:mm"``) and
wraps everything in a :class:`DatetimeFormatsResult` that exposes the
:meth:`~DatetimeFormatsResult.all`,
Expand All @@ -349,22 +340,11 @@ def get_datetime_formats(
A :class:`DatetimeFormatsResult` containing the classified formats.

Raises:
ValidationError: When ``format_type`` is not one of the accepted
values.
DataConnectError: Any :class:`TransportError` from the transport
layer is translated by :func:`translate_error` into the public
API's :class:`DataConnectError` hierarchy.
"""
normalized = (format_type or "all").strip().lower()

if normalized not in _VALID_DATETIME_FORMAT_TYPES:
raise ValidationError(
error_code="VAL_001",
message=(f"Invalid format_type: {format_type!r}. Accepted values: all, date, datetime."),
timestamp=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
)

request = DatetimeFormatsRequest(project_token=project_token, format_type=normalized)
request = DatetimeFormatsRequest(project_token=project_token, format_type=format_type)

try:
raw_formats = self._transport.get_datetime_formats(request)
Expand Down
2 changes: 1 addition & 1 deletion dataconnect/transport/arrow_flight/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ def get_datetime_formats(self, request: DatetimeFormatsRequest) -> list[str]:

Args:
request: A :class:`DatetimeFormatsRequest` carrying the project
token and the (already-validated) ``format_type`` filter.
token and the ``format_type`` filter for server validation.

Returns:
A list of supported format strings. Returns an empty list if the
Expand Down
44 changes: 12 additions & 32 deletions tests/test_get_datetime_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,8 @@ def test_default_formats_is_empty_list_and_not_shared(self) -> None:


class TestServiceGetDatetimeFormats:
"""``DefaultDataConnectService.get_datetime_formats`` must validate the
``format_type`` filter, forward the request to the transport, and classify
each returned format.
"""``DefaultDataConnectService.get_datetime_formats`` must forward the
``format_type`` filter to the transport and classify each returned format.
"""

def test_returns_datetime_formats_result_instance(self) -> None:
Expand Down Expand Up @@ -197,23 +196,13 @@ def test_format_type_datetime_forwarded_to_transport(self) -> None:
assert transport.last_request is not None
assert transport.last_request.format_type == "datetime"

def test_format_type_is_normalised_case_insensitively(self) -> None:
@pytest.mark.parametrize("format_type", ["DATE", " Datetime ", "", "invalid"])
def test_format_type_is_forwarded_unchanged(self, format_type: str) -> None:
service, transport = _make_service()
service.get_datetime_formats(project_token="tok", format_type="DATE")
result = service.get_datetime_formats(project_token="tok", format_type=format_type)
assert transport.last_request is not None
assert transport.last_request.format_type == "date"

def test_format_type_is_normalised_with_whitespace(self) -> None:
service, transport = _make_service()
service.get_datetime_formats(project_token="tok", format_type=" Datetime ")
assert transport.last_request is not None
assert transport.last_request.format_type == "datetime"

def test_empty_string_format_type_is_treated_as_all(self) -> None:
service, transport = _make_service()
service.get_datetime_formats(project_token="tok", format_type="")
assert transport.last_request is not None
assert transport.last_request.format_type == "all"
assert transport.last_request.format_type == format_type
assert [item.format for item in result.all()] == _SAMPLE_FORMATS

# --- response classification ---

Expand Down Expand Up @@ -260,27 +249,18 @@ def test_empty_server_response_yields_empty_result(self) -> None:
assert result.dates() == []
assert result.datetimes() == []

# --- invalid input ---

@pytest.mark.parametrize("bad_type", ["NA", "invalid", "datetimes", "DATETIMEZ", "1"])
def test_invalid_format_type_raises_validation_error(self, bad_type: str) -> None:
service, transport = _make_service()
with pytest.raises(ValidationError):
service.get_datetime_formats(project_token="tok", format_type=bad_type)
# Transport must not be invoked when validation fails up-front.
assert transport.last_request is None

# --- error translation ---

def test_transport_validation_error_is_translated_to_service_error(self) -> None:
err = TransportValidationError(
error_code="VAL_008",
message="invalid project token",
message="invalid format type",
timestamp="2024-01-01T00:00:00Z",
)
service, _ = _make_service(raise_error=err)
with pytest.raises(ValidationError):
service.get_datetime_formats(project_token="bad", format_type="all")
service, transport = _make_service(raise_error=err)
with pytest.raises(ValidationError, match="invalid format type"):
service.get_datetime_formats(project_token="tok", format_type="invalid")
assert transport.last_request == DatetimeFormatsRequest(project_token="tok", format_type="invalid")


# ---------------------------------------------------------------------------
Expand Down
Loading