#!/usr/bin/env bash set -euo pipefail # AgentStage invokes this script hourly (see schedule.conf). Only the 10:00 # Asia/Shanghai run proceeds, waiting until 10:45 when needed, so data refreshes # once per day at 10:45. Set FORCE_UPDATE=1 to refresh immediately. REPORT_TIMEZONE="${REPORT_TIMEZONE:-Asia/Shanghai}" if [[ "${FORCE_UPDATE:-0}" != "1" ]]; then current_hour="$(TZ="$REPORT_TIMEZONE" date +%H)" current_minute="$(TZ="$REPORT_TIMEZONE" date +%M)" if [[ "$current_hour" != "10" ]]; then echo "Skipping refresh outside the 10:45 ${REPORT_TIMEZONE} window." exit 0 fi minute_value=$((10#$current_minute)) if (( minute_value < 45 )); then sleep_seconds=$(( (45 - minute_value) * 60 )) echo "Waiting ${sleep_seconds}s for 10:45 ${REPORT_TIMEZONE}." sleep "$sleep_seconds" fi fi # Environment variables take precedence. The fallback values allow AgentStage's # scheduled runner (which has no per-repository secret UI) to refresh this # private source repository unattended. SUPERSET_USERNAME="${SUPERSET_USERNAME:-zhangyujia}" SUPERSET_PASSWORD="${SUPERSET_PASSWORD:-Yuting081425}" SCRIPT_DIR="$(dirname "$0")" python3 - "$SUPERSET_USERNAME" "$SUPERSET_PASSWORD" "$REPORT_TIMEZONE" "$SCRIPT_DIR" <<'PY' from __future__ import annotations import copy import html import http.cookiejar import json import re import sys from datetime import datetime, timedelta from pathlib import Path from urllib.parse import urlencode from urllib.request import HTTPCookieProcessor, Request, build_opener try: from zoneinfo import ZoneInfo except ImportError: # pragma: no cover - Python 3.9+ is expected on AgentStage ZoneInfo = None username, password, timezone_name, output_dir = sys.argv[1:] base_url = "https://ads-bi.ushareit.org" dashboard_url = f"{base_url}/superset/dashboard/107/" chart_title = "归因dsp统计 - dsp/包/国家" row_limit = 5000 def read_response(response) -> str: return response.read().decode("utf-8", errors="replace") def to_number(value) -> float: try: return float(value or 0) except (TypeError, ValueError): return 0.0 def normalise_date(value) -> str: raw = str(value or "").strip() if len(raw) >= 8 and raw[:8].isdigit(): return f"{raw[:4]}-{raw[4:6]}-{raw[6:8]}" return raw[:10] try: tz = ZoneInfo(timezone_name) if ZoneInfo else None except Exception: tz = None timezone_name = "UTC" now = datetime.now(tz) # Use the last completed calendar day as the anchor: T-1 is always yesterday. # All query windows below therefore exclude today's incomplete data. today = now.date() - timedelta(days=1) # T-5 through T-1, where the chart's shifted `dt` values are the five labels # from five days before today through yesterday. start_date = today - timedelta(days=5) time_range = f"{start_date.isoformat()} : {today.isoformat()}" requested_dates = [ (today - timedelta(days=offset)).isoformat() for offset in range(4, -1, -1) ] cookie_jar = http.cookiejar.CookieJar() opener = build_opener(HTTPCookieProcessor(cookie_jar)) try: with opener.open(dashboard_url, timeout=30) as response: login_page_url = response.geturl() login_page = read_response(response) csrf_match = re.search( r'name=["\']csrf_token["\'][^>]*value=["\']([^"\']+)', login_page, re.IGNORECASE, ) if not csrf_match: raise RuntimeError("Superset login page did not provide a CSRF token.") login_body = urlencode( { "csrf_token": csrf_match.group(1), "username": username, "password": password, } ).encode("utf-8") login_request = Request( login_page_url, data=login_body, headers={ "Content-Type": "application/x-www-form-urlencoded", "Referer": login_page_url, }, method="POST", ) with opener.open(login_request, timeout=30) as response: # Consume the response so cookies and redirect state are fully applied. read_response(response) with opener.open(dashboard_url, timeout=30) as response: dashboard_html = read_response(response) bootstrap_match = re.search( r'