#!/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) today = now.date() # T-5 through T-1. The end boundary is today because Superset treats it as the # exclusive business-day boundary for this chart's shifted time dimension. start_date = today - timedelta(days=5) time_range = f"{start_date.isoformat()} : {today.isoformat()}" # The chart's shifted `dt` values label the five returned business days. 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'