feat: Implement PolyWeather web map API backend and integrate new frontend dashboard components for city weather details.
This commit is contained in:
+14
-14
@@ -22,20 +22,20 @@
|
|||||||
```mermaid
|
```mermaid
|
||||||
mindmap
|
mindmap
|
||||||
root((PolyWeather API))
|
root((PolyWeather API))
|
||||||
城市列表
|
"城市列表"
|
||||||
[GET /api/cities]
|
"GET /api/cities"
|
||||||
城市主数据
|
"城市主数据"
|
||||||
[GET /api/city/:name]
|
"GET /api/city/{name}"
|
||||||
[GET /api/city/:name/summary]
|
"GET /api/city/{name}/summary"
|
||||||
[GET /api/city/:name/detail]
|
"GET /api/city/{name}/detail"
|
||||||
历史数据
|
"历史数据"
|
||||||
[GET /api/history/:name]
|
"GET /api/history/{name}"
|
||||||
关键对象
|
"关键对象"
|
||||||
current
|
"current"
|
||||||
forecast
|
"forecast"
|
||||||
[probabilities (mu + distribution)]
|
"probabilities (mu + distribution)"
|
||||||
[multi_model / multi_model_daily]
|
"multi_model / multi_model_daily"
|
||||||
[market_scan (P0 只读)]
|
"market_scan (P0 只读)"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -17,12 +17,16 @@ export async function GET(
|
|||||||
const { name } = await context.params;
|
const { name } = await context.params;
|
||||||
const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false";
|
const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false";
|
||||||
const marketSlug = req.nextUrl.searchParams.get("market_slug");
|
const marketSlug = req.nextUrl.searchParams.get("market_slug");
|
||||||
|
const targetDate = req.nextUrl.searchParams.get("target_date");
|
||||||
const searchParams = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
force_refresh: forceRefresh,
|
force_refresh: forceRefresh,
|
||||||
});
|
});
|
||||||
if (marketSlug) {
|
if (marketSlug) {
|
||||||
searchParams.set("market_slug", marketSlug);
|
searchParams.set("market_slug", marketSlug);
|
||||||
}
|
}
|
||||||
|
if (targetDate) {
|
||||||
|
searchParams.set("target_date", targetDate);
|
||||||
|
}
|
||||||
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/detail?${searchParams.toString()}`;
|
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/detail?${searchParams.toString()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
} from "@/lib/dashboard-utils";
|
} from "@/lib/dashboard-utils";
|
||||||
|
|
||||||
function normalizeMarketValue(value?: number | null) {
|
function normalizeMarketValue(value?: number | null) {
|
||||||
|
if (value == null) return null;
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
if (!Number.isFinite(numeric)) return null;
|
if (!Number.isFinite(numeric)) return null;
|
||||||
if (numeric > 1) return Math.max(0, Math.min(1, numeric / 100));
|
if (numeric > 1) return Math.max(0, Math.min(1, numeric / 100));
|
||||||
@@ -567,7 +568,13 @@ export function FutureForecastModal() {
|
|||||||
"future-refresh-btn",
|
"future-refresh-btn",
|
||||||
store.loadingState.marketScan && "spinning",
|
store.loadingState.marketScan && "spinning",
|
||||||
)}
|
)}
|
||||||
onClick={() => store.openTodayModal(true)}
|
onClick={() => {
|
||||||
|
if (isToday) {
|
||||||
|
void store.openTodayModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.openFutureModal(dateStr, true);
|
||||||
|
}}
|
||||||
title={locale === "en-US" ? "Refresh Data" : "刷新数据"}
|
title={locale === "en-US" ? "Refresh Data" : "刷新数据"}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -936,6 +943,7 @@ export function FutureForecastModal() {
|
|||||||
detail={detail}
|
detail={detail}
|
||||||
targetDate={dateStr}
|
targetDate={dateStr}
|
||||||
hideTitle
|
hideTitle
|
||||||
|
marketScan={marketScan}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section className="future-modal-section">
|
<section className="future-modal-section">
|
||||||
|
|||||||
@@ -28,12 +28,14 @@ function EmptyState({ text }: { text: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toPercent(value?: number | null) {
|
function toPercent(value?: number | null) {
|
||||||
|
if (value == null) return null;
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
if (!Number.isFinite(numeric)) return null;
|
if (!Number.isFinite(numeric)) return null;
|
||||||
return `${(numeric * 100).toFixed(1)}%`;
|
return `${(numeric * 100).toFixed(1)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPriceCents(value?: number | null) {
|
function toPriceCents(value?: number | null) {
|
||||||
|
if (value == null) return null;
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
if (!Number.isFinite(numeric)) return null;
|
if (!Number.isFinite(numeric)) return null;
|
||||||
const normalized = numeric > 1 ? numeric / 100 : numeric;
|
const normalized = numeric > 1 ? numeric / 100 : numeric;
|
||||||
@@ -54,16 +56,20 @@ function parseTempFromText(value: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getBucketTemp(bucket: ProbabilityBucket) {
|
function getBucketTemp(bucket: ProbabilityBucket) {
|
||||||
const byValue = Number(bucket.value);
|
if (bucket.value != null) {
|
||||||
if (Number.isFinite(byValue)) return byValue;
|
const byValue = Number(bucket.value);
|
||||||
|
if (Number.isFinite(byValue)) return byValue;
|
||||||
|
}
|
||||||
return parseTempFromText(bucket.label || bucket.bucket || bucket.range);
|
return parseTempFromText(bucket.label || bucket.bucket || bucket.range);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMarketBucketTemp(scan?: MarketScan | null) {
|
function getMarketBucketTemp(scan?: MarketScan | null) {
|
||||||
if (!scan) return null;
|
if (!scan) return null;
|
||||||
|
|
||||||
const byBucketValue = Number(scan.temperature_bucket?.value);
|
if (scan.temperature_bucket?.value != null) {
|
||||||
if (Number.isFinite(byBucketValue)) return byBucketValue;
|
const byBucketValue = Number(scan.temperature_bucket.value);
|
||||||
|
if (Number.isFinite(byBucketValue)) return byBucketValue;
|
||||||
|
}
|
||||||
|
|
||||||
const byBucketLabel = parseTempFromText(
|
const byBucketLabel = parseTempFromText(
|
||||||
scan.temperature_bucket?.label ||
|
scan.temperature_bucket?.label ||
|
||||||
@@ -83,21 +89,29 @@ function getMarketBucketTemp(scan?: MarketScan | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMarketYesPrice(scan?: MarketScan | null) {
|
function getMarketYesPrice(scan?: MarketScan | null) {
|
||||||
const preferred = Number(scan?.market_price);
|
if (scan?.market_price != null) {
|
||||||
if (Number.isFinite(preferred)) return preferred;
|
const preferred = Number(scan.market_price);
|
||||||
const implied = Number(scan?.yes_token?.implied_probability);
|
if (Number.isFinite(preferred)) return preferred;
|
||||||
return Number.isFinite(implied) ? implied : null;
|
}
|
||||||
|
if (scan?.yes_token?.implied_probability != null) {
|
||||||
|
const implied = Number(scan.yes_token.implied_probability);
|
||||||
|
if (Number.isFinite(implied)) return implied;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMarketNoPrice(scan?: MarketScan | null) {
|
function getMarketNoPrice(scan?: MarketScan | null) {
|
||||||
const direct = Number(scan?.no_buy);
|
if (scan?.no_buy != null) {
|
||||||
if (Number.isFinite(direct)) return direct;
|
const direct = Number(scan.no_buy);
|
||||||
|
if (Number.isFinite(direct)) return direct;
|
||||||
|
}
|
||||||
const marketYes = getMarketYesPrice(scan);
|
const marketYes = getMarketYesPrice(scan);
|
||||||
if (marketYes != null) return Math.max(0, Math.min(1, 1 - marketYes));
|
if (marketYes != null) return Math.max(0, Math.min(1, 1 - marketYes));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeMarketProbability(value?: number | null) {
|
function normalizeMarketProbability(value?: number | null) {
|
||||||
|
if (value == null) return null;
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
if (!Number.isFinite(numeric)) return null;
|
if (!Number.isFinite(numeric)) return null;
|
||||||
if (numeric > 1) return Math.max(0, Math.min(1, numeric / 100));
|
if (numeric > 1) return Math.max(0, Math.min(1, numeric / 100));
|
||||||
@@ -120,11 +134,15 @@ function getMarketTopBuckets(scan?: MarketScan | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMarketTopBucketKey(bucket: MarketTopBucket) {
|
function getMarketTopBucketKey(bucket: MarketTopBucket) {
|
||||||
const valueNum = Number(bucket?.value);
|
if (bucket?.value != null) {
|
||||||
if (Number.isFinite(valueNum)) return `v:${valueNum.toFixed(2)}`;
|
const valueNum = Number(bucket.value);
|
||||||
|
if (Number.isFinite(valueNum)) return `v:${valueNum.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
const tempNum = Number(bucket?.temp);
|
if (bucket?.temp != null) {
|
||||||
if (Number.isFinite(tempNum)) return `t:${tempNum.toFixed(2)}`;
|
const tempNum = Number(bucket.temp);
|
||||||
|
if (Number.isFinite(tempNum)) return `t:${tempNum.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = parseTempFromText(bucket?.label);
|
const parsed = parseTempFromText(bucket?.label);
|
||||||
if (parsed != null) return `l:${parsed.toFixed(2)}`;
|
if (parsed != null) return `l:${parsed.toFixed(2)}`;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface DashboardStoreValue extends DashboardState {
|
|||||||
futureModalDate: string | null;
|
futureModalDate: string | null;
|
||||||
isGuideOpen: boolean;
|
isGuideOpen: boolean;
|
||||||
loadCities: () => Promise<void>;
|
loadCities: () => Promise<void>;
|
||||||
openFutureModal: (dateStr: string) => void;
|
openFutureModal: (dateStr: string, forceRefresh?: boolean) => void;
|
||||||
openGuide: () => void;
|
openGuide: () => void;
|
||||||
openHistory: () => Promise<void>;
|
openHistory: () => Promise<void>;
|
||||||
openTodayModal: (forceRefresh?: boolean) => Promise<void>;
|
openTodayModal: (forceRefresh?: boolean) => Promise<void>;
|
||||||
@@ -158,6 +158,11 @@ function mergeAiAnalysisIfStable(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMarketScanCacheKey(cityName: string, targetDate?: string | null) {
|
||||||
|
const normalizedDate = String(targetDate || "").trim() || "local";
|
||||||
|
return `${cityName}::${normalizedDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function DashboardStoreProvider({
|
export function DashboardStoreProvider({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -210,8 +215,13 @@ export function DashboardStoreProvider({
|
|||||||
const selectedDetail = selectedCity
|
const selectedDetail = selectedCity
|
||||||
? cityDetailsByName[selectedCity] || null
|
? cityDetailsByName[selectedCity] || null
|
||||||
: null;
|
: null;
|
||||||
|
const selectedMarketDate =
|
||||||
|
futureModalDate || selectedForecastDate || selectedDetail?.local_date || null;
|
||||||
|
const selectedMarketScanKey = selectedCity
|
||||||
|
? getMarketScanCacheKey(selectedCity, selectedMarketDate)
|
||||||
|
: null;
|
||||||
const selectedMarketScan = selectedCity
|
const selectedMarketScan = selectedCity
|
||||||
? marketScanByCityName[selectedCity] || null
|
? marketScanByCityName[selectedMarketScanKey || ""] || null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -277,8 +287,10 @@ export function DashboardStoreProvider({
|
|||||||
cityName: string,
|
cityName: string,
|
||||||
force = false,
|
force = false,
|
||||||
marketSlug?: string | null,
|
marketSlug?: string | null,
|
||||||
|
targetDate?: string | null,
|
||||||
) => {
|
) => {
|
||||||
const cached = marketScanByCityName[cityName];
|
const cacheKey = getMarketScanCacheKey(cityName, targetDate);
|
||||||
|
const cached = marketScanByCityName[cacheKey];
|
||||||
if (!force && cached && !marketSlug) {
|
if (!force && cached && !marketSlug) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
@@ -286,11 +298,12 @@ export function DashboardStoreProvider({
|
|||||||
const latestScan = await dashboardClient.getCityMarketScan(cityName, {
|
const latestScan = await dashboardClient.getCityMarketScan(cityName, {
|
||||||
force,
|
force,
|
||||||
marketSlug,
|
marketSlug,
|
||||||
|
targetDate,
|
||||||
});
|
});
|
||||||
if (latestScan) {
|
if (latestScan) {
|
||||||
setMarketScanByCityName((current) => ({
|
setMarketScanByCityName((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[cityName]: latestScan,
|
[cacheKey]: latestScan,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return latestScan;
|
return latestScan;
|
||||||
@@ -366,7 +379,9 @@ export function DashboardStoreProvider({
|
|||||||
const detail = await ensureCityDetail(cityName);
|
const detail = await ensureCityDetail(cityName);
|
||||||
setSelectedForecastDate(detail.local_date);
|
setSelectedForecastDate(detail.local_date);
|
||||||
// 预热市场数据,不做 await 阻塞,后台静默拉取
|
// 预热市场数据,不做 await 阻塞,后台静默拉取
|
||||||
void ensureCityMarketScan(cityName, false).catch(() => {});
|
void ensureCityMarketScan(cityName, false, null, detail.local_date).catch(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingState((current) => ({ ...current, cityDetail: false }));
|
setLoadingState((current) => ({ ...current, cityDetail: false }));
|
||||||
}
|
}
|
||||||
@@ -464,9 +479,22 @@ export function DashboardStoreProvider({
|
|||||||
isGuideOpen,
|
isGuideOpen,
|
||||||
loadCities,
|
loadCities,
|
||||||
loadingState,
|
loadingState,
|
||||||
openFutureModal: (dateStr: string) => {
|
openFutureModal: (dateStr: string, forceRefresh = false) => {
|
||||||
mapStopMotionRef.current();
|
mapStopMotionRef.current();
|
||||||
setFutureModalDate(dateStr);
|
setFutureModalDate(dateStr);
|
||||||
|
if (!selectedCity) return;
|
||||||
|
const cacheKey = getMarketScanCacheKey(selectedCity, dateStr);
|
||||||
|
setLoadingState((current) => ({ ...current, marketScan: true }));
|
||||||
|
void ensureCityMarketScan(
|
||||||
|
selectedCity,
|
||||||
|
forceRefresh || !marketScanByCityName[cacheKey],
|
||||||
|
null,
|
||||||
|
dateStr,
|
||||||
|
)
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
setLoadingState((current) => ({ ...current, marketScan: false }));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
openGuide: () => setIsGuideOpen(true),
|
openGuide: () => setIsGuideOpen(true),
|
||||||
openHistory,
|
openHistory,
|
||||||
@@ -499,9 +527,15 @@ export function DashboardStoreProvider({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 如果缓存里没有或者想要强制刷新,则拉取最新市场数据
|
// 如果缓存里没有或者想要强制刷新,则拉取最新市场数据
|
||||||
|
const marketKey = getMarketScanCacheKey(
|
||||||
|
selectedCity,
|
||||||
|
detail.local_date,
|
||||||
|
);
|
||||||
await ensureCityMarketScan(
|
await ensureCityMarketScan(
|
||||||
selectedCity,
|
selectedCity,
|
||||||
forceRefresh || !marketScanByCityName[selectedCity],
|
forceRefresh || !marketScanByCityName[marketKey],
|
||||||
|
null,
|
||||||
|
detail.local_date,
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -164,12 +164,19 @@ export const dashboardClient = {
|
|||||||
|
|
||||||
async getCityMarketScan(
|
async getCityMarketScan(
|
||||||
cityName: string,
|
cityName: string,
|
||||||
options?: { force?: boolean; marketSlug?: string | null },
|
options?: {
|
||||||
|
force?: boolean;
|
||||||
|
marketSlug?: string | null;
|
||||||
|
targetDate?: string | null;
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const force = options?.force ?? false;
|
const force = options?.force ?? false;
|
||||||
const marketSlug = options?.marketSlug || null;
|
const marketSlug = options?.marketSlug || null;
|
||||||
|
const targetDate = options?.targetDate || null;
|
||||||
if (!force) {
|
if (!force) {
|
||||||
const requestKey = `${cityName}::cached::${marketSlug || "-"}`;
|
const requestKey = `${cityName}::cached::${marketSlug || "-"}::${
|
||||||
|
targetDate || "-"
|
||||||
|
}`;
|
||||||
const existing = pendingMarketScanRequests.get(requestKey);
|
const existing = pendingMarketScanRequests.get(requestKey);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return existing;
|
||||||
@@ -181,6 +188,9 @@ export const dashboardClient = {
|
|||||||
if (marketSlug) {
|
if (marketSlug) {
|
||||||
params.set("market_slug", marketSlug);
|
params.set("market_slug", marketSlug);
|
||||||
}
|
}
|
||||||
|
if (targetDate) {
|
||||||
|
params.set("target_date", targetDate);
|
||||||
|
}
|
||||||
|
|
||||||
const request = fetchJson<{ market_scan?: MarketScan }>(
|
const request = fetchJson<{ market_scan?: MarketScan }>(
|
||||||
`/api/city/${normalizeCityName(cityName)}/detail?${params.toString()}`,
|
`/api/city/${normalizeCityName(cityName)}/detail?${params.toString()}`,
|
||||||
@@ -201,6 +211,9 @@ export const dashboardClient = {
|
|||||||
if (marketSlug) {
|
if (marketSlug) {
|
||||||
params.set("market_slug", marketSlug);
|
params.set("market_slug", marketSlug);
|
||||||
}
|
}
|
||||||
|
if (targetDate) {
|
||||||
|
params.set("target_date", targetDate);
|
||||||
|
}
|
||||||
|
|
||||||
return fetchJson<{ market_scan?: MarketScan }>(
|
return fetchJson<{ market_scan?: MarketScan }>(
|
||||||
`/api/city/${normalizeCityName(cityName)}/detail?${params.toString()}`,
|
`/api/city/${normalizeCityName(cityName)}/detail?${params.toString()}`,
|
||||||
|
|||||||
+27
-4
@@ -782,10 +782,28 @@ def _build_city_summary_payload(data: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
def _build_city_detail_payload(
|
def _build_city_detail_payload(
|
||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
market_slug: Optional[str] = None,
|
market_slug: Optional[str] = None,
|
||||||
|
target_date: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
distribution = data.get("probabilities", {}).get("distribution", []) or []
|
local_date = str(data.get("local_date") or "").strip()
|
||||||
|
requested_date = str(target_date or "").strip()
|
||||||
|
selected_date = requested_date or local_date
|
||||||
|
|
||||||
|
multi_model_daily = data.get("multi_model_daily") or {}
|
||||||
|
selected_daily = (
|
||||||
|
multi_model_daily.get(selected_date)
|
||||||
|
if isinstance(multi_model_daily, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if not isinstance(selected_daily, dict):
|
||||||
|
selected_daily = {}
|
||||||
|
selected_date = local_date
|
||||||
|
|
||||||
|
distribution = selected_daily.get("probabilities")
|
||||||
|
if not isinstance(distribution, list) or not distribution:
|
||||||
|
distribution = data.get("probabilities", {}).get("distribution", []) or []
|
||||||
|
|
||||||
city_name = str(data.get("name") or "").strip().lower()
|
city_name = str(data.get("name") or "").strip().lower()
|
||||||
model_map = data.get("multi_model") or {}
|
model_map = selected_daily.get("models") or data.get("multi_model") or {}
|
||||||
if not isinstance(model_map, dict):
|
if not isinstance(model_map, dict):
|
||||||
model_map = {}
|
model_map = {}
|
||||||
|
|
||||||
@@ -839,7 +857,7 @@ def _build_city_detail_payload(
|
|||||||
]
|
]
|
||||||
market_scan = _market_layer.build_market_scan(
|
market_scan = _market_layer.build_market_scan(
|
||||||
city=data.get("name"),
|
city=data.get("name"),
|
||||||
target_date=data.get("local_date"),
|
target_date=selected_date or data.get("local_date"),
|
||||||
temperature_bucket=primary_bucket if isinstance(primary_bucket, dict) else None,
|
temperature_bucket=primary_bucket if isinstance(primary_bucket, dict) else None,
|
||||||
model_probability=model_probability,
|
model_probability=model_probability,
|
||||||
fallback_sparkline=fallback_sparkline,
|
fallback_sparkline=fallback_sparkline,
|
||||||
@@ -945,11 +963,16 @@ async def city_detail_aggregate(
|
|||||||
name: str,
|
name: str,
|
||||||
force_refresh: bool = False,
|
force_refresh: bool = False,
|
||||||
market_slug: Optional[str] = None,
|
market_slug: Optional[str] = None,
|
||||||
|
target_date: Optional[str] = None,
|
||||||
):
|
):
|
||||||
_assert_entitlement(request)
|
_assert_entitlement(request)
|
||||||
city = _normalize_city_or_404(name)
|
city = _normalize_city_or_404(name)
|
||||||
data = _analyze(city, force_refresh=force_refresh)
|
data = _analyze(city, force_refresh=force_refresh)
|
||||||
return _build_city_detail_payload(data, market_slug=market_slug)
|
return _build_city_detail_payload(
|
||||||
|
data,
|
||||||
|
market_slug=market_slug,
|
||||||
|
target_date=target_date,
|
||||||
|
)
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────
|
||||||
# Entrypoint
|
# Entrypoint
|
||||||
|
|||||||
Reference in New Issue
Block a user