Skip to content

Commit 67542f2

Browse files
authored
DAS-2326 Return NoDataException warning when the request returns no data (#58)
* DAS-2326 Return NoDataFound warning instead of a failure when the request retuens no data * DAS-2326 Improve the code with PR comments * DAS-2326 update check_range_exception to get_failed_variables * DAS-2325 Update to throw exception if any requested variable is not valid * DAS-2326 Add comment to the check valid variables * DAS-2326 Update change log and some variable naming * DAS-2326 Add missing unit tests * DAS-2326 Change set update to add * DAS-2326 Correct check for exception message in tests * DAS-2326 Update geographic-grid function to capture all failed dimensions * DAS-2326 Update comments * DAS-2326 Update earthdata-varinfo version * DAS-2326 Fix unit test failure * DAS-2326 Add unit tests to test_dimension_utilities * DAS-2326 Add subtest for get_requested_index_ranges * DAS-2326 Add substest for get_spatial_index_ranges for projected * DAS-2326 Update incorrect subtest description * DAS-2326 Update test verification for index ranges for projected grid * DAS-2326 Add subtest for testing multiple temporal dimensions * DAS-2326 Fix trailing space * DAS-2326 Fix bug in get_dimension_indices_from_values * DAS-2326 Add end to end test for NoDataException * DAS-2326 Add a subtest with a descending file * DAS-2326 Update harmony-service-lib version * DAS-2326 Update harmony-service-lib to 2.11
1 parent 0cafcb0 commit 67542f2

17 files changed

Lines changed: 824 additions & 88 deletions

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
## [v1.1.16] - 2025-10-17
1+
## [v1.1.17] - 2025-11-25
2+
3+
### Changed
4+
5+
- Change HOSS behavior to return NoDataException warning instead of a failed
6+
exception when the variable, spatial, temporal request does not return any
7+
data. Also fix a bug where the out of range subset requests were reported
8+
with a default output.
9+
10+
## [v1.1.16] - 2025-11-17
211

312
### Fixed
413

@@ -207,6 +216,7 @@ Repository structure changes include:
207216

208217
For more information on internal releases prior to NASA open-source approval,
209218
see legacy-CHANGELOG.md.
219+
[v1.1.17]: https://github.com/nasa/harmony-opendap-subsetter/releases/tag/1.1.17
210220
[v1.1.16]: https://github.com/nasa/harmony-opendap-subsetter/releases/tag/1.1.16
211221
[v1.1.15]: https://github.com/nasa/harmony-opendap-subsetter/releases/tag/1.1.15
212222
[v1.1.14]: https://github.com/nasa/harmony-opendap-subsetter/releases/tag/1.1.14

docker/service_version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.1.16
1+
1.1.17

hoss/adapter.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@
2929
from tempfile import mkdtemp
3030

3131
from harmony_service_lib import BaseHarmonyAdapter
32+
from harmony_service_lib.exceptions import HarmonyException, NoDataException
3233
from harmony_service_lib.message import Source
33-
from harmony_service_lib.util import HarmonyException, generate_output_filename, stage
34+
from harmony_service_lib.util import generate_output_filename, stage
3435
from pystac import Asset, Item
3536

3637
from hoss.dimension_utilities import is_index_subset
@@ -132,6 +133,9 @@ def process_item(self, item: Item, source: Source):
132133
url, title=staged_filename, media_type=mime, roles=['data']
133134
)
134135

136+
except NoDataException as no_data_exception:
137+
self.logger.exception(no_data_exception)
138+
raise
135139
except Exception as exception:
136140
self.logger.exception(exception)
137141
raise_from_hoss_exception(exception)

hoss/dimension_utilities.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from typing import Dict, Set, Tuple
1515

1616
import numpy as np
17+
from harmony_service_lib.exceptions import NoDataException
1718
from harmony_service_lib.message import Message
1819
from harmony_service_lib.message_utility import rgetattr
1920
from harmony_service_lib.util import Config
@@ -346,6 +347,12 @@ def get_dimension_indices_from_values(
346347
dimension_values = np.flip(dimension)
347348
dimension_indices = np.flip(dimension_indices)
348349

350+
# Check if the minimum and maximum extents are out of range of the dimension values.
351+
if maximum_extent < dimension_values[0] or minimum_extent > dimension_values[-1]:
352+
raise InvalidRequestedRange()
353+
354+
# np.interp does not throw an exception if the extents are outside dimension range
355+
# It just returns the lowest index or highest index.
349356
raw_indices = np.interp(dimension_range, dimension_values, dimension_indices)
350357

351358
if (raw_indices[0] == raw_indices[1]) and (raw_indices[0] % 1 == 0.5):
@@ -546,30 +553,43 @@ def get_requested_index_ranges(
546553
required_dimensions = varinfo.get_required_dimensions(required_variables)
547554

548555
dim_index_ranges = {}
549-
556+
out_of_range_dim_variables = set()
550557
with Dataset(dimensions_path, 'r') as dimensions_file:
551558
for dim in harmony_message.subset.dimensions:
552-
if dim.name in required_dimensions:
553-
dim_is_valid = True
554-
elif dim.name[0] != '/' and f'/{dim.name}' in required_dimensions:
555-
dim.name = f'/{dim.name}'
556-
dim_is_valid = True
557-
else:
558-
dim_is_valid = False
559-
560-
if dim_is_valid:
561-
# Try to extract bounds metadata:
562-
bounds_array = get_dimension_bounds(dim.name, varinfo, dimensions_file)
563-
# Retrieve index ranges for the specifically named dimension:
564-
dim_index_ranges[dim.name] = get_dimension_index_range(
565-
dimensions_file[dim.name][:],
566-
dim.min,
567-
dim.max,
568-
bounds_values=bounds_array,
569-
)
570-
else:
571-
# This requested dimension is not in the required dimension set
572-
raise InvalidNamedDimension(dim.name)
559+
try:
560+
if dim.name in required_dimensions:
561+
dim_is_valid = True
562+
elif dim.name[0] != '/' and f'/{dim.name}' in required_dimensions:
563+
dim.name = f'/{dim.name}'
564+
dim_is_valid = True
565+
else:
566+
dim_is_valid = False
567+
568+
if dim_is_valid:
569+
# Try to extract bounds metadata:
570+
bounds_array = get_dimension_bounds(
571+
dim.name, varinfo, dimensions_file
572+
)
573+
# Retrieve index ranges for the specifically named dimension:
574+
dim_index_ranges[dim.name] = get_dimension_index_range(
575+
dimensions_file[dim.name][:],
576+
dim.min,
577+
dim.max,
578+
bounds_values=bounds_array,
579+
)
580+
else:
581+
# This requested dimension is not in the required dimension set
582+
raise InvalidNamedDimension(dim.name)
583+
# In case subset constraint is out of range for a dimension. Continue
584+
# processing the other dimensions and raise exception for all the
585+
# dimensions that are invalid.
586+
except InvalidRequestedRange:
587+
out_of_range_dim_variables.add(dim.name)
588+
589+
if out_of_range_dim_variables:
590+
raise NoDataException(
591+
f'Input request outside supported dimension range for {out_of_range_dim_variables}'
592+
)
573593

574594
return dim_index_ranges
575595

hoss/spatial.py

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from typing import List, Set
2626

27+
from harmony_service_lib.exceptions import NoDataException
2728
from harmony_service_lib.message import Message
2829
from netCDF4 import Dataset
2930
from numpy.ma.core import MaskedArray
@@ -49,6 +50,7 @@
4950
get_dimension_extents,
5051
get_dimension_index_range,
5152
)
53+
from hoss.exceptions import InvalidRequestedRange
5254
from hoss.projection_utilities import (
5355
get_master_geotransform,
5456
get_projected_x_y_extents,
@@ -101,7 +103,7 @@ def get_spatial_index_ranges(
101103
non_spatial_variables = required_variables.difference(
102104
varinfo.get_spatial_dimensions(required_variables)
103105
)
104-
106+
out_of_range_variables = set()
105107
with Dataset(dimensions_path, 'r') as dimensions_file:
106108
if geographic_dimensions:
107109
# If there is no bounding box, but there is a shape file, calculate
@@ -111,43 +113,60 @@ def get_spatial_index_ranges(
111113
bounding_box = get_geographic_bbox(geojson_content)
112114

113115
for dimension in geographic_dimensions:
114-
index_ranges[dimension] = get_geographic_index_range(
115-
dimension, varinfo, dimensions_file, bounding_box
116-
)
116+
try:
117+
index_ranges[dimension] = get_geographic_index_range(
118+
dimension, varinfo, dimensions_file, bounding_box
119+
)
120+
except InvalidRequestedRange:
121+
out_of_range_variables.add(dimension)
117122

118123
if projected_dimensions:
119124
for non_spatial_variable in non_spatial_variables:
120-
index_ranges.update(
121-
get_projected_x_y_index_ranges(
122-
non_spatial_variable,
123-
varinfo,
124-
dimensions_file,
125-
index_ranges,
126-
bounding_box=bounding_box,
127-
shape_file_path=shape_file_path,
125+
try:
126+
index_ranges.update(
127+
get_projected_x_y_index_ranges(
128+
non_spatial_variable,
129+
varinfo,
130+
dimensions_file,
131+
index_ranges,
132+
bounding_box=bounding_box,
133+
shape_file_path=shape_file_path,
134+
)
128135
)
129-
)
136+
except InvalidRequestedRange:
137+
out_of_range_variables.add(non_spatial_variable)
138+
130139
variables_with_anonymous_dims = get_variables_with_anonymous_dims(
131140
varinfo, required_variables
132141
)
142+
133143
for variable_with_anonymous_dims in variables_with_anonymous_dims:
134-
latitude_coordinates, longitude_coordinates = get_coordinate_variables(
135-
varinfo, [variable_with_anonymous_dims]
136-
)
137-
if latitude_coordinates and longitude_coordinates:
138-
index_ranges.update(
139-
get_x_y_index_ranges_from_coordinates(
140-
variable_with_anonymous_dims,
141-
varinfo,
142-
dimensions_file,
143-
varinfo.get_variable(latitude_coordinates[0]),
144-
varinfo.get_variable(longitude_coordinates[0]),
145-
index_ranges,
146-
bounding_box=bounding_box,
147-
shape_file_path=shape_file_path,
148-
)
144+
try:
145+
latitude_coordinates, longitude_coordinates = get_coordinate_variables(
146+
varinfo, [variable_with_anonymous_dims]
149147
)
150148

149+
if latitude_coordinates and longitude_coordinates:
150+
index_ranges.update(
151+
get_x_y_index_ranges_from_coordinates(
152+
variable_with_anonymous_dims,
153+
varinfo,
154+
dimensions_file,
155+
varinfo.get_variable(latitude_coordinates[0]),
156+
varinfo.get_variable(longitude_coordinates[0]),
157+
index_ranges,
158+
bounding_box=bounding_box,
159+
shape_file_path=shape_file_path,
160+
)
161+
)
162+
except InvalidRequestedRange:
163+
out_of_range_variables.add(variable_with_anonymous_dims)
164+
165+
if out_of_range_variables:
166+
raise NoDataException(
167+
f'Spatial subset request outside supported dimension range for {out_of_range_variables}'
168+
)
169+
151170
return index_ranges
152171

153172

hoss/subset.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from typing import List, Set
99

10+
from harmony_service_lib.exceptions import NoDataException
1011
from harmony_service_lib.message import Message, Source
1112
from harmony_service_lib.message import Variable as HarmonyVariable
1213
from harmony_service_lib.message_utility import rgetattr
@@ -217,8 +218,14 @@ def get_required_variables(
217218
requested_variables = {
218219
f'/{variable.fullPath.lstrip("/")}' for variable in variables
219220
}
220-
221-
if request_is_index_subset and len(requested_variables) == 0:
221+
# If request includes variable subsetting, check that all requested
222+
# variables exist in the granule.
223+
if requested_variables:
224+
check_requested_variables_in_granule(varinfo, requested_variables)
225+
226+
# Otherwise, if request is an index subset and no variables are requested,
227+
# include all variables.
228+
elif request_is_index_subset:
222229
requested_variables = varinfo.get_science_variables().union(
223230
varinfo.get_metadata_variables()
224231
)
@@ -230,6 +237,25 @@ def get_required_variables(
230237
return varinfo.get_required_variables(requested_variables)
231238

232239

240+
def check_requested_variables_in_granule(
241+
varinfo: VarInfoFromDmr, requested_variables: List[str]
242+
) -> bool:
243+
"""Return True if all variables are in the granule. Raise NoDataException
244+
if any of the requested variables are not in the granule.
245+
246+
"""
247+
invalid_requested_variables = {
248+
variable_name
249+
for variable_name in requested_variables
250+
if varinfo.get_variable(variable_name) is None
251+
}
252+
if invalid_requested_variables:
253+
raise NoDataException(
254+
f'Requested variables:{invalid_requested_variables} not found in granule'
255+
)
256+
return True
257+
258+
233259
def fill_variables(
234260
output_path: str,
235261
varinfo: VarInfoFromDmr,

hoss/temporal.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from dateutil import tz
1515
from dateutil.parser import parse as parse_datetime
16+
from harmony_service_lib.exceptions import NoDataException
1617
from harmony_service_lib.message import Message
1718
from netCDF4 import Dataset
1819
from varinfo import VarInfoFromDmr
@@ -22,7 +23,7 @@
2223
get_dimension_bounds,
2324
get_dimension_index_range,
2425
)
25-
from hoss.exceptions import UnsupportedTemporalUnits
26+
from hoss.exceptions import InvalidRequestedRange, UnsupportedTemporalUnits
2627

2728
units_day = {'day', 'days', 'd'}
2829
units_hour = {'hour', 'hours', 'hr', 'h'}
@@ -54,6 +55,7 @@ def get_temporal_index_ranges(
5455
5556
"""
5657
index_ranges = {}
58+
out_of_range_dim_variables = set()
5759
temporal_dimensions = varinfo.get_temporal_dimensions(required_variables)
5860

5961
time_start = get_datetime_with_timezone(
@@ -65,22 +67,34 @@ def get_temporal_index_ranges(
6567

6668
with Dataset(dimensions_path, 'r') as dimensions_file:
6769
for dimension in temporal_dimensions:
68-
time_variable = varinfo.get_variable(dimension)
69-
time_ref, time_delta = get_time_ref(
70-
time_variable.get_attribute_value('units')
71-
)
72-
73-
# Convert the Harmony message start and end datetime values into
74-
# integer or floating point values (e.g., a number of seconds since
75-
# 1970-01-01) using the variable epoch and unit.
76-
minimum_extent = (time_start - time_ref) / time_delta
77-
maximum_extent = (time_end - time_ref) / time_delta
78-
79-
index_ranges[dimension] = get_dimension_index_range(
80-
dimensions_file[dimension][:],
81-
minimum_extent,
82-
maximum_extent,
83-
bounds_values=get_dimension_bounds(dimension, varinfo, dimensions_file),
70+
try:
71+
time_variable = varinfo.get_variable(dimension)
72+
time_ref, time_delta = get_time_ref(
73+
time_variable.get_attribute_value('units')
74+
)
75+
76+
# Convert the Harmony message start and end datetime values into
77+
# integer or floating point values (e.g., a number of seconds since
78+
# 1970-01-01) using the variable epoch and unit.
79+
minimum_extent = (time_start - time_ref) / time_delta
80+
maximum_extent = (time_end - time_ref) / time_delta
81+
82+
index_ranges[dimension] = get_dimension_index_range(
83+
dimensions_file[dimension][:],
84+
minimum_extent,
85+
maximum_extent,
86+
bounds_values=get_dimension_bounds(
87+
dimension, varinfo, dimensions_file
88+
),
89+
)
90+
91+
except InvalidRequestedRange:
92+
out_of_range_dim_variables.add(dimension)
93+
94+
if out_of_range_dim_variables:
95+
raise NoDataException(
96+
f'Temporal range request outside supported dimension range for '
97+
f'{out_of_range_dim_variables}'
8498
)
8599

86100
return index_ranges

pip_requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This file should contain requirements to be installed via Pip.
22
# Open source packages available from PyPI
3-
earthdata-varinfo ~= 3.1.0
4-
harmony-service-lib ~= 2.9.0
3+
earthdata-varinfo ~= 3.3.1
4+
harmony-service-lib ~= 2.11.0
55
netCDF4 ~= 1.7.2
66
numpy ~= 2.2.6
77
pyproj ~= 3.7.1

0 commit comments

Comments
 (0)