Repository: pandas-dev/pandas

Issue:
ENH: Support datetime.time objects with tzinfo in between_time and at_time
The functions for DataFrame, "between_time" and "at_time" are unfortunately not time zone aware.

I suggest adding a timezone parameter to these functions to clearly specify which time zone the requested time belongs to. The sampler should take into account the time zone of data frame, and the supplied time zone.

It will also be interesting to be able to sample at a specific time both daylight savings adjustment aware and agnostic. For example it will be interesting to sample at every 09:00 New York time (whether it is summer time or winter time, and irrespective of how we stored the time in our dataframe). For this a time zone parameter will not be sufficient, a time zone history will need to be supplied.

Time zone history for a specific city can be obtained by the user and supplied to the function:
https://www.timeanddate.com/time/zone/usa/new-york




Discussion:
@cozgenc : Thanks for reporting this!  Could you provide an example to illustrate the concern that you bring up in the first line?

cc @mroeschke 
`between_time` and `at_time` already function when the index is tz-aware (the specified times are local to that timezone). I'm also not clear of the primary concern.

```
In [1]: dti = pd.date_range('2018', periods=3, freq='H', tz='US/Pacific')

In [4]: df
Out[4]:
                           0
2018-01-01 00:00:00-08:00  0
2018-01-01 01:00:00-08:00  1
2018-01-01 02:00:00-08:00  2

In [5]: df.between_time('1:00', '2:00')
Out[5]:
                           0
2018-01-01 01:00:00-08:00  1
2018-01-01 02:00:00-08:00  2

In [6]: df.at_time('1:00')
Out[6]:
                           0
2018-01-01 01:00:00-08:00  1
```
Please tell me how you will be able to get the rows for US/Eastern 09:00am, if the dataframe time zone is set to Europe/Berlin for example without actually accounting for time difference?

You are simply creating dependency between the code that prepared the dataframe, and the code that consumes the dataframe. If you add timezone parameter to these functions you may explicitly specify which time you are talking about. Time without a timezone doesn't represent a universal point in time.
When using `at_time` and `between_time`, specified arguments are assumed to be in the local time relative to the timezone of the DatetimeIndex. Therefore, you can use `tz_convert` to switch between timezones when indexing with these methods.

However, it appears that currently passing `datetime.time` objects with `tzinfo` are not supported yet, which would address your primary concern I believe:

```
In [3]: df
Out[3]:
                           0
2018-01-01 00:00:00-08:00  0
2018-01-01 01:00:00-08:00  1
2018-01-01 02:00:00-08:00  2

In [8]: t_eastern = time(4, tzinfo=pytz.timezone('US/Eastern'))

In [10]: df.at_time(t_eastern)
NotImplementedError: argument 'time' with timezone info is not supported
```

Resolving patch:
diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py
--- a/pandas/core/indexes/datetimes.py
+++ b/pandas/core/indexes/datetimes.py
@@ -1302,20 +1302,19 @@ def indexer_at_time(self, time, asof=False):
         --------
         indexer_between_time, DataFrame.at_time
         """
-        from dateutil.parser import parse
-
         if asof:
             raise NotImplementedError("'asof' argument is not supported")
 
         if isinstance(time, compat.string_types):
+            from dateutil.parser import parse
             time = parse(time).time()
 
         if time.tzinfo:
-            # TODO
-            raise NotImplementedError("argument 'time' with timezone info is "
-                                      "not supported")
-
-        time_micros = self._get_time_micros()
+            if self.tz is None:
+                raise ValueError("Index must be timezone aware.")
+            time_micros = self.tz_convert(time.tzinfo)._get_time_micros()
+        else:
+            time_micros = self._get_time_micros()
         micros = _time_to_micros(time)
         return (micros == time_micros).nonzero()[0]
 


Test patch:
diff --git a/pandas/tests/frame/test_timeseries.py b/pandas/tests/frame/test_timeseries.py
--- a/pandas/tests/frame/test_timeseries.py
+++ b/pandas/tests/frame/test_timeseries.py
@@ -6,6 +6,7 @@
 
 import numpy as np
 import pytest
+import pytz
 
 from pandas.compat import product
 
@@ -647,6 +648,28 @@ def test_at_time(self):
         rs = ts.at_time('16:00')
         assert len(rs) == 0
 
+    @pytest.mark.parametrize('hour', ['1:00', '1:00AM', time(1),
+                                      time(1, tzinfo=pytz.UTC)])
+    def test_at_time_errors(self, hour):
+        # GH 24043
+        dti = pd.date_range('2018', periods=3, freq='H')
+        df = pd.DataFrame(list(range(len(dti))), index=dti)
+        if getattr(hour, 'tzinfo', None) is None:
+            result = df.at_time(hour)
+            expected = df.iloc[1:2]
+            tm.assert_frame_equal(result, expected)
+        else:
+            with pytest.raises(ValueError, match="Index must be timezone"):
+                df.at_time(hour)
+
+    def test_at_time_tz(self):
+        # GH 24043
+        dti = pd.date_range('2018', periods=3, freq='H', tz='US/Pacific')
+        df = pd.DataFrame(list(range(len(dti))), index=dti)
+        result = df.at_time(time(4, tzinfo=pytz.timezone('US/Eastern')))
+        expected = df.iloc[1:2]
+        tm.assert_frame_equal(result, expected)
+
     def test_at_time_raises(self):
         # GH20725
         df = pd.DataFrame([[1, 2, 3], [4, 5, 6]])