拆分 analysis_service:时钟工具和概率桶函数移至 analysis_utils.py
抽取 clock_minutes/format_clock_minutes/next_observation_clock 和 bucket_label/top_probability_bucket/add_signal 到独立模块。analysis_service 2152→2080 行。
This commit is contained in:
+8
-78
@@ -39,6 +39,14 @@ from web.services.observation_freshness import (
|
|||||||
build_observation_freshness as _build_observation_freshness,
|
build_observation_freshness as _build_observation_freshness,
|
||||||
observation_age_min as _observation_age_min,
|
observation_age_min as _observation_age_min,
|
||||||
)
|
)
|
||||||
|
from web.services.analysis_utils import (
|
||||||
|
add_signal as _add_signal,
|
||||||
|
bucket_label as _bucket_label,
|
||||||
|
bucket_label_from_value as _bucket_label_from_value,
|
||||||
|
format_clock_minutes as _format_clock_minutes,
|
||||||
|
next_observation_clock as _next_observation_clock,
|
||||||
|
top_probability_bucket as _top_probability_bucket,
|
||||||
|
)
|
||||||
from web.services.analysis_signals import (
|
from web.services.analysis_signals import (
|
||||||
_build_deviation_monitor,
|
_build_deviation_monitor,
|
||||||
_build_taf_signal,
|
_build_taf_signal,
|
||||||
@@ -305,84 +313,6 @@ def _maybe_enrich_dynamic_commentary_with_groq(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _clock_minutes(value: Any) -> Optional[int]:
|
|
||||||
text = str(value or "").strip()
|
|
||||||
match = re.search(r"\b(\d{1,2}):(\d{2})\b", text)
|
|
||||||
if not match:
|
|
||||||
return None
|
|
||||||
hour = int(match.group(1))
|
|
||||||
minute = int(match.group(2))
|
|
||||||
if hour < 0 or hour > 23 or minute < 0 or minute > 59:
|
|
||||||
return None
|
|
||||||
return hour * 60 + minute
|
|
||||||
|
|
||||||
|
|
||||||
def _format_clock_minutes(value: int) -> str:
|
|
||||||
value = max(0, min(23 * 60 + 59, int(value)))
|
|
||||||
return f"{value // 60:02d}:{value % 60:02d}"
|
|
||||||
|
|
||||||
|
|
||||||
def _next_observation_clock(local_time: Any) -> str:
|
|
||||||
minutes = _clock_minutes(local_time)
|
|
||||||
if minutes is None:
|
|
||||||
return "--"
|
|
||||||
next_slot = ((minutes // 30) + 1) * 30
|
|
||||||
if next_slot > 23 * 60 + 59:
|
|
||||||
return "23:59"
|
|
||||||
return _format_clock_minutes(next_slot)
|
|
||||||
|
|
||||||
|
|
||||||
def _bucket_label_from_value(value: Optional[float], unit: str) -> Optional[str]:
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return f"{int(round(float(value)))}{unit or '°C'}"
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _top_probability_bucket(distribution: Any) -> Optional[Dict[str, Any]]:
|
|
||||||
if not isinstance(distribution, list):
|
|
||||||
return None
|
|
||||||
candidates = [row for row in distribution if isinstance(row, dict)]
|
|
||||||
if not candidates:
|
|
||||||
return None
|
|
||||||
return max(candidates, key=lambda row: _sf(row.get("probability")) or -1.0)
|
|
||||||
|
|
||||||
|
|
||||||
def _bucket_label(row: Optional[Dict[str, Any]], unit: str) -> Optional[str]:
|
|
||||||
if not isinstance(row, dict):
|
|
||||||
return None
|
|
||||||
for key in ("label", "bucket", "range"):
|
|
||||||
raw = str(row.get(key) or "").strip()
|
|
||||||
if raw:
|
|
||||||
return raw
|
|
||||||
return _bucket_label_from_value(_sf(row.get("value")), unit)
|
|
||||||
|
|
||||||
|
|
||||||
def _add_signal(
|
|
||||||
signals: list,
|
|
||||||
*,
|
|
||||||
label: str,
|
|
||||||
direction: str,
|
|
||||||
strength: str,
|
|
||||||
summary: str,
|
|
||||||
label_en: Optional[str] = None,
|
|
||||||
summary_en: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
signals.append(
|
|
||||||
{
|
|
||||||
"label": label,
|
|
||||||
"label_en": label_en or label,
|
|
||||||
"direction": direction,
|
|
||||||
"strength": strength,
|
|
||||||
"summary": summary,
|
|
||||||
"summary_en": summary_en or summary,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_intraday_meteorology(data: Dict[str, Any]) -> Dict[str, Any]:
|
def _build_intraday_meteorology(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Build a paid-product intraday meteorology read from existing layers."""
|
"""Build a paid-product intraday meteorology read from existing layers."""
|
||||||
current = data.get("current") or {}
|
current = data.get("current") or {}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"""Analysis utility functions extracted from analysis_service.py.
|
||||||
|
|
||||||
|
Pure helpers: clock arithmetic, bucket labelling, signal packaging.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from web.core import _sf
|
||||||
|
|
||||||
|
|
||||||
|
# ── Clock / time-slot helpers ──────────────────────────────────────────
|
||||||
|
|
||||||
|
def clock_minutes(value: Any) -> Optional[int]:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
match = re.search(r"\b(\d{1,2}):(\d{2})\b", text)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
hour = int(match.group(1))
|
||||||
|
minute = int(match.group(2))
|
||||||
|
if hour < 0 or hour > 23 or minute < 0 or minute > 59:
|
||||||
|
return None
|
||||||
|
return hour * 60 + minute
|
||||||
|
|
||||||
|
|
||||||
|
def format_clock_minutes(value: int) -> str:
|
||||||
|
value = max(0, min(23 * 60 + 59, int(value)))
|
||||||
|
return f"{value // 60:02d}:{value % 60:02d}"
|
||||||
|
|
||||||
|
|
||||||
|
def next_observation_clock(local_time: Any) -> str:
|
||||||
|
minutes = clock_minutes(local_time)
|
||||||
|
if minutes is None:
|
||||||
|
return "--"
|
||||||
|
next_slot = ((minutes // 30) + 1) * 30
|
||||||
|
if next_slot > 23 * 60 + 59:
|
||||||
|
return "23:59"
|
||||||
|
return format_clock_minutes(next_slot)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Probability bucket helpers ─────────────────────────────────────────
|
||||||
|
|
||||||
|
def bucket_label_from_value(value: Optional[float], unit: str) -> Optional[str]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return f"{int(round(float(value)))}{unit or '°C'}"
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def top_probability_bucket(distribution: Any) -> Optional[Dict[str, Any]]:
|
||||||
|
if not isinstance(distribution, list):
|
||||||
|
return None
|
||||||
|
candidates = [row for row in distribution if isinstance(row, dict)]
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
return max(candidates, key=lambda row: _sf(row.get("probability")) or -1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def bucket_label(row: Optional[Dict[str, Any]], unit: str) -> Optional[str]:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
return None
|
||||||
|
for key in ("label", "bucket", "range"):
|
||||||
|
raw = str(row.get(key) or "").strip()
|
||||||
|
if raw:
|
||||||
|
return raw
|
||||||
|
return bucket_label_from_value(_sf(row.get("value")), unit)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Signal packaging ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def add_signal(
|
||||||
|
signals: list,
|
||||||
|
*,
|
||||||
|
label: str,
|
||||||
|
direction: str,
|
||||||
|
strength: str,
|
||||||
|
summary: str,
|
||||||
|
label_en: Optional[str] = None,
|
||||||
|
summary_en: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
signals.append(
|
||||||
|
{
|
||||||
|
"label": label,
|
||||||
|
"label_en": label_en or label,
|
||||||
|
"direction": direction,
|
||||||
|
"strength": strength,
|
||||||
|
"summary": summary,
|
||||||
|
"summary_en": summary_en or summary,
|
||||||
|
}
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user