Files
ss-tools/backend/src/services/maintenance/_chart_manager.py
busya 4205618ee6 chore: eliminate all deprecation warnings from tests and linter
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.
2026-05-26 19:18:28 +03:00

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