Source code for ds_provider_xledger_py_lib.dataset.engines._read_incremental

"""
Incremental read helpers for Xledger read execution.
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any

from ...errors import InvalidIncrementalWatermarkException, UnsupportedIncrementalKindException

if TYPE_CHECKING:
    from collections.abc import Sequence

    from ...utils.introspection import IncrementalMetaData
    from ._read_checkpoint import Checkpoint

_LOGICAL_FILTER_KEYS = ("AND", "OR")

FilterDict = dict[str, Any]


[docs] def greatest_incremental_value(values: Sequence[Any], *, kind: str) -> Any | None: """Return the greatest watermark among observed values for the given strategy. Args: values: Non-empty sequence of observed watermark candidates (nulls should be excluded by callers). kind: Incremental strategy from metadata (e.g. ``time_field``). Returns: The winning original value from ``values``, or ``None`` when ``values`` is empty. Raises: InvalidIncrementalWatermarkException: When ``time_field`` values are not strings or parsing fails. UnsupportedIncrementalKindException: When ``kind`` is not supported. """ if not values: return None if kind == "time_field": return _greatest_time_field_value(values) raise UnsupportedIncrementalKindException( message=f"Unsupported incremental kind: {kind!r}", details={"kind": kind}, )
[docs] def _parse_iso8601_timestamp(value: str) -> datetime: """Parse an ISO-8601 timestamp string to an aware UTC datetime for comparison.""" normalized = value.replace("Z", "+00:00") if value.endswith("Z") else value try: parsed = datetime.fromisoformat(normalized) except ValueError as exc: msg = f"Unparseable time_field watermark: {value!r}" raise InvalidIncrementalWatermarkException(message=msg, details={"value": value}) from exc if parsed.tzinfo is None: return parsed.replace(tzinfo=timezone.utc) return parsed.astimezone(timezone.utc)
[docs] def _greatest_time_field_value(values: Sequence[Any]) -> Any: """Return the original value that sorts last by parsed UTC ``datetime``.""" if not values: return None parsed: list[tuple[datetime, Any]] = [] for raw in values: if not isinstance(raw, str): msg = f"time_field watermarks must be strings, got {type(raw).__name__}" raise InvalidIncrementalWatermarkException(message=msg, details={"value": raw}) parsed.append((_parse_iso8601_timestamp(raw), raw)) return max(parsed, key=lambda item: item[0])[1]
[docs] def compose_incremental_filter( *, existing_filter: FilterDict | None, checkpoint: Checkpoint, incremental: IncrementalMetaData | None, ) -> FilterDict | None: """Apply checkpoint precedence for the incremental boundary. Args: existing_filter: User-provided filter from read settings. checkpoint: Read checkpoint containing persisted continuation state. incremental: Incremental section from read metadata, when configured. Returns: The effective filter with checkpoint boundary applied. When no incremental section or watermark exists, the original filter is returned unchanged. """ if incremental is None: return existing_filter boundary = checkpoint.incremental.value if boundary is None: return existing_filter cleaned_filter = remove_incremental_boundary( existing_filter=existing_filter, incremental=incremental, ) incremental_filter = {incremental.filter_field: boundary} if cleaned_filter is None: return incremental_filter if tuple(cleaned_filter.keys()) == ("AND",) and isinstance(cleaned_filter["AND"], list): return {"AND": [*cleaned_filter["AND"], incremental_filter]} return {"AND": [cleaned_filter, incremental_filter]}
[docs] def remove_incremental_boundary( *, existing_filter: FilterDict | None, incremental: IncrementalMetaData, ) -> FilterDict | None: """Remove existing boundary clauses for the incremental field/operator. Args: existing_filter: User-provided filter from read settings. incremental: Incremental section from read metadata. Returns: A cleaned filter where the incremental boundary for the configured filter key has been removed. Empty filters are returned as ``None``. Note: For ``and`` / ``or`` lists, nested filter objects are cleaned recursively. Non-mapping entries are passed through unchanged (order preserved). """ if existing_filter is None: return None cleaned_filter: FilterDict = {} for key, value in existing_filter.items(): if key == incremental.filter_field: continue if key in _LOGICAL_FILTER_KEYS and isinstance(value, list): cleaned_items: list[Any] = [] for item in value: if isinstance(item, dict): cleaned = remove_incremental_boundary(existing_filter=item, incremental=incremental) if cleaned is not None: cleaned_items.append(cleaned) else: cleaned_items.append(item) if cleaned_items: cleaned_filter[key] = cleaned_items continue cleaned_filter[key] = value return cleaned_filter or None