238 lines
10 KiB
Python
238 lines
10 KiB
Python
# [DEF:backend.src.core.superset_profile_lookup:Module]
|
|
#
|
|
# @TIER: STANDARD
|
|
# @SEMANTICS: superset, users, lookup, profile, pagination, normalization
|
|
# @PURPOSE: Provides environment-scoped Superset account lookup adapter with stable normalized output.
|
|
# @LAYER: Core
|
|
# @RELATION: DEPENDS_ON -> backend.src.core.utils.network.APIClient
|
|
# @RELATION: DEPENDS_ON -> backend.src.core.logger
|
|
#
|
|
# @INVARIANT: Adapter never leaks raw upstream payload shape to API consumers.
|
|
|
|
# [SECTION: IMPORTS]
|
|
import json
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from .logger import logger, belief_scope
|
|
from .utils.network import APIClient, AuthenticationError, SupersetAPIError
|
|
# [/SECTION]
|
|
|
|
|
|
# [DEF:SupersetAccountLookupAdapter:Class]
|
|
# @TIER: STANDARD
|
|
# @PURPOSE: Lookup Superset users and normalize candidates for profile binding.
|
|
class SupersetAccountLookupAdapter:
|
|
# [DEF:__init__:Function]
|
|
# @PURPOSE: Initializes lookup adapter with authenticated API client and environment context.
|
|
# @PRE: network_client supports request(method, endpoint, params=...).
|
|
# @POST: Adapter is ready to perform users lookup requests.
|
|
def __init__(self, network_client: APIClient, environment_id: str):
|
|
self.network_client = network_client
|
|
self.environment_id = str(environment_id or "")
|
|
# [/DEF:__init__:Function]
|
|
|
|
# [DEF:get_users_page:Function]
|
|
# @PURPOSE: Fetch one users page from Superset with passthrough search/sort parameters.
|
|
# @PRE: page_index >= 0 and page_size >= 1.
|
|
# @POST: Returns deterministic payload with normalized items and total count.
|
|
# @RETURN: Dict[str, Any]
|
|
def get_users_page(
|
|
self,
|
|
search: Optional[str] = None,
|
|
page_index: int = 0,
|
|
page_size: int = 20,
|
|
sort_column: str = "username",
|
|
sort_order: str = "desc",
|
|
) -> Dict[str, Any]:
|
|
with belief_scope("SupersetAccountLookupAdapter.get_users_page"):
|
|
normalized_page_index = max(int(page_index), 0)
|
|
normalized_page_size = max(int(page_size), 1)
|
|
|
|
normalized_sort_column = str(sort_column or "username").strip().lower() or "username"
|
|
normalized_sort_order = str(sort_order or "desc").strip().lower()
|
|
if normalized_sort_order not in {"asc", "desc"}:
|
|
normalized_sort_order = "desc"
|
|
|
|
query: Dict[str, Any] = {
|
|
"page": normalized_page_index,
|
|
"page_size": normalized_page_size,
|
|
"order_column": normalized_sort_column,
|
|
"order_direction": normalized_sort_order,
|
|
}
|
|
|
|
normalized_search = str(search or "").strip()
|
|
if normalized_search:
|
|
query["filters"] = [{"col": "username", "opr": "ct", "value": normalized_search}]
|
|
|
|
logger.reason(
|
|
"[REASON] Lookup Superset users "
|
|
f"(env={self.environment_id}, page={normalized_page_index}, page_size={normalized_page_size})"
|
|
)
|
|
logger.reflect(
|
|
"[REFLECT] Prepared Superset users lookup query "
|
|
f"(env={self.environment_id}, order_column={normalized_sort_column}, "
|
|
f"normalized_sort_order={normalized_sort_order}, "
|
|
f"payload_order_direction={query.get('order_direction')})"
|
|
)
|
|
|
|
primary_error: Optional[Exception] = None
|
|
last_error: Optional[Exception] = None
|
|
for attempt_index, endpoint in enumerate(("/security/users/", "/security/users"), start=1):
|
|
try:
|
|
logger.reason(
|
|
"[REASON] Users lookup request attempt "
|
|
f"(env={self.environment_id}, attempt={attempt_index}, endpoint={endpoint})"
|
|
)
|
|
response = self.network_client.request(
|
|
method="GET",
|
|
endpoint=endpoint,
|
|
params={"q": json.dumps(query)},
|
|
)
|
|
logger.reflect(
|
|
"[REFLECT] Users lookup endpoint succeeded "
|
|
f"(env={self.environment_id}, attempt={attempt_index}, endpoint={endpoint})"
|
|
)
|
|
return self._normalize_lookup_payload(
|
|
response=response,
|
|
page_index=normalized_page_index,
|
|
page_size=normalized_page_size,
|
|
)
|
|
except Exception as exc:
|
|
if primary_error is None:
|
|
primary_error = exc
|
|
last_error = exc
|
|
cause = getattr(exc, "__cause__", None)
|
|
cause_response = getattr(cause, "response", None)
|
|
status_code = getattr(cause_response, "status_code", None)
|
|
logger.explore(
|
|
"[EXPLORE] Users lookup endpoint failed "
|
|
f"(env={self.environment_id}, attempt={attempt_index}, endpoint={endpoint}, "
|
|
f"error_type={type(exc).__name__}, status_code={status_code}, "
|
|
f"payload_order_direction={query.get('order_direction')}): {exc}"
|
|
)
|
|
|
|
if last_error is not None:
|
|
selected_error: Exception = last_error
|
|
if (
|
|
primary_error is not None
|
|
and primary_error is not last_error
|
|
and isinstance(last_error, AuthenticationError)
|
|
and not isinstance(primary_error, AuthenticationError)
|
|
):
|
|
selected_error = primary_error
|
|
logger.reflect(
|
|
"[REFLECT] Preserving primary lookup failure over fallback auth error "
|
|
f"(env={self.environment_id}, primary_error_type={type(primary_error).__name__}, "
|
|
f"fallback_error_type={type(last_error).__name__})"
|
|
)
|
|
|
|
logger.explore(
|
|
"[EXPLORE] All Superset users lookup endpoints failed "
|
|
f"(env={self.environment_id}, payload_order_direction={query.get('order_direction')}, "
|
|
f"selected_error_type={type(selected_error).__name__})"
|
|
)
|
|
raise selected_error
|
|
raise SupersetAPIError("Superset users lookup failed without explicit error")
|
|
# [/DEF:get_users_page:Function]
|
|
|
|
# [DEF:_normalize_lookup_payload:Function]
|
|
# @PURPOSE: Convert Superset users response variants into stable candidates payload.
|
|
# @PRE: response can be dict/list in any supported upstream shape.
|
|
# @POST: Output contains canonical keys: status, environment_id, page_index, page_size, total, items.
|
|
# @RETURN: Dict[str, Any]
|
|
def _normalize_lookup_payload(
|
|
self,
|
|
response: Any,
|
|
page_index: int,
|
|
page_size: int,
|
|
) -> Dict[str, Any]:
|
|
with belief_scope("SupersetAccountLookupAdapter._normalize_lookup_payload"):
|
|
payload = response
|
|
if isinstance(payload, dict) and isinstance(payload.get("result"), dict):
|
|
payload = payload.get("result")
|
|
|
|
raw_items: List[Any] = []
|
|
total = 0
|
|
|
|
if isinstance(payload, dict):
|
|
if isinstance(payload.get("result"), list):
|
|
raw_items = payload.get("result") or []
|
|
total = int(payload.get("count", len(raw_items)) or 0)
|
|
elif isinstance(payload.get("users"), list):
|
|
raw_items = payload.get("users") or []
|
|
total = int(payload.get("total", len(raw_items)) or 0)
|
|
elif isinstance(payload.get("items"), list):
|
|
raw_items = payload.get("items") or []
|
|
total = int(payload.get("total", len(raw_items)) or 0)
|
|
elif isinstance(payload, list):
|
|
raw_items = payload
|
|
total = len(raw_items)
|
|
|
|
normalized_items: List[Dict[str, Any]] = []
|
|
seen_usernames = set()
|
|
|
|
for raw_user in raw_items:
|
|
candidate = self.normalize_user_payload(raw_user)
|
|
username_key = str(candidate.get("username") or "").strip().lower()
|
|
if not username_key:
|
|
continue
|
|
if username_key in seen_usernames:
|
|
continue
|
|
seen_usernames.add(username_key)
|
|
normalized_items.append(candidate)
|
|
|
|
logger.reflect(
|
|
"[REFLECT] Normalized lookup payload "
|
|
f"(env={self.environment_id}, items={len(normalized_items)}, total={max(total, len(normalized_items))})"
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"environment_id": self.environment_id,
|
|
"page_index": max(int(page_index), 0),
|
|
"page_size": max(int(page_size), 1),
|
|
"total": max(int(total), len(normalized_items)),
|
|
"items": normalized_items,
|
|
}
|
|
# [/DEF:_normalize_lookup_payload:Function]
|
|
|
|
# [DEF:normalize_user_payload:Function]
|
|
# @PURPOSE: Project raw Superset user object to canonical candidate shape.
|
|
# @PRE: raw_user may have heterogenous key names between Superset versions.
|
|
# @POST: Returns normalized candidate keys (environment_id, username, display_name, email, is_active).
|
|
# @RETURN: Dict[str, Any]
|
|
def normalize_user_payload(self, raw_user: Any) -> Dict[str, Any]:
|
|
if not isinstance(raw_user, dict):
|
|
raw_user = {}
|
|
|
|
username = str(
|
|
raw_user.get("username")
|
|
or raw_user.get("userName")
|
|
or raw_user.get("name")
|
|
or ""
|
|
).strip()
|
|
|
|
full_name = str(raw_user.get("full_name") or "").strip()
|
|
first_name = str(raw_user.get("first_name") or "").strip()
|
|
last_name = str(raw_user.get("last_name") or "").strip()
|
|
display_name = full_name or " ".join(
|
|
part for part in [first_name, last_name] if part
|
|
).strip()
|
|
if not display_name:
|
|
display_name = username or None
|
|
|
|
email = str(raw_user.get("email") or "").strip() or None
|
|
is_active_raw = raw_user.get("is_active")
|
|
is_active = bool(is_active_raw) if is_active_raw is not None else None
|
|
|
|
return {
|
|
"environment_id": self.environment_id,
|
|
"username": username,
|
|
"display_name": display_name,
|
|
"email": email,
|
|
"is_active": is_active,
|
|
}
|
|
# [/DEF:normalize_user_payload:Function]
|
|
# [/DEF:SupersetAccountLookupAdapter:Class]
|
|
|
|
# [/DEF:backend.src.core.superset_profile_lookup:Module] |