subagents
This commit is contained in:
@@ -115,6 +115,7 @@ class AsyncAPIClient:
|
||||
# @DATA_CONTRACT: None -> Output[Dict[str, str]]
|
||||
# @RELATION: [CALLS] ->[SupersetAuthCache.get]
|
||||
# @RELATION: [CALLS] ->[SupersetAuthCache.set]
|
||||
# @RELATION: [CALLS] ->[AsyncAPIClient._get_auth_lock]
|
||||
async def authenticate(self) -> Dict[str, str]:
|
||||
cached_tokens = SupersetAuthCache.get(self._auth_cache_key)
|
||||
if cached_tokens and cached_tokens.get("access_token") and cached_tokens.get("csrf_token"):
|
||||
@@ -227,6 +228,12 @@ class AsyncAPIClient:
|
||||
# @PURPOSE: Translate upstream HTTP errors into stable domain exceptions.
|
||||
# @POST: Raises domain-specific exception for caller flow control.
|
||||
# @DATA_CONTRACT: Input[httpx.HTTPStatusError] -> Exception
|
||||
# @RELATION: [CALLS] ->[AsyncAPIClient._is_dashboard_endpoint]
|
||||
# @RELATION: [DEPENDS_ON] ->[DashboardNotFoundError]
|
||||
# @RELATION: [DEPENDS_ON] ->[SupersetAPIError]
|
||||
# @RELATION: [DEPENDS_ON] ->[PermissionDeniedError]
|
||||
# @RELATION: [DEPENDS_ON] ->[AuthenticationError]
|
||||
# @RELATION: [DEPENDS_ON] ->[NetworkError]
|
||||
def _handle_http_error(self, exc: httpx.HTTPStatusError, endpoint: str) -> None:
|
||||
with belief_scope("AsyncAPIClient._handle_http_error"):
|
||||
status_code = exc.response.status_code
|
||||
@@ -264,13 +271,14 @@ class AsyncAPIClient:
|
||||
if normalized_endpoint.startswith("/api/v1/"):
|
||||
normalized_endpoint = normalized_endpoint[len("/api/v1"):]
|
||||
return normalized_endpoint.startswith("/dashboard/") or normalized_endpoint == "/dashboard"
|
||||
# [/DEF:backend.src.core.utils.async_network.AsyncAPIClient._is_dashboard_endpoint:Function]
|
||||
# [/DEF:AsyncAPIClient._is_dashboard_endpoint:Function]
|
||||
|
||||
# [DEF:backend.src.core.utils.async_network.AsyncAPIClient._handle_network_error:Function]
|
||||
# [DEF:AsyncAPIClient._handle_network_error:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Translate generic httpx errors into NetworkError.
|
||||
# @POST: Raises NetworkError with URL context.
|
||||
# @DATA_CONTRACT: Input[httpx.HTTPError] -> NetworkError
|
||||
# @RELATION: [DEPENDS_ON] ->[NetworkError]
|
||||
def _handle_network_error(self, exc: httpx.HTTPError, url: str) -> None:
|
||||
with belief_scope("AsyncAPIClient._handle_network_error"):
|
||||
if isinstance(exc, httpx.TimeoutException):
|
||||
@@ -280,16 +288,17 @@ class AsyncAPIClient:
|
||||
else:
|
||||
message = f"Unknown network error: {exc}"
|
||||
raise NetworkError(message, url=url) from exc
|
||||
# [/DEF:backend.src.core.utils.async_network.AsyncAPIClient._handle_network_error:Function]
|
||||
# [/DEF:AsyncAPIClient._handle_network_error:Function]
|
||||
|
||||
# [DEF:backend.src.core.utils.async_network.AsyncAPIClient.aclose:Function]
|
||||
# [DEF:AsyncAPIClient.aclose:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Close underlying httpx client.
|
||||
# @POST: Client resources are released.
|
||||
# @SIDE_EFFECT: Closes network connections.
|
||||
# @RELATION: [DEPENDS_ON] ->[AsyncAPIClient.__init__]
|
||||
async def aclose(self) -> None:
|
||||
await self._client.aclose()
|
||||
# [/DEF:backend.src.core.utils.async_network.AsyncAPIClient.aclose:Function]
|
||||
# [/DEF:backend.src.core.utils.async_network.AsyncAPIClient:Class]
|
||||
# [/DEF:AsyncAPIClient.aclose:Function]
|
||||
# [/DEF:AsyncAPIClient:Class]
|
||||
|
||||
# [/DEF:backend.src.core.utils.async_network:Module]
|
||||
# [/DEF:AsyncNetworkModule:Module]
|
||||
|
||||
@@ -111,6 +111,7 @@ class SupersetAuthCache:
|
||||
return (str(base_url or "").strip(), username, bool(verify_ssl))
|
||||
|
||||
@classmethod
|
||||
# [DEF:SupersetAuthCache.get:Function]
|
||||
def get(cls, key: Tuple[str, str, bool]) -> Optional[Dict[str, str]]:
|
||||
now = time.time()
|
||||
with cls._lock:
|
||||
@@ -129,8 +130,10 @@ class SupersetAuthCache:
|
||||
"access_token": str(tokens.get("access_token") or ""),
|
||||
"csrf_token": str(tokens.get("csrf_token") or ""),
|
||||
}
|
||||
# [/DEF:SupersetAuthCache.get:Function]
|
||||
|
||||
@classmethod
|
||||
# [DEF:SupersetAuthCache.set:Function]
|
||||
def set(cls, key: Tuple[str, str, bool], tokens: Dict[str, str], ttl_seconds: Optional[int] = None) -> None:
|
||||
normalized_ttl = max(int(ttl_seconds or cls.TTL_SECONDS), 1)
|
||||
with cls._lock:
|
||||
@@ -141,6 +144,7 @@ class SupersetAuthCache:
|
||||
},
|
||||
"expires_at": time.time() + normalized_ttl,
|
||||
}
|
||||
# [/DEF:SupersetAuthCache.set:Function]
|
||||
|
||||
@classmethod
|
||||
def invalidate(cls, key: Tuple[str, str, bool]) -> None:
|
||||
@@ -156,7 +160,7 @@ class SupersetAuthCache:
|
||||
class APIClient:
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
# [DEF:__init__:Function]
|
||||
# [DEF:APIClient.__init__:Function]
|
||||
# @PURPOSE: Инициализирует API клиент с конфигурацией, сессией и логгером.
|
||||
# @PARAM: config (Dict[str, Any]) - Конфигурация.
|
||||
# @PARAM: verify_ssl (bool) - Проверять ли SSL.
|
||||
@@ -179,7 +183,7 @@ class APIClient:
|
||||
)
|
||||
self._authenticated = False
|
||||
app_logger.info("[APIClient.__init__][Exit] APIClient initialized.")
|
||||
# [/DEF:__init__:Function]
|
||||
# [/DEF:APIClient.__init__:Function]
|
||||
|
||||
# [DEF:_init_session:Function]
|
||||
# @PURPOSE: Создает и настраивает `requests.Session` с retry-логикой.
|
||||
@@ -261,6 +265,8 @@ class APIClient:
|
||||
# @POST: `self._tokens` заполнен, `self._authenticated` установлен в `True`.
|
||||
# @RETURN: Dict[str, str] - Словарь с токенами.
|
||||
# @THROW: AuthenticationError, NetworkError - при ошибках.
|
||||
# @RELATION: [CALLS] ->[SupersetAuthCache.get]
|
||||
# @RELATION: [CALLS] ->[SupersetAuthCache.set]
|
||||
def authenticate(self) -> Dict[str, str]:
|
||||
with belief_scope("authenticate"):
|
||||
app_logger.info("[authenticate][Enter] Authenticating to %s", self.base_url)
|
||||
|
||||
@@ -224,13 +224,13 @@ class SupersetCompilationAdapter:
|
||||
# @PURPOSE: Request preview compilation through explicit client support backed by real Superset endpoints only.
|
||||
# @RELATION: [CALLS] ->[SupersetClient.compile_dataset_preview]
|
||||
# @PRE: payload contains a valid dataset identifier and deterministic execution inputs for one preview attempt.
|
||||
# @POST: returns one normalized upstream compilation response without endpoint guessing.
|
||||
# @SIDE_EFFECT: issues one Superset chart-data request through the client.
|
||||
# @POST: returns one normalized upstream compilation response including the chosen strategy metadata.
|
||||
# @SIDE_EFFECT: issues one or more Superset preview requests through the client fallback chain.
|
||||
# @DATA_CONTRACT: Input[PreviewCompilationPayload] -> Output[Dict[str,Any]]
|
||||
def _request_superset_preview(self, payload: PreviewCompilationPayload) -> Dict[str, Any]:
|
||||
try:
|
||||
logger.reason(
|
||||
"Attempting deterministic Superset preview compilation via chart/data",
|
||||
"Attempting deterministic Superset preview compilation through supported endpoint strategies",
|
||||
extra={
|
||||
"dataset_id": payload.dataset_id,
|
||||
"session_id": payload.session_id,
|
||||
@@ -245,7 +245,7 @@ class SupersetCompilationAdapter:
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.explore(
|
||||
"Superset preview compilation via chart/data failed",
|
||||
"Superset preview compilation failed across supported endpoint strategies",
|
||||
extra={
|
||||
"dataset_id": payload.dataset_id,
|
||||
"session_id": payload.session_id,
|
||||
@@ -256,7 +256,7 @@ class SupersetCompilationAdapter:
|
||||
|
||||
normalized = self._normalize_preview_response(response)
|
||||
if normalized is None:
|
||||
raise RuntimeError("Superset chart/data compilation response could not be normalized")
|
||||
raise RuntimeError("Superset preview compilation response could not be normalized")
|
||||
return normalized
|
||||
# [/DEF:SupersetCompilationAdapter._request_superset_preview:Function]
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from __future__ import annotations
|
||||
# [DEF:SupersetContextExtractor.imports:Block]
|
||||
import json
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from urllib.parse import parse_qs, unquote, urlparse
|
||||
@@ -128,6 +129,14 @@ class SupersetContextExtractor:
|
||||
if isinstance(permalink_state, dict):
|
||||
for key, value in permalink_state.items():
|
||||
query_state.setdefault(key, value)
|
||||
# Extract filters from permalink dataMask
|
||||
data_mask = permalink_state.get("dataMask")
|
||||
if isinstance(data_mask, dict) and data_mask:
|
||||
query_state["dataMask"] = data_mask
|
||||
logger.reason(
|
||||
"Extracted native filters from permalink dataMask",
|
||||
extra={"filter_count": len(data_mask)},
|
||||
)
|
||||
resolved_dashboard_id = self._extract_dashboard_id_from_state(permalink_state)
|
||||
resolved_chart_id = self._extract_chart_id_from_state(permalink_state)
|
||||
if resolved_dashboard_id is not None:
|
||||
@@ -182,10 +191,44 @@ class SupersetContextExtractor:
|
||||
"Resolving dashboard-bound dataset from Superset",
|
||||
extra={"dashboard_ref": resolved_dashboard_ref},
|
||||
)
|
||||
|
||||
# Resolve dashboard detail first — handles both numeric ID and slug,
|
||||
# ensuring dashboard_id is available for the native_filters_key fetch below.
|
||||
dashboard_detail = self.client.get_dashboard_detail(resolved_dashboard_ref)
|
||||
resolved_dashboard_id = dashboard_detail.get("id")
|
||||
if resolved_dashboard_id is not None:
|
||||
dashboard_id = int(resolved_dashboard_id)
|
||||
|
||||
# Check for native_filters_key in query params and fetch filter state.
|
||||
# This must run AFTER dashboard_id is resolved from slug above.
|
||||
native_filters_key = query_params.get("native_filters_key", [None])[0]
|
||||
if native_filters_key and dashboard_id is not None:
|
||||
try:
|
||||
logger.reason(
|
||||
"Fetching native filter state from Superset",
|
||||
extra={"dashboard_id": dashboard_id, "filter_key": native_filters_key},
|
||||
)
|
||||
extracted = self.client.extract_native_filters_from_key(
|
||||
dashboard_id, native_filters_key
|
||||
)
|
||||
data_mask = extracted.get("dataMask")
|
||||
if isinstance(data_mask, dict) and data_mask:
|
||||
query_state["native_filter_state"] = data_mask
|
||||
logger.reason(
|
||||
"Extracted native filter state from Superset via native_filters_key",
|
||||
extra={"filter_count": len(data_mask)},
|
||||
)
|
||||
else:
|
||||
logger.explore(
|
||||
"Native filter state returned empty dataMask",
|
||||
extra={"dashboard_id": dashboard_id, "filter_key": native_filters_key},
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.explore(
|
||||
"Failed to fetch native filter state from Superset",
|
||||
extra={"dashboard_id": dashboard_id, "filter_key": native_filters_key, "error": str(exc)},
|
||||
)
|
||||
|
||||
datasets = dashboard_detail.get("datasets") or []
|
||||
if datasets:
|
||||
first_dataset = datasets[0]
|
||||
@@ -287,6 +330,114 @@ class SupersetContextExtractor:
|
||||
with belief_scope("SupersetContextExtractor.recover_imported_filters"):
|
||||
recovered_filters: List[Dict[str, Any]] = []
|
||||
seen_filter_keys: Set[str] = set()
|
||||
metadata_filters: List[Dict[str, Any]] = []
|
||||
metadata_filters_by_id: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def merge_recovered_filter(candidate: Dict[str, Any]) -> None:
|
||||
filter_key = candidate["filter_name"].strip().lower()
|
||||
existing_index = next(
|
||||
(
|
||||
index
|
||||
for index, existing in enumerate(recovered_filters)
|
||||
if existing["filter_name"].strip().lower() == filter_key
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing_index is None:
|
||||
seen_filter_keys.add(filter_key)
|
||||
recovered_filters.append(candidate)
|
||||
return
|
||||
|
||||
existing = recovered_filters[existing_index]
|
||||
if existing.get("display_name") in {None, "", existing.get("filter_name")} and candidate.get("display_name"):
|
||||
existing["display_name"] = candidate["display_name"]
|
||||
if existing.get("raw_value") is None and candidate.get("raw_value") is not None:
|
||||
existing["raw_value"] = candidate["raw_value"]
|
||||
existing["confidence_state"] = candidate.get("confidence_state", "imported")
|
||||
existing["requires_confirmation"] = candidate.get("requires_confirmation", False)
|
||||
existing["recovery_status"] = candidate.get("recovery_status", "recovered")
|
||||
existing["source"] = candidate.get("source", existing.get("source"))
|
||||
if existing.get("normalized_value") is None and candidate.get("normalized_value") is not None:
|
||||
existing["normalized_value"] = deepcopy(candidate["normalized_value"])
|
||||
if existing.get("notes") and candidate.get("notes") and candidate["notes"] not in existing["notes"]:
|
||||
existing["notes"] = f'{existing["notes"]}; {candidate["notes"]}'
|
||||
|
||||
if parsed_context.dashboard_id is not None:
|
||||
try:
|
||||
dashboard_payload = self.client.get_dashboard(parsed_context.dashboard_id)
|
||||
dashboard_record = (
|
||||
dashboard_payload.get("result", dashboard_payload)
|
||||
if isinstance(dashboard_payload, dict)
|
||||
else {}
|
||||
)
|
||||
json_metadata = dashboard_record.get("json_metadata")
|
||||
if isinstance(json_metadata, str) and json_metadata.strip():
|
||||
json_metadata = json.loads(json_metadata)
|
||||
if not isinstance(json_metadata, dict):
|
||||
json_metadata = {}
|
||||
|
||||
native_filter_configuration = json_metadata.get("native_filter_configuration") or []
|
||||
default_filters = json_metadata.get("default_filters") or {}
|
||||
if isinstance(default_filters, str) and default_filters.strip():
|
||||
try:
|
||||
default_filters = json.loads(default_filters)
|
||||
except Exception:
|
||||
logger.explore(
|
||||
"Superset default_filters payload was not valid JSON",
|
||||
extra={"dashboard_id": parsed_context.dashboard_id},
|
||||
)
|
||||
default_filters = {}
|
||||
|
||||
for item in native_filter_configuration:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
filter_name = str(
|
||||
item.get("name")
|
||||
or item.get("filter_name")
|
||||
or item.get("column")
|
||||
or ""
|
||||
).strip()
|
||||
if not filter_name:
|
||||
continue
|
||||
|
||||
display_name = item.get("label") or item.get("name") or filter_name
|
||||
filter_id = str(item.get("id") or "").strip()
|
||||
|
||||
default_value = None
|
||||
if isinstance(default_filters, dict):
|
||||
default_value = default_filters.get(filter_name)
|
||||
|
||||
metadata_filter = self._normalize_imported_filter_payload(
|
||||
{
|
||||
"filter_name": filter_name,
|
||||
"display_name": display_name,
|
||||
"raw_value": default_value,
|
||||
"source": "superset_native",
|
||||
"recovery_status": "recovered" if default_value is not None else "partial",
|
||||
"requires_confirmation": default_value is None,
|
||||
"notes": "Recovered from Superset dashboard native filter configuration",
|
||||
},
|
||||
default_source="superset_native",
|
||||
default_note="Recovered from Superset dashboard native filter configuration",
|
||||
)
|
||||
metadata_filters.append(metadata_filter)
|
||||
|
||||
if filter_id:
|
||||
metadata_filters_by_id[filter_id.lower()] = {
|
||||
"filter_name": filter_name,
|
||||
"display_name": display_name,
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.explore(
|
||||
"Dashboard native filter enrichment failed; preserving partial imported filters",
|
||||
extra={
|
||||
"dashboard_id": parsed_context.dashboard_id,
|
||||
"error": str(exc),
|
||||
"filter_count": len(recovered_filters),
|
||||
},
|
||||
)
|
||||
metadata_filters = []
|
||||
metadata_filters_by_id = {}
|
||||
|
||||
for item in parsed_context.imported_filters:
|
||||
normalized = self._normalize_imported_filter_payload(
|
||||
@@ -294,11 +445,24 @@ class SupersetContextExtractor:
|
||||
default_source="superset_url",
|
||||
default_note="Recovered from Superset URL state",
|
||||
)
|
||||
filter_key = normalized["filter_name"].strip().lower()
|
||||
if filter_key in seen_filter_keys:
|
||||
continue
|
||||
seen_filter_keys.add(filter_key)
|
||||
recovered_filters.append(normalized)
|
||||
metadata_match = metadata_filters_by_id.get(normalized["filter_name"].strip().lower())
|
||||
if metadata_match is not None:
|
||||
normalized["filter_name"] = metadata_match["filter_name"]
|
||||
normalized["display_name"] = metadata_match["display_name"]
|
||||
normalized["notes"] = (
|
||||
"Recovered from Superset URL state and reconciled against dashboard native filter metadata"
|
||||
)
|
||||
|
||||
merge_recovered_filter(normalized)
|
||||
logger.reflect(
|
||||
"Recovered filter from URL state",
|
||||
extra={
|
||||
"filter_name": normalized["filter_name"],
|
||||
"source": normalized["source"],
|
||||
"has_value": normalized["raw_value"] is not None,
|
||||
"canonicalized": metadata_match is not None,
|
||||
},
|
||||
)
|
||||
|
||||
if parsed_context.dashboard_id is None:
|
||||
logger.reflect(
|
||||
@@ -311,108 +475,48 @@ class SupersetContextExtractor:
|
||||
)
|
||||
return recovered_filters
|
||||
|
||||
try:
|
||||
dashboard_payload = self.client.get_dashboard(parsed_context.dashboard_id)
|
||||
dashboard_record = (
|
||||
dashboard_payload.get("result", dashboard_payload)
|
||||
if isinstance(dashboard_payload, dict)
|
||||
else {}
|
||||
for saved_filter in metadata_filters:
|
||||
merge_recovered_filter(saved_filter)
|
||||
logger.reflect(
|
||||
"Recovered filter from dashboard metadata",
|
||||
extra={
|
||||
"filter_name": saved_filter["filter_name"],
|
||||
"has_value": saved_filter["raw_value"] is not None,
|
||||
},
|
||||
)
|
||||
json_metadata = dashboard_record.get("json_metadata")
|
||||
if isinstance(json_metadata, str) and json_metadata.strip():
|
||||
json_metadata = json.loads(json_metadata)
|
||||
if not isinstance(json_metadata, dict):
|
||||
json_metadata = {}
|
||||
|
||||
native_filter_configuration = json_metadata.get("native_filter_configuration") or []
|
||||
default_filters = json_metadata.get("default_filters") or {}
|
||||
if isinstance(default_filters, str) and default_filters.strip():
|
||||
try:
|
||||
default_filters = json.loads(default_filters)
|
||||
except Exception:
|
||||
logger.explore(
|
||||
"Superset default_filters payload was not valid JSON",
|
||||
extra={"dashboard_id": parsed_context.dashboard_id},
|
||||
)
|
||||
default_filters = {}
|
||||
|
||||
for item in native_filter_configuration:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
filter_name = str(
|
||||
item.get("name")
|
||||
or item.get("filter_name")
|
||||
or item.get("column")
|
||||
or ""
|
||||
).strip()
|
||||
if not filter_name:
|
||||
continue
|
||||
|
||||
filter_key = filter_name.lower()
|
||||
if filter_key in seen_filter_keys:
|
||||
continue
|
||||
|
||||
default_value = None
|
||||
if isinstance(default_filters, dict):
|
||||
default_value = default_filters.get(filter_name)
|
||||
|
||||
saved_filter = self._normalize_imported_filter_payload(
|
||||
if not recovered_filters:
|
||||
recovered_filters.append(
|
||||
self._normalize_imported_filter_payload(
|
||||
{
|
||||
"filter_name": filter_name,
|
||||
"display_name": item.get("label") or item.get("name"),
|
||||
"raw_value": default_value,
|
||||
"filter_name": f"dashboard_{parsed_context.dashboard_id}_filters",
|
||||
"display_name": "Dashboard native filters",
|
||||
"raw_value": None,
|
||||
"source": "superset_native",
|
||||
"recovery_status": "recovered" if default_value is not None else "partial",
|
||||
"requires_confirmation": default_value is None,
|
||||
"notes": "Recovered from Superset dashboard native filter configuration",
|
||||
"recovery_status": "partial",
|
||||
"requires_confirmation": True,
|
||||
"notes": "Superset dashboard filter configuration could not be recovered fully",
|
||||
},
|
||||
default_source="superset_native",
|
||||
default_note="Recovered from Superset dashboard native filter configuration",
|
||||
default_note="Superset dashboard filter configuration could not be recovered fully",
|
||||
)
|
||||
seen_filter_keys.add(filter_key)
|
||||
recovered_filters.append(saved_filter)
|
||||
)
|
||||
|
||||
logger.reflect(
|
||||
"Imported filter recovery completed with dashboard enrichment",
|
||||
extra={
|
||||
"dashboard_id": parsed_context.dashboard_id,
|
||||
"filter_count": len(recovered_filters),
|
||||
"partial_entries": len(
|
||||
[
|
||||
item
|
||||
for item in recovered_filters
|
||||
if item["recovery_status"] == "partial"
|
||||
]
|
||||
),
|
||||
},
|
||||
)
|
||||
return recovered_filters
|
||||
except Exception as exc:
|
||||
logger.explore(
|
||||
"Dashboard native filter enrichment failed; preserving partial imported filters",
|
||||
extra={
|
||||
"dashboard_id": parsed_context.dashboard_id,
|
||||
"error": str(exc),
|
||||
"filter_count": len(recovered_filters),
|
||||
},
|
||||
)
|
||||
if not recovered_filters:
|
||||
recovered_filters.append(
|
||||
self._normalize_imported_filter_payload(
|
||||
{
|
||||
"filter_name": f"dashboard_{parsed_context.dashboard_id}_filters",
|
||||
"display_name": "Dashboard native filters",
|
||||
"raw_value": None,
|
||||
"source": "superset_native",
|
||||
"recovery_status": "partial",
|
||||
"requires_confirmation": True,
|
||||
"notes": "Superset dashboard filter configuration could not be recovered fully",
|
||||
},
|
||||
default_source="superset_native",
|
||||
default_note="Superset dashboard filter configuration could not be recovered fully",
|
||||
)
|
||||
)
|
||||
return recovered_filters
|
||||
logger.reflect(
|
||||
"Imported filter recovery completed with dashboard enrichment",
|
||||
extra={
|
||||
"dashboard_id": parsed_context.dashboard_id,
|
||||
"filter_count": len(recovered_filters),
|
||||
"partial_entries": len(
|
||||
[
|
||||
item
|
||||
for item in recovered_filters
|
||||
if item["recovery_status"] == "partial"
|
||||
]
|
||||
),
|
||||
},
|
||||
)
|
||||
return recovered_filters
|
||||
# [/DEF:SupersetContextExtractor.recover_imported_filters:Function]
|
||||
|
||||
# [DEF:SupersetContextExtractor.discover_template_variables:Function]
|
||||
@@ -692,11 +796,23 @@ class SupersetContextExtractor:
|
||||
or item.get("name")
|
||||
or f"native_filter_{index}"
|
||||
)
|
||||
direct_clause = None
|
||||
if item.get("column") and ("value" in item or "val" in item):
|
||||
direct_clause = {
|
||||
"col": item.get("column"),
|
||||
"op": item.get("op") or ("IN" if isinstance(item.get("value"), list) else "=="),
|
||||
"val": item.get("val", item.get("value")),
|
||||
}
|
||||
imported_filters.append(
|
||||
{
|
||||
"filter_name": str(filter_name),
|
||||
"raw_value": item.get("value"),
|
||||
"display_name": item.get("label") or item.get("name"),
|
||||
"normalized_value": {
|
||||
"filter_clauses": [direct_clause] if isinstance(direct_clause, dict) else [],
|
||||
"extra_form_data": {},
|
||||
"value_origin": "native_filters",
|
||||
},
|
||||
"source": "superset_url",
|
||||
"recovery_status": "recovered"
|
||||
if item.get("value") is not None
|
||||
@@ -706,6 +822,7 @@ class SupersetContextExtractor:
|
||||
}
|
||||
)
|
||||
|
||||
# Extract filters from permalink dataMask
|
||||
dashboard_data_mask = query_state.get("dataMask")
|
||||
if isinstance(dashboard_data_mask, dict):
|
||||
for filter_key, item in dashboard_data_mask.items():
|
||||
@@ -715,20 +832,54 @@ class SupersetContextExtractor:
|
||||
extra_form_data = item.get("extraFormData")
|
||||
display_name = None
|
||||
raw_value = None
|
||||
normalized_value = {
|
||||
"filter_clauses": [],
|
||||
"extra_form_data": deepcopy(extra_form_data) if isinstance(extra_form_data, dict) else {},
|
||||
"value_origin": "unresolved",
|
||||
}
|
||||
|
||||
# Try to get value from filterState
|
||||
if isinstance(filter_state, dict):
|
||||
display_name = filter_state.get("label")
|
||||
raw_value = filter_state.get("value")
|
||||
if raw_value is None and isinstance(extra_form_data, dict):
|
||||
# Superset filterState uses 'value' for single values, 'values' for multi-select
|
||||
raw_value = filter_state.get("value") or filter_state.get("values")
|
||||
if raw_value is not None:
|
||||
normalized_value["value_origin"] = "filter_state"
|
||||
|
||||
# Preserve exact Superset clauses from extraFormData.filters
|
||||
if isinstance(extra_form_data, dict):
|
||||
extra_filters = extra_form_data.get("filters")
|
||||
if isinstance(extra_filters, list) and extra_filters:
|
||||
first_filter = extra_filters[0]
|
||||
if isinstance(first_filter, dict):
|
||||
raw_value = first_filter.get("val")
|
||||
if isinstance(extra_filters, list):
|
||||
normalized_value["filter_clauses"] = [
|
||||
deepcopy(extra_filter)
|
||||
for extra_filter in extra_filters
|
||||
if isinstance(extra_filter, dict)
|
||||
]
|
||||
|
||||
# If no value found, try extraFormData.filters
|
||||
if raw_value is None and normalized_value["filter_clauses"]:
|
||||
first_filter = normalized_value["filter_clauses"][0]
|
||||
raw_value = first_filter.get("val")
|
||||
if raw_value is None:
|
||||
raw_value = first_filter.get("value")
|
||||
if raw_value is not None:
|
||||
normalized_value["value_origin"] = "extra_form_data.filters"
|
||||
|
||||
# If still no value, try extraFormData directly for time_range, time_grain, etc.
|
||||
if raw_value is None and isinstance(extra_form_data, dict):
|
||||
# Common Superset filter fields
|
||||
for field in ["time_range", "time_grain_sqla", "time_column", "granularity"]:
|
||||
if field in extra_form_data:
|
||||
raw_value = extra_form_data[field]
|
||||
normalized_value["value_origin"] = f"extra_form_data.{field}"
|
||||
break
|
||||
|
||||
imported_filters.append(
|
||||
{
|
||||
"filter_name": str(item.get("id") or filter_key),
|
||||
"raw_value": raw_value,
|
||||
"display_name": display_name,
|
||||
"normalized_value": normalized_value,
|
||||
"source": "superset_permalink",
|
||||
"recovery_status": "recovered" if raw_value is not None else "partial",
|
||||
"requires_confirmation": raw_value is None,
|
||||
@@ -736,6 +887,73 @@ class SupersetContextExtractor:
|
||||
}
|
||||
)
|
||||
|
||||
# Extract filters from native_filter_state (fetched from Superset via native_filters_key)
|
||||
native_filter_state = query_state.get("native_filter_state")
|
||||
if isinstance(native_filter_state, dict):
|
||||
for filter_key, item in native_filter_state.items():
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
# Handle both single filter format and multi-filter format
|
||||
filter_id = item.get("id") or filter_key
|
||||
filter_state = item.get("filterState")
|
||||
extra_form_data = item.get("extraFormData")
|
||||
display_name = None
|
||||
raw_value = None
|
||||
normalized_value = {
|
||||
"filter_clauses": [],
|
||||
"extra_form_data": deepcopy(extra_form_data) if isinstance(extra_form_data, dict) else {},
|
||||
"value_origin": "unresolved",
|
||||
}
|
||||
|
||||
# Try to get value from filterState
|
||||
if isinstance(filter_state, dict):
|
||||
display_name = filter_state.get("label")
|
||||
# Superset filterState uses 'value' for single values, 'values' for multi-select
|
||||
raw_value = filter_state.get("value") or filter_state.get("values")
|
||||
if raw_value is not None:
|
||||
normalized_value["value_origin"] = "filter_state"
|
||||
|
||||
# Preserve exact Superset clauses from extraFormData.filters
|
||||
if isinstance(extra_form_data, dict):
|
||||
extra_filters = extra_form_data.get("filters")
|
||||
if isinstance(extra_filters, list):
|
||||
normalized_value["filter_clauses"] = [
|
||||
deepcopy(extra_filter)
|
||||
for extra_filter in extra_filters
|
||||
if isinstance(extra_filter, dict)
|
||||
]
|
||||
|
||||
# If no value found, try extraFormData.filters
|
||||
if raw_value is None and normalized_value["filter_clauses"]:
|
||||
first_filter = normalized_value["filter_clauses"][0]
|
||||
raw_value = first_filter.get("val")
|
||||
if raw_value is None:
|
||||
raw_value = first_filter.get("value")
|
||||
if raw_value is not None:
|
||||
normalized_value["value_origin"] = "extra_form_data.filters"
|
||||
|
||||
# If still no value, try extraFormData directly for time_range, time_grain, etc.
|
||||
if raw_value is None and isinstance(extra_form_data, dict):
|
||||
# Common Superset filter fields
|
||||
for field in ["time_range", "time_grain_sqla", "time_column", "granularity"]:
|
||||
if field in extra_form_data:
|
||||
raw_value = extra_form_data[field]
|
||||
normalized_value["value_origin"] = f"extra_form_data.{field}"
|
||||
break
|
||||
|
||||
imported_filters.append(
|
||||
{
|
||||
"filter_name": str(filter_id),
|
||||
"raw_value": raw_value,
|
||||
"display_name": display_name,
|
||||
"normalized_value": normalized_value,
|
||||
"source": "superset_native_filters_key",
|
||||
"recovery_status": "recovered" if raw_value is not None else "partial",
|
||||
"requires_confirmation": raw_value is None,
|
||||
"notes": "Recovered from Superset native_filters_key state",
|
||||
}
|
||||
)
|
||||
|
||||
form_data_payload = query_state.get("form_data")
|
||||
if isinstance(form_data_payload, dict):
|
||||
extra_filters = form_data_payload.get("extra_filters") or []
|
||||
@@ -748,6 +966,11 @@ class SupersetContextExtractor:
|
||||
"filter_name": str(filter_name),
|
||||
"raw_value": item.get("val"),
|
||||
"display_name": item.get("label"),
|
||||
"normalized_value": {
|
||||
"filter_clauses": [deepcopy(item)],
|
||||
"extra_form_data": {},
|
||||
"value_origin": "form_data.extra_filters",
|
||||
},
|
||||
"source": "superset_url",
|
||||
"recovery_status": "recovered"
|
||||
if item.get("val") is not None
|
||||
|
||||
Reference in New Issue
Block a user