Source code for kaxanuk.attribution_analysis.portfolio_data_pipeline.date_utils

"""
Date parsing utilities for portfolio operations.

This module provides date parsing functions used across portfolio-related modules
to avoid circular dependencies.
"""


import datetime
from typing import Literal

from kaxanuk.attribution_analysis.exceptions import DateParsingError

DATE_FORMATS = [
    "%Y-%m-%d",
    "%Y/%m/%d",
    "%Y-%m-%d %H:%M:%S",
    "%d/%m/%Y",
    "%m/%d/%Y",
]


[docs] def parse_date_with_formats( date_str: str, formats: list[str] ) -> datetime.datetime: """ Parse a date string trying multiple formats. Parameters ---------- date_str : str Date string to parse. formats : list[str] List of date format strings to try. Returns ------- datetime.datetime Parsed datetime object. Raises ------ ValueError If date cannot be parsed with any format. Examples -------- >>> parse_date_with_formats("2020-01-01", DATE_FORMATS) datetime.datetime(2020, 1, 1, 0, 0) >>> parse_date_with_formats("01/01/2020", DATE_FORMATS) datetime.datetime(2020, 1, 1, 0, 0) """ date_str_clean = date_str.split('.', maxsplit=1)[0].strip() for fmt in formats: try: return datetime.datetime.strptime(date_str_clean, fmt) # noqa: DTZ007 except ValueError: continue formats_str = "', '".join(formats) raise DateParsingError( f"Cannot parse date '{date_str}'. Expected formats: '{formats_str}'" )
[docs] def parse_config_date( value: str | datetime.date | datetime.datetime | None, ) -> datetime.date | Literal["auto"]: """Parse a date value from any configuration source into a ``datetime.date`` or ``"auto"``. This is the single shared utility for configuration-level date parsing. It handles the full range of inputs that may come from Excel cells, YAML files, JSON files, or programmatic parameters. Parameters ---------- value : str | datetime.date | datetime.datetime | None The raw date value. Accepted inputs: * ``None`` — interpreted as ``"auto"``. * The string ``"auto"`` (case-insensitive) — returned as ``"auto"``. * A ``datetime.datetime`` — converted to ``datetime.date`` via ``.date()``. * A ``datetime.date`` — returned unchanged. * An ISO-formatted string (``YYYY-MM-DD``) — parsed via ``date.fromisoformat()``. * Other date-string formats (``YYYY/MM/DD``, ``DD/MM/YYYY``, ``MM/DD/YYYY``, ``YYYY-MM-DD HH:MM:SS``) — parsed via ``parse_date_with_formats()`` as a fallback. Returns ------- datetime.date | Literal["auto"] A resolved date or the sentinel string ``"auto"``. Raises ------ DateParsingError If *value* is a string that cannot be parsed with any known format, or if *value* is an unsupported type. Examples -------- >>> parse_config_date(None) 'auto' >>> parse_config_date("auto") 'auto' >>> parse_config_date("2024-01-15") datetime.date(2024, 1, 15) >>> parse_config_date(datetime.date(2024, 1, 15)) datetime.date(2024, 1, 15) """ if value is None: return "auto" if isinstance(value, str): stripped = value.strip() if stripped.lower() == "auto": return "auto" try: return datetime.date.fromisoformat(stripped) except ValueError: pass try: return parse_date_with_formats(stripped, DATE_FORMATS).date() except ValueError: raise DateParsingError( f"Could not parse date '{value}'. " f"Expected ISO format (YYYY-MM-DD), YYYY/MM/DD, " f"DD/MM/YYYY, MM/DD/YYYY, or 'auto'." ) from None if isinstance(value, datetime.datetime): return value.date() if isinstance(value, datetime.date): return value raise DateParsingError( f"Invalid date value type: {type(value).__name__}. " f"Expected str, datetime.date, datetime.datetime, or None." )