# [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]