from datetime import datetime, timedelta from typing import List import pytz from delphi_api.errors import Codes, RangeError from delphi_api.v2.enums import DateConfig class DateUtils: """Static date utility functions""" @staticmethod def date_range(start: str, end: str, fmt: str = DateConfig.FormatString.value) -> List[str]: """Pass two date strings to this method to build a range of dates Args: start: start date as string end: end date as string fmt: format for parsing datetime input strings and formatting datetime output strings Raises: RangeError: if start date > end date Returns: List[str]: a list of all date strings between ``start`` and ``end``, inclusively """ DateUtils.valid_range(start, end, fmt=fmt) start_date = datetime.strptime(start, fmt) end_date = datetime.strptime(end, fmt) # probably faster to do the iter via total (delta) days instead of 1..n date comparisons delta: timedelta = end_date - start_date dates = [] for x in range(delta.days + 1): date_str = (start_date + timedelta(days=x)).strftime(fmt) dates.append(date_str) return dates @staticmethod def today() -> datetime: """Better than calling ``datetime.today()``, this includes tzinfo """ return datetime.now(tz=pytz.utc) @staticmethod def as_str(date: datetime, fmt: str = DateConfig.FormatString.value) -> str: """Formats a date as a string using config format as a default Args: date: datetime object fmt: format string for ouput Returns: str: string of date in specified format """ return date.strftime(fmt) @staticmethod def from_str(date: str, fmt: str = DateConfig.FormatString.value) -> datetime: """Create a datetime object from a string using config format as a default and adding tz Args: date: a date string fmt: format of the date string Returns: datetime: a datetime object """ return datetime.strptime(date, fmt).replace(tzinfo=pytz.utc) @staticmethod def before(date1: datetime, date2: datetime) -> bool: """Compares if ``date1`` occurs before ``date2`` with support for mixed offset-aware and offset-naïve dates Args: date1: datetime object date2: datetime object Returns: bool: True if ``date1`` occurs before ``date2`` """ return date1.replace(tzinfo=pytz.utc) < date2.replace(tzinfo=pytz.utc) @staticmethod def valid_range(start: str, end: str, fmt: str = DateConfig.FormatString.value) -> bool: """Checks start date end date combination is valid Args: start: start date as string end: end date as string fmt: format for parsing datetime input strings and formatting datetime output strings Raises: RangeError: if start date > end date """ start_date = datetime.strptime(start, fmt) end_date = datetime.strptime(end, fmt) if not start_date <= end_date: raise RangeError({ 'code': Codes.invalid_input.value, 'description': 'Range start_date must be less than or equal to end_date', }) return True