Repository: opendatacube/datacube-core

Issue:
Allow open-ended time ranges
When specifying a time range, be able to have the start or end of the range be open, to indicate all data before or after a date. For example:

`time=('2001', None)`
could indicate all data from 2001 onward.

`time=(None, '2010')`
could indicate all data before 2010.


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/opendatacube/datacube-core/pull/1443?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube) Report
Patch coverage: **`71.42`**% and project coverage change: **`+0.05`** :tada:
> Comparison is base [(`72ccd88`)](https://app.codecov.io/gh/opendatacube/datacube-core/commit/72ccd88d01ae9f8224a490d89c2b0508233b6d0b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube) 91.77% compared to head [(`5d8c439`)](https://app.codecov.io/gh/opendatacube/datacube-core/pull/1443?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube) 91.82%.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop    #1443      +/-   ##
===========================================
+ Coverage    91.77%   91.82%   +0.05%     
===========================================
  Files          132      132              
  Lines        14490    14485       -5     
===========================================
+ Hits         13298    13301       +3     
+ Misses        1192     1184       -8     
```


| [Impacted Files](https://app.codecov.io/gh/opendatacube/datacube-core/pull/1443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube) | Coverage Δ | |
|---|---|---|
| [datacube/api/core.py](https://app.codecov.io/gh/opendatacube/datacube-core/pull/1443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-ZGF0YWN1YmUvYXBpL2NvcmUucHk=) | `95.64% <ø> (ø)` | |
| [datacube/ui/expression.py](https://app.codecov.io/gh/opendatacube/datacube-core/pull/1443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-ZGF0YWN1YmUvdWkvZXhwcmVzc2lvbi5weQ==) | `74.07% <33.33%> (ø)` | |
| [datacube/api/query.py](https://app.codecov.io/gh/opendatacube/datacube-core/pull/1443?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-ZGF0YWN1YmUvYXBpL3F1ZXJ5LnB5) | `93.83% <100.00%> (+3.56%)` | :arrow_up: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/opendatacube/datacube-core/pull/1443?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube).


Patch:
diff --git a/datacube/api/core.py b/datacube/api/core.py
index 47a8c179..a07d4500 100644
--- a/datacube/api/core.py
+++ b/datacube/api/core.py
@@ -232,12 +232,17 @@ class Datacube(object):
 
                 x=(1516200, 1541300), y=(-3867375, -3867350), crs='EPSG:3577'
 
-            The ``time`` dimension can be specified using a tuple of datetime objects or strings with
-            ``YYYY-MM-DD hh:mm:ss`` format. Data will be loaded inclusive of the start and finish times. E.g::
+            The ``time`` dimension can be specified using a single or tuple of datetime objects or strings with
+            ``YYYY-MM-DD hh:mm:ss`` format. Data will be loaded inclusive of the start and finish times.
+            A ``None`` value in the range indicates an open range, with the provided date serving as either the
+            upper or lower bound. E.g::
 
                 time=('2000-01-01', '2001-12-31')
                 time=('2000-01', '2001-12')
                 time=('2000', '2001')
+                time=('2000')
+                time=('2000', None)  # all data from 2000 onward
+                time=(None, '2000')  # all data up to and including 2000
 
             For 3D datasets, where the product definition contains an ``extra_dimension`` specification,
             these dimensions can be queried using that dimension's name. E.g.::
diff --git a/datacube/api/query.py b/datacube/api/query.py
index f72a4147..fe77c9f1 100644
--- a/datacube/api/query.py
+++ b/datacube/api/query.py
@@ -128,8 +128,8 @@ class Query(object):
                 if time_coord is not None:
                     self.search['time'] = _time_to_search_dims(
                         (pandas_to_datetime(time_coord.values[0]).to_pydatetime(),
-                         pandas_to_datetime(time_coord.values[-1]).to_pydatetime()
-                         + datetime.timedelta(milliseconds=1))  # TODO: inclusive time searches
+                            pandas_to_datetime(time_coord.values[-1]).to_pydatetime()
+                            + datetime.timedelta(milliseconds=1))  # TODO: inclusive time searches
                     )
 
     @property
@@ -342,7 +342,11 @@ def _time_to_search_dims(time_range):
         if hasattr(tr_end, 'isoformat'):
             tr_end = tr_end.isoformat()
 
+        if tr_start is None:
+            tr_start = datetime.datetime.fromtimestamp(0)
         start = _to_datetime(tr_start)
+        if tr_end is None:
+            tr_end = datetime.datetime.now().strftime("%Y-%m-%d")
         end = _to_datetime(pandas.Period(tr_end)
                            .end_time
                            .to_pydatetime())
@@ -354,19 +358,6 @@ def _time_to_search_dims(time_range):
         return tr
 
 
-def _time_to_open_range(time, lower_bound: bool):
-    with warnings.catch_warnings():
-        warnings.simplefilter("ignore", UserWarning)
-
-        if lower_bound:  # from date provided (not inclusive) to latest available
-            start = _to_datetime(pandas.Period(time).end_time.to_pydatetime())
-            end = _to_datetime(datetime.datetime.now())
-        else:  # from earliest available to date provided (not inclusive)
-            start = _to_datetime(datetime.datetime.fromtimestamp(0))
-            end = _to_datetime(pandas.Period(time).start_time.to_pydatetime())
-        return Range(start, end)
-
-
 def _convert_to_solar_time(utc, longitude):
     seconds_per_degree = 240
     offset_seconds = int(longitude * seconds_per_degree)
diff --git a/datacube/ui/expression.py b/datacube/ui/expression.py
index 4e9c44d5..8ef1fdf7 100644
--- a/datacube/ui/expression.py
+++ b/datacube/ui/expression.py
@@ -20,7 +20,7 @@ and START, END are either numbers or dates.
 
 from lark import Lark, v_args, Transformer
 
-from datacube.api.query import _time_to_search_dims, _time_to_open_range
+from datacube.api.query import _time_to_search_dims
 from datacube.model import Range
 
 
@@ -119,11 +119,10 @@ class TreeToSearchExprs(Transformer):
         return _time_to_search_dims((start, end))
     
     def range_lower_bound(self, date):
-        return _time_to_open_range(date, lower_bound=True)
+        return _time_to_search_dims((date, None))
 
     def range_upper_bound(self, date):
-        return _time_to_open_range(date, lower_bound=False)
-
+        return _time_to_search_dims((None, date))
 
     def date(self, y, m=None, d=None):
         return "-".join(x for x in [y, m, d] if x is not None)
diff --git a/docs/about/whats_new.rst b/docs/about/whats_new.rst
index d897720a..fe863fe8 100644
--- a/docs/about/whats_new.rst
+++ b/docs/about/whats_new.rst
@@ -14,7 +14,7 @@ v1.8.next
 - Documentation fixes (:pull:`1417`, :pull:`1418`, :pull:`1430`)
 - ``datacube dataset`` cli commands print error message if missing argument (:pull:`1437`)
 - Add pre-commit hook to verify license headers (:pull:`1438`)
-- Support open-ended date ranges in `datacube dataset search` (:pull:`1439`)
+- Support open-ended date ranges in `datacube dataset search`, `dc.load`, and `dc.find_datasets` (:pull:`1439`, :pull:`1443`)
 
 
 v1.8.12 (7th March 2023)


Test patch:
diff --git a/tests/api/test_query.py b/tests/api/test_query.py
index 78b13890..d7d8e0b5 100644
--- a/tests/api/test_query.py
+++ b/tests/api/test_query.py
@@ -140,7 +140,11 @@ testdata = [
     ((datetime.datetime(2008, 1, 1), datetime.datetime(2008, 1, 10, 23, 59, 40)),
      format_test('2008-01-01T00:00:00', '2008-01-10T23:59:40.999999')),
     ((datetime.date(2008, 1, 1)),
-     format_test('2008-01-01T00:00:00', '2008-01-01T23:59:59.999999'))
+     format_test('2008-01-01T00:00:00', '2008-01-01T23:59:59.999999')),
+    ((datetime.date(2008, 1, 1), None),
+     format_test('2008-01-01T00:00:00', datetime.datetime.now().strftime("%Y-%m-%dT23:59:59.999999"))),
+    ((None, '2008'),
+     format_test(datetime.datetime.fromtimestamp(0).strftime("%Y-%m-%dT%H:%M:%S"), '2008-12-31T23:59:59.999999'))
 ]