183 lines
6.5 KiB
Python
183 lines
6.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Reload ODS tasks by fixed time windows with optional sleep between windows.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import logging
|
|
import subprocess
|
|
import sys
|
|
import time as time_mod
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from dateutil import parser as dtparser
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from config.settings import AppConfig
|
|
from utils.logging_utils import build_log_path, configure_logging
|
|
|
|
MIN_RELOAD_WINDOW_DAYS = 30
|
|
|
|
def _parse_dt(value: str, tz: ZoneInfo, *, is_end: bool) -> datetime:
|
|
raw = (value or "").strip()
|
|
if not raw:
|
|
raise ValueError("empty datetime")
|
|
has_time = any(ch in raw for ch in (":", "T"))
|
|
dt = dtparser.parse(raw)
|
|
if dt.tzinfo is None:
|
|
dt = dt.replace(tzinfo=tz)
|
|
else:
|
|
dt = dt.astimezone(tz)
|
|
if not has_time:
|
|
dt = dt.replace(hour=23 if is_end else 0, minute=59 if is_end else 0, second=59 if is_end else 0, microsecond=0)
|
|
return dt
|
|
|
|
|
|
def _iter_windows(start: datetime, end: datetime, window_size: timedelta):
|
|
if window_size.total_seconds() <= 0:
|
|
raise ValueError("window_size must be > 0")
|
|
cur = start
|
|
while cur < end:
|
|
nxt = min(cur + window_size, end)
|
|
yield cur, nxt
|
|
cur = nxt
|
|
|
|
|
|
def _run_task_window(
|
|
task_code: str,
|
|
window_start: datetime,
|
|
window_end: datetime,
|
|
api_page_size: int,
|
|
api_timeout: int,
|
|
logger: logging.Logger,
|
|
) -> None:
|
|
cmd = [
|
|
sys.executable,
|
|
"-m",
|
|
"cli.main",
|
|
"--pipeline-flow",
|
|
"FULL",
|
|
"--tasks",
|
|
task_code,
|
|
"--window-start",
|
|
window_start.strftime("%Y-%m-%d %H:%M:%S"),
|
|
"--window-end",
|
|
window_end.strftime("%Y-%m-%d %H:%M:%S"),
|
|
"--force-window-override",
|
|
]
|
|
if api_page_size > 0:
|
|
cmd += ["--api-page-size", str(api_page_size)]
|
|
if api_timeout > 0:
|
|
cmd += ["--api-timeout", str(api_timeout)]
|
|
logger.info(
|
|
"RUN_TASK task=%s window_start=%s window_end=%s",
|
|
task_code,
|
|
window_start.isoformat(),
|
|
window_end.isoformat(),
|
|
)
|
|
logger.debug("CMD %s", " ".join(cmd))
|
|
subprocess.run(cmd, check=True, cwd=str(PROJECT_ROOT))
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="Reload ODS tasks by window slices.")
|
|
ap.add_argument("--tasks", required=True, help="comma-separated ODS task codes")
|
|
ap.add_argument("--start", required=True, help="start datetime, e.g. 2025-07-01")
|
|
ap.add_argument("--end", default="", help="end datetime (default: now)")
|
|
ap.add_argument("--window-days", type=int, default=1, help="days per window (default: 1)")
|
|
ap.add_argument("--window-hours", type=int, default=0, help="hours per window (default: 0)")
|
|
ap.add_argument("--sleep-seconds", type=float, default=0, help="sleep seconds after each window")
|
|
ap.add_argument("--api-page-size", type=int, default=200, help="API page size override")
|
|
ap.add_argument("--api-timeout", type=int, default=20, help="API timeout seconds override")
|
|
ap.add_argument("--log-file", default="", help="log file path (default: logs/reload_ods_windowed_YYYYMMDD_HHMMSS.log)")
|
|
ap.add_argument("--log-dir", default="", help="log directory (default: logs)")
|
|
ap.add_argument("--log-level", default="INFO", help="log level (default: INFO)")
|
|
ap.add_argument("--no-log-console", action="store_true", help="disable console logging")
|
|
args = ap.parse_args()
|
|
|
|
log_dir = Path(args.log_dir) if args.log_dir else (PROJECT_ROOT / "logs")
|
|
log_file = Path(args.log_file) if args.log_file else build_log_path(log_dir, "reload_ods_windowed")
|
|
log_console = not args.no_log_console
|
|
|
|
with configure_logging(
|
|
"reload_ods_windowed",
|
|
log_file,
|
|
level=args.log_level,
|
|
console=log_console,
|
|
tee_std=True,
|
|
) as logger:
|
|
cfg = AppConfig.load({})
|
|
tz = ZoneInfo(cfg.get("app.timezone", "Asia/Taipei"))
|
|
|
|
start = _parse_dt(args.start, tz, is_end=False)
|
|
end = datetime.now(tz) if not args.end else _parse_dt(args.end, tz, is_end=True)
|
|
window_days = int(args.window_days)
|
|
window_hours = int(args.window_hours)
|
|
min_hours = MIN_RELOAD_WINDOW_DAYS * 24
|
|
if window_hours > 0:
|
|
if window_hours < min_hours:
|
|
logger.warning(
|
|
"window_hours=%s too small; adjust to %s",
|
|
window_hours,
|
|
min_hours,
|
|
)
|
|
window_hours = min_hours
|
|
elif window_days < MIN_RELOAD_WINDOW_DAYS:
|
|
logger.warning(
|
|
"window_days=%s too small; adjust to %s",
|
|
window_days,
|
|
MIN_RELOAD_WINDOW_DAYS,
|
|
)
|
|
window_days = MIN_RELOAD_WINDOW_DAYS
|
|
window_size = timedelta(hours=window_hours) if window_hours > 0 else timedelta(days=window_days)
|
|
|
|
task_codes = [t.strip().upper() for t in args.tasks.split(",") if t.strip()]
|
|
if not task_codes:
|
|
raise SystemExit("no tasks specified")
|
|
|
|
logger.info(
|
|
"START range=%s~%s window_days=%s window_hours=%s sleep=%.2f",
|
|
start.isoformat(),
|
|
end.isoformat(),
|
|
window_days,
|
|
window_hours,
|
|
args.sleep_seconds,
|
|
)
|
|
|
|
for task_code in task_codes:
|
|
logger.info("TASK_START task=%s", task_code)
|
|
for window_start, window_end in _iter_windows(start, end, window_size):
|
|
start_ts = time_mod.monotonic()
|
|
_run_task_window(
|
|
task_code=task_code,
|
|
window_start=window_start,
|
|
window_end=window_end,
|
|
api_page_size=args.api_page_size,
|
|
api_timeout=args.api_timeout,
|
|
logger=logger,
|
|
)
|
|
elapsed = time_mod.monotonic() - start_ts
|
|
logger.info(
|
|
"WINDOW_DONE task=%s window_start=%s window_end=%s elapsed=%.2fs",
|
|
task_code,
|
|
window_start.isoformat(),
|
|
window_end.isoformat(),
|
|
elapsed,
|
|
)
|
|
if args.sleep_seconds > 0:
|
|
logger.debug("SLEEP seconds=%.2f", args.sleep_seconds)
|
|
time_mod.sleep(args.sleep_seconds)
|
|
logger.info("TASK_DONE task=%s", task_code)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|