No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

95 líneas
2.7 KiB

  1. import logging
  2. from datetime import date, datetime, timezone
  3. from email.utils import parsedate_to_datetime
  4. logger = logging.getLogger(__name__)
  5. # Common absolute-date formats encountered in news markup when ISO parsing fails.
  6. _FALLBACK_FORMATS = (
  7. "%Y-%m-%d %H:%M:%S",
  8. "%Y-%m-%d %H:%M",
  9. "%Y/%m/%d %H:%M:%S",
  10. "%B %d, %Y",
  11. "%b %d, %Y",
  12. "%B %d, %Y %I:%M %p",
  13. "%b %d, %Y %I:%M %p",
  14. )
  15. def parse_datetime(value):
  16. """Best-effort parse of a publish timestamp into a ``datetime``.
  17. Handles ISO 8601 (including trailing ``Z`` and offsets), RFC 2822/email
  18. dates, and a few common human formats. Returns ``None`` when the value
  19. cannot be parsed — relative strings like "3 hours ago" are intentionally
  20. skipped.
  21. """
  22. if not value or not isinstance(value, str):
  23. return None
  24. text = value.strip()
  25. if not text:
  26. return None
  27. try:
  28. return datetime.fromisoformat(text)
  29. except ValueError:
  30. pass
  31. try:
  32. return parsedate_to_datetime(text)
  33. except (TypeError, ValueError, OverflowError):
  34. pass
  35. for fmt in _FALLBACK_FORMATS:
  36. try:
  37. return datetime.strptime(text, fmt)
  38. except ValueError:
  39. pass
  40. logger.debug("Could not parse datetime from value %r", value)
  41. return None
  42. def parse_date_arg(value):
  43. """Parse a ``YYYY-MM-DD`` CLI argument, raising a friendly error otherwise."""
  44. try:
  45. return date.fromisoformat(value)
  46. except ValueError:
  47. raise ValueError("Invalid date %r; expected YYYY-MM-DD" % value)
  48. def date_part(dt):
  49. """Return ``dt`` as a ``date``, normalized to UTC when timezone-aware."""
  50. if dt.tzinfo is not None:
  51. return dt.astimezone(timezone.utc).date()
  52. return dt.date()
  53. def filter_by_date_window(headlines, since=None, until=None):
  54. """Keep headlines whose publication date falls in ``[since, until]``.
  55. ``since``/``until`` are ``datetime.date`` (or ``None`` for an open bound).
  56. When either bound is set, headlines with no parsable publication date are
  57. dropped because their recency cannot be established; without any bound they
  58. are all kept. Returns ``(kept, dropped_undated)``.
  59. """
  60. kept = []
  61. dropped_undated = 0
  62. bounding = since is not None or until is not None
  63. for h in headlines:
  64. if h.published_at is None:
  65. if bounding:
  66. dropped_undated += 1
  67. continue
  68. kept.append(h)
  69. continue
  70. d = date_part(h.published_at)
  71. if since is not None and d < since:
  72. continue
  73. if until is not None and d > until:
  74. continue
  75. kept.append(h)
  76. return kept, dropped_undated