Break monolithic modules >400 lines into focused sub-modules while preserving backward-compatible imports and all test coverage: Backend (Python): - TranslationExecutor: 1974→241 lines, split into 9 sub-modules - Translate plugin: orchestrator (1137→148), preview (1303→244), service (1052→275), dictionary (1007→68) - ProfileService: 857→172 with 4 extracted sub-modules - TaskManager: 708→322 with graph/event_bus/lifecycle extracted - Test dictionary: 1199→split into 6 focused test files Frontend (Svelte): - SettingsPage: 1451→291 with 6 extracted tab components - GitManager: 1220→228 with 5 extracted panels - DatasetReviewWorkspace: 1202→314 - translate.js API: 664→28 barrel with 6 domain modules Protocol: - Remove single-contract 150-line limit from INV_7 (keep CC≤10) - Fix unclosed #endregion tags across 11 files - Fix 19 test regressions from stale mock paths - All 294 tests passing
173 lines
7.5 KiB
Python
173 lines
7.5 KiB
Python
# #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 -> [sqlalchemy.orm.Session]
|
|
# @RELATION DEPENDS_ON -> [ProfilePreferenceService]
|
|
# @RELATION DEPENDS_ON -> [SupersetLookupService]
|
|
# @RELATION DEPENDS_ON -> [SecurityBadgeService]
|
|
# @RELATION DEPENDS_ON -> [profile_utils]
|
|
#
|
|
# @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 (profile_utils) 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 ..core.logger import belief_scope, logger
|
|
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,
|
|
sanitize_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 -> [profile_utils]
|
|
# @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.
|
|
def lookup_superset_accounts(
|
|
self,
|
|
current_user: User,
|
|
request: SupersetAccountLookupRequest,
|
|
) -> SupersetAccountLookupResponse:
|
|
return 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
|