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
55 changes: 54 additions & 1 deletion api/app/v1/endpoints/read/query_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,60 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from datetime import datetime, timezone

from app import VERSIONING
from fastapi import Depends, Query
from dateutil.parser import isoparse
from fastapi import Depends, HTTPException, Query, status


def _validation_error(message):
return HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail={
"code": 422,
"type": "error",
"message": message,
},
)


def _parse_iso_datetime(value, parameter_name):
try:
parsed = isoparse(value)
except (TypeError, ValueError) as exc:
raise _validation_error(
f"Invalid {parameter_name}: expected an ISO 8601 datetime"
) from exc

if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=datetime.now().astimezone().tzinfo)

return parsed.astimezone(timezone.utc)


def _validate_time_travel_params(as_of, from_to):
if as_of and from_to:
raise _validation_error("$as_of and $from_to cannot be used together")

if as_of:
as_of_value = _parse_iso_datetime(as_of, "$as_of")
if as_of_value > datetime.now(timezone.utc):
raise _validation_error("$as_of value cannot be in the future")

if from_to:
values = from_to.split("/", 1)
if len(values) != 2 or not values[0] or not values[1]:
raise _validation_error(
"Invalid $from_to: expected format is $from_to=<start>/<end>"
)

start = _parse_iso_datetime(values[0], "$from_to start")
end = _parse_iso_datetime(values[1], "$from_to end")
if start > end:
raise _validation_error(
"$from_to start cannot be greater than $from_to end"
)


class CommonQueryParams:
Expand Down Expand Up @@ -73,6 +125,7 @@ def __init__(
self.filter = filter
self.as_of = as_of
self.from_to = from_to
_validate_time_travel_params(as_of, from_to)


def get_common_query_params(
Expand Down
79 changes: 79 additions & 0 deletions test/unit/test_query_parameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from datetime import datetime, timedelta, timezone

import pytest
from fastapi import HTTPException

from app.v1.endpoints.read.query_parameters import CommonQueryParams


def _future_datetime():
return (datetime.now(timezone.utc) + timedelta(days=1)).isoformat().replace(
"+00:00", "Z"
)


def test_common_query_params_accepts_valid_as_of():
params = CommonQueryParams(as_of="2024-01-01T00:00:00Z")

assert params.as_of == "2024-01-01T00:00:00Z"


def test_common_query_params_rejects_invalid_as_of():
with pytest.raises(HTTPException) as exc_info:
CommonQueryParams(as_of="not-a-date")

assert exc_info.value.status_code == 422
assert "$as_of" in exc_info.value.detail["message"]


def test_common_query_params_rejects_future_as_of():
with pytest.raises(HTTPException) as exc_info:
CommonQueryParams(as_of=_future_datetime())

assert exc_info.value.status_code == 422
assert "future" in exc_info.value.detail["message"]


def test_common_query_params_accepts_valid_from_to():
params = CommonQueryParams(
from_to="2024-01-01T00:00:00Z/2024-01-02T00:00:00Z"
)

assert params.from_to == "2024-01-01T00:00:00Z/2024-01-02T00:00:00Z"


@pytest.mark.parametrize(
"from_to",
[
"2024-01-01T00:00:00Z",
"/2024-01-02T00:00:00Z",
"2024-01-01T00:00:00Z/",
"not-a-date/2024-01-02T00:00:00Z",
],
)
def test_common_query_params_rejects_invalid_from_to(from_to):
with pytest.raises(HTTPException) as exc_info:
CommonQueryParams(from_to=from_to)

assert exc_info.value.status_code == 422


def test_common_query_params_rejects_reversed_from_to():
with pytest.raises(HTTPException) as exc_info:
CommonQueryParams(
from_to="2024-01-02T00:00:00Z/2024-01-01T00:00:00Z"
)

assert exc_info.value.status_code == 422
assert "greater" in exc_info.value.detail["message"]


def test_common_query_params_rejects_as_of_with_from_to():
with pytest.raises(HTTPException) as exc_info:
CommonQueryParams(
as_of="2024-01-01T00:00:00Z",
from_to="2024-01-01T00:00:00Z/2024-01-02T00:00:00Z",
)

assert exc_info.value.status_code == 422
assert "cannot be used together" in exc_info.value.detail["message"]