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
+
+ """
+ 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.
+ 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)
"""
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:
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 date_and_time_to_datetime(date: datetime.date,
- time: datetime.time) -> datetime.datetime:
+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
+
+ """
+ tz = time.tzinfo
+ return datetime.datetime.combine(now_pacific(), 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.
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.
return datetime_to_date_and_time(dt)[1]
-class TimeUnit(enum.Enum):
+class TimeUnit(enum.IntEnum):
"""An enum to represent units with which we can compute deltas."""
+
MONDAYS = 0
TUESDAYS = 1
WEDNESDAYS = 2
@classmethod
def is_valid(cls, value: Any):
- if type(value) is int:
- return value in cls._value2member_map_
- elif type(value) is TimeUnit:
- return value.value in cls._value2member_map_
- elif type(value) is str:
- return value in cls._member_names_
+ if isinstance(value, int):
+ return cls(value) is not None
+ elif isinstance(value, TimeUnit):
+ return cls(value.value) is not None
+ elif isinstance(value, str):
+ return cls.__members__[value] is not None
else:
print(type(value))
return False
-def n_timeunits_from_base(
- count: int,
- unit: TimeUnit,
- base: datetime.datetime
-) -> datetime.datetime:
+def n_timeunits_from_base(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
years before base datetime, 13 minutes after base datetime, etc...
>>> n_timeunits_from_base(50, TimeUnit.SECONDS, base)
datetime.datetime(2021, 9, 10, 11, 25, 41, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+ Next month corner case -- it will try to make Feb 31, 2022 then count
+ backwards.
+ >>> base = string_to_datetime("2022/01/31 11:24:51AM-0700")[0]
+ >>> n_timeunits_from_base(1, TimeUnit.MONTHS, base)
+ datetime.datetime(2022, 2, 28, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Last month with the same corner case
+ >>> base = string_to_datetime("2022/03/31 11:24:51AM-0700")[0]
+ >>> n_timeunits_from_base(-1, TimeUnit.MONTHS, base)
+ datetime.datetime(2022, 2, 28, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
"""
assert TimeUnit.is_valid(unit)
if count == 0:
base += timedelta
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
- ):
+ if base.weekday() < 5 and datetime.date(base.year, base.month, base.day) not in skips:
count -= 1
return base
new_month %= 12
year_term += 1
new_year = base.year + year_term
- return datetime.datetime(
- new_year,
- new_month,
- base.day,
- base.hour,
- base.minute,
- base.second,
- base.microsecond,
- base.tzinfo,
- )
+ day = base.day
+ while True:
+ try:
+ ret = datetime.datetime(
+ new_year,
+ new_month,
+ day,
+ base.hour,
+ base.minute,
+ base.second,
+ base.microsecond,
+ base.tzinfo,
+ )
+ break
+ except ValueError:
+ day -= 1
+ return ret
# N years from base
elif unit == TimeUnit.YEARS:
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)
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
date_time_separator=date_time_separator,
include_timezone=include_timezone,
include_dayname=include_dayname,
+ use_month_abbrevs=use_month_abbrevs,
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
date_time_separator=date_time_separator,
include_timezone=include_timezone,
include_dayname=include_dayname,
+ use_month_abbrevs=use_month_abbrevs,
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:
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.
'1 day, and 10 minutes'
"""
- return describe_duration(delta.total_seconds())
+ return describe_duration(int(delta.total_seconds())) # Note: drops milliseconds
def describe_duration_briefly(seconds: int, *, include_seconds=False) -> str:
'1d 10m'
"""
- return describe_duration_briefly(delta.total_seconds())
+ return describe_duration_briefly(int(delta.total_seconds())) # Note: drops milliseconds
if __name__ == '__main__':
import doctest
+
doctest.testmod()