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_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."
)