|
- import logging
- from datetime import date, datetime, timezone
- from email.utils import parsedate_to_datetime
-
- logger = logging.getLogger(__name__)
-
- # Common absolute-date formats encountered in news markup when ISO parsing fails.
- _FALLBACK_FORMATS = (
- "%Y-%m-%d %H:%M:%S",
- "%Y-%m-%d %H:%M",
- "%Y/%m/%d %H:%M:%S",
- "%B %d, %Y",
- "%b %d, %Y",
- "%B %d, %Y %I:%M %p",
- "%b %d, %Y %I:%M %p",
- )
-
-
- def parse_datetime(value):
- """Best-effort parse of a publish timestamp into a ``datetime``.
-
- Handles ISO 8601 (including trailing ``Z`` and offsets), RFC 2822/email
- dates, and a few common human formats. Returns ``None`` when the value
- cannot be parsed — relative strings like "3 hours ago" are intentionally
- skipped.
- """
- if not value or not isinstance(value, str):
- return None
- text = value.strip()
- if not text:
- return None
-
- try:
- return datetime.fromisoformat(text)
- except ValueError:
- pass
-
- try:
- return parsedate_to_datetime(text)
- except (TypeError, ValueError, OverflowError):
- pass
-
- for fmt in _FALLBACK_FORMATS:
- try:
- return datetime.strptime(text, fmt)
- except ValueError:
- pass
-
- logger.debug("Could not parse datetime from value %r", value)
- return None
-
-
- def parse_date_arg(value):
- """Parse a ``YYYY-MM-DD`` CLI argument, raising a friendly error otherwise."""
- try:
- return date.fromisoformat(value)
- except ValueError:
- raise ValueError("Invalid date %r; expected YYYY-MM-DD" % value)
-
-
- def date_part(dt):
- """Return ``dt`` as a ``date``, normalized to UTC when timezone-aware."""
- if dt.tzinfo is not None:
- return dt.astimezone(timezone.utc).date()
- return dt.date()
-
-
- def filter_by_date_window(headlines, since=None, until=None):
- """Keep headlines whose publication date falls in ``[since, until]``.
-
- ``since``/``until`` are ``datetime.date`` (or ``None`` for an open bound).
- When either bound is set, headlines with no parsable publication date are
- dropped because their recency cannot be established; without any bound they
- are all kept. Returns ``(kept, dropped_undated)``.
- """
- kept = []
- dropped_undated = 0
- bounding = since is not None or until is not None
-
- for h in headlines:
- if h.published_at is None:
- if bounding:
- dropped_undated += 1
- continue
- kept.append(h)
- continue
-
- d = date_part(h.published_at)
- if since is not None and d < since:
- continue
- if until is not None and d > until:
- continue
- kept.append(h)
-
- return kept, dropped_undated
|