import logging
from datetime import UTC, datetime
from pathlib import Path
logger = logging.getLogger(__name__)
DATETIME_FORMATS = [
"%Y-%m-%dT%H:%M:%S.%f", # 2023-10-27T10:00:00.000000 ISO 8601
"%Y-%m-%dT%H:%M:%S", # 2023-10-27T10:00:00 ISO 8601 without milliseconds
"%Y-%m-%d %H:%M:%S", # 2023-10-27 10:00:00
"%Y-%m-%d", # 2023-10-27 ISO 8601 without time
"%Y/%m/%d", # 2023/10/27
"%d/%m/%Y %H:%M:%S", # 27/10/2023 10:00:00
"%d/%m/%Y", # 27/10/2023
]
def __try_strptime(val: str, formt: str) -> datetime | None:
"""Tries to parse a string into a datetime using a given format.
Args:
val: The string to parse.
formt: The format to use for parsing.
Returns:
datetime | None: The parsed datetime if the format matches, None otherwise.
"""
try:
return datetime.strptime(val, formt).replace(tzinfo=UTC)
except ValueError:
return None
def __try_timestamp(val: float) -> datetime | None:
"""Tries to convert a timestamp to a datetime object.
Args:
val: The timestamp to convert.
Returns:
datetime | None: The parsed datetime if successful, None otherwise.
"""
try:
return datetime.fromtimestamp(val, tz=UTC)
except (ValueError, OverflowError, OSError):
return None
def __parse_str(val: str) -> datetime | None:
"""Tries to parse a string into a datetime.
Args:
val: The string to parse.
Returns:
datetime | None: The parsed datetime if successful, None otherwise.
"""
normalized = val.replace("Z", "+00:00")
try:
date = datetime.fromisoformat(normalized)
if date.tzinfo is None:
return date.replace(tzinfo=UTC)
return date.astimezone(UTC)
except ValueError:
pass # Fall back to the next parsing attempts
for formt in DATETIME_FORMATS:
parsed = __try_strptime(val, formt)
if parsed is not None:
return parsed
try:
ts = float(val)
except ValueError:
return None
return __try_timestamp(ts)
[docs]
def parse_datetime(value: str | int | float | datetime | None) -> datetime | None:
"""Parses a value into a datetime.
Args:
value: The value to parse.
Returns:
datetime | None: The parsed datetime if successful, None otherwise.
"""
if value is None or value == "":
return None
if isinstance(value, str) and value.strip().lower() == "never":
return None
if isinstance(value, datetime):
if value.tzinfo is None:
return value.replace(tzinfo=UTC)
return value.astimezone(UTC)
if isinstance(value, (int, float)):
return __try_timestamp(value)
if isinstance(value, str):
result = __parse_str(value.strip())
if result is None:
logger.warning("Unable to parse datetime value: %s", value)
return result
return None
[docs]
def add_date_to_filename(path: str | Path, date_format: str = "%Y_%m_%d") -> str:
"""Replaces the 'DATE' placeholder in the filename with today's date, if present.
Args:
path: Original file path (e.g. 'outputs/audit_report_DATE.pdf').
date_format: strftime format used for the date.
Returns:
str: The path with 'DATE' replaced by today's date (e.g.
'outputs/audit_report_2026_07_08.pdf'), or unchanged if the filename
has no 'DATE' placeholder (e.g. 'outputs/audit_report.pdf').
"""
p = Path(path)
if "DATE" not in p.name:
return str(p)
today = datetime.now(UTC).strftime(date_format)
return str(p.with_name(p.name.replace("DATE", today)))