We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Time range strings behave differently between datacube and xarray
When specifying a time range in a datacube:
`dc.load(..., time=('2003-01', '2003-03'))`
is interpreted to be 
_2003-01-01 00:00:00 to 2003-03-01 00:00:00_
i.e. values rounds down

vs

In xarray:
`data.sel(time=slice('2003-01', '2003-02'))`
is interpreted to be:
_2003-01-01 00:00:00 to 2003-03-31 23:59:59.999_
i.e. values round out

Ideally they would behave the same.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `datacube/api/query.py`
Content:
```
1 #
2 #    Licensed under the Apache License, Version 2.0 (the "License");
3 #    you may not use this file except in compliance with the License.
4 #    You may obtain a copy of the License at
5 #
6 #        http://www.apache.org/licenses/LICENSE-2.0
7 #
8 #    Unless required by applicable law or agreed to in writing, software
9 #    distributed under the License is distributed on an "AS IS" BASIS,
10 #    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 #    See the License for the specific language governing permissions and
12 #    limitations under the License.
13 
14 """
15 Storage Query and Access API module
16 """
17 
18 from __future__ import absolute_import, division, print_function
19 
20 import logging
21 import datetime
22 import collections
23 import warnings
24 
25 from dateutil import tz
26 from pandas import to_datetime as pandas_to_datetime
27 import numpy as np
28 
29 from ..compat import string_types, integer_types
30 from ..model import Range
31 from ..utils import geometry, datetime_to_seconds_since_1970
32 
33 _LOG = logging.getLogger(__name__)
34 
35 
36 GroupBy = collections.namedtuple('GroupBy', ['dimension', 'group_by_func', 'units', 'sort_key'])
37 
38 FLOAT_TOLERANCE = 0.0000001  # TODO: For DB query, use some sort of 'contains' query, rather than range overlap.
39 SPATIAL_KEYS = ('latitude', 'lat', 'y', 'longitude', 'lon', 'long', 'x')
40 CRS_KEYS = ('crs', 'coordinate_reference_system')
41 OTHER_KEYS = ('measurements', 'group_by', 'output_crs', 'resolution', 'set_nan', 'product', 'geopolygon', 'like',
42               'source_filter')
43 
44 
45 class Query(object):
46     def __init__(self, index=None, product=None, geopolygon=None, like=None, **search_terms):
47         """Parses search terms in preparation for querying the Data Cube Index.
48 
49         Create a :class:`Query` object by passing it a set of search terms as keyword arguments.
50 
51         >>> query = Query(product='ls5_nbar_albers', time=('2001-01-01', '2002-01-01'))
52 
53         Use by accessing :attr:`search_terms`:
54 
55         >>> query.search_terms['time']  # doctest: +NORMALIZE_WHITESPACE
56         Range(begin=datetime.datetime(2001, 1, 1, 0, 0, tzinfo=<UTC>), \
57         end=datetime.datetime(2002, 1, 1, 0, 0, tzinfo=<UTC>))
58 
59         By passing in an ``index``, the search parameters will be validated as existing on the ``product``.
60 
61         Used by :meth:`datacube.Datacube.find_datasets` and :meth:`datacube.Datacube.load`.
62 
63         :param datacube.index.Index index: An optional `index` object, if checking of field names is desired.
64         :param str product: name of product
65         :param geopolygon: spatial bounds of the search
66         :type geopolygon: geometry.Geometry or None
67         :param xarray.Dataset like: spatio-temporal bounds of `like` are used for the search
68         :param search_terms:
69          * `measurements` - list of measurements to retrieve
70          * `latitude`, `lat`, `y`, `longitude`, `lon`, `long`, `x` - tuples (min, max) bounding spatial dimensions
71          * `crs` - spatial coordinate reference system to interpret the spatial bounds
72          * `group_by` - observation grouping method. One of `time`, `solar_day`. Default is `time`
73         """
74         self.product = product
75         self.geopolygon = query_geopolygon(geopolygon=geopolygon, **search_terms)
76         if 'source_filter' in search_terms and search_terms['source_filter'] is not None:
77             self.source_filter = Query(**search_terms['source_filter'])
78         else:
79             self.source_filter = None
80 
81         remaining_keys = set(search_terms.keys()) - set(SPATIAL_KEYS + CRS_KEYS + OTHER_KEYS)
82         if index:
83             unknown_keys = remaining_keys - set(index.datasets.get_field_names())
84             # TODO: What about keys source filters, and what if the keys don't match up with this product...
85             if unknown_keys:
86                 raise LookupError('Unknown arguments: ', unknown_keys)
87 
88         self.search = {}
89         for key in remaining_keys:
90             self.search.update(_values_to_search(**{key: search_terms[key]}))
91 
92         if like:
93             assert self.geopolygon is None, "'like' with other spatial bounding parameters is not supported"
94             self.geopolygon = getattr(like, 'extent', self.geopolygon)
95 
96             if 'time' not in self.search:
97                 time_coord = like.coords.get('time')
98                 if time_coord is not None:
99                     self.search['time'] = _time_to_search_dims(
100                         (pandas_to_datetime(time_coord.values[0]).to_pydatetime(),
101                          pandas_to_datetime(time_coord.values[-1]).to_pydatetime()
102                          + datetime.timedelta(milliseconds=1))  # TODO: inclusive time searches
103                     )
104 
105     @property
106     def search_terms(self):
107         """
108         Access the search terms as a dictionary.
109 
110         :type: dict
111         """
112         kwargs = {}
113         kwargs.update(self.search)
114         if self.geopolygon:
115             geo_bb = self.geopolygon.to_crs(geometry.CRS('EPSG:4326')).boundingbox
116             if geo_bb.bottom != geo_bb.top:
117                 kwargs['lat'] = Range(geo_bb.bottom, geo_bb.top)
118             else:
119                 kwargs['lat'] = geo_bb.bottom
120             if geo_bb.left != geo_bb.right:
121                 kwargs['lon'] = Range(geo_bb.left, geo_bb.right)
122             else:
123                 kwargs['lon'] = geo_bb.left
124         if self.product:
125             kwargs['product'] = self.product
126         if self.source_filter:
127             kwargs['source_filter'] = self.source_filter.search_terms
128         return kwargs
129 
130     def __repr__(self):
131         return self.__str__()
132 
133     def __str__(self):
134         return """Datacube Query:
135         type = {type}
136         search = {search}
137         geopolygon = {geopolygon}
138         """.format(type=self.product,
139                    search=self.search,
140                    geopolygon=self.geopolygon)
141 
142 
143 def query_geopolygon(geopolygon=None, **kwargs):
144     spatial_dims = {dim: v for dim, v in kwargs.items() if dim in SPATIAL_KEYS}
145     crs = {v for k, v in kwargs.items() if k in CRS_KEYS}
146     if len(crs) == 1:
147         spatial_dims['crs'] = crs.pop()
148     elif len(crs) > 1:
149         raise ValueError('Spatial dimensions must be in the same coordinate reference system: {}'.format(crs))
150 
151     if geopolygon is not None and len(spatial_dims) > 0:
152         raise ValueError('Cannot specify "geopolygon" and one of %s at the same time' % (SPATIAL_KEYS + CRS_KEYS,))
153 
154     if geopolygon is None:
155         return _range_to_geopolygon(**spatial_dims)
156 
157     return geopolygon
158 
159 
160 def query_group_by(group_by='time', **kwargs):
161     time_grouper = GroupBy(dimension='time',
162                            group_by_func=lambda ds: ds.center_time,
163                            units='seconds since 1970-01-01 00:00:00',
164                            sort_key=lambda ds: ds.center_time)
165 
166     solar_day_grouper = GroupBy(dimension='time',
167                                 group_by_func=solar_day,
168                                 units='seconds since 1970-01-01 00:00:00',
169                                 sort_key=lambda ds: ds.center_time)
170 
171     group_by_map = {
172         None: time_grouper,
173         'time': time_grouper,
174         'solar_day': solar_day_grouper
175     }
176 
177     try:
178         return group_by_map[group_by]
179     except KeyError:
180         raise LookupError('No group by function for', group_by)
181 
182 
183 def _range_to_geopolygon(**kwargs):
184     input_crs = None
185     input_coords = {'left': None, 'bottom': None, 'right': None, 'top': None}
186     for key, value in kwargs.items():
187         if value is None:
188             continue
189         key = key.lower()
190         if key in ['latitude', 'lat', 'y']:
191             input_coords['top'], input_coords['bottom'] = _value_to_range(value)
192         if key in ['longitude', 'lon', 'long', 'x']:
193             input_coords['left'], input_coords['right'] = _value_to_range(value)
194         if key in ['crs', 'coordinate_reference_system']:
195             input_crs = geometry.CRS(value)
196     input_crs = input_crs or geometry.CRS('EPSG:4326')
197     if any(v is not None for v in input_coords.values()):
198         if input_coords['left'] == input_coords['right']:
199             if input_coords['top'] == input_coords['bottom']:
200                 return geometry.point(input_coords['left'], input_coords['top'], crs=input_crs)
201             else:
202                 points = [(input_coords['left'], input_coords['bottom']),
203                           (input_coords['left'], input_coords['top'])]
204                 return geometry.line(points, crs=input_crs)
205         else:
206             if input_coords['top'] == input_coords['bottom']:
207                 points = [(input_coords['left'], input_coords['top']),
208                           (input_coords['right'], input_coords['top'])]
209                 return geometry.line(points, crs=input_crs)
210             else:
211                 points = [
212                     (input_coords['left'], input_coords['top']),
213                     (input_coords['right'], input_coords['top']),
214                     (input_coords['right'], input_coords['bottom']),
215                     (input_coords['left'], input_coords['bottom']),
216                     (input_coords['left'], input_coords['top'])
217                 ]
218                 return geometry.polygon(points, crs=input_crs)
219     return None
220 
221 
222 def _value_to_range(value):
223     if isinstance(value, string_types + integer_types + (float,)):
224         value = float(value)
225         return value, value
226     else:
227         return float(value[0]), float(value[-1])
228 
229 
230 def _values_to_search(**kwargs):
231     search = {}
232     for key, value in kwargs.items():
233         if key.lower() in ('time', 't'):
234             search['time'] = _time_to_search_dims(value)
235         elif key not in ['latitude', 'lat', 'y'] + ['longitude', 'lon', 'x']:
236             if isinstance(value, collections.Sequence) and len(value) == 2:
237                 search[key] = Range(*value)
238             else:
239                 search[key] = value
240     return search
241 
242 
243 def _datetime_to_timestamp(dt):
244     if not isinstance(dt, datetime.datetime) and not isinstance(dt, datetime.date):
245         dt = _to_datetime(dt)
246     return datetime_to_seconds_since_1970(dt)
247 
248 
249 def _to_datetime(t):
250     if isinstance(t, integer_types + (float,)):
251         t = datetime.datetime.fromtimestamp(t, tz=tz.tzutc())
252 
253     if isinstance(t, tuple):
254         t = datetime.datetime(*t, tzinfo=tz.tzutc())
255     elif isinstance(t, string_types):
256         try:
257             t = datetime.datetime.strptime(t, "%Y-%m-%dT%H:%M:%S.%fZ")
258         except ValueError:
259             pass
260     elif isinstance(t, datetime.datetime):
261         if t.tzinfo is None:
262             t = t.replace(tzinfo=tz.tzutc())
263         return t
264 
265     return pandas_to_datetime(t, utc=True, infer_datetime_format=True).to_pydatetime()
266 
267 
268 def _time_to_search_dims(time_range):
269     if hasattr(time_range, '__iter__') and len(time_range) == 2:
270         time_range = Range(_to_datetime(time_range[0]), _to_datetime(time_range[1]))
271         if time_range[0] == time_range[1]:
272             return time_range[0]
273         return time_range
274     else:
275         return _to_datetime(time_range)
276 
277 
278 def _convert_to_solar_time(utc, longitude):
279     seconds_per_degree = 240
280     offset_seconds = int(longitude * seconds_per_degree)
281     offset = datetime.timedelta(seconds=offset_seconds)
282     return utc + offset
283 
284 
285 def solar_day(dataset):
286     utc = dataset.center_time
287     bb = dataset.extent.to_crs(geometry.CRS('WGS84')).boundingbox
288     assert bb.left < bb.right  # TODO: Handle dateline?
289     longitude = (bb.left + bb.right) * 0.5
290     solar_time = _convert_to_solar_time(utc, longitude)
291     return np.datetime64(solar_time.date(), 'D')
292
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/datacube/api/query.py b/datacube/api/query.py
--- a/datacube/api/query.py
+++ b/datacube/api/query.py
@@ -21,9 +21,13 @@
 import datetime
 import collections
 import warnings
+import calendar
+import re
+import pandas
 
 from dateutil import tz
 from pandas import to_datetime as pandas_to_datetime
+from pypeg2 import word, attr, List, maybe_some, parse as peg_parse
 import numpy as np
 
 from ..compat import string_types, integer_types
@@ -54,7 +58,7 @@
 
         >>> query.search_terms['time']  # doctest: +NORMALIZE_WHITESPACE
         Range(begin=datetime.datetime(2001, 1, 1, 0, 0, tzinfo=<UTC>), \
-        end=datetime.datetime(2002, 1, 1, 0, 0, tzinfo=<UTC>))
+        end=datetime.datetime(2002, 1, 1, 23, 59, 59, 999999, tzinfo=tzutc()))
 
         By passing in an ``index``, the search parameters will be validated as existing on the ``product``.
 
@@ -264,15 +268,34 @@
 
     return pandas_to_datetime(t, utc=True, infer_datetime_format=True).to_pydatetime()
 
-
 def _time_to_search_dims(time_range):
-    if hasattr(time_range, '__iter__') and len(time_range) == 2:
-        time_range = Range(_to_datetime(time_range[0]), _to_datetime(time_range[1]))
-        if time_range[0] == time_range[1]:
-            return time_range[0]
-        return time_range
-    else:
-        return _to_datetime(time_range)
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", UserWarning)
+        if hasattr(time_range, '__iter__') and len(time_range) == 2:
+            if all(isinstance(n, datetime.datetime) for n in time_range):
+                timelist = list(time_range)
+                timelist[0], timelist[1] = timelist[0].isoformat(), timelist[0].isoformat()
+                time_range = tuple(timelist)
+            time_range = Range(_to_datetime(time_range[0]),
+                               _to_datetime(pandas.Period(time_range[1]).end_time.to_pydatetime()))
+            if time_range[0] == time_range[1]:
+                return time_range[0]
+            return time_range
+
+        elif isinstance(time_range, str):
+            start_time, end_time = Range(_to_datetime(time_range),
+                                         _to_datetime(pandas.Period(time_range).end_time.to_pydatetime()))
+            if start_time == end_time:
+                return start_time
+            time_range = Range(start_time, end_time)
+            return time_range
+        else:
+            timelist = list(time_range)
+            timelist[0], timelist[1] = timelist[0].isoformat(), timelist[0].isoformat()
+            time_range = tuple(timelist)
+            time_range = Range(_to_datetime(time_range[0]),
+                               _to_datetime(pandas.Period(time_range[1]).end_time.to_pydatetime()))
+            return time_range
 
 
 def _convert_to_solar_time(utc, longitude):

{"golden_diff": "diff --git a/datacube/api/query.py b/datacube/api/query.py\n--- a/datacube/api/query.py\n+++ b/datacube/api/query.py\n@@ -21,9 +21,13 @@\n import datetime\n import collections\n import warnings\n+import calendar\n+import re\n+import pandas\n \n from dateutil import tz\n from pandas import to_datetime as pandas_to_datetime\n+from pypeg2 import word, attr, List, maybe_some, parse as peg_parse\n import numpy as np\n \n from ..compat import string_types, integer_types\n@@ -54,7 +58,7 @@\n \n         >>> query.search_terms['time']  # doctest: +NORMALIZE_WHITESPACE\n         Range(begin=datetime.datetime(2001, 1, 1, 0, 0, tzinfo=<UTC>), \\\n-        end=datetime.datetime(2002, 1, 1, 0, 0, tzinfo=<UTC>))\n+        end=datetime.datetime(2002, 1, 1, 23, 59, 59, 999999, tzinfo=tzutc()))\n \n         By passing in an ``index``, the search parameters will be validated as existing on the ``product``.\n \n@@ -264,15 +268,34 @@\n \n     return pandas_to_datetime(t, utc=True, infer_datetime_format=True).to_pydatetime()\n \n-\n def _time_to_search_dims(time_range):\n-    if hasattr(time_range, '__iter__') and len(time_range) == 2:\n-        time_range = Range(_to_datetime(time_range[0]), _to_datetime(time_range[1]))\n-        if time_range[0] == time_range[1]:\n-            return time_range[0]\n-        return time_range\n-    else:\n-        return _to_datetime(time_range)\n+    with warnings.catch_warnings():\n+        warnings.simplefilter(\"ignore\", UserWarning)\n+        if hasattr(time_range, '__iter__') and len(time_range) == 2:\n+            if all(isinstance(n, datetime.datetime) for n in time_range):\n+                timelist = list(time_range)\n+                timelist[0], timelist[1] = timelist[0].isoformat(), timelist[0].isoformat()\n+                time_range = tuple(timelist)\n+            time_range = Range(_to_datetime(time_range[0]),\n+                               _to_datetime(pandas.Period(time_range[1]).end_time.to_pydatetime()))\n+            if time_range[0] == time_range[1]:\n+                return time_range[0]\n+            return time_range\n+\n+        elif isinstance(time_range, str):\n+            start_time, end_time = Range(_to_datetime(time_range),\n+                                         _to_datetime(pandas.Period(time_range).end_time.to_pydatetime()))\n+            if start_time == end_time:\n+                return start_time\n+            time_range = Range(start_time, end_time)\n+            return time_range\n+        else:\n+            timelist = list(time_range)\n+            timelist[0], timelist[1] = timelist[0].isoformat(), timelist[0].isoformat()\n+            time_range = tuple(timelist)\n+            time_range = Range(_to_datetime(time_range[0]),\n+                               _to_datetime(pandas.Period(time_range[1]).end_time.to_pydatetime()))\n+            return time_range\n \n \n def _convert_to_solar_time(utc, longitude):\n", "issue": "Time range strings behave differently between datacube and xarray\nWhen specifying a time range in a datacube:\r\n`dc.load(..., time=('2003-01', '2003-03'))`\r\nis interpreted to be \r\n_2003-01-01 00:00:00 to 2003-03-01 00:00:00_\r\ni.e. values rounds down\r\n\r\nvs\r\n\r\nIn xarray:\r\n`data.sel(time=slice('2003-01', '2003-02'))`\r\nis interpreted to be:\r\n_2003-01-01 00:00:00 to 2003-03-31 23:59:59.999_\r\ni.e. values round out\r\n\r\nIdeally they would behave the same.\r\n\n", "before_files": [{"content": "#\n#    Licensed under the Apache License, Version 2.0 (the \"License\");\n#    you may not use this file except in compliance with the License.\n#    You may obtain a copy of the License at\n#\n#        http://www.apache.org/licenses/LICENSE-2.0\n#\n#    Unless required by applicable law or agreed to in writing, software\n#    distributed under the License is distributed on an \"AS IS\" BASIS,\n#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#    See the License for the specific language governing permissions and\n#    limitations under the License.\n\n\"\"\"\nStorage Query and Access API module\n\"\"\"\n\nfrom __future__ import absolute_import, division, print_function\n\nimport logging\nimport datetime\nimport collections\nimport warnings\n\nfrom dateutil import tz\nfrom pandas import to_datetime as pandas_to_datetime\nimport numpy as np\n\nfrom ..compat import string_types, integer_types\nfrom ..model import Range\nfrom ..utils import geometry, datetime_to_seconds_since_1970\n\n_LOG = logging.getLogger(__name__)\n\n\nGroupBy = collections.namedtuple('GroupBy', ['dimension', 'group_by_func', 'units', 'sort_key'])\n\nFLOAT_TOLERANCE = 0.0000001  # TODO: For DB query, use some sort of 'contains' query, rather than range overlap.\nSPATIAL_KEYS = ('latitude', 'lat', 'y', 'longitude', 'lon', 'long', 'x')\nCRS_KEYS = ('crs', 'coordinate_reference_system')\nOTHER_KEYS = ('measurements', 'group_by', 'output_crs', 'resolution', 'set_nan', 'product', 'geopolygon', 'like',\n              'source_filter')\n\n\nclass Query(object):\n    def __init__(self, index=None, product=None, geopolygon=None, like=None, **search_terms):\n        \"\"\"Parses search terms in preparation for querying the Data Cube Index.\n\n        Create a :class:`Query` object by passing it a set of search terms as keyword arguments.\n\n        >>> query = Query(product='ls5_nbar_albers', time=('2001-01-01', '2002-01-01'))\n\n        Use by accessing :attr:`search_terms`:\n\n        >>> query.search_terms['time']  # doctest: +NORMALIZE_WHITESPACE\n        Range(begin=datetime.datetime(2001, 1, 1, 0, 0, tzinfo=<UTC>), \\\n        end=datetime.datetime(2002, 1, 1, 0, 0, tzinfo=<UTC>))\n\n        By passing in an ``index``, the search parameters will be validated as existing on the ``product``.\n\n        Used by :meth:`datacube.Datacube.find_datasets` and :meth:`datacube.Datacube.load`.\n\n        :param datacube.index.Index index: An optional `index` object, if checking of field names is desired.\n        :param str product: name of product\n        :param geopolygon: spatial bounds of the search\n        :type geopolygon: geometry.Geometry or None\n        :param xarray.Dataset like: spatio-temporal bounds of `like` are used for the search\n        :param search_terms:\n         * `measurements` - list of measurements to retrieve\n         * `latitude`, `lat`, `y`, `longitude`, `lon`, `long`, `x` - tuples (min, max) bounding spatial dimensions\n         * `crs` - spatial coordinate reference system to interpret the spatial bounds\n         * `group_by` - observation grouping method. One of `time`, `solar_day`. Default is `time`\n        \"\"\"\n        self.product = product\n        self.geopolygon = query_geopolygon(geopolygon=geopolygon, **search_terms)\n        if 'source_filter' in search_terms and search_terms['source_filter'] is not None:\n            self.source_filter = Query(**search_terms['source_filter'])\n        else:\n            self.source_filter = None\n\n        remaining_keys = set(search_terms.keys()) - set(SPATIAL_KEYS + CRS_KEYS + OTHER_KEYS)\n        if index:\n            unknown_keys = remaining_keys - set(index.datasets.get_field_names())\n            # TODO: What about keys source filters, and what if the keys don't match up with this product...\n            if unknown_keys:\n                raise LookupError('Unknown arguments: ', unknown_keys)\n\n        self.search = {}\n        for key in remaining_keys:\n            self.search.update(_values_to_search(**{key: search_terms[key]}))\n\n        if like:\n            assert self.geopolygon is None, \"'like' with other spatial bounding parameters is not supported\"\n            self.geopolygon = getattr(like, 'extent', self.geopolygon)\n\n            if 'time' not in self.search:\n                time_coord = like.coords.get('time')\n                if time_coord is not None:\n                    self.search['time'] = _time_to_search_dims(\n                        (pandas_to_datetime(time_coord.values[0]).to_pydatetime(),\n                         pandas_to_datetime(time_coord.values[-1]).to_pydatetime()\n                         + datetime.timedelta(milliseconds=1))  # TODO: inclusive time searches\n                    )\n\n    @property\n    def search_terms(self):\n        \"\"\"\n        Access the search terms as a dictionary.\n\n        :type: dict\n        \"\"\"\n        kwargs = {}\n        kwargs.update(self.search)\n        if self.geopolygon:\n            geo_bb = self.geopolygon.to_crs(geometry.CRS('EPSG:4326')).boundingbox\n            if geo_bb.bottom != geo_bb.top:\n                kwargs['lat'] = Range(geo_bb.bottom, geo_bb.top)\n            else:\n                kwargs['lat'] = geo_bb.bottom\n            if geo_bb.left != geo_bb.right:\n                kwargs['lon'] = Range(geo_bb.left, geo_bb.right)\n            else:\n                kwargs['lon'] = geo_bb.left\n        if self.product:\n            kwargs['product'] = self.product\n        if self.source_filter:\n            kwargs['source_filter'] = self.source_filter.search_terms\n        return kwargs\n\n    def __repr__(self):\n        return self.__str__()\n\n    def __str__(self):\n        return \"\"\"Datacube Query:\n        type = {type}\n        search = {search}\n        geopolygon = {geopolygon}\n        \"\"\".format(type=self.product,\n                   search=self.search,\n                   geopolygon=self.geopolygon)\n\n\ndef query_geopolygon(geopolygon=None, **kwargs):\n    spatial_dims = {dim: v for dim, v in kwargs.items() if dim in SPATIAL_KEYS}\n    crs = {v for k, v in kwargs.items() if k in CRS_KEYS}\n    if len(crs) == 1:\n        spatial_dims['crs'] = crs.pop()\n    elif len(crs) > 1:\n        raise ValueError('Spatial dimensions must be in the same coordinate reference system: {}'.format(crs))\n\n    if geopolygon is not None and len(spatial_dims) > 0:\n        raise ValueError('Cannot specify \"geopolygon\" and one of %s at the same time' % (SPATIAL_KEYS + CRS_KEYS,))\n\n    if geopolygon is None:\n        return _range_to_geopolygon(**spatial_dims)\n\n    return geopolygon\n\n\ndef query_group_by(group_by='time', **kwargs):\n    time_grouper = GroupBy(dimension='time',\n                           group_by_func=lambda ds: ds.center_time,\n                           units='seconds since 1970-01-01 00:00:00',\n                           sort_key=lambda ds: ds.center_time)\n\n    solar_day_grouper = GroupBy(dimension='time',\n                                group_by_func=solar_day,\n                                units='seconds since 1970-01-01 00:00:00',\n                                sort_key=lambda ds: ds.center_time)\n\n    group_by_map = {\n        None: time_grouper,\n        'time': time_grouper,\n        'solar_day': solar_day_grouper\n    }\n\n    try:\n        return group_by_map[group_by]\n    except KeyError:\n        raise LookupError('No group by function for', group_by)\n\n\ndef _range_to_geopolygon(**kwargs):\n    input_crs = None\n    input_coords = {'left': None, 'bottom': None, 'right': None, 'top': None}\n    for key, value in kwargs.items():\n        if value is None:\n            continue\n        key = key.lower()\n        if key in ['latitude', 'lat', 'y']:\n            input_coords['top'], input_coords['bottom'] = _value_to_range(value)\n        if key in ['longitude', 'lon', 'long', 'x']:\n            input_coords['left'], input_coords['right'] = _value_to_range(value)\n        if key in ['crs', 'coordinate_reference_system']:\n            input_crs = geometry.CRS(value)\n    input_crs = input_crs or geometry.CRS('EPSG:4326')\n    if any(v is not None for v in input_coords.values()):\n        if input_coords['left'] == input_coords['right']:\n            if input_coords['top'] == input_coords['bottom']:\n                return geometry.point(input_coords['left'], input_coords['top'], crs=input_crs)\n            else:\n                points = [(input_coords['left'], input_coords['bottom']),\n                          (input_coords['left'], input_coords['top'])]\n                return geometry.line(points, crs=input_crs)\n        else:\n            if input_coords['top'] == input_coords['bottom']:\n                points = [(input_coords['left'], input_coords['top']),\n                          (input_coords['right'], input_coords['top'])]\n                return geometry.line(points, crs=input_crs)\n            else:\n                points = [\n                    (input_coords['left'], input_coords['top']),\n                    (input_coords['right'], input_coords['top']),\n                    (input_coords['right'], input_coords['bottom']),\n                    (input_coords['left'], input_coords['bottom']),\n                    (input_coords['left'], input_coords['top'])\n                ]\n                return geometry.polygon(points, crs=input_crs)\n    return None\n\n\ndef _value_to_range(value):\n    if isinstance(value, string_types + integer_types + (float,)):\n        value = float(value)\n        return value, value\n    else:\n        return float(value[0]), float(value[-1])\n\n\ndef _values_to_search(**kwargs):\n    search = {}\n    for key, value in kwargs.items():\n        if key.lower() in ('time', 't'):\n            search['time'] = _time_to_search_dims(value)\n        elif key not in ['latitude', 'lat', 'y'] + ['longitude', 'lon', 'x']:\n            if isinstance(value, collections.Sequence) and len(value) == 2:\n                search[key] = Range(*value)\n            else:\n                search[key] = value\n    return search\n\n\ndef _datetime_to_timestamp(dt):\n    if not isinstance(dt, datetime.datetime) and not isinstance(dt, datetime.date):\n        dt = _to_datetime(dt)\n    return datetime_to_seconds_since_1970(dt)\n\n\ndef _to_datetime(t):\n    if isinstance(t, integer_types + (float,)):\n        t = datetime.datetime.fromtimestamp(t, tz=tz.tzutc())\n\n    if isinstance(t, tuple):\n        t = datetime.datetime(*t, tzinfo=tz.tzutc())\n    elif isinstance(t, string_types):\n        try:\n            t = datetime.datetime.strptime(t, \"%Y-%m-%dT%H:%M:%S.%fZ\")\n        except ValueError:\n            pass\n    elif isinstance(t, datetime.datetime):\n        if t.tzinfo is None:\n            t = t.replace(tzinfo=tz.tzutc())\n        return t\n\n    return pandas_to_datetime(t, utc=True, infer_datetime_format=True).to_pydatetime()\n\n\ndef _time_to_search_dims(time_range):\n    if hasattr(time_range, '__iter__') and len(time_range) == 2:\n        time_range = Range(_to_datetime(time_range[0]), _to_datetime(time_range[1]))\n        if time_range[0] == time_range[1]:\n            return time_range[0]\n        return time_range\n    else:\n        return _to_datetime(time_range)\n\n\ndef _convert_to_solar_time(utc, longitude):\n    seconds_per_degree = 240\n    offset_seconds = int(longitude * seconds_per_degree)\n    offset = datetime.timedelta(seconds=offset_seconds)\n    return utc + offset\n\n\ndef solar_day(dataset):\n    utc = dataset.center_time\n    bb = dataset.extent.to_crs(geometry.CRS('WGS84')).boundingbox\n    assert bb.left < bb.right  # TODO: Handle dateline?\n    longitude = (bb.left + bb.right) * 0.5\n    solar_time = _convert_to_solar_time(utc, longitude)\n    return np.datetime64(solar_time.date(), 'D')\n", "path": "datacube/api/query.py"}], "after_files": [{"content": "#\n#    Licensed under the Apache License, Version 2.0 (the \"License\");\n#    you may not use this file except in compliance with the License.\n#    You may obtain a copy of the License at\n#\n#        http://www.apache.org/licenses/LICENSE-2.0\n#\n#    Unless required by applicable law or agreed to in writing, software\n#    distributed under the License is distributed on an \"AS IS\" BASIS,\n#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#    See the License for the specific language governing permissions and\n#    limitations under the License.\n\n\"\"\"\nStorage Query and Access API module\n\"\"\"\n\nfrom __future__ import absolute_import, division, print_function\n\nimport logging\nimport datetime\nimport collections\nimport warnings\nimport calendar\nimport re\nimport pandas\n\nfrom dateutil import tz\nfrom pandas import to_datetime as pandas_to_datetime\nfrom pypeg2 import word, attr, List, maybe_some, parse as peg_parse\nimport numpy as np\n\nfrom ..compat import string_types, integer_types\nfrom ..model import Range\nfrom ..utils import geometry, datetime_to_seconds_since_1970\n\n_LOG = logging.getLogger(__name__)\n\n\nGroupBy = collections.namedtuple('GroupBy', ['dimension', 'group_by_func', 'units', 'sort_key'])\n\nFLOAT_TOLERANCE = 0.0000001  # TODO: For DB query, use some sort of 'contains' query, rather than range overlap.\nSPATIAL_KEYS = ('latitude', 'lat', 'y', 'longitude', 'lon', 'long', 'x')\nCRS_KEYS = ('crs', 'coordinate_reference_system')\nOTHER_KEYS = ('measurements', 'group_by', 'output_crs', 'resolution', 'set_nan', 'product', 'geopolygon', 'like',\n              'source_filter')\n\n\nclass Query(object):\n    def __init__(self, index=None, product=None, geopolygon=None, like=None, **search_terms):\n        \"\"\"Parses search terms in preparation for querying the Data Cube Index.\n\n        Create a :class:`Query` object by passing it a set of search terms as keyword arguments.\n\n        >>> query = Query(product='ls5_nbar_albers', time=('2001-01-01', '2002-01-01'))\n\n        Use by accessing :attr:`search_terms`:\n\n        >>> query.search_terms['time']  # doctest: +NORMALIZE_WHITESPACE\n        Range(begin=datetime.datetime(2001, 1, 1, 0, 0, tzinfo=<UTC>), \\\n        end=datetime.datetime(2002, 1, 1, 23, 59, 59, 999999, tzinfo=tzutc()))\n\n        By passing in an ``index``, the search parameters will be validated as existing on the ``product``.\n\n        Used by :meth:`datacube.Datacube.find_datasets` and :meth:`datacube.Datacube.load`.\n\n        :param datacube.index.Index index: An optional `index` object, if checking of field names is desired.\n        :param str product: name of product\n        :param geopolygon: spatial bounds of the search\n        :type geopolygon: geometry.Geometry or None\n        :param xarray.Dataset like: spatio-temporal bounds of `like` are used for the search\n        :param search_terms:\n         * `measurements` - list of measurements to retrieve\n         * `latitude`, `lat`, `y`, `longitude`, `lon`, `long`, `x` - tuples (min, max) bounding spatial dimensions\n         * `crs` - spatial coordinate reference system to interpret the spatial bounds\n         * `group_by` - observation grouping method. One of `time`, `solar_day`. Default is `time`\n        \"\"\"\n        self.product = product\n        self.geopolygon = query_geopolygon(geopolygon=geopolygon, **search_terms)\n        if 'source_filter' in search_terms and search_terms['source_filter'] is not None:\n            self.source_filter = Query(**search_terms['source_filter'])\n        else:\n            self.source_filter = None\n\n        remaining_keys = set(search_terms.keys()) - set(SPATIAL_KEYS + CRS_KEYS + OTHER_KEYS)\n        if index:\n            unknown_keys = remaining_keys - set(index.datasets.get_field_names())\n            # TODO: What about keys source filters, and what if the keys don't match up with this product...\n            if unknown_keys:\n                raise LookupError('Unknown arguments: ', unknown_keys)\n\n        self.search = {}\n        for key in remaining_keys:\n            self.search.update(_values_to_search(**{key: search_terms[key]}))\n\n        if like:\n            assert self.geopolygon is None, \"'like' with other spatial bounding parameters is not supported\"\n            self.geopolygon = getattr(like, 'extent', self.geopolygon)\n\n            if 'time' not in self.search:\n                time_coord = like.coords.get('time')\n                if time_coord is not None:\n                    self.search['time'] = _time_to_search_dims(\n                        (pandas_to_datetime(time_coord.values[0]).to_pydatetime(),\n                         pandas_to_datetime(time_coord.values[-1]).to_pydatetime()\n                         + datetime.timedelta(milliseconds=1))  # TODO: inclusive time searches\n                    )\n\n    @property\n    def search_terms(self):\n        \"\"\"\n        Access the search terms as a dictionary.\n\n        :type: dict\n        \"\"\"\n        kwargs = {}\n        kwargs.update(self.search)\n        if self.geopolygon:\n            geo_bb = self.geopolygon.to_crs(geometry.CRS('EPSG:4326')).boundingbox\n            if geo_bb.bottom != geo_bb.top:\n                kwargs['lat'] = Range(geo_bb.bottom, geo_bb.top)\n            else:\n                kwargs['lat'] = geo_bb.bottom\n            if geo_bb.left != geo_bb.right:\n                kwargs['lon'] = Range(geo_bb.left, geo_bb.right)\n            else:\n                kwargs['lon'] = geo_bb.left\n        if self.product:\n            kwargs['product'] = self.product\n        if self.source_filter:\n            kwargs['source_filter'] = self.source_filter.search_terms\n        return kwargs\n\n    def __repr__(self):\n        return self.__str__()\n\n    def __str__(self):\n        return \"\"\"Datacube Query:\n        type = {type}\n        search = {search}\n        geopolygon = {geopolygon}\n        \"\"\".format(type=self.product,\n                   search=self.search,\n                   geopolygon=self.geopolygon)\n\n\ndef query_geopolygon(geopolygon=None, **kwargs):\n    spatial_dims = {dim: v for dim, v in kwargs.items() if dim in SPATIAL_KEYS}\n    crs = {v for k, v in kwargs.items() if k in CRS_KEYS}\n    if len(crs) == 1:\n        spatial_dims['crs'] = crs.pop()\n    elif len(crs) > 1:\n        raise ValueError('Spatial dimensions must be in the same coordinate reference system: {}'.format(crs))\n\n    if geopolygon is not None and len(spatial_dims) > 0:\n        raise ValueError('Cannot specify \"geopolygon\" and one of %s at the same time' % (SPATIAL_KEYS + CRS_KEYS,))\n\n    if geopolygon is None:\n        return _range_to_geopolygon(**spatial_dims)\n\n    return geopolygon\n\n\ndef query_group_by(group_by='time', **kwargs):\n    time_grouper = GroupBy(dimension='time',\n                           group_by_func=lambda ds: ds.center_time,\n                           units='seconds since 1970-01-01 00:00:00',\n                           sort_key=lambda ds: ds.center_time)\n\n    solar_day_grouper = GroupBy(dimension='time',\n                                group_by_func=solar_day,\n                                units='seconds since 1970-01-01 00:00:00',\n                                sort_key=lambda ds: ds.center_time)\n\n    group_by_map = {\n        None: time_grouper,\n        'time': time_grouper,\n        'solar_day': solar_day_grouper\n    }\n\n    try:\n        return group_by_map[group_by]\n    except KeyError:\n        raise LookupError('No group by function for', group_by)\n\n\ndef _range_to_geopolygon(**kwargs):\n    input_crs = None\n    input_coords = {'left': None, 'bottom': None, 'right': None, 'top': None}\n    for key, value in kwargs.items():\n        if value is None:\n            continue\n        key = key.lower()\n        if key in ['latitude', 'lat', 'y']:\n            input_coords['top'], input_coords['bottom'] = _value_to_range(value)\n        if key in ['longitude', 'lon', 'long', 'x']:\n            input_coords['left'], input_coords['right'] = _value_to_range(value)\n        if key in ['crs', 'coordinate_reference_system']:\n            input_crs = geometry.CRS(value)\n    input_crs = input_crs or geometry.CRS('EPSG:4326')\n    if any(v is not None for v in input_coords.values()):\n        if input_coords['left'] == input_coords['right']:\n            if input_coords['top'] == input_coords['bottom']:\n                return geometry.point(input_coords['left'], input_coords['top'], crs=input_crs)\n            else:\n                points = [(input_coords['left'], input_coords['bottom']),\n                          (input_coords['left'], input_coords['top'])]\n                return geometry.line(points, crs=input_crs)\n        else:\n            if input_coords['top'] == input_coords['bottom']:\n                points = [(input_coords['left'], input_coords['top']),\n                          (input_coords['right'], input_coords['top'])]\n                return geometry.line(points, crs=input_crs)\n            else:\n                points = [\n                    (input_coords['left'], input_coords['top']),\n                    (input_coords['right'], input_coords['top']),\n                    (input_coords['right'], input_coords['bottom']),\n                    (input_coords['left'], input_coords['bottom']),\n                    (input_coords['left'], input_coords['top'])\n                ]\n                return geometry.polygon(points, crs=input_crs)\n    return None\n\n\ndef _value_to_range(value):\n    if isinstance(value, string_types + integer_types + (float,)):\n        value = float(value)\n        return value, value\n    else:\n        return float(value[0]), float(value[-1])\n\n\ndef _values_to_search(**kwargs):\n    search = {}\n    for key, value in kwargs.items():\n        if key.lower() in ('time', 't'):\n            search['time'] = _time_to_search_dims(value)\n        elif key not in ['latitude', 'lat', 'y'] + ['longitude', 'lon', 'x']:\n            if isinstance(value, collections.Sequence) and len(value) == 2:\n                search[key] = Range(*value)\n            else:\n                search[key] = value\n    return search\n\n\ndef _datetime_to_timestamp(dt):\n    if not isinstance(dt, datetime.datetime) and not isinstance(dt, datetime.date):\n        dt = _to_datetime(dt)\n    return datetime_to_seconds_since_1970(dt)\n\n\ndef _to_datetime(t):\n    if isinstance(t, integer_types + (float,)):\n        t = datetime.datetime.fromtimestamp(t, tz=tz.tzutc())\n\n    if isinstance(t, tuple):\n        t = datetime.datetime(*t, tzinfo=tz.tzutc())\n    elif isinstance(t, string_types):\n        try:\n            t = datetime.datetime.strptime(t, \"%Y-%m-%dT%H:%M:%S.%fZ\")\n        except ValueError:\n            pass\n    elif isinstance(t, datetime.datetime):\n        if t.tzinfo is None:\n            t = t.replace(tzinfo=tz.tzutc())\n        return t\n\n    return pandas_to_datetime(t, utc=True, infer_datetime_format=True).to_pydatetime()\n\ndef _time_to_search_dims(time_range):\n    with warnings.catch_warnings():\n        warnings.simplefilter(\"ignore\", UserWarning)\n        if hasattr(time_range, '__iter__') and len(time_range) == 2:\n            if all(isinstance(n, datetime.datetime) for n in time_range):\n                timelist = list(time_range)\n                timelist[0], timelist[1] = timelist[0].isoformat(), timelist[0].isoformat()\n                time_range = tuple(timelist)\n            time_range = Range(_to_datetime(time_range[0]),\n                               _to_datetime(pandas.Period(time_range[1]).end_time.to_pydatetime()))\n            if time_range[0] == time_range[1]:\n                return time_range[0]\n            return time_range\n\n        elif isinstance(time_range, str):\n            start_time, end_time = Range(_to_datetime(time_range),\n                                         _to_datetime(pandas.Period(time_range).end_time.to_pydatetime()))\n            if start_time == end_time:\n                return start_time\n            time_range = Range(start_time, end_time)\n            return time_range\n        else:\n            timelist = list(time_range)\n            timelist[0], timelist[1] = timelist[0].isoformat(), timelist[0].isoformat()\n            time_range = tuple(timelist)\n            time_range = Range(_to_datetime(time_range[0]),\n                               _to_datetime(pandas.Period(time_range[1]).end_time.to_pydatetime()))\n            return time_range\n\n\ndef _convert_to_solar_time(utc, longitude):\n    seconds_per_degree = 240\n    offset_seconds = int(longitude * seconds_per_degree)\n    offset = datetime.timedelta(seconds=offset_seconds)\n    return utc + offset\n\n\ndef solar_day(dataset):\n    utc = dataset.center_time\n    bb = dataset.extent.to_crs(geometry.CRS('WGS84')).boundingbox\n    assert bb.left < bb.right  # TODO: Handle dateline?\n    longitude = (bb.left + bb.right) * 0.5\n    solar_time = _convert_to_solar_time(utc, longitude)\n    return np.datetime64(solar_time.date(), 'D')\n", "path": "datacube/api/query.py"}]}