Remove sync city refresh fetch escape hatch
This commit is contained in:
@@ -4,30 +4,6 @@ import asyncio
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
|
||||||
def _sample_panel_payload() -> dict:
|
|
||||||
return {
|
|
||||||
"name": "shanghai",
|
|
||||||
"temp_symbol": "°C",
|
|
||||||
"updated_at": "2026-06-14T01:02:03+00:00",
|
|
||||||
"current": {
|
|
||||||
"temp": 31.2,
|
|
||||||
"source_code": "amsc_awos",
|
|
||||||
"settlement_source": "amsc_awos",
|
|
||||||
"settlement_source_label": "AMSC AWOS runway-point air temperature",
|
|
||||||
"station_code": "ZSPD",
|
|
||||||
"station_name": "Shanghai Pudong",
|
|
||||||
"observed_at": "2026-06-14T01:01:00+00:00",
|
|
||||||
"obs_time": "09:01",
|
|
||||||
"freshness": {
|
|
||||||
"freshness_status": "fresh",
|
|
||||||
"age_sec": 63,
|
|
||||||
"observed_at": "2026-06-14T01:01:00+00:00",
|
|
||||||
"observed_at_local": "09:01",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_canonical_temperature_roles_understand_adapter_source_names():
|
def test_canonical_temperature_roles_understand_adapter_source_names():
|
||||||
from web.services.canonical_temperature import build_canonical_temperature
|
from web.services.canonical_temperature import build_canonical_temperature
|
||||||
|
|
||||||
@@ -80,33 +56,80 @@ def test_db_manager_stores_canonical_temperature_latest(tmp_path):
|
|||||||
assert row["payload"]["observed_at"] == "2026-06-14T01:01:00+00:00"
|
assert row["payload"]["observed_at"] == "2026-06-14T01:01:00+00:00"
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_city_panel_cache_persists_canonical_temperature(monkeypatch):
|
def test_refresh_city_panel_cache_reads_canonical_without_sync_fetch(monkeypatch):
|
||||||
import web.services.city_runtime as city_runtime
|
import web.services.city_runtime as city_runtime
|
||||||
|
|
||||||
writes = {}
|
enqueued = []
|
||||||
|
|
||||||
class FakeDB:
|
class FakeDB:
|
||||||
def set_city_cache(self, kind, city, payload, **_kwargs):
|
def get_city_cache(self, kind, city):
|
||||||
writes["city_cache"] = (kind, city, payload)
|
assert kind == "panel"
|
||||||
|
assert city == "shanghai"
|
||||||
|
return None
|
||||||
|
|
||||||
def set_canonical_temperature(self, city, payload):
|
def get_canonical_temperature(self, city):
|
||||||
writes["canonical"] = (city, payload)
|
assert city == "shanghai"
|
||||||
|
return {
|
||||||
|
"payload": {
|
||||||
|
"city": "shanghai",
|
||||||
|
"value": 31.2,
|
||||||
|
"temp_symbol": "°C",
|
||||||
|
"source": "amsc_awos",
|
||||||
|
"source_label": "AMSC AWOS runway-point air temperature",
|
||||||
|
"source_role": "settlement_proxy",
|
||||||
|
"observed_at": "2026-06-14T01:01:00+00:00",
|
||||||
|
"observed_at_local": "09:01",
|
||||||
|
"freshness_sec": 63,
|
||||||
|
"freshness_status": "fresh",
|
||||||
|
"fetched_at": "2026-06-14T01:02:03+00:00",
|
||||||
|
"confidence": 0.92,
|
||||||
|
"explanation": "AMSC AWOS runway-point air temperature updated 63s ago.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def enqueue_observation_refresh_request(self, **kwargs):
|
||||||
|
enqueued.append(kwargs)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def fail_analyze(*_args, **_kwargs):
|
||||||
|
raise AssertionError("business cache refresh must not call _analyze")
|
||||||
|
|
||||||
monkeypatch.setattr(city_runtime, "_CACHE_DB", FakeDB())
|
monkeypatch.setattr(city_runtime, "_CACHE_DB", FakeDB())
|
||||||
monkeypatch.setattr(
|
if hasattr(city_runtime, "_analyze"):
|
||||||
city_runtime,
|
monkeypatch.setattr(city_runtime, "_analyze", fail_analyze)
|
||||||
"_analyze",
|
|
||||||
lambda city, force_refresh=False, detail_mode="panel": _sample_panel_payload(),
|
|
||||||
)
|
|
||||||
|
|
||||||
payload = city_runtime._refresh_city_panel_cache("shanghai", allow_external_fetch=True)
|
payload = city_runtime._refresh_city_panel_cache("shanghai")
|
||||||
|
|
||||||
assert payload["canonical_temperature"]["value"] == 31.2
|
assert payload["canonical_temperature"]["value"] == 31.2
|
||||||
assert payload["canonical_temperature"]["source"] == "amsc_awos"
|
assert payload["canonical_temperature"]["source"] == "amsc_awos"
|
||||||
assert payload["canonical_temperature"]["freshness_sec"] == 63
|
assert payload["canonical_temperature"]["freshness_sec"] == 63
|
||||||
assert payload["canonical_temperature"]["source_role"] == "settlement_proxy"
|
assert payload["canonical_temperature"]["source_role"] == "settlement_proxy"
|
||||||
assert writes["canonical"][0] == "shanghai"
|
assert enqueued == [
|
||||||
assert writes["canonical"][1]["observed_at"] == "2026-06-14T01:01:00+00:00"
|
{
|
||||||
|
"city": "shanghai",
|
||||||
|
"kind": "panel",
|
||||||
|
"priority": "high",
|
||||||
|
"reason": "canonical_fallback",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_city_runtime_refresh_helpers_have_no_sync_fetch_escape_hatch():
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
import web.services.city_runtime as city_runtime
|
||||||
|
|
||||||
|
for name in (
|
||||||
|
"_refresh_city_summary_cache",
|
||||||
|
"_refresh_city_panel_cache",
|
||||||
|
"_refresh_city_nearby_cache",
|
||||||
|
"_refresh_city_market_cache",
|
||||||
|
"_refresh_city_full_cache",
|
||||||
|
):
|
||||||
|
signature = inspect.signature(getattr(city_runtime, name))
|
||||||
|
assert "allow_external_fetch" not in signature.parameters
|
||||||
|
assert not hasattr(city_runtime, "_analyze")
|
||||||
|
assert not hasattr(city_runtime, "_analyze_summary")
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_city_panel_cache_defaults_to_queue_without_sync_analyze(monkeypatch):
|
def test_refresh_city_panel_cache_defaults_to_queue_without_sync_analyze(monkeypatch):
|
||||||
@@ -128,11 +151,7 @@ def test_refresh_city_panel_cache_defaults_to_queue_without_sync_analyze(monkeyp
|
|||||||
enqueued.append(kwargs)
|
enqueued.append(kwargs)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def fail_analyze(*_args, **_kwargs):
|
|
||||||
raise AssertionError("business cache refresh must not call _analyze by default")
|
|
||||||
|
|
||||||
monkeypatch.setattr(city_runtime, "_CACHE_DB", FakeDB())
|
monkeypatch.setattr(city_runtime, "_CACHE_DB", FakeDB())
|
||||||
monkeypatch.setattr(city_runtime, "_analyze", fail_analyze)
|
|
||||||
|
|
||||||
payload = city_runtime._refresh_city_panel_cache("shanghai")
|
payload = city_runtime._refresh_city_panel_cache("shanghai")
|
||||||
|
|
||||||
|
|||||||
+10
-118
@@ -23,17 +23,12 @@ from src.data_collection.city_registry import ALIASES
|
|||||||
from src.data_collection.city_time import get_city_utc_offset_seconds # noqa: F401 - compatibility export for transitional routers
|
from src.data_collection.city_time import get_city_utc_offset_seconds # noqa: F401 - compatibility export for transitional routers
|
||||||
from src.utils.refresh_policy import OBSERVATION_REFRESH_SEC, SCAN_ROWS_REFRESH_SEC
|
from src.utils.refresh_policy import OBSERVATION_REFRESH_SEC, SCAN_ROWS_REFRESH_SEC
|
||||||
from web.analysis_service import (
|
from web.analysis_service import (
|
||||||
_analyze,
|
|
||||||
_analyze_summary,
|
|
||||||
_build_city_chart_detail_payload, # noqa: F401 - compatibility export for chart detail batches
|
_build_city_chart_detail_payload, # noqa: F401 - compatibility export for chart detail batches
|
||||||
_build_city_detail_payload, # noqa: F401 - compatibility export for tests and transitional routers
|
_build_city_detail_payload, # noqa: F401 - compatibility export for tests and transitional routers
|
||||||
_build_city_market_scan_payload,
|
_build_city_market_scan_payload,
|
||||||
_build_city_summary_payload,
|
|
||||||
)
|
)
|
||||||
from web.services.canonical_temperature import (
|
from web.services.canonical_temperature import (
|
||||||
attach_canonical_temperature,
|
|
||||||
build_city_weather_from_canonical,
|
build_city_weather_from_canonical,
|
||||||
store_canonical_temperature_from_payload,
|
|
||||||
)
|
)
|
||||||
from web.scan_terminal_service import build_scan_terminal_payload # noqa: F401 - compatibility export for tests and transitional routers
|
from web.scan_terminal_service import build_scan_terminal_payload # noqa: F401 - compatibility export for tests and transitional routers
|
||||||
from web.core import (
|
from web.core import (
|
||||||
@@ -277,17 +272,6 @@ def _refresh_market_scan_payload_from_cached_analysis(
|
|||||||
return payload.get("market_scan_payload") or {}
|
return payload.get("market_scan_payload") or {}
|
||||||
|
|
||||||
|
|
||||||
def _attach_and_store_canonical_temperature(city: str, payload: dict) -> dict:
|
|
||||||
if not isinstance(payload, dict):
|
|
||||||
return payload
|
|
||||||
attach_canonical_temperature(payload, city=city)
|
|
||||||
try:
|
|
||||||
store_canonical_temperature_from_payload(_CACHE_DB, city, payload)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.debug("canonical temperature store skipped city={}: {}", city, exc)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def _enqueue_city_observation_refresh(city: str, kind: str, *, reason: str) -> bool:
|
def _enqueue_city_observation_refresh(city: str, kind: str, *, reason: str) -> bool:
|
||||||
try:
|
try:
|
||||||
enqueue = getattr(_CACHE_DB, "enqueue_observation_refresh_request", None)
|
enqueue = getattr(_CACHE_DB, "enqueue_observation_refresh_request", None)
|
||||||
@@ -428,116 +412,24 @@ def _queued_city_cache_payload(city: str, kind: str, *, force_refresh: bool = Fa
|
|||||||
return _initializing_city_payload(normalized, detail_depth=kind)
|
return _initializing_city_payload(normalized, detail_depth=kind)
|
||||||
|
|
||||||
|
|
||||||
def _refresh_city_summary_cache(
|
def _refresh_city_summary_cache(city: str, force_refresh: bool = False) -> dict:
|
||||||
city: str,
|
return _queued_city_cache_payload(city, "summary", force_refresh=force_refresh)
|
||||||
force_refresh: bool = False,
|
|
||||||
*,
|
|
||||||
allow_external_fetch: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
if not allow_external_fetch:
|
|
||||||
return _queued_city_cache_payload(city, "summary", force_refresh=force_refresh)
|
|
||||||
|
|
||||||
data = _analyze_summary(city, force_refresh=force_refresh)
|
|
||||||
_attach_and_store_canonical_temperature(city, data)
|
|
||||||
payload = _build_city_summary_payload(data)
|
|
||||||
if data.get("canonical_temperature"):
|
|
||||||
payload["canonical_temperature"] = data["canonical_temperature"]
|
|
||||||
_CACHE_DB.set_city_cache(
|
|
||||||
"summary",
|
|
||||||
city,
|
|
||||||
payload,
|
|
||||||
version="v1",
|
|
||||||
source_fingerprint=f"{city}:summary",
|
|
||||||
)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def _refresh_city_panel_cache(
|
def _refresh_city_panel_cache(city: str, force_refresh: bool = False) -> dict:
|
||||||
city: str,
|
return _queued_city_cache_payload(city, "panel", force_refresh=force_refresh)
|
||||||
force_refresh: bool = False,
|
|
||||||
*,
|
|
||||||
allow_external_fetch: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
if not allow_external_fetch:
|
|
||||||
return _queued_city_cache_payload(city, "panel", force_refresh=force_refresh)
|
|
||||||
|
|
||||||
payload = _analyze(city, force_refresh=force_refresh, detail_mode="panel")
|
|
||||||
_attach_and_store_canonical_temperature(city, payload)
|
|
||||||
_CACHE_DB.set_city_cache(
|
|
||||||
"panel",
|
|
||||||
city,
|
|
||||||
payload,
|
|
||||||
version="v1",
|
|
||||||
source_fingerprint=f"{city}:panel",
|
|
||||||
)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def _refresh_city_nearby_cache(
|
def _refresh_city_nearby_cache(city: str, force_refresh: bool = False) -> dict:
|
||||||
city: str,
|
return _queued_city_cache_payload(city, "nearby", force_refresh=force_refresh)
|
||||||
force_refresh: bool = False,
|
|
||||||
*,
|
|
||||||
allow_external_fetch: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
if not allow_external_fetch:
|
|
||||||
return _queued_city_cache_payload(city, "nearby", force_refresh=force_refresh)
|
|
||||||
|
|
||||||
payload = _analyze(city, force_refresh=force_refresh, detail_mode="nearby")
|
|
||||||
_attach_and_store_canonical_temperature(city, payload)
|
|
||||||
_CACHE_DB.set_city_cache(
|
|
||||||
"nearby",
|
|
||||||
city,
|
|
||||||
payload,
|
|
||||||
version="v1",
|
|
||||||
source_fingerprint=f"{city}:nearby",
|
|
||||||
)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def _refresh_city_market_cache(
|
def _refresh_city_market_cache(city: str, force_refresh: bool = False) -> dict:
|
||||||
city: str,
|
return _queued_city_cache_payload(city, "market", force_refresh=force_refresh)
|
||||||
force_refresh: bool = False,
|
|
||||||
*,
|
|
||||||
allow_external_fetch: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
if not allow_external_fetch:
|
|
||||||
return _queued_city_cache_payload(city, "market", force_refresh=force_refresh)
|
|
||||||
|
|
||||||
payload = _analyze(city, force_refresh=force_refresh, detail_mode="market")
|
|
||||||
_attach_and_store_canonical_temperature(city, payload)
|
|
||||||
now_ts = time.time()
|
|
||||||
payload["market_analysis_cached_at"] = datetime.now().isoformat()
|
|
||||||
payload["market_analysis_cached_at_ts"] = now_ts
|
|
||||||
_attach_market_scan_payload(payload)
|
|
||||||
_CACHE_DB.set_city_cache(
|
|
||||||
"market",
|
|
||||||
city,
|
|
||||||
payload,
|
|
||||||
version="v1",
|
|
||||||
source_fingerprint=f"{city}:market",
|
|
||||||
)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def _refresh_city_full_cache(
|
def _refresh_city_full_cache(city: str, force_refresh: bool = False) -> dict:
|
||||||
city: str,
|
return _queued_city_cache_payload(city, "full", force_refresh=force_refresh)
|
||||||
force_refresh: bool = False,
|
|
||||||
*,
|
|
||||||
allow_external_fetch: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
if not allow_external_fetch:
|
|
||||||
return _queued_city_cache_payload(city, "full", force_refresh=force_refresh)
|
|
||||||
|
|
||||||
payload = _analyze(city, force_refresh=force_refresh, detail_mode="full")
|
|
||||||
_attach_and_store_canonical_temperature(city, payload)
|
|
||||||
_CACHE_DB.set_city_cache(
|
|
||||||
"full",
|
|
||||||
city,
|
|
||||||
payload,
|
|
||||||
version="v1",
|
|
||||||
source_fingerprint=f"{city}:full",
|
|
||||||
)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def _overlay_wunderground_current(payload: dict, wunderground: dict) -> dict:
|
def _overlay_wunderground_current(payload: dict, wunderground: dict) -> dict:
|
||||||
|
|||||||
Reference in New Issue
Block a user