subagents

This commit is contained in:
2026-03-20 17:20:24 +03:00
parent b89b9a66f2
commit 1149e8df1d
36 changed files with 4313 additions and 327 deletions

View File

@@ -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]

View File

@@ -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)

View File

@@ -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]

View File

@@ -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