Warnings fixed: - datetime.utcnow() → datetime.now(UTC) across 48+ files (src/ + tests/) - datetime.utcnow (callback ref) → lambda: datetime.now(UTC) in model fields (18 files) - Pydantic class Config → model_config = ConfigDict(...) (16 files) - Pydantic .dict() → .model_dump() (8 files) - ConfigDict(allow_population_by_field_name=True) → validate_by_name=True - SQLAlchemy declarative_base() import path updated - FastAPI on_event → lifespan context manager (app.py) - Import sorting (ruff I001) auto-fixed across all files - Fixed broken re-export chains that ruff F401 cleanup broke: _validate_bcp47: service.py now imports from dictionary_validation directly job_to_response: _job_routes.py and test imports from service_utils directly fetch_datasource_metadata: restored re-export in service.py - Added missing TranslateJobService import in _job_routes.py (was deleted by F401) - Added ConfigDict(protected_namespaces=()) for DashboardDatasetItem schema field - pytest.ini: replaced deprecated importmode with asyncio_mode All 440 tests pass with zero deprecation warnings.
357 lines
14 KiB
Python
357 lines
14 KiB
Python
# #region MaintenanceChartManager [C:3] [TYPE Module] [SEMANTICS maintenance, chart, banner, lifecycle]
|
|
# @BRIEF Chart operations for maintenance banners: create/get banner charts, process
|
|
# per-dashboard state transitions for start and end orchestrators.
|
|
# @LAYER Service
|
|
# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]
|
|
# @RELATION DEPENDS_ON -> [MaintenanceModels]
|
|
|
|
from typing import Any
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ...core.logger import belief_scope, logger as app_logger
|
|
from ...core.superset_client import SupersetClient
|
|
from ...models.maintenance import (
|
|
MaintenanceDashboardBanner,
|
|
MaintenanceDashboardBannerStatus,
|
|
MaintenanceDashboardState,
|
|
MaintenanceDashboardStateStatus,
|
|
)
|
|
from ._banner_renderer import _build_banner_text_for_dashboard, rebuild_banner
|
|
from ._dashboard_scanner import _resolve_dashboard_title
|
|
|
|
|
|
# #region ensure_banner_chart [C:3] [TYPE Function] [SEMANTICS maintenance, banner, chart, creation]
|
|
# @BRIEF Get or create a MaintenanceDashboardBanner for a dashboard in the target environment.
|
|
# If an active banner exists in DB, return it. Otherwise, create a markdown chart in Superset,
|
|
# update dashboard layout, and store the banner row.
|
|
# @PRE dashboard_id exists in Superset. superset_client has write access.
|
|
# @POST Returns MaintenanceDashboardBanner with chart_id set. Chart is placed at top of dashboard.
|
|
# @SIDE_EFFECT Creates chart in Superset, modifies dashboard layout, writes DB row.
|
|
# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]
|
|
def ensure_banner_chart(
|
|
dashboard_id: int,
|
|
environment_id: str,
|
|
superset_client: SupersetClient,
|
|
db_session: Session,
|
|
) -> MaintenanceDashboardBanner:
|
|
with belief_scope(
|
|
"ensure_banner_chart",
|
|
f"dashboard_id={dashboard_id}",
|
|
):
|
|
app_logger.reason(
|
|
"Ensuring banner chart exists",
|
|
extra={"dashboard_id": dashboard_id},
|
|
)
|
|
|
|
# Check for existing active banner
|
|
existing = (
|
|
db_session.query(MaintenanceDashboardBanner)
|
|
.filter(
|
|
MaintenanceDashboardBanner.dashboard_id == dashboard_id,
|
|
MaintenanceDashboardBanner.environment_id == environment_id,
|
|
MaintenanceDashboardBanner.status == MaintenanceDashboardBannerStatus.ACTIVE,
|
|
)
|
|
.first()
|
|
)
|
|
if existing:
|
|
app_logger.reason(
|
|
"Found existing active banner, verifying chart in Superset",
|
|
extra={"banner_id": existing.id, "chart_id": existing.chart_id},
|
|
)
|
|
# Verify the chart still exists in Superset
|
|
try:
|
|
superset_client.get_chart(existing.chart_id)
|
|
app_logger.reflect(
|
|
"Existing banner chart is alive, reusing",
|
|
extra={"banner_id": existing.id, "chart_id": existing.chart_id},
|
|
)
|
|
return existing
|
|
except Exception:
|
|
app_logger.explore(
|
|
"Existing banner chart is stale (not found in Superset), marking as REMOVED",
|
|
extra={
|
|
"banner_id": existing.id,
|
|
"chart_id": existing.chart_id,
|
|
},
|
|
)
|
|
existing.status = MaintenanceDashboardBannerStatus.REMOVED
|
|
db_session.flush()
|
|
# Fall through to create a new chart
|
|
|
|
# Create markdown chart in Superset
|
|
try:
|
|
chart_id = superset_client.create_markdown_chart(
|
|
dashboard_id, "*Maintenance banner placeholder*"
|
|
)
|
|
except Exception as e:
|
|
app_logger.explore(
|
|
"Failed to create markdown chart",
|
|
extra={"dashboard_id": dashboard_id},
|
|
error=str(e),
|
|
)
|
|
raise
|
|
|
|
# Insert chart at top of dashboard layout
|
|
try:
|
|
superset_client.update_dashboard_layout(dashboard_id, chart_id)
|
|
except Exception as e:
|
|
app_logger.explore(
|
|
"Failed to update dashboard layout, deleting chart",
|
|
extra={"dashboard_id": dashboard_id, "chart_id": chart_id},
|
|
error=str(e),
|
|
)
|
|
# Clean up the chart we just created
|
|
try:
|
|
superset_client.delete_chart(chart_id)
|
|
except Exception:
|
|
pass
|
|
raise
|
|
|
|
# Persist banner row
|
|
banner = MaintenanceDashboardBanner(
|
|
environment_id=environment_id,
|
|
dashboard_id=dashboard_id,
|
|
chart_id=chart_id,
|
|
banner_text="",
|
|
status=MaintenanceDashboardBannerStatus.ACTIVE,
|
|
)
|
|
db_session.add(banner)
|
|
db_session.flush()
|
|
|
|
app_logger.reflect(
|
|
"Banner chart created and persisted",
|
|
extra={
|
|
"banner_id": banner.id,
|
|
"chart_id": chart_id,
|
|
"dashboard_id": dashboard_id,
|
|
},
|
|
)
|
|
return banner
|
|
# #endregion ensure_banner_chart
|
|
|
|
|
|
# #region _process_dashboards_for_start [C:3] [TYPE Function]
|
|
# @BRIEF Process each dashboard for a start maintenance event: ensure banner chart,
|
|
# create dashboard state, build banner text, update chart markdown.
|
|
# @SIDE_EFFECT Creates/updates Superset charts, writes DB rows.
|
|
# @RELATION CALLS -> [ensure_banner_chart]
|
|
# @RELATION CALLS -> [_build_banner_text_for_dashboard]
|
|
# @RELATION CALLS -> [_resolve_dashboard_title]
|
|
def _process_dashboards_for_start(
|
|
dashboard_ids: list[int],
|
|
event_id: str,
|
|
environment_id: str,
|
|
template: str,
|
|
timezone_str: str,
|
|
superset_client: SupersetClient,
|
|
db_session: Session,
|
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[MaintenanceDashboardBanner]]:
|
|
successful_dashboards: list[dict[str, Any]] = []
|
|
failed_dashboards: list[dict[str, Any]] = []
|
|
banners_created: list[MaintenanceDashboardBanner] = []
|
|
|
|
for dash_id in dashboard_ids:
|
|
app_logger.reason(
|
|
"Processing dashboard",
|
|
extra={"dashboard_id": dash_id, "event_id": event_id},
|
|
)
|
|
try:
|
|
# Ensure banner chart exists
|
|
banner = ensure_banner_chart(
|
|
dash_id,
|
|
environment_id,
|
|
superset_client,
|
|
db_session,
|
|
)
|
|
banners_created.append(banner)
|
|
|
|
# Create dashboard state
|
|
state = MaintenanceDashboardState(
|
|
event_id=event_id,
|
|
dashboard_id=dash_id,
|
|
banner_id=banner.id,
|
|
status=MaintenanceDashboardStateStatus.ACTIVE,
|
|
)
|
|
db_session.add(state)
|
|
db_session.flush() # Ensure state is visible to subsequent queries
|
|
|
|
# Build banner text for this dashboard's active events
|
|
banner_text = _build_banner_text_for_dashboard(
|
|
banner.id, db_session, template, timezone_str
|
|
)
|
|
|
|
# Update banner content on dashboard (native MARKDOWN in layout)
|
|
if banner.chart_id:
|
|
try:
|
|
superset_client.update_banner_on_dashboard(
|
|
dash_id, banner.chart_id, banner_text
|
|
)
|
|
except Exception as e:
|
|
app_logger.explore(
|
|
"Failed to update banner on dashboard",
|
|
extra={
|
|
"chart_id": banner.chart_id,
|
|
"dashboard_id": dash_id,
|
|
},
|
|
error=str(e),
|
|
)
|
|
state.status = MaintenanceDashboardStateStatus.APPLY_FAILED
|
|
failed_dashboards.append(
|
|
{"id": dash_id, "title": str(dash_id), "error": str(e)}
|
|
)
|
|
continue
|
|
|
|
# Update banner text in DB
|
|
banner.banner_text = banner_text
|
|
db_session.flush()
|
|
|
|
# Resolve dashboard title
|
|
title = _resolve_dashboard_title(dash_id, superset_client)
|
|
successful_dashboards.append(
|
|
{
|
|
"id": dash_id,
|
|
"title": title,
|
|
"chart_id": banner.chart_id,
|
|
}
|
|
)
|
|
app_logger.reflect(
|
|
"Dashboard processed successfully",
|
|
extra={"dashboard_id": dash_id},
|
|
)
|
|
|
|
except Exception as e:
|
|
app_logger.explore(
|
|
"Failed to process dashboard",
|
|
extra={"dashboard_id": dash_id},
|
|
error=str(e),
|
|
)
|
|
failed_dashboards.append(
|
|
{"id": dash_id, "title": str(dash_id), "error": str(e)}
|
|
)
|
|
|
|
return successful_dashboards, failed_dashboards, banners_created
|
|
# #endregion _process_dashboards_for_start
|
|
|
|
|
|
# #region _process_states_for_end [C:3] [TYPE Function]
|
|
# @BRIEF Process each dashboard state for an end maintenance event: if other events
|
|
# still use same banner → rebuild; if no other events → remove banner (chart + layout).
|
|
# @SIDE_EFFECT Modifies Superset dashboards (removes charts, updates layouts). Writes DB.
|
|
# @RELATION CALLS -> [rebuild_banner]
|
|
def _process_states_for_end(
|
|
states: list[MaintenanceDashboardState],
|
|
event_id: str,
|
|
superset_client: SupersetClient,
|
|
db_session: Session,
|
|
) -> tuple[int, list[dict[str, Any]]]:
|
|
removed_count = 0
|
|
failed_removals: list[dict[str, Any]] = []
|
|
|
|
for state in states:
|
|
dash_id = state.dashboard_id
|
|
app_logger.reason(
|
|
"Processing dashboard state for end",
|
|
extra={"dashboard_id": dash_id, "state_id": state.id},
|
|
)
|
|
|
|
try:
|
|
banner_id = state.banner_id
|
|
if not banner_id:
|
|
# No banner linked, just mark as removed
|
|
state.status = MaintenanceDashboardStateStatus.REMOVED
|
|
removed_count += 1
|
|
continue
|
|
|
|
# Check if other active events still use this banner
|
|
other_active = db_session.query(MaintenanceDashboardState).filter(
|
|
MaintenanceDashboardState.banner_id == banner_id,
|
|
MaintenanceDashboardState.status == MaintenanceDashboardStateStatus.ACTIVE,
|
|
MaintenanceDashboardState.event_id != event_id,
|
|
).count()
|
|
|
|
banner = db_session.query(MaintenanceDashboardBanner).filter(
|
|
MaintenanceDashboardBanner.id == banner_id
|
|
).first()
|
|
|
|
if other_active > 0 and banner:
|
|
# Other events still active — rebuild banner text
|
|
state.status = MaintenanceDashboardStateStatus.REMOVED
|
|
db_session.flush()
|
|
rebuild_banner(banner_id, db_session, superset_client)
|
|
app_logger.reflect(
|
|
"Banner rebuilt after removing event",
|
|
extra={"banner_id": banner_id, "dashboard_id": dash_id},
|
|
)
|
|
elif banner:
|
|
# No other events — remove banner entirely
|
|
state.status = MaintenanceDashboardStateStatus.REMOVING
|
|
db_session.flush()
|
|
|
|
# Remove chart from layout
|
|
if banner.chart_id:
|
|
try:
|
|
superset_client.remove_chart_from_layout(
|
|
dash_id, banner.chart_id
|
|
)
|
|
except Exception as e:
|
|
app_logger.explore(
|
|
"Failed to remove chart from layout",
|
|
extra={
|
|
"dashboard_id": dash_id,
|
|
"chart_id": banner.chart_id,
|
|
},
|
|
error=str(e),
|
|
)
|
|
state.status = MaintenanceDashboardStateStatus.REMOVAL_FAILED
|
|
failed_removals.append(
|
|
{"id": dash_id, "title": str(dash_id), "error": str(e)}
|
|
)
|
|
continue
|
|
|
|
# Delete chart
|
|
try:
|
|
superset_client.delete_chart(banner.chart_id)
|
|
except Exception as e:
|
|
app_logger.explore(
|
|
"Failed to delete chart",
|
|
extra={
|
|
"chart_id": banner.chart_id,
|
|
"dashboard_id": dash_id,
|
|
},
|
|
error=str(e),
|
|
)
|
|
state.status = MaintenanceDashboardStateStatus.REMOVAL_FAILED
|
|
failed_removals.append(
|
|
{"id": dash_id, "title": str(dash_id), "error": str(e)}
|
|
)
|
|
continue
|
|
|
|
# Mark banner as removed
|
|
banner.status = MaintenanceDashboardBannerStatus.REMOVED
|
|
state.status = MaintenanceDashboardStateStatus.REMOVED
|
|
app_logger.reflect(
|
|
"Banner fully removed",
|
|
extra={"banner_id": banner_id, "dashboard_id": dash_id},
|
|
)
|
|
else:
|
|
state.status = MaintenanceDashboardStateStatus.REMOVED
|
|
|
|
removed_count += 1
|
|
|
|
except Exception as e:
|
|
app_logger.explore(
|
|
"Failed to end maintenance for dashboard",
|
|
extra={"dashboard_id": dash_id},
|
|
error=str(e),
|
|
)
|
|
state.status = MaintenanceDashboardStateStatus.REMOVAL_FAILED
|
|
failed_removals.append(
|
|
{"id": dash_id, "title": str(dash_id), "error": str(e)}
|
|
)
|
|
|
|
return removed_count, failed_removals
|
|
# #endregion _process_states_for_end
|
|
|
|
# #endregion MaintenanceChartManager
|