Skip to content

Commit

Permalink
Merge pull request #169 from sentinel-hub/feat/open_time_intervals
Browse files Browse the repository at this point in the history
Open time intervals and "cloudy" collections
  • Loading branch information
AleksMat committed Mar 3, 2021
2 parents 7eefaae + b071d16 commit b0fc135
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 17 deletions.
2 changes: 1 addition & 1 deletion sentinelhub/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ class HistogramType(Enum):


class MimeType(Enum):
""" Enum class to represent supported image file formats
""" Enum class to represent supported file formats
Supported file formats are TIFF 8-bit, TIFF 16-bit, TIFF 32-bit float, PNG, JPEG, JPEG2000, JSON, CSV, ZIP, HDF5,
XML, GML, RAW
Expand Down
14 changes: 10 additions & 4 deletions sentinelhub/data_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class DataCollectionDefinition:
Check `DataCollection.define` for more info about attributes of this class
"""
# pylint: disable=too-many-instance-attributes
api_id: str = None
catalog_id: str = None
wfs_id: str = None
Expand All @@ -152,6 +153,7 @@ class DataCollectionDefinition:
bands: Tuple[str, ...] = None
collection_id: str = None
is_timeless: bool = False
has_cloud_coverage: bool = False

def __post_init__(self):
""" In case a list of bands has been given this makes sure to cast it into a tuple
Expand Down Expand Up @@ -195,7 +197,8 @@ class DataCollection(Enum, metaclass=_DataCollectionMeta):
collection_type=_CollectionType.SENTINEL2,
sensor_type=_SensorType.MSI,
processing_level=_ProcessingLevel.L1C,
bands=_Bands.SENTINEL2_L1C
bands=_Bands.SENTINEL2_L1C,
has_cloud_coverage=True
)
SENTINEL2_L2A = DataCollectionDefinition(
api_id='S2L2A',
Expand All @@ -204,7 +207,8 @@ class DataCollection(Enum, metaclass=_DataCollectionMeta):
collection_type=_CollectionType.SENTINEL2,
sensor_type=_SensorType.MSI,
processing_level=_ProcessingLevel.L2A,
bands=_Bands.SENTINEL2_L2A
bands=_Bands.SENTINEL2_L2A,
has_cloud_coverage=True
)

SENTINEL1 = DataCollectionDefinition(
Expand Down Expand Up @@ -273,7 +277,8 @@ class DataCollection(Enum, metaclass=_DataCollectionMeta):
service_url=ServiceUrl.USWEST,
collection_type=_CollectionType.LANDSAT8,
processing_level=_ProcessingLevel.L1C,
bands=_Bands.LANDSAT8
bands=_Bands.LANDSAT8,
has_cloud_coverage=True
)

SENTINEL5P = DataCollectionDefinition(
Expand Down Expand Up @@ -304,7 +309,8 @@ class DataCollection(Enum, metaclass=_DataCollectionMeta):
collection_type=_CollectionType.SENTINEL3,
sensor_type=_SensorType.SLSTR,
processing_level=_ProcessingLevel.L1B,
bands=_Bands.SENTINEL3_SLSTR
bands=_Bands.SENTINEL3_SLSTR,
has_cloud_coverage=True
)

# EOCloud collections (which are only available on a development eocloud service):
Expand Down
2 changes: 1 addition & 1 deletion sentinelhub/sentinelhub_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def search(self, collection, *, time, bbox=None, geometry=None, ids=None, query=
url = f'{self.catalog_url}/search'

collection_id = self._parse_collection_id(collection)
start_time, end_time = serialize_time(parse_time_interval(time), use_tz=True)
start_time, end_time = serialize_time(parse_time_interval(time, allow_undefined=True), use_tz=True)

if bbox and bbox.crs is not CRS.WGS84:
bbox = bbox.transform_bounds(CRS.WGS84)
Expand Down
2 changes: 1 addition & 1 deletion sentinelhub/sentinelhub_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ def _get_data_filters(data_collection, time_interval, maxcc, mosaicking_order):
data_filter = {}

if time_interval:
start_time, end_time = serialize_time(parse_time_interval(time_interval), use_tz=True)
start_time, end_time = serialize_time(parse_time_interval(time_interval, allow_undefined=True), use_tz=True)
data_filter['timeRange'] = {'from': start_time, 'to': end_time}

if maxcc is not None:
Expand Down
28 changes: 21 additions & 7 deletions sentinelhub/time_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,24 @@ def is_valid_time(time):
return False


def parse_time(time_input, *, force_datetime=False, **kwargs):
def parse_time(time_input, *, force_datetime=False, allow_undefined=False, **kwargs):
""" Parse input time/date string
:param time_input: time/date to parse
:type time_input: str or datetime.date or datetime.datetime
:param force_datetime: If True it will always return datetime.datetime object, if False it can also return only
datetime.date object if only date is provided as input.
:type force_datetime: bool
:param allow_undefined: Flag to allow parsing None or '..' into None
:param allow_undefined: bool (default is False)
:param kwargs: Any keyword arguments to be passed to `dateutil.parser.parse`. Example: `ignoretz=True`
:return: A datetime object
:rtype: datetime.datetime or datetime.date
"""

if allow_undefined and time_input in [None, '..']:
return None

if isinstance(time_input, dt.date):
if force_datetime and not isinstance(time_input, dt.datetime):
return date_to_datetime(time_input)
Expand All @@ -144,7 +150,7 @@ def parse_time(time_input, *, force_datetime=False, **kwargs):
return time.date()


def parse_time_interval(time, **kwargs):
def parse_time_interval(time, allow_undefined=False, **kwargs):
""" Parse input into an interval of two times, specifying start and end time, into datetime objects.
The input time can have the following formats, which will be parsed as:
Expand All @@ -157,27 +163,32 @@ def parse_time_interval(time, **kwargs):
All input times can also be specified as `datetime` objects. Instances of `datetime.date` will be treated as
`YYYY-MM-DD` and instance of `datetime.datetime` will be treated as `YYYY-MM-DDThh:mm:ss`.
:param allow_undefined: Boolean flag controls if None or '..' are allowed
:param allow_undefined: bool
:param time: An input time
:type time: str or datetime.datetime or (str, str) or (datetime.datetime, datetime.datetime)
:return: interval of start and end date of the form `YYYY-MM-DDThh:mm:ss`
:rtype: (datetime.datetime, datetime.datetime)
:raises: ValueError
"""
if isinstance(time, (str, dt.date)):
if allow_undefined and time in [None, '..']:
date_interval = None, None
elif isinstance(time, (str, dt.date)):
parsed_time = parse_time(time, **kwargs)
date_interval = parsed_time, parsed_time
elif isinstance(time, (tuple, list)) and len(time) == 2:
date_interval = parse_time(time[0], **kwargs), parse_time(time[1], **kwargs)
date_interval = parse_time(time[0], allow_undefined=allow_undefined, **kwargs), \
parse_time(time[1], allow_undefined=allow_undefined, **kwargs)
else:
raise ValueError('Time must be a string/datetime object or tuple/list of 2 strings/datetime objects')

start_time, end_time = date_interval
if not isinstance(start_time, dt.datetime):
if not isinstance(start_time, dt.datetime) and start_time is not None:
start_time = date_to_datetime(start_time)
if not isinstance(end_time, dt.datetime):
if not isinstance(end_time, dt.datetime) and end_time is not None:
end_time = date_to_datetime(end_time, dt.time(hour=23, minute=59, second=59))

if start_time > end_time:
if start_time and end_time and start_time > end_time:
raise ValueError('Start of time interval is larger than end of time interval')

return start_time, end_time
Expand All @@ -197,6 +208,9 @@ def serialize_time(timestamp_input, *, use_tz=False):
if isinstance(timestamp_input, tuple):
return tuple(serialize_time(timestamp, use_tz=use_tz) for timestamp in timestamp_input)

if timestamp_input is None:
return '..'

if not isinstance(timestamp_input, dt.date):
raise ValueError('Expected a datetime object or a tuple of datetime objects')

Expand Down
16 changes: 13 additions & 3 deletions tests/test_time_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ def test_is_valid_time(time_input, is_valid):
('2015.4.12T12:32:14', {}, TEST_DATETIME),
('2015.4.12T12:32:14Z', {}, TEST_DATETIME_TZ),
('2015.4.12T12:32:14Z', {'ignoretz': True}, TEST_DATETIME),
('..', {'allow_undefined': True}, None),
(None, {'allow_undefined': True}, None),
(TEST_DATETIME, {}, TEST_DATETIME),
(TEST_DATETIME_TZ, {}, TEST_DATETIME_TZ),
(TEST_DATETIME_TZ, {'ignoretz': True}, TEST_DATETIME),
Expand All @@ -67,22 +69,30 @@ def test_parse_time(time_input, params, expected_output):
(('2015-4-12', '2017-4-12'), {}, (TEST_TIME_START, TEST_TIME_END.replace(year=2017))),
(('2015.4.12T12:32:14', '2017.4.12T12:32:14'), {}, (TEST_DATETIME, TEST_DATETIME.replace(year=2017))),
((TEST_DATE, TEST_DATE.replace(year=2017)), {}, (TEST_TIME_START, TEST_TIME_END.replace(year=2017))),
((TEST_DATETIME, TEST_DATETIME.replace(year=2017)), {}, (TEST_DATETIME, TEST_DATETIME.replace(year=2017)))
((TEST_DATETIME, TEST_DATETIME.replace(year=2017)), {}, (TEST_DATETIME, TEST_DATETIME.replace(year=2017))),
(None, {'allow_undefined': True}, (None, None)),
((None, None), {'allow_undefined': True}, (None, None)),
((TEST_DATETIME, None), {'allow_undefined': True}, (TEST_DATETIME, None)),
((None, TEST_DATE), {'allow_undefined': True}, (None, TEST_TIME_END)),
])
def test_parse_time_interval(time_input, params, expected_output):
parsed_interval = time_utils.parse_time_interval(time_input, **params)
assert parsed_interval == expected_output


@pytest.mark.parametrize('time_input,params,expected_output', [
(None, {}, '..'),
(TEST_DATE, {}, '2015-04-12'),
(TEST_DATETIME, {}, '2015-04-12T12:32:14'),
(TEST_DATETIME, {'use_tz': True}, '2015-04-12T12:32:14Z'),
(TEST_DATETIME_TZ, {'use_tz': False}, '2015-04-12T12:32:14'),
(TEST_DATETIME_TZ, {'use_tz': True}, '2015-04-12T12:32:14Z'),
((TEST_DATE, TEST_DATETIME, TEST_DATETIME_TZ), {}, ('2015-04-12', '2015-04-12T12:32:14', '2015-04-12T12:32:14'))
((TEST_DATE, TEST_DATETIME, TEST_DATETIME_TZ), {}, ('2015-04-12', '2015-04-12T12:32:14', '2015-04-12T12:32:14')),
((None, None), {}, ('..', '..')),
((TEST_DATETIME, None), {}, ('2015-04-12T12:32:14', '..')),
((None, TEST_DATETIME), {}, ('..', '2015-04-12T12:32:14'))
])
def test_parse_time_interval(time_input, params, expected_output):
def test_serialize_time(time_input, params, expected_output):
serialized_result = time_utils.serialize_time(time_input, **params)
assert serialized_result == expected_output

Expand Down

0 comments on commit b0fc135

Please sign in to comment.