Optionally surface exceptions that happen under executors by reading
[python_utils.git] / datetime_utils.py
index c100057d3a306a0efb482418479a3857dcc07b93..3565936fce66c1197a04a8926f902452e6350ac4 100644 (file)
@@ -16,18 +16,87 @@ import constants
 logger = logging.getLogger(__name__)
 
 
-def replace_timezone(dt: datetime.datetime,
-                     tz: datetime.tzinfo) -> datetime.datetime:
+def is_timezone_aware(dt: datetime.datetime) -> bool:
+    """See: https://docs.python.org/3/library/datetime.html
+                               #determining-if-an-object-is-aware-or-naive
+
+    >>> is_timezone_aware(datetime.datetime.now())
+    False
+
+    >>> is_timezone_aware(now_pacific())
+    True
+
     """
-    Replaces the timezone on a datetime object.
+    return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
+
+
+def is_timezone_naive(dt: datetime.datetime) -> bool:
+    return not is_timezone_aware(dt)
+
+
+def replace_timezone(dt: datetime.datetime, tz: datetime.tzinfo) -> datetime.datetime:
+    """
+    Replaces the timezone on a datetime object directly (leaving
+    the year, month, day, hour, minute, second, micro, etc... alone).
+    Note: this changes the instant to which this dt refers.
 
     >>> from pytz import UTC
     >>> d = now_pacific()
     >>> d.tzinfo.tzname(d)[0]     # Note: could be PST or PDT
     'P'
+    >>> h = d.hour
     >>> o = replace_timezone(d, UTC)
     >>> o.tzinfo.tzname(o)
     'UTC'
+    >>> o.hour == h
+    True
+
+    """
+    return datetime.datetime(
+        dt.year,
+        dt.month,
+        dt.day,
+        dt.hour,
+        dt.minute,
+        dt.second,
+        dt.microsecond,
+        tzinfo=tz,
+    )
+
+
+def replace_time_timezone(t: datetime.time, tz: datetime.tzinfo) -> datetime.time:
+    """
+    Replaces the timezone on a datetime.time directly without performing
+    any translation.
+
+    >>> t = datetime.time(8, 15, 12, 0, pytz.UTC)
+    >>> t.tzname()
+    'UTC'
+
+    >>> t = replace_time_timezone(t, pytz.timezone('US/Pacific'))
+    >>> t.tzname()
+    'US/Pacific'
+
+    """
+    return t.replace(tzinfo=tz)
+
+
+def translate_timezone(dt: datetime.datetime, tz: datetime.tzinfo) -> datetime.datetime:
+    """
+    Translates dt into a different timezone by adjusting the year, month,
+    day, hour, minute, second, micro, etc... appropriately.  The returned
+    dt is the same instant in another timezone.
+
+    >>> from pytz import UTC
+    >>> d = now_pacific()
+    >>> d.tzinfo.tzname(d)[0]     # Note: could be PST or PDT
+    'P'
+    >>> h = d.hour
+    >>> o = translate_timezone(d, UTC)
+    >>> o.tzinfo.tzname(o)
+    'UTC'
+    >>> o.hour == h
+    False
 
     """
     return dt.replace(tzinfo=None).astimezone(tz=tz)
@@ -42,9 +111,9 @@ def now() -> datetime.datetime:
 
 def now_pacific() -> datetime.datetime:
     """
-    What time is it?  Result in US/Pacifit time (PST/PDT)
+    What time is it?  Result in US/Pacific time (PST/PDT)
     """
-    return replace_timezone(now(), pytz.timezone("US/Pacific"))
+    return datetime.datetime.now(pytz.timezone("US/Pacific"))
 
 
 def date_to_datetime(date: datetime.date) -> datetime.datetime:
@@ -56,16 +125,48 @@ def date_to_datetime(date: datetime.date) -> datetime.datetime:
     datetime.datetime(2021, 12, 25, 0, 0)
 
     """
-    return datetime.datetime(
-        date.year,
-        date.month,
-        date.day,
-        0, 0, 0, 0
-    )
+    return datetime.datetime(date.year, date.month, date.day, 0, 0, 0, 0)
+
+
+def time_to_datetime_today(time: datetime.time) -> datetime.datetime:
+    """
+    Given a time, returns that time as a datetime with a date component
+    set based on the current date.  If the time passed is timezone aware,
+    the resulting datetime will also be (and will use the same tzinfo).
+    If the time is timezone naive, the datetime returned will be too.
+
+    >>> t = datetime.time(13, 14, 0)
+    >>> d = now_pacific().date()
+    >>> dt = time_to_datetime_today(t)
+    >>> dt.date() == d
+    True
+
+    >>> dt.time() == t
+    True
+
+    >>> dt.tzinfo == t.tzinfo
+    True
+
+    >>> dt.tzinfo == None
+    True
+
+    >>> t = datetime.time(8, 15, 12, 0, pytz.UTC)
+    >>> t.tzinfo == None
+    False
 
+    >>> dt = time_to_datetime_today(t)
+    >>> dt.tzinfo == None
+    False
 
-def date_and_time_to_datetime(date: datetime.date,
-                              time: datetime.time) -> datetime.datetime:
+    """
+    now = now_pacific()
+    tz = time.tzinfo
+    return datetime.datetime.combine(now, time, tz)
+
+
+def date_and_time_to_datetime(
+    date: datetime.date, time: datetime.time
+) -> datetime.datetime:
     """
     Given a date and time, merge them and return a datetime.
 
@@ -88,7 +189,7 @@ def date_and_time_to_datetime(date: datetime.date,
 
 
 def datetime_to_date_and_time(
-        dt: datetime.datetime
+    dt: datetime.datetime,
 ) -> Tuple[datetime.date, datetime.time]:
     """Return the component date and time objects of a datetime.
 
@@ -130,6 +231,7 @@ def datetime_to_time(dt: datetime.datetime) -> datetime.time:
 
 class TimeUnit(enum.Enum):
     """An enum to represent units with which we can compute deltas."""
+
     MONDAYS = 0
     TUESDAYS = 1
     WEDNESDAYS = 2
@@ -160,9 +262,7 @@ class TimeUnit(enum.Enum):
 
 
 def n_timeunits_from_base(
-    count: int,
-    unit: TimeUnit,
-    base: datetime.datetime
+    count: int, unit: TimeUnit, base: datetime.datetime
 ) -> datetime.datetime:
     """Return a datetime that is N units before/after a base datetime.
     e.g.  3 Wednesdays from base datetime, 2 weeks from base date, 10
@@ -243,10 +343,8 @@ def n_timeunits_from_base(
             if base.year != old_year:
                 skips = holidays.US(years=base.year).keys()
             if (
-                    base.weekday() < 5 and
-                    datetime.date(base.year,
-                                  base.month,
-                                  base.day) not in skips
+                base.weekday() < 5
+                and datetime.date(base.year, base.month, base.day) not in skips
             ):
                 count -= 1
         return base
@@ -291,13 +389,17 @@ def n_timeunits_from_base(
             base.tzinfo,
         )
 
-    if unit not in set([TimeUnit.MONDAYS,
-                        TimeUnit.TUESDAYS,
-                        TimeUnit.WEDNESDAYS,
-                        TimeUnit.THURSDAYS,
-                        TimeUnit.FRIDAYS,
-                        TimeUnit.SATURDAYS,
-                        TimeUnit.SUNDAYS]):
+    if unit not in set(
+        [
+            TimeUnit.MONDAYS,
+            TimeUnit.TUESDAYS,
+            TimeUnit.WEDNESDAYS,
+            TimeUnit.THURSDAYS,
+            TimeUnit.FRIDAYS,
+            TimeUnit.SATURDAYS,
+            TimeUnit.SUNDAYS,
+        ]
+    ):
         raise ValueError(unit)
 
     # N weekdays from base (e.g. 4 wednesdays from today)
@@ -315,14 +417,14 @@ def n_timeunits_from_base(
 
 
 def get_format_string(
-        *,
-        date_time_separator=" ",
-        include_timezone=True,
-        include_dayname=False,
-        use_month_abbrevs=False,
-        include_seconds=True,
-        include_fractional=False,
-        twelve_hour=True,
+    *,
+    date_time_separator=" ",
+    include_timezone=True,
+    include_dayname=False,
+    use_month_abbrevs=False,
+    include_seconds=True,
+    include_fractional=False,
+    twelve_hour=True,
 ) -> str:
     """
     Helper to return a format string without looking up the documentation
@@ -397,20 +499,21 @@ def datetime_to_string(
         include_dayname=include_dayname,
         include_seconds=include_seconds,
         include_fractional=include_fractional,
-        twelve_hour=twelve_hour)
+        twelve_hour=twelve_hour,
+    )
     return dt.strftime(fstring).strip()
 
 
 def string_to_datetime(
-        txt: str,
-        *,
-        date_time_separator=" ",
-        include_timezone=True,
-        include_dayname=False,
-        use_month_abbrevs=False,
-        include_seconds=True,
-        include_fractional=False,
-        twelve_hour=True,
+    txt: str,
+    *,
+    date_time_separator=" ",
+    include_timezone=True,
+    include_dayname=False,
+    use_month_abbrevs=False,
+    include_seconds=True,
+    include_fractional=False,
+    twelve_hour=True,
 ) -> Tuple[datetime.datetime, str]:
     """A nice way to convert a string into a datetime.  Returns both the
     datetime and the format string used to parse it.  Also consider
@@ -429,11 +532,9 @@ def string_to_datetime(
         include_dayname=include_dayname,
         include_seconds=include_seconds,
         include_fractional=include_fractional,
-        twelve_hour=twelve_hour)
-    return (
-        datetime.datetime.strptime(txt, fstring),
-        fstring
+        twelve_hour=twelve_hour,
     )
+    return (datetime.datetime.strptime(txt, fstring), fstring)
 
 
 def timestamp() -> str:
@@ -600,7 +701,7 @@ def parse_duration(duration: str) -> int:
     return seconds
 
 
-def describe_duration(seconds: int, *, include_seconds = False) -> str:
+def describe_duration(seconds: int, *, include_seconds=False) -> str:
     """
     Describe a duration represented as a count of seconds nicely.
 
@@ -711,4 +812,5 @@ def describe_timedelta_briefly(delta: datetime.timedelta) -> str:
 
 if __name__ == '__main__':
     import doctest
+
     doctest.testmod()