# #region profile_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, profile, preference, superset, lookup] # # @BRIEF Composite facade orchestrating profile preference persistence, Superset account lookup, # security badges, and deterministic actor matching by delegating to focused sub-services. # @LAYER Domain # @RELATION DEPENDS_ON -> [UserDashboardPreference] # @RELATION DEPENDS_ON -> [ProfilePreferenceResponse] # @RELATION DEPENDS_ON -> [SupersetClient] # @RELATION DEPENDS_ON -> [AuthRepositoryModule] # @RELATION DEPENDS_ON -> [User] # @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy.orm.Session] # @RELATION DEPENDS_ON -> [ProfilePreferenceService] # @RELATION DEPENDS_ON -> [SupersetLookupService] # @RELATION DEPENDS_ON -> [SecurityBadgeService] # @RELATION DEPENDS_ON -> [ProfileUtils] # # @INVARIANT Profile ID needs to be unique per-user session # # @TEST_CONTRACT ProfilePreferenceUpdateRequest -> ProfilePreferenceResponse # @TEST_FIXTURE valid_profile_update -> {"user_id":"u-1","superset_username":"John_Doe","show_only_my_dashboards":true} # @TEST_EDGE enable_without_username -> toggle=true with empty username returns validation error # @TEST_EDGE cross_user_mutation -> attempt to update another user preference returns forbidden # @TEST_EDGE lookup_env_not_found -> unknown environment_id returns not found # @TEST_INVARIANT normalization_consistency -> VERIFIED_BY: [valid_profile_update, enable_without_username] # @DATA_CONTRACT Profile_id -> ProfileInfo; session_id -> valid UUID # @PRE Session is active and valid # @POST Profile with updated fields populated and # @SIDE_EFFECT Database read/write operations # @RATIONALE Decomposed from monolithic 770-line ProfileService into three focused services # (ProfilePreferenceService, SupersetLookupService, SecurityBadgeService) plus pure # utility functions (ProfileUtils) to satisfy INV_7. This module is a thin facade # that preserves the public API contract for all existing callers. # @REJECTED Keeping all three domains in a single ProfileService class was rejected — it violated # INV_7 (770 lines vs 150 max), mixed I/O patterns (DB writes + HTTP calls), and created # unnecessary coupling between preference persistence and Superset network operations. from collections.abc import Iterable from typing import Any from sqlalchemy.orm import Session from ..models.auth import User from ..schemas.profile import ( ProfilePreferenceResponse, ProfilePreferenceUpdateRequest, SupersetAccountLookupRequest, SupersetAccountLookupResponse, ) from .profile_preference_service import ProfilePreferenceService from .profile_utils import ( EnvironmentNotFoundError, ProfileAuthorizationError, ProfileValidationError, normalize_owner_tokens, normalize_username, ) from .security_badge_service import SecurityBadgeService from .superset_lookup_service import SupersetLookupService # Re-export exception classes for backward-compatible imports __all__ = [ "EnvironmentNotFoundError", "ProfileAuthorizationError", "ProfilePreferenceService", "ProfileService", "ProfileValidationError", "SecurityBadgeService", "SupersetLookupService", ] # #region ProfileService [C:4] [TYPE Class] [SEMANTICS profile,facade,delegate,coordinator] # @BRIEF Facade that composes ProfilePreferenceService, SupersetLookupService, and # SecurityBadgeService into a single interface for backward compatibility. # @RELATION DEPENDS_ON -> [ProfilePreferenceService] # @RELATION DEPENDS_ON -> [SupersetLookupService] # @RELATION DEPENDS_ON -> [SecurityBadgeService] # @RELATION DEPENDS_ON -> [ProfileUtils] # @PRE Caller provides authenticated User context for external service methods. # @POST Delegates to sub-services and returns normalized profile/lookup responses. # @SIDE_EFFECT Writes preference records and encrypted tokens; performs external account lookups when requested. # @DATA_CONTRACT Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool] # @INVARIANT Profile data integrity maintained, cache consistency with database state # @RATIONALE Thin coordinator — all sub-10-line delegating methods. Actual business logic lives # in the injected sub-services. class ProfileService: """Facade composing preference, lookup, and security services.""" # region init [TYPE Function] # @BRIEF Initialize facade and create sub-services. # @PRE db session is active and config_manager supports get_environments(). # @POST All sub-services are initialized and ready. def __init__(self, db: Session, config_manager: Any, plugin_loader: Any = None): self.preference_service = ProfilePreferenceService(db, config_manager, plugin_loader) self.lookup_service = SupersetLookupService(config_manager) self.security_badge_service = SecurityBadgeService(plugin_loader) self.db = db self.config_manager = config_manager self.plugin_loader = plugin_loader # endregion init # region get_my_preference [TYPE Function] # @BRIEF Delegate to ProfilePreferenceService. def get_my_preference(self, current_user: User) -> ProfilePreferenceResponse: return self.preference_service.get_my_preference(current_user) # endregion get_my_preference # region get_dashboard_filter_binding [TYPE Function] # @BRIEF Delegate to ProfilePreferenceService. def get_dashboard_filter_binding(self, current_user: User) -> dict: return self.preference_service.get_dashboard_filter_binding(current_user) # endregion get_dashboard_filter_binding # region update_my_preference [TYPE Function] # @BRIEF Delegate to ProfilePreferenceService. def update_my_preference( self, current_user: User, payload: ProfilePreferenceUpdateRequest, target_user_id: str | None = None, ) -> ProfilePreferenceResponse: return self.preference_service.update_my_preference( current_user, payload, target_user_id ) # endregion update_my_preference # region lookup_superset_accounts [TYPE Function] # @BRIEF Delegate to SupersetLookupService. # @SIDE_EFFECT Асинхронный HTTP-вызов к Superset API. async def lookup_superset_accounts( self, current_user: User, request: SupersetAccountLookupRequest, ) -> SupersetAccountLookupResponse: return await self.lookup_service.lookup_superset_accounts(current_user, request) # endregion lookup_superset_accounts # region matches_dashboard_actor [TYPE Function] # @BRIEF Apply trim+case-insensitive actor match across owners OR modified_by. # @PRE bound_username can be empty; owners may contain mixed payload. # @POST Returns True when normalized username matches owners or modified_by. def matches_dashboard_actor( self, bound_username: str | None, owners: Iterable[Any] | None, modified_by: str | None, ) -> bool: normalized_actor = normalize_username(bound_username) if not normalized_actor: return False owner_tokens = normalize_owner_tokens(owners) modified_token = normalize_username(modified_by) if normalized_actor in owner_tokens: return True if modified_token and normalized_actor == modified_token: return True return False # endregion matches_dashboard_actor # #endregion ProfileService # #endregion profile_service