Files
ss-tools/examples/maintenance-api-python.py
busya ec6421de35 rename ss-tools to superset-tools across the entire project
- Replace all occurrences of 'ss-tools' with 'superset-tools' in 104 files
- Rename git bundle file ss-tools.bundle → superset-tools.bundle
- Update .gitignore pattern accordingly
- Preserve variable names (hasSsTools etc.) and code identifiers
2026-06-16 11:15:19 +03:00

332 lines
11 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Example: External tool triggers maintenance via superset-tools API.
This script demonstrates how an ETL pipeline (Airflow DAG, cron job, CI/CD)
can start and end maintenance banners on Superset dashboards using the
superset-tools API Key authentication.
Requirements: Python 3.10+, requests (pip install requests)
Usage:
# Start maintenance
python maintenance-api-python.py start \
--api-key ssk_M7xqaP2zL9vR4nF8kC1bH5jT6dW0yA3 \
--base-url https://superset-tools.example.com \
--tables public.messages,public.users \
--duration-hours 4 \
--message "Scheduled ETL refresh"
# End maintenance by event ID
python maintenance-api-python.py end \
--api-key ssk_M7xqaP2zL9vR4nF8kC1bH5jT6dW0yA3 \
--base-url https://superset-tools.example.com \
--event-id m-abc123
# End ALL maintenance (use with caution!)
python maintenance-api-python.py end-all \
--api-key ssk_M7xqaP2zL9vR4nF8kC1bH5jT6dW0yA3 \
--base-url https://superset-tools.example.com \
--environment ss-dev
"""
import argparse
import sys
from datetime import datetime, timezone, timedelta
try:
import requests
except ImportError:
print("Error: 'requests' library is required. Install with: pip install requests")
sys.exit(1)
API_KEY_HEADER = "X-API-Key"
def start_maintenance(base_url: str, api_key: str, tables: list[str],
environment_id: str, duration_hours: int = 4,
message: str | None = None,
timeout: int = 30) -> dict:
"""
Start a maintenance event.
Args:
base_url: superset-tools base URL (e.g. https://superset-tools.example.com)
api_key: API key with 'maintenance:start' permission
tables: List of table names to flag (e.g. ["public.messages"])
environment_id: Superset environment (e.g. "ss-dev", "ss-prod")
duration_hours: How long the maintenance window lasts
message: Optional custom message for the banner
timeout: Request timeout in seconds
Returns:
API response dict with task_id and maintenance_id
Raises:
requests.HTTPError: On 4xx/5xx response with error details
"""
now = datetime.now(timezone.utc)
start_time = now.isoformat()
end_time = (now + timedelta(hours=duration_hours)).isoformat()
payload = {
"tables": tables,
"start_time": start_time,
"end_time": end_time,
"environment_id": environment_id,
}
if message:
payload["message"] = message
print(f"[maintenance] Starting maintenance for tables: {tables}")
print(f"[maintenance] Window: {start_time}{end_time}")
print(f"[maintenance] Environment: {environment_id}")
response = requests.post(
f"{base_url}/api/maintenance/start",
json=payload,
headers={API_KEY_HEADER: api_key},
timeout=timeout,
)
if response.status_code == 202:
result = response.json()
print(f"[maintenance] ✅ Event created (id={result['maintenance_id']})")
print(f"[maintenance] Task: {result.get('task_id', 'N/A')}")
return result
if response.status_code == 400:
error = _format_error(response)
print(f"[maintenance] ❌ Validation error: {error}")
elif response.status_code == 401:
print("[maintenance] ❌ Authentication failed: invalid or revoked API key")
elif response.status_code == 403:
print("[maintenance] ❌ Permission denied: API key lacks 'maintenance:start'")
else:
print(f"[maintenance] ❌ Unexpected error ({response.status_code}): {response.text[:500]}")
response.raise_for_status()
return {} # unreachable
def end_maintenance(base_url: str, api_key: str, event_id: str,
timeout: int = 30) -> dict:
"""
End a specific maintenance event and remove its banners.
Args:
base_url: superset-tools base URL
api_key: API key with 'maintenance:end' permission
event_id: The event ID returned by start_maintenance
timeout: Request timeout in seconds
Returns:
API response dict
Raises:
requests.HTTPError: On error
"""
print(f"[maintenance] Ending maintenance event: {event_id}")
response = requests.post(
f"{base_url}/api/maintenance/{event_id}/end",
headers={API_KEY_HEADER: api_key},
timeout=timeout,
)
if response.status_code == 202:
result = response.json()
print(f"[maintenance] ✅ Event end scheduled (task={result.get('task_id', 'N/A')})")
return result
if response.status_code == 404:
data = response.json()
if data.get("status") == "already_completed":
print("[maintenance] Event was already completed (idempotent)")
return data
print(f"[maintenance] ❌ Event not found: {event_id}")
elif response.status_code == 401:
print("[maintenance] ❌ Authentication failed: invalid or revoked API key")
elif response.status_code == 403:
print("[maintenance] ❌ Permission denied: API key lacks 'maintenance:end'")
else:
print(f"[maintenance] ❌ Unexpected error ({response.status_code}): {response.text[:500]}")
response.raise_for_status()
return {}
def end_all_maintenance(base_url: str, api_key: str,
environment_id: str | None = None,
timeout: int = 30) -> dict:
"""
End ALL active maintenance events in the given environment.
USE WITH CAUTION — this removes banners from all dashboards.
Args:
base_url: superset-tools base URL
api_key: API key with 'maintenance:end_all' permission
environment_id: If set, only end events in this environment.
If None, uses the API key's default scope.
timeout: Request timeout in seconds
Returns:
API response dict
Raises:
requests.HTTPError: On error
"""
print(f"[maintenance] Ending ALL maintenance events "
f"(env={environment_id or 'key-scope'})")
print("[maintenance] ⚠️ This will remove banners from ALL affected dashboards!")
payload = {}
if environment_id:
payload["environment_id"] = environment_id
response = requests.post(
f"{base_url}/api/maintenance/end-all",
json=payload,
headers={API_KEY_HEADER: api_key},
timeout=timeout,
)
if response.status_code == 202:
result = response.json()
print(f"[maintenance] ✅ All events end scheduled (task={result.get('task_id', 'N/A')})")
return result
if response.status_code == 401:
print("[maintenance] ❌ Authentication failed: invalid or revoked API key")
elif response.status_code == 403:
print("[maintenance] ❌ Permission denied: API key lacks 'maintenance:end_all'")
elif response.status_code == 400:
print(f"[maintenance] ❌ {_format_error(response)}")
else:
print(f"[maintenance] ❌ Unexpected error ({response.status_code}): {response.text[:500]}")
response.raise_for_status()
return {}
def _format_error(response: requests.Response) -> str:
"""Extract human-readable error from API response."""
try:
data = response.json()
if "detail" in data:
detail = data["detail"]
if isinstance(detail, list):
return "; ".join(d.get("msg", str(d)) for d in detail)
return str(detail)
return response.text[:200]
except Exception:
return response.text[:200]
def main():
parser = argparse.ArgumentParser(
description="superset-tools Maintenance Banner CLI",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Start 4-hour maintenance on public.messages
%(prog)s start --api-key ssk_... --tables public.messages \\\\
--environment ss-dev --duration-hours 4
# End maintenance by event ID
%(prog)s end --api-key ssk_... --event-id m-abc123
# Emergency: end ALL maintenance in ss-dev
%(prog)s end-all --api-key ssk_... --environment ss-dev
""",
)
parser.add_argument(
"--base-url",
default="http://localhost:8000",
help="superset-tools base URL (default: %(default)s)",
)
parser.add_argument(
"--api-key",
required=True,
help="API key with required permissions",
)
subparsers = parser.add_subparsers(dest="command", required=True)
# start
start_parser = subparsers.add_parser("start", help="Start maintenance")
start_parser.add_argument(
"--tables",
required=True,
help="Comma-separated table names (e.g. 'public.messages,public.users')",
)
start_parser.add_argument(
"--environment",
required=True,
dest="environment_id",
help="Superset environment ID (e.g. 'ss-dev', 'ss-prod')",
)
start_parser.add_argument(
"--duration-hours",
type=int,
default=4,
help="Maintenance window in hours (default: %(default)s)",
)
start_parser.add_argument(
"--message",
default=None,
help="Optional message for the banner",
)
# end
end_parser = subparsers.add_parser("end", help="End a specific maintenance event")
end_parser.add_argument(
"--event-id",
required=True,
help="Maintenance event ID (from start response)",
)
# end-all
end_all_parser = subparsers.add_parser(
"end-all", help="End ALL maintenance events (use with caution)"
)
end_all_parser.add_argument(
"--environment",
dest="environment_id",
default=None,
help="Limit to specific environment (optional)",
)
args = parser.parse_args()
try:
if args.command == "start":
tables = [t.strip() for t in args.tables.split(",")]
start_maintenance(
base_url=args.base_url,
api_key=args.api_key,
tables=tables,
environment_id=args.environment_id,
duration_hours=args.duration_hours,
message=args.message,
)
elif args.command == "end":
end_maintenance(
base_url=args.base_url,
api_key=args.api_key,
event_id=args.event_id,
)
elif args.command == "end-all":
end_all_maintenance(
base_url=args.base_url,
api_key=args.api_key,
environment_id=args.environment_id,
)
except requests.HTTPError:
sys.exit(1)
if __name__ == "__main__":
main()