Skip to content

Commit

Permalink
Merge pull request #1070 from xcube-dev/b-yogesh-1066-stats-optional-…
Browse files Browse the repository at this point in the history
…time

Make `time` parameter optional in xcube server's `/statistic` endpoint
  • Loading branch information
forman authored Sep 17, 2024
2 parents 8bfd7cf + 9b3f312 commit 5c6ddb2
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,6 @@ ENV/
mydask.png

.cfgs

# Alternative xcube demo
examples/serve/demo2/
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Changes in 1.7.1 (in development)

### Fixes

* The `time` query parameter of the `/statistics` endpoint of xcube server has
now been made optional. (#1066)

## Changes in 1.7.0

Expand Down
128 changes: 128 additions & 0 deletions test/webapi/res/config-stats.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
DatasetAttribution:
- © by Brockmann Consult GmbH 2020, contains modified Copernicus Data 2019, processed by ESA

Datasets:
- Identifier: demo
Title: xcube-server Demonstration L2C Cube
GroupTitle: Demo
Tags: ["demo", "zarr"]
Path: ../../../examples/serve/demo/cube-1-250-250.zarr
Variables:
- "conc_chl"
- "chl_category"
- "conc_tsm"
- "chl_tsm_sum"
- "kd489"
- "*"
Style: default
Attribution: © by EU H2020 CyanoAlert project

- Identifier: demo-1w
Title: xcube-server Demonstration L3 Cube
GroupTitle: Demo
Tags: ["demo", "zarr", "computed"]
FileSystem: memory
Path: script.py
Variables:
- "conc_chl"
- "chl_category"
- "conc_tsm"
- "chl_tsm_sum"
- "kd489"
- "*"
Function: compute_dataset
InputDatasets: ["demo"]
InputParameters:
period: "1W"
incl_stdev: True
Style: default

- Identifier: cog_local
Title: COG example
GroupTitle: GeoTIFF
FileSystem: file
Path: ../../../examples/serve/demo/sample-cog.tif
Style: tif_style

PlaceGroups:
- Identifier: inside-cube
Title: Points inside the cube
Path: places/inside-cube.geojson
- Identifier: outside-cube
Title: Points outside the cube
Path: places/outside-cube.geojson

Styles:
- Identifier: default
ColorMappings:
conc_chl:
ColorBar: my_cmap
ValueRange: [0., 20.]
conc_tsm:
ColorBar: cmap_bloom_risk
ValueRange: [0., 1.]
kd489:
ColorBar: jet
ValueRange: [0., 6.]

CustomColorMaps:
- Identifier: my_cmap
Type: continuous # or categorical, stepwise
Colors:
- Value: 0
Color: red
Label: low
- Value: 12
Color: "#0000FF"
Label: medium
- Value: 18
Color: [0, 255, 0]
Label: mediumhigh
- Value: 24
Color: [0, 1, 0, 0.3]
Label: high
- Identifier: cmap_bloom_risk
Type: categorical
Colors:
- [ 0, [0, 1, 0., 0.5]]
- [ 1, orange]
- [ 2, [1, 0, 0]]
- Identifier: s2_l2_scl
Type: categorical
Colors:
- [ 0, red, no data]
- [ 1, yellow, defective]
- [ 2, black, dark area pixels]
- [ 3, gray, cloud shadows]
- [ 4, green, vegetation]
- [ 5, tan, bare soils]
- [ 6, blue, water]
- [ 7, "#aaaabb", clouds low prob ]
- [ 8, "#bbbbcc", clouds medium prob]
- [ 9, "#ccccdd", clouds high prob]
- [10, "#ddddee", cirrus]
- [11, "#ffffff", snow or ice]
- [11, "#ffffff", snow or ice]

Viewer:
Configuration:
Path: viewer

ServiceProvider:
ProviderName: "Brockmann Consult GmbH"
ProviderSite: "https://www.brockmann-consult.de"
ServiceContact:
IndividualName: "Norman Fomferra"
PositionName: "Senior Software Engineer"
ContactInfo:
Phone:
Voice: "+49 4152 889 303"
Facsimile: "+49 4152 889 330"
Address:
DeliveryPoint: "HZG / GITZ"
City: "Geesthacht"
AdministrativeArea: "Herzogtum Lauenburg"
PostalCode: "21502"
Country: "Germany"
ElectronicMailAddress: "[email protected]"

78 changes: 73 additions & 5 deletions test/webapi/statistics/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,96 @@


class StatisticsRoutesTest(RoutesTestCase):
def test_fetch_statistics_ok(self):

def get_config_filename(self) -> str:
"""Get configuration filename.
Default impl. returns ``'config.yml'``."""
return "config-stats.yml"

def test_fetch_post_statistics_ok(self):
response = self.fetch(
"/statistics/demo/conc_chl?time=2017-01-16+10:09:21",
method="POST",
body='{"type": "Point", "coordinates": [1.768, 51.465]}',
)
self.assertResponseOK(response)

def test_fetch_statistics_missing_time(self):
def test_fetch_post_statistics_missing_time_with_time_dimension_dataset(self):
response = self.fetch(
"/statistics/demo/conc_chl",
method="POST",
body='{"type": "Point", "coordinates": [1.768, 51.465]}',
)
self.assertBadRequestResponse(response, "Missing " "query parameter 'time'")

def test_fetch_post_statistics_missing_time_without_time_dimension_dataset(self):
response = self.fetch(
"/statistics/cog_local/band-1",
method="POST",
body='{"type": "Point", "coordinates": [-105.591, 35.751]}',
)
self.assertResponseOK(response)

def test_fetch_post_statistics_with_time_without_time_dimension_dataset(self):
response = self.fetch(
"/statistics/cog_local/band-1?time=2017-01-16+10:09:21",
method="POST",
body='{"type": "Point", "coordinates": [-105.591, 35.751]}',
)
self.assertBadRequestResponse(
response, "Missing required query parameter 'time'"
response,
"Query parameter 'time' must not be given since "
"dataset does not contain a 'time' dimension",
)

def test_fetch_statistics_invalid_geometry(self):
def test_fetch_post_statistics_invalid_geometry(self):
response = self.fetch(
"/statistics/demo/conc_chl?time=2017-01-16+10:09:21",
method="POST",
body="[1.768, 51.465]",
)
self.assertBadRequestResponse(response, "Invalid GeoJSON geometry encountered")
self.assertBadRequestResponse(
response, "Invalid " "GeoJSON geometry encountered"
)

def test_fetch_get_statistics_ok(self):
response = self.fetch(
"/statistics/demo/conc_chl?"
"lat=1.786&lon=51.465&time=2017-01-16+10:09:21",
method="GET",
)
self.assertResponseOK(response)

def test_fetch_get_statistics_missing_time_with_time_dimension_dataset(self):
response = self.fetch(
"/statistics/demo/conc_chl?lat=1.786&lon=51.465", method="GET"
)
self.assertBadRequestResponse(response, "Missing " "query parameter 'time'")

def test_fetch_get_statistics_missing_time_without_time_dimension_dataset(self):
response = self.fetch(
"/statistics/cog_local/band-1?lat=-105.591&" "lon=35.751&type=Point",
method="GET",
)
self.assertResponseOK(response)

def test_fetch_get_statistics_with_time_without_time_dimension_dataset(self):
response = self.fetch(
"/statistics/cog_local/band-1?lat=-105.591&lon=35.751&"
"type=Point&time=2017-01-16+10:09:21",
method="GET",
body='{"type": "Point", "coordinates": [-105.591, 35.751]}',
)
self.assertBadRequestResponse(
response,
"Query parameter 'time' must not be given since "
"dataset does not contain a 'time' dimension",
)

def test_fetch_get_statistics_invalid_geometry(self):
response = self.fetch(
"/statistics/demo/conc_chl?time=2017-01-16+10:09:21&"
"lon=1.768&lat=51.465",
method="GET",
)
self.assertResponseOK(response)
24 changes: 17 additions & 7 deletions xcube/webapi/statistics/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,22 @@ def _compute_statistics(
dataset = ml_dataset.get_dataset(0)
grid_mapping = ml_dataset.grid_mapping

try:
time = np.array(time_label, dtype=dataset.time.dtype)
except (TypeError, ValueError) as e:
raise ApiError.BadRequest("Invalid 'time'") from e
dataset_contains_time = "time" in dataset

if dataset_contains_time:
if time_label is not None:
try:
time = np.array(time_label, dtype=dataset.time.dtype)
dataset = dataset.sel(time=time, method="nearest")
except (TypeError, ValueError) as e:
raise ApiError.BadRequest("Invalid query parameter " "'time'") from e
else:
raise ApiError.BadRequest("Missing query parameter 'time'")
elif time_label is not None:
raise ApiError.BadRequest(
"Query parameter 'time' must not be given"
" since dataset does not contain a 'time' dimension"
)

if isinstance(geometry, tuple):
compact_mode = True
Expand All @@ -60,12 +72,10 @@ def _compute_statistics(
try:
geometry = shapely.geometry.shape(geometry)
except (TypeError, ValueError, AttributeError) as e:
raise ApiError.BadRequest("Invalid GeoJSON geometry encountered") from e
raise ApiError.BadRequest("Invalid GeoJSON geometry " "encountered") from e

nan_result = NAN_RESULT_COMPACT if compact_mode else NAN_RESULT

dataset = dataset.sel(time=time, method="nearest")

x_name, y_name = grid_mapping.xy_dim_names
if isinstance(geometry, shapely.geometry.Point):
bounds = get_dataset_geometry(dataset)
Expand Down
6 changes: 3 additions & 3 deletions xcube/webapi/statistics/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"name": "time",
"in": "query",
"description": 'Timestamp using format "YYYY-MM-DD hh:mm:ss"',
"required": True,
"required": False,
"schema": {"type": "string", "format": "datetime"},
}

Expand All @@ -54,7 +54,7 @@ class StatisticsHandler(ApiHandler[StatisticsContext]):
async def get(self, datasetId: str, varName: str):
lon = self.request.get_query_arg("lon", type=float, default=UNDEFINED)
lat = self.request.get_query_arg("lat", type=float, default=UNDEFINED)
time = self.request.get_query_arg("time", type=str, default=UNDEFINED)
time = self.request.get_query_arg("time", type=str, default=None)
trace_perf = self.request.get_query_arg(
"debug", default=self.ctx.datasets_ctx.trace_perf
)
Expand Down Expand Up @@ -89,7 +89,7 @@ async def get(self, datasetId: str, varName: str):
],
)
async def post(self, datasetId: str, varName: str):
time = self.request.get_query_arg("time", type=str, default=UNDEFINED)
time = self.request.get_query_arg("time", type=str, default=None)
trace_perf = self.request.get_query_arg(
"debug", default=self.ctx.datasets_ctx.trace_perf
)
Expand Down

0 comments on commit 5c6ddb2

Please sign in to comment.