Skip to content

Commit

Permalink
patch temporal cmr query to allow RFC3339 datetime
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentsarago committed Feb 8, 2024
1 parent f58a960 commit 1255dfb
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 28 deletions.
149 changes: 149 additions & 0 deletions tests/test_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""test titiler-pgstac dependencies."""

import pytest
from starlette.requests import Request

from titiler.cmr import dependencies
from titiler.cmr.enums import MediaType
from titiler.cmr.errors import InvalidDatetime


def test_media_type():
"""test accept_media_type dependency."""
assert (
dependencies.accept_media_type(
"application/json;q=0.9, text/html;q=1.0",
[MediaType.json, MediaType.html],
)
== MediaType.html
)

assert (
dependencies.accept_media_type(
"application/json;q=0.9, text/html;q=0.8",
[MediaType.json, MediaType.html],
)
== MediaType.json
)

# if no quality then default to 1.0
assert (
dependencies.accept_media_type(
"application/json;q=0.9, text/html",
[MediaType.json, MediaType.html],
)
== MediaType.html
)

# Invalid Quality
assert (
dependencies.accept_media_type(
"application/json;q=w, , text/html;q=0.1",
[MediaType.json, MediaType.html],
)
== MediaType.html
)

assert (
dependencies.accept_media_type(
"*",
[MediaType.json, MediaType.html],
)
== MediaType.json
)


def test_output_type():
"""test OutputType dependency."""
req = Request(
{
"type": "http",
"client": None,
"query_string": "",
"headers": ((b"accept", b"application/json"),),
},
None,
)
assert (
dependencies.OutputType(
req,
)
== MediaType.json
)

req = Request(
{
"type": "http",
"client": None,
"query_string": "",
"headers": ((b"accept", b"text/html"),),
},
None,
)
assert (
dependencies.OutputType(
req,
)
== MediaType.html
)

req = Request(
{"type": "http", "client": None, "query_string": "", "headers": ()}, None
)
assert not dependencies.OutputType(req)

# FastAPI will parse the request first and inject `f=json` in the dependency
req = Request(
{
"type": "http",
"client": None,
"query_string": "f=json",
"headers": ((b"accept", b"text/html"),),
},
None,
)
assert dependencies.OutputType(req, f="json") == MediaType.json


@pytest.mark.parametrize(
"temporal,res",
[
("2018-02-12T09:00:00Z", ("2018-02-12", "2018-02-12")),
("2018-02-12T09:00:00Z/", ("2018-02-12", None)),
("2018-02-12T09:00:00Z/..", ("2018-02-12", None)),
("/2018-02-12T09:00:00Z", (None, "2018-02-12")),
("../2018-02-12T09:00:00Z", (None, "2018-02-12")),
("2018-02-12T09:00:00Z/2019-02-12T09:00:00Z", ("2018-02-12", "2019-02-12")),
],
)
def test_cmr_query(temporal, res):
"""test cmr query dependency."""
assert (
dependencies.cmr_query(concept_id="something", temporal=temporal)["temporal"]
== res
)


def test_cmr_query_more():
"""test cmr query dependency."""
assert dependencies.cmr_query(
concept_id="something",
) == {"concept_id": "something"}

with pytest.raises(InvalidDatetime):
dependencies.cmr_query(
concept_id="something",
temporal="yo/yo/yo",
)

with pytest.raises(InvalidDatetime):
dependencies.cmr_query(
concept_id="something",
temporal="2019-02-12",
)

with pytest.raises(InvalidDatetime):
dependencies.cmr_query(
concept_id="something",
temporal="2019-02-12T09:00:00Z/2019-02-12",
)
55 changes: 27 additions & 28 deletions titiler/cmr/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""titiler-cmr dependencies."""

from datetime import datetime
from typing import Any, Dict, List, Literal, Optional, get_args

from fastapi import HTTPException, Query
from ciso8601 import parse_rfc3339
from fastapi import Query
from starlette.requests import Request
from typing_extensions import Annotated

Expand Down Expand Up @@ -87,12 +87,14 @@ def cmr_query(
temporal: Annotated[
Optional[str],
Query(
description="Either a date-time or an interval. Date and time expressions adhere to 'YYYY-MM-DD' format. Intervals may be bounded or half-bounded (double-dots at start or end).",
description="Either a date-time or an interval. Date and time expressions adhere to rfc3339 ('2020-06-01T09:00:00Z') format. Intervals may be bounded or half-bounded (double-dots at start or end).",
openapi_examples={
"A date-time": {"value": "2018-02-12"},
"A bounded interval": {"value": "2018-02-12/2018-03-18"},
"Half-bounded intervals (start)": {"value": "2018-02-12/.."},
"Half-bounded intervals (end)": {"value": "../2018-03-18"},
"A date-time": {"value": "2018-02-12T09:00:00Z"},
"A bounded interval": {
"value": "2018-02-12T09:00:00Z/2018-03-18T09:00:00Z"
},
"Half-bounded intervals (start)": {"value": "2018-02-12T09:00:00Z/.."},
"Half-bounded intervals (end)": {"value": "../2018-03-18T09:00:00Z"},
},
),
] = None,
Expand All @@ -103,36 +105,33 @@ def cmr_query(
if temporal:
dt = temporal.split("/")
if len(dt) > 2:
raise HTTPException(status_code=422, detail="Invalid temporal: {temporal}")

start: Optional[str]
end: Optional[str]
raise InvalidDatetime("Invalid temporal: {temporal}")

dates: List[Optional[str]] = [None, None]
if len(dt) == 1:
start = end = dt[0]
dates = [dt[0], dt[0]]

else:
start = dt[0] if dt[0] not in ["..", ""] else None
end = dt[1] if dt[1] not in ["..", ""] else None
dates[0] = dt[0] if dt[0] not in ["..", ""] else None
dates[1] = dt[1] if dt[1] not in ["..", ""] else None

# TODO: once https://github.com/nsidc/earthaccess/pull/451 is publish
# we can move to Datetime object instead of String
start: Optional[str] = None
end: Optional[str] = None

if start:
if dates[0]:
try:
datetime.strptime(
start, "%Y-%m-%d"
), f"Start datetime {start} not in form of 'YYYY-MM-DD'"
except ValueError as e:
raise InvalidDatetime(
f"Start datetime {start} not in form of 'YYYY-MM-DD'"
) from e
start = parse_rfc3339(dates[0]).strftime("%Y-%m-%d")
except Exception as e:
raise InvalidDatetime(f"Start datetime {dates[0]} not valid.") from e

if end:
if dates[1]:
try:
datetime.strptime(
end, "%Y-%m-%d"
), f"Start datetime {start} not in form of 'YYYY-MM-DD'"
except ValueError as e:
end = parse_rfc3339(dates[1]).strftime("%Y-%m-%d")
except Exception as e:
raise InvalidDatetime(
f"End datetime {end} not in form of 'YYYY-MM-DD'"
f"End datetime {dates[1]} not in form of 'YYYY-MM-DD'"
) from e

query["temporal"] = (start, end)
Expand Down

0 comments on commit 1255dfb

Please sign in to comment.