From 4b60ab34c336d32677d145306904ffd2e8abf276 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 22 May 2026 01:42:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(task-watcher):=20mode.py=20=E2=80=94=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EB=8C=80+=ED=9C=B4=EC=9E=A5=EC=9D=BC=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=20(SP-10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit current_mode(now, holidays): 비휴장 평일 07:00–16:30 → trading, 그 외 free. fetch_holidays(): NAS /api/stock/holidays 조회 (실패 시 빈 set = free 안전). TRADING_START/END env로 윈도우 조정. idle 감지 생략 (박재오 결정). 6 tests (평일 장중/장전/장후, 주말, 휴장, 경계). Plan-B-Infra Phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) --- services/task-watcher/mode.py | 57 ++++++++++++++++++++++++ services/task-watcher/tests/__init__.py | 0 services/task-watcher/tests/test_mode.py | 44 ++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 services/task-watcher/mode.py create mode 100644 services/task-watcher/tests/__init__.py create mode 100644 services/task-watcher/tests/test_mode.py diff --git a/services/task-watcher/mode.py b/services/task-watcher/mode.py new file mode 100644 index 0000000..075ac65 --- /dev/null +++ b/services/task-watcher/mode.py @@ -0,0 +1,57 @@ +"""시간대 + 휴장일 기반 모드 판정 (idle 감지 생략 — 박재오 결정 2026-05-22). + +trading: 비휴장 평일 07:00–16:30 (장중) → queue:paused SET +free: 그 외 (장 전/후, 주말, 휴장) → queue:paused DEL +""" +from __future__ import annotations + +import datetime as dt +import logging +import os +from typing import Set +from zoneinfo import ZoneInfo + +import httpx + +logger = logging.getLogger(__name__) + +KST = ZoneInfo("Asia/Seoul") +STOCK_BASE_URL = os.getenv("STOCK_BASE_URL", "http://192.168.45.54:18500") + +# 트레이딩 윈도우 (HH:MM, KST). .env로 조정 가능. +TRADING_START = os.getenv("TRADING_START", "07:00") +TRADING_END = os.getenv("TRADING_END", "16:30") + + +def _parse_hhmm(s: str) -> dt.time: + hh, mm = s.split(":") + return dt.time(int(hh), int(mm)) + + +def current_mode(now: dt.datetime, holidays: Set[str]) -> str: + """now(KST aware) + holidays(ISO date set) → 'trading' | 'free'.""" + # 주말 (토=5, 일=6) + if now.weekday() >= 5: + return "free" + # 휴장일 + if now.date().isoformat() in holidays: + return "free" + # 트레이딩 윈도우 [start, end) + start = _parse_hhmm(TRADING_START) + end = _parse_hhmm(TRADING_END) + t = now.timetz().replace(tzinfo=None) + if start <= t < end: + return "trading" + return "free" + + +def fetch_holidays() -> Set[str]: + """NAS stock /api/stock/holidays 조회. 실패 시 빈 set (안전 — free로 판정).""" + try: + r = httpx.get(f"{STOCK_BASE_URL}/api/stock/holidays", timeout=10.0) + if r.status_code == 200: + return set(r.json().get("holidays", [])) + logger.warning("holidays fetch returned %d", r.status_code) + except Exception: + logger.exception("holidays fetch 실패") + return set() diff --git a/services/task-watcher/tests/__init__.py b/services/task-watcher/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/task-watcher/tests/test_mode.py b/services/task-watcher/tests/test_mode.py new file mode 100644 index 0000000..42d2995 --- /dev/null +++ b/services/task-watcher/tests/test_mode.py @@ -0,0 +1,44 @@ +"""current_mode — 시간대 + 휴장일 판정 (순수 함수).""" +import datetime as dt +from zoneinfo import ZoneInfo + +from mode import current_mode + +KST = ZoneInfo("Asia/Seoul") +HOLIDAYS = {"2026-05-25"} # 가상 휴장일 (월요일) + + +def _kst(y, m, d, hh, mm): + return dt.datetime(y, m, d, hh, mm, tzinfo=KST) + + +def test_weekday_trading_hours_is_trading(): + # 2026-05-22 금요일 10:00 — 트레이딩 시간대 + assert current_mode(_kst(2026, 5, 22, 10, 0), HOLIDAYS) == "trading" + + +def test_weekday_before_open_is_free(): + # 평일 06:00 — 장 전 + assert current_mode(_kst(2026, 5, 22, 6, 0), HOLIDAYS) == "free" + + +def test_weekday_after_close_is_free(): + # 평일 17:00 — 장 마감 후 + assert current_mode(_kst(2026, 5, 22, 17, 0), HOLIDAYS) == "free" + + +def test_weekend_is_free(): + # 2026-05-23 토요일 10:00 + assert current_mode(_kst(2026, 5, 23, 10, 0), HOLIDAYS) == "free" + + +def test_holiday_weekday_is_free(): + # 2026-05-25 월요일이지만 휴장일 → 트레이딩 시간대라도 free + assert current_mode(_kst(2026, 5, 25, 10, 0), HOLIDAYS) == "free" + + +def test_trading_boundary_inclusive_start_exclusive_end(): + # 07:00 정각 = 트레이딩 시작, 16:30 정각 = 마감 (16:30은 free) + assert current_mode(_kst(2026, 5, 22, 7, 0), HOLIDAYS) == "trading" + assert current_mode(_kst(2026, 5, 22, 16, 29), HOLIDAYS) == "trading" + assert current_mode(_kst(2026, 5, 22, 16, 30), HOLIDAYS) == "free"