dates.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. from __future__ import annotations
  2. from datetime import datetime, timedelta
  3. from zoneinfo import ZoneInfo
  4. from supply_infra.config import InfraSettings, get_infra_settings
  5. CHINA_TIMEZONE = ZoneInfo("Asia/Shanghai")
  6. def validate_biz_dt(value: str) -> str:
  7. text = str(value).strip()
  8. parsed = datetime.strptime(text, "%Y%m%d")
  9. if parsed.strftime("%Y%m%d") != text:
  10. raise ValueError(f"Invalid biz_dt: {value!r}")
  11. return text
  12. def resolve_biz_dt(
  13. biz_dt: str | None,
  14. *,
  15. settings: InfraSettings | None = None,
  16. now: datetime | None = None,
  17. ) -> str:
  18. if biz_dt:
  19. return validate_biz_dt(biz_dt)
  20. active = settings or get_infra_settings()
  21. local_now = now or datetime.now(ZoneInfo(active.scheduler_timezone))
  22. if local_now.tzinfo is None:
  23. local_now = local_now.replace(tzinfo=ZoneInfo(active.scheduler_timezone))
  24. return local_now.astimezone(ZoneInfo(active.scheduler_timezone)).strftime("%Y%m%d")
  25. def build_date_snapshot(biz_dt: str) -> dict[str, str]:
  26. resolved = validate_biz_dt(biz_dt)
  27. day = datetime.strptime(resolved, "%Y%m%d")
  28. previous = (day - timedelta(days=1)).strftime("%Y%m%d")
  29. return {
  30. "biz_dt": resolved,
  31. "global_tree_partition": previous,
  32. "demand_pool_partition": resolved,
  33. "video_decode_partition": previous,
  34. }
  35. def china_now() -> datetime:
  36. """Return a naive China Standard Time value for MySQL DATETIME columns."""
  37. return datetime.now(CHINA_TIMEZONE).replace(tzinfo=None)
  38. def next_schedule_china(
  39. *,
  40. settings: InfraSettings | None = None,
  41. now: datetime | None = None,
  42. ) -> datetime:
  43. active = settings or get_infra_settings()
  44. zone = ZoneInfo(active.scheduler_timezone)
  45. local_now = now or datetime.now(zone)
  46. if local_now.tzinfo is None:
  47. local_now = local_now.replace(tzinfo=zone)
  48. else:
  49. local_now = local_now.astimezone(zone)
  50. candidate = local_now.replace(
  51. hour=active.scheduler_cron_hour,
  52. minute=active.scheduler_cron_minute,
  53. second=0,
  54. microsecond=0,
  55. )
  56. if candidate <= local_now:
  57. candidate += timedelta(days=1)
  58. return candidate.astimezone(CHINA_TIMEZONE).replace(tzinfo=None)
  59. def next_daily_deadline_china(
  60. *,
  61. settings: InfraSettings | None = None,
  62. now: datetime | None = None,
  63. ) -> datetime:
  64. """Return the next local calendar day's scheduler start as China-time naive."""
  65. active = settings or get_infra_settings()
  66. zone = ZoneInfo(active.scheduler_timezone)
  67. local_now = now or datetime.now(zone)
  68. if local_now.tzinfo is None:
  69. local_now = local_now.replace(tzinfo=zone)
  70. else:
  71. local_now = local_now.astimezone(zone)
  72. candidate = (local_now + timedelta(days=1)).replace(
  73. hour=active.scheduler_cron_hour,
  74. minute=active.scheduler_cron_minute,
  75. second=0,
  76. microsecond=0,
  77. )
  78. return candidate.astimezone(CHINA_TIMEZONE).replace(tzinfo=None)
  79. def deadline_for_trigger(
  80. trigger_type: str,
  81. *,
  82. settings: InfraSettings | None = None,
  83. now: datetime | None = None,
  84. ) -> datetime | None:
  85. """Scheduled daily runs have a next-day deadline; manual runs do not."""
  86. if trigger_type not in {"cron", "reconcile"}:
  87. return None
  88. return next_daily_deadline_china(settings=settings, now=now)