fix(llm): add fetch-models endpoint, fix SQL Lab INSERT (client_id truncation, sync mode, target_column, timestamp normalization)
- Add POST /api/llm/providers/fetch-models route with LLMClient.fetch_models() - Add target_column to TranslationJob model/schema/service/orchestrator - Fix SQL Lab execute: truncate client_id to 11 chars (varchar(11)) - Switch SQL Lab to sync mode (runAsync: false) — no Celery workers - Fix polling: unwrap nested result from Superset query API - Fix ClickHouse timestamp: normalize float timestamps to YYYY-MM-DD
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
# #region SrcRoot [TYPE Module]
|
# #region SrcRoot [TYPE Module] [SEMANTICS root, package]
|
||||||
# @BRIEF Canonical backend package root for application, scripts, and tests.
|
# @BRIEF Canonical backend package root for application, scripts, and tests.
|
||||||
# #endregion SrcRoot
|
# #endregion SrcRoot
|
||||||
|
|||||||
@@ -75,6 +75,69 @@ async def get_providers(
|
|||||||
# #endregion get_providers
|
# #endregion get_providers
|
||||||
|
|
||||||
|
|
||||||
|
# #region fetch_models [TYPE Function]
|
||||||
|
# @BRIEF Fetch available models from an LLM provider by base_url+provider_type, or by provider_id.
|
||||||
|
# @PRE: User is authenticated. Either provider_id or base_url+provider_type must be provided.
|
||||||
|
# @POST: Returns a list of available model IDs.
|
||||||
|
# @RELATION CALLS -> [LLMProviderService]
|
||||||
|
# @RELATION CALLS -> [LLMClient]
|
||||||
|
@router.post("/providers/fetch-models")
|
||||||
|
async def fetch_models(
|
||||||
|
payload: dict,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
from ...plugins.llm_analysis.service import LLMClient
|
||||||
|
from ...plugins.llm_analysis.models import LLMProviderType
|
||||||
|
|
||||||
|
base_url = (payload.get("base_url") or "").strip()
|
||||||
|
provider_type_str = (payload.get("provider_type") or "").strip()
|
||||||
|
api_key = payload.get("api_key") or ""
|
||||||
|
provider_id = payload.get("provider_id") or ""
|
||||||
|
|
||||||
|
# Resolve provider_id to stored credentials if no direct api_key given
|
||||||
|
if not api_key and provider_id:
|
||||||
|
service = LLMProviderService(db)
|
||||||
|
db_provider = service.get_provider(provider_id)
|
||||||
|
if not db_provider:
|
||||||
|
raise HTTPException(status_code=404, detail="Provider not found")
|
||||||
|
base_url = base_url or db_provider.base_url
|
||||||
|
provider_type_str = provider_type_str or db_provider.provider_type
|
||||||
|
stored_key = service.get_decrypted_api_key(provider_id)
|
||||||
|
if stored_key:
|
||||||
|
api_key = stored_key
|
||||||
|
|
||||||
|
if not base_url:
|
||||||
|
raise HTTPException(status_code=400, detail="base_url is required")
|
||||||
|
if not provider_type_str:
|
||||||
|
raise HTTPException(status_code=400, detail="provider_type is required")
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider_type = LLMProviderType(provider_type_str)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid provider_type: {provider_type_str}")
|
||||||
|
|
||||||
|
client = LLMClient(
|
||||||
|
provider_type=provider_type,
|
||||||
|
api_key=api_key or "sk-placeholder",
|
||||||
|
base_url=base_url,
|
||||||
|
default_model="",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
models = await client.fetch_models()
|
||||||
|
return {"models": models}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"[llm_routes.fetch_models] Failed to fetch models: {e}",
|
||||||
|
extra={"src": "llm_routes.fetch_models"},
|
||||||
|
)
|
||||||
|
raise HTTPException(status_code=502, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# #endregion fetch_models
|
||||||
|
|
||||||
|
|
||||||
# #region get_llm_status [TYPE Function]
|
# #region get_llm_status [TYPE Function]
|
||||||
# @BRIEF Returns whether LLM runtime is configured for dashboard validation.
|
# @BRIEF Returns whether LLM runtime is configured for dashboard validation.
|
||||||
# @PRE: User is authenticated.
|
# @PRE: User is authenticated.
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
# @BRIEF Translation Run execution, history, status, records and batches routes.
|
# @BRIEF Translation Run execution, history, status, records and batches routes.
|
||||||
# @LAYER: API
|
# @LAYER: API
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ....core.database import get_db
|
from ....core.database import get_db, SessionLocal
|
||||||
from ....core.logger import logger, belief_scope
|
from ....core.logger import logger, belief_scope
|
||||||
from ....schemas.auth import User
|
from ....schemas.auth import User
|
||||||
from ....dependencies import get_current_user, has_permission, get_config_manager
|
from ....dependencies import get_current_user, has_permission, get_config_manager
|
||||||
@@ -39,11 +40,107 @@ async def run_translation(
|
|||||||
try:
|
try:
|
||||||
orch = TranslationOrchestrator(db, config_manager, current_user.username)
|
orch = TranslationOrchestrator(db, config_manager, current_user.username)
|
||||||
run = orch.start_run(job_id=job_id, is_scheduled=False)
|
run = orch.start_run(job_id=job_id, is_scheduled=False)
|
||||||
# Execute asynchronously in background
|
# The request-scoped db session will be closed after this handler returns.
|
||||||
|
# The background thread must use its OWN session to avoid operating on a
|
||||||
|
# closed or expunged session.
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
def _background_execute():
|
||||||
|
bg_db = SessionLocal()
|
||||||
|
try:
|
||||||
|
bg_orch = TranslationOrchestrator(
|
||||||
|
bg_db, config_manager,
|
||||||
|
current_user.username if current_user else None,
|
||||||
|
)
|
||||||
|
# Re-fetch the run within the background session to get a fresh,
|
||||||
|
# attached object
|
||||||
|
from ....models.translate import TranslationRun as TRModel
|
||||||
|
bg_run = bg_db.query(TRModel).filter(TRModel.id == run.id).first()
|
||||||
|
if bg_run is None:
|
||||||
|
logger.explore(
|
||||||
|
"Background execute: run not found",
|
||||||
|
extra={"src": "translate_routes", "run_id": run.id},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# execute_run internally calls self.db.commit()
|
||||||
|
bg_orch.execute_run(bg_run)
|
||||||
|
|
||||||
|
# ----- VERIFICATION -----
|
||||||
|
# After execute_run the run object from bg_db is detached after
|
||||||
|
# commit. Re-query to verify the status was persisted in the
|
||||||
|
# database and is a terminal state.
|
||||||
|
check_run = bg_db.query(TRModel).filter(TRModel.id == run.id).first()
|
||||||
|
if check_run and check_run.status in ("COMPLETED", "FAILED", "CANCELLED"):
|
||||||
|
logger.reason(
|
||||||
|
"Background execute verified",
|
||||||
|
extra={
|
||||||
|
"src": "translate_routes",
|
||||||
|
"run_id": run.id,
|
||||||
|
"status": check_run.status,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# execute_run appeared to succeed but the run is still in a
|
||||||
|
# non-terminal state — manually fail it so the frontend
|
||||||
|
# polling can detect the terminal state and stop spinning.
|
||||||
|
actual_status = check_run.status if check_run else "NOT_FOUND"
|
||||||
|
logger.explore(
|
||||||
|
"Background execute: run not in terminal state after commit",
|
||||||
|
extra={
|
||||||
|
"src": "translate_routes",
|
||||||
|
"run_id": run.id,
|
||||||
|
"status": actual_status,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if check_run:
|
||||||
|
check_run.status = "FAILED"
|
||||||
|
check_run.error_message = (
|
||||||
|
"Background execution did not reach terminal state "
|
||||||
|
f"(was: {actual_status})"
|
||||||
|
)
|
||||||
|
bg_db.commit()
|
||||||
|
|
||||||
|
except Exception as bg_err:
|
||||||
|
logger.explore(
|
||||||
|
"Background execute failed",
|
||||||
|
extra={
|
||||||
|
"src": "translate_routes",
|
||||||
|
"run_id": run.id,
|
||||||
|
"error": str(bg_err),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Mark the run as FAILED so the frontend polling can detect a
|
||||||
|
# terminal state and stop spinning.
|
||||||
|
try:
|
||||||
|
fb_db = SessionLocal()
|
||||||
|
try:
|
||||||
|
fb_run = fb_db.query(TRModel).filter(TRModel.id == run.id).first()
|
||||||
|
if fb_run:
|
||||||
|
fb_run.status = "FAILED"
|
||||||
|
fb_run.error_message = f"Background execute error: {bg_err}"
|
||||||
|
fb_run.completed_at = datetime.now(timezone.utc)
|
||||||
|
fb_db.commit()
|
||||||
|
logger.reason(
|
||||||
|
"Background execute: run marked FAILED",
|
||||||
|
extra={"src": "translate_routes", "run_id": run.id},
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
fb_db.close()
|
||||||
|
except Exception as fb_err:
|
||||||
|
logger.explore(
|
||||||
|
"Background execute: unable to mark run FAILED",
|
||||||
|
extra={
|
||||||
|
"src": "translate_routes",
|
||||||
|
"run_id": run.id,
|
||||||
|
"error": str(fb_err),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
bg_db.close()
|
||||||
|
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=orch.execute_run,
|
target=_background_execute,
|
||||||
args=(run,),
|
|
||||||
daemon=True,
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
return _run_to_response(run)
|
return _run_to_response(run)
|
||||||
|
|||||||
@@ -484,6 +484,24 @@ def _ensure_translation_jobs_columns(bind_engine):
|
|||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
if "target_database_id" not in existing_columns:
|
||||||
|
try:
|
||||||
|
with bind_engine.begin() as connection:
|
||||||
|
connection.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE translation_jobs "
|
||||||
|
"ADD COLUMN target_database_id VARCHAR"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.reflect(
|
||||||
|
"Added target_database_id column to translation_jobs",
|
||||||
|
)
|
||||||
|
except Exception as migration_error:
|
||||||
|
logger.explore(
|
||||||
|
"Failed to add target_database_id to translation_jobs",
|
||||||
|
extra={"error": str(migration_error)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_dataset_review_session_columns(bind_engine):
|
def _ensure_dataset_review_session_columns(bind_engine):
|
||||||
with belief_scope("_ensure_dataset_review_session_columns"):
|
with belief_scope("_ensure_dataset_review_session_columns"):
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class SupersetDatabasesMixin:
|
|||||||
# @RELATION: CALLS -> [SupersetClientGetDatabases]
|
# @RELATION: CALLS -> [SupersetClientGetDatabases]
|
||||||
def get_databases_summary(self) -> List[Dict]:
|
def get_databases_summary(self) -> List[Dict]:
|
||||||
with belief_scope("SupersetClient.get_databases_summary"):
|
with belief_scope("SupersetClient.get_databases_summary"):
|
||||||
query = {"columns": ["uuid", "database_name", "backend"]}
|
query = {"columns": ["id", "uuid", "database_name", "backend"]}
|
||||||
_, databases = self.get_databases(query=query)
|
_, databases = self.get_databases(query=query)
|
||||||
# Map 'backend' to 'engine' for consistency with contracts
|
# Map 'backend' to 'engine' for consistency with contracts
|
||||||
for db in databases:
|
for db in databases:
|
||||||
|
|||||||
@@ -243,11 +243,45 @@ class APIClient:
|
|||||||
with belief_scope("authenticate"):
|
with belief_scope("authenticate"):
|
||||||
app_logger.info("[authenticate][Enter] Authenticating to %s", self.base_url)
|
app_logger.info("[authenticate][Enter] Authenticating to %s", self.base_url)
|
||||||
cached_tokens = SupersetAuthCache.get(self._auth_cache_key)
|
cached_tokens = SupersetAuthCache.get(self._auth_cache_key)
|
||||||
if cached_tokens and cached_tokens.get("access_token") and cached_tokens.get("csrf_token"):
|
if cached_tokens and cached_tokens.get("access_token"):
|
||||||
self._tokens = cached_tokens
|
# Cache hit — we have a valid access_token, but we need to
|
||||||
|
# re-establish the session cookie in this new requests.Session.
|
||||||
|
# Superset CSRF protection pairs the X-CSRFToken header with a
|
||||||
|
# Flask session cookie set during login. A fresh requests.Session
|
||||||
|
# lacks that cookie, so even a valid cached CSRF token would fail
|
||||||
|
# with "CSRF session token is missing". We re-fetch the CSRF
|
||||||
|
# token to both establish the session cookie AND get a fresh
|
||||||
|
# CSRF token for the current session.
|
||||||
|
try:
|
||||||
|
csrf_url = f"{self.api_base_url}/security/csrf_token/"
|
||||||
|
csrf_response = self.session.get(
|
||||||
|
csrf_url,
|
||||||
|
headers={"Authorization": f"Bearer {cached_tokens['access_token']}"},
|
||||||
|
timeout=self.request_settings["timeout"],
|
||||||
|
)
|
||||||
|
csrf_response.raise_for_status()
|
||||||
|
csrf_token = csrf_response.json()["result"]
|
||||||
|
self._tokens = {
|
||||||
|
"access_token": cached_tokens["access_token"],
|
||||||
|
"csrf_token": csrf_token,
|
||||||
|
}
|
||||||
self._authenticated = True
|
self._authenticated = True
|
||||||
app_logger.info("[authenticate][CacheHit] Reusing cached Superset auth tokens for %s", self.base_url)
|
# Update cache with fresh CSRF token for this session
|
||||||
|
SupersetAuthCache.set(self._auth_cache_key, self._tokens)
|
||||||
|
app_logger.info(
|
||||||
|
"[authenticate][CacheHit] Reusing cached Superset auth tokens for %s "
|
||||||
|
"(CSRF token refreshed for new session)",
|
||||||
|
self.base_url,
|
||||||
|
)
|
||||||
return self._tokens
|
return self._tokens
|
||||||
|
except Exception as refresh_err:
|
||||||
|
app_logger.warning(
|
||||||
|
"[authenticate][CacheRefreshFailed] CSRF token refresh failed, "
|
||||||
|
"falling back to full re-authentication: %s",
|
||||||
|
refresh_err,
|
||||||
|
)
|
||||||
|
SupersetAuthCache.invalidate(self._auth_cache_key)
|
||||||
|
# Fall through to full authentication below
|
||||||
try:
|
try:
|
||||||
login_url = f"{self.api_base_url}/security/login"
|
login_url = f"{self.api_base_url}/security/login"
|
||||||
# Log the payload keys and values (masking password)
|
# Log the payload keys and values (masking password)
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ class TranslationJob(Base):
|
|||||||
# Column mapping
|
# Column mapping
|
||||||
source_key_cols = Column(JSON, nullable=True, comment="Source key column names for composite key")
|
source_key_cols = Column(JSON, nullable=True, comment="Source key column names for composite key")
|
||||||
target_key_cols = Column(JSON, nullable=True, comment="Target key column names for composite key")
|
target_key_cols = Column(JSON, nullable=True, comment="Target key column names for composite key")
|
||||||
translation_column = Column(String, nullable=True, comment="The column whose values will be translated")
|
translation_column = Column(String, nullable=True, comment="Source column whose values will be translated")
|
||||||
|
target_column = Column(String, nullable=True, comment="Target column for translated output (defaults to translation_column)")
|
||||||
context_columns = Column(JSON, nullable=True, comment="Context column names included in LLM prompt")
|
context_columns = Column(JSON, nullable=True, comment="Context column names included in LLM prompt")
|
||||||
|
|
||||||
# LLM & processing settings
|
# LLM & processing settings
|
||||||
@@ -53,6 +54,7 @@ class TranslationJob(Base):
|
|||||||
|
|
||||||
# Environment association
|
# Environment association
|
||||||
environment_id = Column(String, nullable=True, comment="Superset environment ID for datasource access")
|
environment_id = Column(String, nullable=True, comment="Superset environment ID for datasource access")
|
||||||
|
target_database_id = Column(String, nullable=True, comment="Superset database ID for SQL Lab insert target")
|
||||||
|
|
||||||
created_by = Column(String, nullable=True)
|
created_by = Column(String, nullable=True)
|
||||||
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||||
@@ -119,6 +121,7 @@ class TranslationRecord(Base):
|
|||||||
source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset
|
source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset
|
||||||
source_object_id = Column(String, nullable=True)
|
source_object_id = Column(String, nullable=True)
|
||||||
source_object_name = Column(String, nullable=True)
|
source_object_name = Column(String, nullable=True)
|
||||||
|
source_data = Column(JSON, nullable=True, comment="Original source row key columns for upsert matching")
|
||||||
status = Column(String, nullable=False, default="PENDING") # PENDING, SUCCESS, FAILED, SKIPPED
|
status = Column(String, nullable=False, default="PENDING") # PENDING, SUCCESS, FAILED, SKIPPED
|
||||||
error_message = Column(Text, nullable=True)
|
error_message = Column(Text, nullable=True)
|
||||||
token_count_input = Column(Integer, nullable=True)
|
token_count_input = Column(Integer, nullable=True)
|
||||||
@@ -174,6 +177,7 @@ class TranslationPreviewRecord(Base):
|
|||||||
source_object_type = Column(String, nullable=True)
|
source_object_type = Column(String, nullable=True)
|
||||||
source_object_id = Column(String, nullable=True)
|
source_object_id = Column(String, nullable=True)
|
||||||
source_object_name = Column(String, nullable=True)
|
source_object_name = Column(String, nullable=True)
|
||||||
|
source_data = Column(JSON, nullable=True, comment="Original source row key columns for upsert matching")
|
||||||
status = Column(String, nullable=False, default="PENDING") # PENDING, APPROVED, REJECTED
|
status = Column(String, nullable=False, default="PENDING") # PENDING, APPROVED, REJECTED
|
||||||
feedback = Column(Text, nullable=True)
|
feedback = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# #region GitLLMExtensionModule [C:3] [TYPE Module] [SEMANTICS git, llm, commit, message, generation]
|
# #region GitLLMExtensionModule [C:3] [TYPE Module] [SEMANTICS git, llm, commit, message, generation]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @BRIEF LLM-based extensions for the Git plugin, specifically for commit message generation.
|
# @BRIEF LLM-based extensions for the Git plugin, specifically for commit message generation.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
# @RELATION DEPENDS_ON -> [LLMClient]
|
# @RELATION DEPENDS_ON -> [LLMClient]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region TestClientHeaders [TYPE Module]
|
# region TestClientHeaders [TYPE Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, llm-client, openrouter, headers
|
# @SEMANTICS: tests, llm-client, openrouter, headers
|
||||||
# @PURPOSE: Verify OpenRouter client initialization includes provider-specific headers.
|
# @PURPOSE: Verify OpenRouter client initialization includes provider-specific headers.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region TestScreenshotService [TYPE Module]
|
# region TestScreenshotService [TYPE Module]
|
||||||
# @RELATION: VERIFIES ->[src.plugins.llm_analysis.service.ScreenshotService]
|
# @RELATION: VERIFIES ->[src.plugins.llm_analysis.service.ScreenshotService]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, screenshot-service, navigation, timeout-regression
|
# @SEMANTICS: tests, screenshot-service, navigation, timeout-regression
|
||||||
# @PURPOSE: Protect dashboard screenshot navigation from brittle networkidle waits.
|
# @PURPOSE: Protect dashboard screenshot navigation from brittle networkidle waits.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region TestService [TYPE Module]
|
# region TestService [TYPE Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, llm-analysis, fallback, provider-error, unknown-status
|
# @SEMANTICS: tests, llm-analysis, fallback, provider-error, unknown-status
|
||||||
# @PURPOSE: Verify LLM analysis transport/provider failures do not masquerade as dashboard FAIL results.
|
# @PURPOSE: Verify LLM analysis transport/provider failures do not masquerade as dashboard FAIL results.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# #region LLMAnalysisPlugin [C:3] [TYPE Module] [SEMANTICS llm, analysis, dashboard, validation, documentation]
|
# #region LLMAnalysisPlugin [C:3] [TYPE Module] [SEMANTICS llm, analysis, dashboard, validation, documentation]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @BRIEF Implements DashboardValidationPlugin and DocumentationPlugin.
|
# @BRIEF Implements DashboardValidationPlugin and DocumentationPlugin.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
# @RELATION INHERITS -> [PluginBase]
|
# @RELATION INHERITS -> [PluginBase]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# #region LLMAnalysisScheduler [C:3] [TYPE Module] [SEMANTICS scheduler, llm, validation, task, cron]
|
# #region LLMAnalysisScheduler [C:3] [TYPE Module] [SEMANTICS scheduler, llm, validation, task, cron]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @BRIEF Provides helper functions to schedule LLM-based validation tasks.
|
# @BRIEF Provides helper functions to schedule LLM-based validation tasks.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
# @RELATION DEPENDS_ON -> [SchedulerService]
|
# @RELATION DEPENDS_ON -> [SchedulerService]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# #region LLMAnalysisService [C:3] [TYPE Module] [SEMANTICS llm, screenshot, playwright, openai, tenacity]
|
# #region LLMAnalysisService [C:3] [TYPE Module] [SEMANTICS llm, screenshot, playwright, openai, tenacity]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @BRIEF Services for LLM interaction and dashboard screenshots.
|
# @BRIEF Services for LLM interaction and dashboard screenshots.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
# @RELATION DEPENDS_ON -> playwright
|
# @RELATION DEPENDS_ON -> playwright
|
||||||
@@ -876,6 +875,29 @@ class LLMClient:
|
|||||||
return await self.get_json_completion(messages)
|
return await self.get_json_completion(messages)
|
||||||
# endregion LLMClient.test_runtime_connection
|
# endregion LLMClient.test_runtime_connection
|
||||||
|
|
||||||
|
# region LLMClient.fetch_models [TYPE Function]
|
||||||
|
# @PURPOSE: Fetch available models from the provider's API.
|
||||||
|
# @PRE: Client is initialized with provider credentials.
|
||||||
|
# @POST: Returns a list of model ID strings.
|
||||||
|
# @SIDE_EFFECT: Calls external LLM API /v1/models endpoint.
|
||||||
|
async def fetch_models(self) -> List[str]:
|
||||||
|
with belief_scope("LLMClient.fetch_models"):
|
||||||
|
try:
|
||||||
|
response = await self.client.models.list()
|
||||||
|
model_ids = [m.id for m in response.data]
|
||||||
|
model_ids.sort()
|
||||||
|
logger.reason(
|
||||||
|
f"[LLMClient.fetch_models] Fetched {len(model_ids)} models from {self.base_url}",
|
||||||
|
extra={"src": "LLMClient.fetch_models"},
|
||||||
|
)
|
||||||
|
return model_ids
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"[LLMClient.fetch_models] Failed to fetch models: {e}",
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
# endregion LLMClient.fetch_models
|
||||||
|
|
||||||
# region LLMClient.analyze_dashboard [TYPE Function]
|
# region LLMClient.analyze_dashboard [TYPE Function]
|
||||||
# @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis.
|
# @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis.
|
||||||
# @PRE: screenshot_path exists, logs is a list of strings.
|
# @PRE: screenshot_path exists, logs is a list of strings.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region ClickHouseInsertIntegration [TYPE Module]
|
# region ClickHouseInsertIntegration [TYPE Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: test, clickhouse, integration, insert, join
|
# @SEMANTICS: test, clickhouse, integration, insert, join
|
||||||
# @PURPOSE: Integration tests for ClickHouse INSERT with timestamp normalization and JOIN verification.
|
# @PURPOSE: Integration tests for ClickHouse INSERT with timestamp normalization and JOIN verification.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
@@ -369,6 +368,8 @@ class TestOrchestratorInsertFlow:
|
|||||||
) as MockExecutor:
|
) as MockExecutor:
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
MockExecutor.return_value = mock_executor
|
MockExecutor.return_value = mock_executor
|
||||||
|
mock_executor.resolve_database_id.return_value = None
|
||||||
|
mock_executor.get_database_backend.return_value = None
|
||||||
mock_executor.execute_and_poll.return_value = {
|
mock_executor.execute_and_poll.return_value = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"query_id": "q-123",
|
"query_id": "q-123",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region DictionaryTests [TYPE Module]
|
# region DictionaryTests [TYPE Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, dictionary, crud, import, filter
|
# @SEMANTICS: tests, dictionary, crud, import, filter
|
||||||
# @PURPOSE: Validate DictionaryManager CRUD, import, deletion guards, and batch filtering.
|
# @PURPOSE: Validate DictionaryManager CRUD, import, deletion guards, and batch filtering.
|
||||||
# @RELATION: BINDS_TO -> [DictionaryManager:Class]
|
# @RELATION: BINDS_TO -> [DictionaryManager:Class]
|
||||||
@@ -38,7 +37,6 @@ from src.plugins.translate._utils import _normalize_term, _detect_delimiter
|
|||||||
|
|
||||||
|
|
||||||
# region _FakeJob [TYPE Class]
|
# region _FakeJob [TYPE Class]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Helper to create inline TranslationJob records.
|
# @PURPOSE: Helper to create inline TranslationJob records.
|
||||||
class _FakeJob:
|
class _FakeJob:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region OrchestratorTests [TYPE Module]
|
# region OrchestratorTests [TYPE Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: test, translate, orchestrator, events
|
# @SEMANTICS: test, translate, orchestrator, events
|
||||||
# @PURPOSE: Tests for TranslationOrchestrator: run lifecycle, partial failure, batch retry, event invariants, NULL handling.
|
# @PURPOSE: Tests for TranslationOrchestrator: run lifecycle, partial failure, batch retry, event invariants, NULL handling.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
@@ -10,9 +9,10 @@
|
|||||||
# @TEST_FIXTURE: mock_config_manager -> MagicMock ConfigManager
|
# @TEST_FIXTURE: mock_config_manager -> MagicMock ConfigManager
|
||||||
# @TEST_EDGE: missing_preview -> raises ValueError
|
# @TEST_EDGE: missing_preview -> raises ValueError
|
||||||
# @TEST_EDGE: invalid_run_status -> raises ValueError
|
# @TEST_EDGE: invalid_run_status -> raises ValueError
|
||||||
|
# @TEST_EDGE: executor_failure -> run is marked FAILED
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from src.plugins.translate.orchestrator import TranslationOrchestrator
|
from src.plugins.translate.orchestrator import TranslationOrchestrator
|
||||||
@@ -234,6 +234,210 @@ class TestTranslationOrchestrator:
|
|||||||
with pytest.raises(ValueError, match="PENDING"):
|
with pytest.raises(ValueError, match="PENDING"):
|
||||||
orch.execute_run(run)
|
orch.execute_run(run)
|
||||||
|
|
||||||
|
# region test_execute_run_success [TYPE Function]
|
||||||
|
# @BRIEF Happy path: executor completes, SQL generated, Superset submits.
|
||||||
|
def test_execute_run_success(self, mock_job: MagicMock) -> None:
|
||||||
|
db = MagicMock()
|
||||||
|
config_manager = MagicMock()
|
||||||
|
mock_job.target_table = "target_tbl"
|
||||||
|
|
||||||
|
# Mock job query — first call returns mock_job, final re-query returns completed_run
|
||||||
|
completed_run_response = MagicMock()
|
||||||
|
completed_run_response.id = "run-1"
|
||||||
|
completed_run_response.job_id = "job-123"
|
||||||
|
completed_run_response.status = "COMPLETED"
|
||||||
|
completed_run_response.total_records = 10
|
||||||
|
completed_run_response.successful_records = 10
|
||||||
|
completed_run_response.failed_records = 0
|
||||||
|
completed_run_response.skipped_records = 0
|
||||||
|
completed_run_response.completed_at = datetime.now(timezone.utc)
|
||||||
|
completed_run_response.error_message = None
|
||||||
|
completed_run_response.insert_status = "success"
|
||||||
|
completed_run_response.superset_execution_id = "q-1"
|
||||||
|
completed_run_response.superset_execution_log = {}
|
||||||
|
# query job -> mock_job, query TranslationRun -> completed_run_response
|
||||||
|
db.query.return_value.filter.return_value.first.side_effect = [
|
||||||
|
mock_job,
|
||||||
|
completed_run_response,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create a PENDING input run
|
||||||
|
run = MagicMock()
|
||||||
|
run.id = "run-1"
|
||||||
|
run.job_id = "job-123"
|
||||||
|
run.status = "PENDING"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"src.plugins.translate.orchestrator.TranslationExecutor"
|
||||||
|
) as MockExecutor:
|
||||||
|
mock_executor_instance = MagicMock()
|
||||||
|
mock_executor_instance.execute_run.return_value = completed_run_response
|
||||||
|
MockExecutor.return_value = mock_executor_instance
|
||||||
|
|
||||||
|
orch = TranslationOrchestrator(db, config_manager, "test-user")
|
||||||
|
with patch.object(orch, "event_log"):
|
||||||
|
with patch.object(
|
||||||
|
orch, "_generate_and_insert_sql",
|
||||||
|
return_value={"status": "success", "query_id": "q-1", "rows_affected": 10},
|
||||||
|
):
|
||||||
|
result = orch.execute_run(run)
|
||||||
|
|
||||||
|
assert result.status == "COMPLETED"
|
||||||
|
assert result.insert_status == "success"
|
||||||
|
assert result.superset_execution_id == "q-1"
|
||||||
|
assert result.total_records == 10
|
||||||
|
assert result.successful_records == 10
|
||||||
|
|
||||||
|
# endregion test_execute_run_success
|
||||||
|
|
||||||
|
# region test_execute_run_executor_failure [TYPE Function]
|
||||||
|
# @BRIEF Executor raises exception, run is marked FAILED.
|
||||||
|
def test_execute_run_executor_failure(self, mock_job: MagicMock) -> None:
|
||||||
|
db = MagicMock()
|
||||||
|
config_manager = MagicMock()
|
||||||
|
|
||||||
|
# Mock job query
|
||||||
|
db.query.return_value.filter.return_value.first.return_value = mock_job
|
||||||
|
|
||||||
|
run = MagicMock()
|
||||||
|
run.id = "run-1"
|
||||||
|
run.job_id = "job-123"
|
||||||
|
run.status = "PENDING"
|
||||||
|
run.error_message = None
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"src.plugins.translate.orchestrator.TranslationExecutor"
|
||||||
|
) as MockExecutor:
|
||||||
|
mock_executor_instance = MagicMock()
|
||||||
|
mock_executor_instance.execute_run.side_effect = ValueError(
|
||||||
|
"LLM provider unavailable"
|
||||||
|
)
|
||||||
|
MockExecutor.return_value = mock_executor_instance
|
||||||
|
|
||||||
|
orch = TranslationOrchestrator(db, config_manager, "test-user")
|
||||||
|
with patch.object(orch, "event_log"):
|
||||||
|
result = orch.execute_run(run)
|
||||||
|
|
||||||
|
assert result.status == "FAILED"
|
||||||
|
assert "LLM provider unavailable" in (result.error_message or "")
|
||||||
|
|
||||||
|
# endregion test_execute_run_executor_failure
|
||||||
|
|
||||||
|
# region test_execute_run_skip_insert [TYPE Function]
|
||||||
|
# @BRIEF skip_insert=True completes run without SQL generation or Superset submission.
|
||||||
|
def test_execute_run_skip_insert(self, mock_job: MagicMock) -> None:
|
||||||
|
db = MagicMock()
|
||||||
|
config_manager = MagicMock()
|
||||||
|
|
||||||
|
db.query.return_value.filter.return_value.first.return_value = mock_job
|
||||||
|
|
||||||
|
run = MagicMock()
|
||||||
|
run.id = "run-1"
|
||||||
|
run.job_id = "job-123"
|
||||||
|
run.status = "PENDING"
|
||||||
|
run.completed_at = None
|
||||||
|
|
||||||
|
completed_run = MagicMock()
|
||||||
|
completed_run.id = "run-1"
|
||||||
|
completed_run.job_id = "job-123"
|
||||||
|
completed_run.status = "COMPLETED"
|
||||||
|
completed_run.total_records = 5
|
||||||
|
completed_run.successful_records = 5
|
||||||
|
completed_run.failed_records = 0
|
||||||
|
completed_run.skipped_records = 0
|
||||||
|
completed_run.completed_at = datetime.now(timezone.utc)
|
||||||
|
completed_run.error_message = None
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"src.plugins.translate.orchestrator.TranslationExecutor"
|
||||||
|
) as MockExecutor:
|
||||||
|
mock_executor_instance = MagicMock()
|
||||||
|
mock_executor_instance.execute_run.return_value = completed_run
|
||||||
|
MockExecutor.return_value = mock_executor_instance
|
||||||
|
|
||||||
|
orch = TranslationOrchestrator(db, config_manager, "test-user")
|
||||||
|
with patch.object(orch, "_generate_and_insert_sql") as mock_gen_sql:
|
||||||
|
with patch.object(orch, "event_log"):
|
||||||
|
result = orch.execute_run(run, skip_insert=True)
|
||||||
|
|
||||||
|
assert result.status == "COMPLETED"
|
||||||
|
# _generate_and_insert_sql should NOT be called in skip_insert mode
|
||||||
|
mock_gen_sql.assert_not_called()
|
||||||
|
assert result.total_records == 5
|
||||||
|
|
||||||
|
# endregion test_execute_run_skip_insert
|
||||||
|
|
||||||
|
# region test_execute_run_no_job [TYPE Function]
|
||||||
|
# @BRIEF Job not found raises ValueError.
|
||||||
|
def test_execute_run_no_job(self) -> None:
|
||||||
|
db = MagicMock()
|
||||||
|
config_manager = MagicMock()
|
||||||
|
|
||||||
|
# Mock job query returns None (job not found)
|
||||||
|
db.query.return_value.filter.return_value.first.return_value = None
|
||||||
|
|
||||||
|
run = MagicMock()
|
||||||
|
run.id = "run-1"
|
||||||
|
run.job_id = "job-missing"
|
||||||
|
run.status = "PENDING"
|
||||||
|
|
||||||
|
orch = TranslationOrchestrator(db, config_manager, "test-user")
|
||||||
|
with pytest.raises(ValueError, match="not found"):
|
||||||
|
orch.execute_run(run)
|
||||||
|
|
||||||
|
# endregion test_execute_run_no_job
|
||||||
|
|
||||||
|
# region test_execute_run_insert_failure [TYPE Function]
|
||||||
|
# @BRIEF SQL generation/insert returns failure, run still completes but with error logged.
|
||||||
|
def test_execute_run_insert_failure(self, mock_job: MagicMock) -> None:
|
||||||
|
db = MagicMock()
|
||||||
|
config_manager = MagicMock()
|
||||||
|
|
||||||
|
# First query returns mock_job, re-query returns completed_run
|
||||||
|
completed_run_response = MagicMock()
|
||||||
|
completed_run_response.id = "run-1"
|
||||||
|
completed_run_response.job_id = "job-123"
|
||||||
|
completed_run_response.status = "COMPLETED"
|
||||||
|
completed_run_response.total_records = 3
|
||||||
|
completed_run_response.successful_records = 3
|
||||||
|
completed_run_response.failed_records = 0
|
||||||
|
completed_run_response.skipped_records = 0
|
||||||
|
completed_run_response.completed_at = datetime.now(timezone.utc)
|
||||||
|
completed_run_response.error_message = None
|
||||||
|
completed_run_response.insert_status = "failed"
|
||||||
|
completed_run_response.superset_execution_id = ""
|
||||||
|
completed_run_response.superset_execution_log = {}
|
||||||
|
db.query.return_value.filter.return_value.first.side_effect = [
|
||||||
|
mock_job,
|
||||||
|
completed_run_response,
|
||||||
|
]
|
||||||
|
|
||||||
|
run = MagicMock()
|
||||||
|
run.id = "run-1"
|
||||||
|
run.job_id = "job-123"
|
||||||
|
run.status = "PENDING"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"src.plugins.translate.orchestrator.TranslationExecutor"
|
||||||
|
) as MockExecutor:
|
||||||
|
mock_executor_instance = MagicMock()
|
||||||
|
mock_executor_instance.execute_run.return_value = completed_run_response
|
||||||
|
MockExecutor.return_value = mock_executor_instance
|
||||||
|
|
||||||
|
orch = TranslationOrchestrator(db, config_manager, "test-user")
|
||||||
|
with patch.object(
|
||||||
|
orch, "_generate_and_insert_sql",
|
||||||
|
return_value={"status": "failed", "error_message": "timeout", "query_id": None},
|
||||||
|
):
|
||||||
|
with patch.object(orch, "event_log"):
|
||||||
|
result = orch.execute_run(run)
|
||||||
|
|
||||||
|
assert result.status == "COMPLETED"
|
||||||
|
assert result.insert_status == "failed"
|
||||||
|
assert "timeout" in (result.error_message or "")
|
||||||
|
|
||||||
|
# endregion test_execute_run_insert_failure
|
||||||
|
|
||||||
# region test_cancel_run [TYPE Function]
|
# region test_cancel_run [TYPE Function]
|
||||||
# @PURPOSE: Cancel a run changes status to CANCELLED.
|
# @PURPOSE: Cancel a run changes status to CANCELLED.
|
||||||
def test_cancel_run(self) -> None:
|
def test_cancel_run(self) -> None:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region TranslationPreviewTests [TYPE Module]
|
# region TranslationPreviewTests [TYPE Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: test, translate, preview, session
|
# @SEMANTICS: test, translate, preview, session
|
||||||
# @PURPOSE: Test TranslationPreview service: preview, approve/edit/reject state transitions, cost estimation, acceptance gate.
|
# @PURPOSE: Test TranslationPreview service: preview, approve/edit/reject state transitions, cost estimation, acceptance gate.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region SQLGeneratorTests [TYPE Module]
|
# region SQLGeneratorTests [TYPE Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: test, translate, sql_generator
|
# @SEMANTICS: test, translate, sql_generator
|
||||||
# @PURPOSE: Tests for SQLGenerator: PostgreSQL INSERT + UPSERT, ClickHouse INSERT, dialect quoting, NULL handling, injection safety.
|
# @PURPOSE: Tests for SQLGenerator: PostgreSQL INSERT + UPSERT, ClickHouse INSERT, dialect quoting, NULL handling, injection safety.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ from ._utils import _normalize_term, _detect_delimiter
|
|||||||
# @SIDE_EFFECT: Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.
|
# @SIDE_EFFECT: Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.
|
||||||
class DictionaryManager:
|
class DictionaryManager:
|
||||||
# region DictionaryManager.create_dictionary [TYPE Function]
|
# region DictionaryManager.create_dictionary [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Create a new terminology dictionary.
|
# @PURPOSE: Create a new terminology dictionary.
|
||||||
# @PRE: payload contains name, source_dialect, target_dialect.
|
# @PRE: payload contains name, source_dialect, target_dialect.
|
||||||
# @POST: New TerminologyDictionary row is created and returned.
|
# @POST: New TerminologyDictionary row is created and returned.
|
||||||
@@ -60,7 +59,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.create_dictionary
|
# endregion DictionaryManager.create_dictionary
|
||||||
|
|
||||||
# region DictionaryManager.update_dictionary [TYPE Function]
|
# region DictionaryManager.update_dictionary [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Update an existing terminology dictionary.
|
# @PURPOSE: Update an existing terminology dictionary.
|
||||||
# @PRE: dict_id exists in terminology_dictionaries table.
|
# @PRE: dict_id exists in terminology_dictionaries table.
|
||||||
# @POST: Dictionary metadata is updated and returned.
|
# @POST: Dictionary metadata is updated and returned.
|
||||||
@@ -92,7 +90,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.update_dictionary
|
# endregion DictionaryManager.update_dictionary
|
||||||
|
|
||||||
# region DictionaryManager.delete_dictionary [TYPE Function]
|
# region DictionaryManager.delete_dictionary [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Delete a dictionary, blocked if attached to active/scheduled jobs.
|
# @PURPOSE: Delete a dictionary, blocked if attached to active/scheduled jobs.
|
||||||
# @PRE: dict_id exists.
|
# @PRE: dict_id exists.
|
||||||
# @POST: Dictionary and its entries are deleted, unless attached to ACTIVE/READY/RUNNING/SCHEDULED jobs.
|
# @POST: Dictionary and its entries are deleted, unless attached to ACTIVE/READY/RUNNING/SCHEDULED jobs.
|
||||||
@@ -136,7 +133,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.delete_dictionary
|
# endregion DictionaryManager.delete_dictionary
|
||||||
|
|
||||||
# region DictionaryManager.get_dictionary [TYPE Function]
|
# region DictionaryManager.get_dictionary [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Get a single dictionary by ID with entry count.
|
# @PURPOSE: Get a single dictionary by ID with entry count.
|
||||||
# @PRE: dict_id exists.
|
# @PRE: dict_id exists.
|
||||||
# @POST: Returns dict with dictionary + entry_count or raises ValueError.
|
# @POST: Returns dict with dictionary + entry_count or raises ValueError.
|
||||||
@@ -149,7 +145,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.get_dictionary
|
# endregion DictionaryManager.get_dictionary
|
||||||
|
|
||||||
# region DictionaryManager.list_dictionaries [TYPE Function]
|
# region DictionaryManager.list_dictionaries [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: List dictionaries with pagination and entry counts.
|
# @PURPOSE: List dictionaries with pagination and entry counts.
|
||||||
# @PRE: page >= 1, page_size between 1 and 100.
|
# @PRE: page >= 1, page_size between 1 and 100.
|
||||||
# @POST: Returns (list of dicts, total_count).
|
# @POST: Returns (list of dicts, total_count).
|
||||||
@@ -169,7 +164,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.list_dictionaries
|
# endregion DictionaryManager.list_dictionaries
|
||||||
|
|
||||||
# region DictionaryManager.add_entry [TYPE Function]
|
# region DictionaryManager.add_entry [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Add an entry to a dictionary, enforcing unique source_term_normalized.
|
# @PURPOSE: Add an entry to a dictionary, enforcing unique source_term_normalized.
|
||||||
# @PRE: dict_id exists. source_term and target_term are non-empty.
|
# @PRE: dict_id exists. source_term and target_term are non-empty.
|
||||||
# @POST: New DictionaryEntry row is created or raises on duplicate.
|
# @POST: New DictionaryEntry row is created or raises on duplicate.
|
||||||
@@ -210,7 +204,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.add_entry
|
# endregion DictionaryManager.add_entry
|
||||||
|
|
||||||
# region DictionaryManager.edit_entry [TYPE Function]
|
# region DictionaryManager.edit_entry [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Edit an existing dictionary entry with duplicate-aware normalization.
|
# @PURPOSE: Edit an existing dictionary entry with duplicate-aware normalization.
|
||||||
# @PRE: entry_id exists.
|
# @PRE: entry_id exists.
|
||||||
# @POST: Entry fields are updated.
|
# @POST: Entry fields are updated.
|
||||||
@@ -255,7 +248,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.edit_entry
|
# endregion DictionaryManager.edit_entry
|
||||||
|
|
||||||
# region DictionaryManager.delete_entry [TYPE Function]
|
# region DictionaryManager.delete_entry [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Delete a single dictionary entry.
|
# @PURPOSE: Delete a single dictionary entry.
|
||||||
# @PRE: entry_id exists.
|
# @PRE: entry_id exists.
|
||||||
# @POST: Entry is deleted.
|
# @POST: Entry is deleted.
|
||||||
@@ -272,7 +264,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.delete_entry
|
# endregion DictionaryManager.delete_entry
|
||||||
|
|
||||||
# region DictionaryManager.clear_entries [TYPE Function]
|
# region DictionaryManager.clear_entries [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Delete all entries for a dictionary.
|
# @PURPOSE: Delete all entries for a dictionary.
|
||||||
# @PRE: dict_id exists.
|
# @PRE: dict_id exists.
|
||||||
# @POST: All entries for the dictionary are deleted.
|
# @POST: All entries for the dictionary are deleted.
|
||||||
@@ -290,7 +281,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.clear_entries
|
# endregion DictionaryManager.clear_entries
|
||||||
|
|
||||||
# region DictionaryManager.list_entries [TYPE Function]
|
# region DictionaryManager.list_entries [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: List entries for a dictionary with pagination.
|
# @PURPOSE: List entries for a dictionary with pagination.
|
||||||
# @PRE: dict_id exists.
|
# @PRE: dict_id exists.
|
||||||
# @POST: Returns (list of entries, total_count).
|
# @POST: Returns (list of entries, total_count).
|
||||||
@@ -316,7 +306,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.list_entries
|
# endregion DictionaryManager.list_entries
|
||||||
|
|
||||||
# region DictionaryManager.import_entries [TYPE Function]
|
# region DictionaryManager.import_entries [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Import entries from CSV/TSV content with duplicate detection and conflict resolution.
|
# @PURPOSE: Import entries from CSV/TSV content with duplicate detection and conflict resolution.
|
||||||
# @PRE: content is valid CSV or TSV. dict_id exists.
|
# @PRE: content is valid CSV or TSV. dict_id exists.
|
||||||
# @POST: Entries are created/updated/skipped per conflict mode. Returns result summary.
|
# @POST: Entries are created/updated/skipped per conflict mode. Returns result summary.
|
||||||
@@ -456,7 +445,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.import_entries
|
# endregion DictionaryManager.import_entries
|
||||||
|
|
||||||
# region DictionaryManager.filter_for_batch [TYPE Function]
|
# region DictionaryManager.filter_for_batch [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job.
|
# @PURPOSE: Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job.
|
||||||
# @PRE: job_id exists and source_texts is a list of strings.
|
# @PRE: job_id exists and source_texts is a list of strings.
|
||||||
# @POST: Returns list of matched entries with match info, sorted by priority across attached dictionaries.
|
# @POST: Returns list of matched entries with match info, sorted by priority across attached dictionaries.
|
||||||
@@ -555,7 +543,6 @@ class DictionaryManager:
|
|||||||
|
|
||||||
|
|
||||||
# region DictionaryManager.submit_correction [TYPE Function]
|
# region DictionaryManager.submit_correction [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Submit a term correction from a run result. Validates language match, detects conflicts.
|
# @PURPOSE: Submit a term correction from a run result. Validates language match, detects conflicts.
|
||||||
# @PRE: source_term, incorrect_target_term, corrected_target_term are non-empty. dict_id exists.
|
# @PRE: source_term, incorrect_target_term, corrected_target_term are non-empty. dict_id exists.
|
||||||
# @POST: Entry created or updated; origin tracking populated. Returns action + conflict info.
|
# @POST: Entry created or updated; origin tracking populated. Returns action + conflict info.
|
||||||
@@ -653,7 +640,6 @@ class DictionaryManager:
|
|||||||
# endregion DictionaryManager.submit_correction
|
# endregion DictionaryManager.submit_correction
|
||||||
|
|
||||||
# region DictionaryManager.submit_bulk_corrections [TYPE Function]
|
# region DictionaryManager.submit_bulk_corrections [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Submit multiple term corrections atomically — all succeed or all fail.
|
# @PURPOSE: Submit multiple term corrections atomically — all succeed or all fail.
|
||||||
# @PRE: corrections list is non-empty. dict_id exists.
|
# @PRE: corrections list is non-empty. dict_id exists.
|
||||||
# @POST: All corrections applied or none applied with conflict list.
|
# @POST: All corrections applied or none applied with conflict list.
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ class TranslationEventLog:
|
|||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
# region log_event [TYPE Function]
|
# region log_event [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Write an immutable event. Enforces terminal event invariant for non-null run_id.
|
# @PURPOSE: Write an immutable event. Enforces terminal event invariant for non-null run_id.
|
||||||
# @PRE: event_type must be a known type. If run_id is not None, enforce terminal invariant.
|
# @PRE: event_type must be a known type. If run_id is not None, enforce terminal invariant.
|
||||||
# @POST: TranslationEvent row is created.
|
# @POST: TranslationEvent row is created.
|
||||||
|
|||||||
@@ -164,12 +164,102 @@ class TranslationExecutor:
|
|||||||
# endregion execute_run
|
# endregion execute_run
|
||||||
|
|
||||||
# region _fetch_source_rows [TYPE Function]
|
# region _fetch_source_rows [TYPE Function]
|
||||||
# @PURPOSE: Fetch source rows from the accepted preview session for this job.
|
# @PURPOSE: Fetch full source dataset from Superset (via datasource) for full translation.
|
||||||
# @PRE: job_id exists.
|
# @PRE: job_id exists. Job may have source_datasource_id for full fetch.
|
||||||
# @POST: Returns list of dicts with source data.
|
# @POST: Returns list of dicts with source data (all rows from the source datasource).
|
||||||
|
# @SIDE_EFFECT: Makes HTTP call to Superset chart data API when datasource is configured.
|
||||||
def _fetch_source_rows(self, job_id: str, run_id: str) -> List[Dict[str, Any]]:
|
def _fetch_source_rows(self, job_id: str, run_id: str) -> List[Dict[str, Any]]:
|
||||||
with belief_scope("TranslationExecutor._fetch_source_rows"):
|
with belief_scope("TranslationExecutor._fetch_source_rows"):
|
||||||
# Get the latest APPLIED preview session
|
job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()
|
||||||
|
|
||||||
|
# If source_datasource_id is configured, fetch ALL rows from the Superset chart data API
|
||||||
|
if job and job.source_datasource_id:
|
||||||
|
try:
|
||||||
|
logger.reason("Fetching full dataset from Superset datasource", {
|
||||||
|
"run_id": run_id,
|
||||||
|
"datasource_id": job.source_datasource_id,
|
||||||
|
"environment_id": job.environment_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Determine environment
|
||||||
|
environments = self.config_manager.get_environments()
|
||||||
|
target_env_id = job.environment_id or job.source_dialect or ""
|
||||||
|
env_config = next(
|
||||||
|
(e for e in environments if e.id == target_env_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if not env_config and environments:
|
||||||
|
env_config = environments[0]
|
||||||
|
|
||||||
|
if env_config:
|
||||||
|
from ...core.superset_client import SupersetClient
|
||||||
|
client = SupersetClient(env_config)
|
||||||
|
|
||||||
|
# Fetch dataset detail to build proper query context
|
||||||
|
dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))
|
||||||
|
|
||||||
|
# Build query context (same approach as preview but without row_limit)
|
||||||
|
query_context = client.build_dataset_preview_query_context(
|
||||||
|
dataset_id=int(job.source_datasource_id),
|
||||||
|
dataset_record=dataset_detail,
|
||||||
|
template_params={},
|
||||||
|
effective_filters=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove row_limit to get ALL rows; use result_type="samples"
|
||||||
|
queries = query_context.get("queries", [])
|
||||||
|
if queries:
|
||||||
|
queries[0].pop("row_limit", None)
|
||||||
|
queries[0].pop("result_type", None)
|
||||||
|
queries[0]["metrics"] = []
|
||||||
|
query_context["result_type"] = "samples"
|
||||||
|
form_data = query_context.get("form_data", {})
|
||||||
|
form_data.pop("query_mode", None)
|
||||||
|
|
||||||
|
response = client.network.request(
|
||||||
|
method="POST",
|
||||||
|
endpoint="/api/v1/chart/data",
|
||||||
|
data=json.dumps(query_context),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract rows
|
||||||
|
rows = self._extract_chart_data_rows(response)
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
logger.reason(f"Fetched {len(rows)} rows from Superset datasource", {
|
||||||
|
"run_id": run_id,
|
||||||
|
})
|
||||||
|
# Map rows to source_rows format
|
||||||
|
source_rows = []
|
||||||
|
for idx, row in enumerate(rows):
|
||||||
|
source_data_dict = dict(row) if row else None
|
||||||
|
source_text = str(row.get(job.translation_column, "")) if job.translation_column else json.dumps(row)
|
||||||
|
source_rows.append({
|
||||||
|
"row_index": str(idx),
|
||||||
|
"source_text": source_text,
|
||||||
|
"approved_translation": None,
|
||||||
|
"source_object_name": f"Row {idx}",
|
||||||
|
"source_data": source_data_dict,
|
||||||
|
})
|
||||||
|
return source_rows
|
||||||
|
else:
|
||||||
|
logger.explore("Superset datasource returned no rows", {
|
||||||
|
"run_id": run_id,
|
||||||
|
"datasource_id": job.source_datasource_id,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
logger.explore("No environment config found for datasource fetch", {
|
||||||
|
"env_id": target_env_id,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.explore("Failed to fetch full dataset from Superset, falling back to preview", {
|
||||||
|
"run_id": run_id,
|
||||||
|
"error": str(e),
|
||||||
|
})
|
||||||
|
# Fall through to preview-based fetch
|
||||||
|
|
||||||
|
# Fallback: get the latest APPLIED preview session
|
||||||
session = (
|
session = (
|
||||||
self.db.query(TranslationPreviewSession)
|
self.db.query(TranslationPreviewSession)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -195,20 +285,48 @@ class TranslationExecutor:
|
|||||||
|
|
||||||
source_rows = []
|
source_rows = []
|
||||||
for rec in records:
|
for rec in records:
|
||||||
|
source_data_dict = None
|
||||||
|
if hasattr(rec, "source_data") and rec.source_data:
|
||||||
|
source_data_dict = dict(rec.source_data)
|
||||||
source_rows.append({
|
source_rows.append({
|
||||||
"row_index": rec.source_object_id or "0",
|
"row_index": rec.source_object_id or "0",
|
||||||
"source_text": rec.source_sql or "",
|
"source_text": rec.source_sql or "",
|
||||||
"approved_translation": rec.target_sql if rec.status == "APPROVED" else None,
|
"approved_translation": rec.target_sql if rec.status == "APPROVED" else None,
|
||||||
"source_object_name": rec.source_object_name or "",
|
"source_object_name": rec.source_object_name or "",
|
||||||
|
"source_data": source_data_dict,
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.reason(f"Fetched {len(source_rows)} source rows from preview", {
|
logger.reason(f"Fetched {len(source_rows)} source rows from preview fallback", {
|
||||||
"run_id": run_id,
|
"run_id": run_id,
|
||||||
"session_id": session.id,
|
"session_id": session.id,
|
||||||
})
|
})
|
||||||
return source_rows
|
return source_rows
|
||||||
# endregion _fetch_source_rows
|
# endregion _fetch_source_rows
|
||||||
|
|
||||||
|
# region _extract_chart_data_rows [TYPE Function]
|
||||||
|
# @PURPOSE: Extract data rows from Superset chart data API response.
|
||||||
|
# @POST: Returns list of dicts with column-value pairs.
|
||||||
|
@staticmethod
|
||||||
|
def _extract_chart_data_rows(response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
result = response.get("result")
|
||||||
|
if isinstance(result, list):
|
||||||
|
for item in result:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
data = item.get("data")
|
||||||
|
if isinstance(data, list) and data:
|
||||||
|
return data
|
||||||
|
if isinstance(result, dict):
|
||||||
|
data = result.get("data")
|
||||||
|
if isinstance(data, list) and data:
|
||||||
|
return data
|
||||||
|
data = response.get("data")
|
||||||
|
if isinstance(data, list) and data:
|
||||||
|
return data
|
||||||
|
if isinstance(result, list):
|
||||||
|
return result
|
||||||
|
return []
|
||||||
|
# endregion _extract_chart_data_rows
|
||||||
|
|
||||||
# region _process_batch [TYPE Function]
|
# region _process_batch [TYPE Function]
|
||||||
# @PURPOSE: Process a single batch: filter dict, build prompt, call LLM, persist records.
|
# @PURPOSE: Process a single batch: filter dict, build prompt, call LLM, persist records.
|
||||||
# @PRE: job and batch_rows are valid.
|
# @PRE: job and batch_rows are valid.
|
||||||
@@ -272,6 +390,7 @@ class TranslationExecutor:
|
|||||||
source_object_type="table_row",
|
source_object_type="table_row",
|
||||||
source_object_id=row.get("row_index"),
|
source_object_id=row.get("row_index"),
|
||||||
source_object_name=row.get("source_object_name", ""),
|
source_object_name=row.get("source_object_name", ""),
|
||||||
|
source_data=row.get("source_data"),
|
||||||
status="SUCCESS",
|
status="SUCCESS",
|
||||||
)
|
)
|
||||||
self.db.add(record)
|
self.db.add(record)
|
||||||
@@ -398,6 +517,7 @@ class TranslationExecutor:
|
|||||||
source_object_type="table_row",
|
source_object_type="table_row",
|
||||||
source_object_id=row.get("row_index"),
|
source_object_id=row.get("row_index"),
|
||||||
source_object_name=row.get("source_object_name", ""),
|
source_object_name=row.get("source_object_name", ""),
|
||||||
|
source_data=row.get("source_data"),
|
||||||
status="FAILED",
|
status="FAILED",
|
||||||
error_message=f"LLM call failed after {retries} retries: {last_error}",
|
error_message=f"LLM call failed after {retries} retries: {last_error}",
|
||||||
)
|
)
|
||||||
@@ -425,6 +545,7 @@ class TranslationExecutor:
|
|||||||
source_object_type="table_row",
|
source_object_type="table_row",
|
||||||
source_object_id=row.get("row_index"),
|
source_object_id=row.get("row_index"),
|
||||||
source_object_name=row.get("source_object_name", ""),
|
source_object_name=row.get("source_object_name", ""),
|
||||||
|
source_data=row.get("source_data"),
|
||||||
status="SKIPPED",
|
status="SKIPPED",
|
||||||
error_message=f"LLM parse failure: {e}",
|
error_message=f"LLM parse failure: {e}",
|
||||||
)
|
)
|
||||||
@@ -456,6 +577,7 @@ class TranslationExecutor:
|
|||||||
source_object_type="table_row",
|
source_object_type="table_row",
|
||||||
source_object_id=row.get("row_index"),
|
source_object_id=row.get("row_index"),
|
||||||
source_object_name=row.get("source_object_name", ""),
|
source_object_name=row.get("source_object_name", ""),
|
||||||
|
source_data=row.get("source_data"),
|
||||||
status="SKIPPED",
|
status="SKIPPED",
|
||||||
error_message="NULL translation returned by LLM",
|
error_message="NULL translation returned by LLM",
|
||||||
)
|
)
|
||||||
@@ -474,6 +596,7 @@ class TranslationExecutor:
|
|||||||
source_object_type="table_row",
|
source_object_type="table_row",
|
||||||
source_object_id=row.get("row_index"),
|
source_object_id=row.get("row_index"),
|
||||||
source_object_name=row.get("source_object_name", ""),
|
source_object_name=row.get("source_object_name", ""),
|
||||||
|
source_data=row.get("source_data"),
|
||||||
status="SKIPPED",
|
status="SKIPPED",
|
||||||
error_message="Empty translation returned by LLM",
|
error_message="Empty translation returned by LLM",
|
||||||
)
|
)
|
||||||
@@ -490,6 +613,7 @@ class TranslationExecutor:
|
|||||||
source_object_type="table_row",
|
source_object_type="table_row",
|
||||||
source_object_id=row.get("row_index"),
|
source_object_id=row.get("row_index"),
|
||||||
source_object_name=row.get("source_object_name", ""),
|
source_object_name=row.get("source_object_name", ""),
|
||||||
|
source_data=row.get("source_data"),
|
||||||
status="SUCCESS",
|
status="SUCCESS",
|
||||||
)
|
)
|
||||||
self.db.add(record)
|
self.db.add(record)
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ class TranslationOrchestrator:
|
|||||||
self._job: Optional[TranslationJob] = None
|
self._job: Optional[TranslationJob] = None
|
||||||
|
|
||||||
# region start_run [TYPE Function]
|
# region start_run [TYPE Function]
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @PURPOSE: Start a new translation run for a job.
|
# @PURPOSE: Start a new translation run for a job.
|
||||||
# @PRE: job_id exists. For manual runs, there must be an accepted preview session.
|
# @PRE: job_id exists. For manual runs, there must be an accepted preview session.
|
||||||
# @POST: TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded.
|
# @POST: TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded.
|
||||||
@@ -105,6 +104,7 @@ class TranslationOrchestrator:
|
|||||||
"source_key_cols": job.source_key_cols,
|
"source_key_cols": job.source_key_cols,
|
||||||
"target_key_cols": job.target_key_cols,
|
"target_key_cols": job.target_key_cols,
|
||||||
"translation_column": job.translation_column,
|
"translation_column": job.translation_column,
|
||||||
|
"target_column": job.target_column,
|
||||||
"context_columns": job.context_columns,
|
"context_columns": job.context_columns,
|
||||||
"target_language": job.target_language,
|
"target_language": job.target_language,
|
||||||
"provider_id": job.provider_id,
|
"provider_id": job.provider_id,
|
||||||
@@ -263,7 +263,11 @@ class TranslationOrchestrator:
|
|||||||
run.insert_status = insert_result.get("status")
|
run.insert_status = insert_result.get("status")
|
||||||
run.superset_execution_id = str(insert_result.get("query_id") or "")
|
run.superset_execution_id = str(insert_result.get("query_id") or "")
|
||||||
run.superset_execution_log = insert_result
|
run.superset_execution_log = insert_result
|
||||||
run.status = "COMPLETED" if insert_result.get("status") == "success" else "COMPLETED"
|
# Preserve the translation-phase status. If the executor already
|
||||||
|
# marked the run FAILED (all LLM calls failed) we do NOT upgrade it
|
||||||
|
# to COMPLETED just because the insert phase ran.
|
||||||
|
if run.status != "FAILED":
|
||||||
|
run.status = "COMPLETED"
|
||||||
if insert_result.get("error_message"):
|
if insert_result.get("error_message"):
|
||||||
run.error_message = insert_result["error_message"]
|
run.error_message = insert_result["error_message"]
|
||||||
run.completed_at = datetime.now(timezone.utc)
|
run.completed_at = datetime.now(timezone.utc)
|
||||||
@@ -288,7 +292,9 @@ class TranslationOrchestrator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
self.db.refresh(run)
|
# Re-query run after commit — refresh may fail if the object was
|
||||||
|
# created in a different session or became detached during commit.
|
||||||
|
run = self.db.query(TranslationRun).filter(TranslationRun.id == run.id).first()
|
||||||
|
|
||||||
logger.reflect("Run execution complete", {
|
logger.reflect("Run execution complete", {
|
||||||
"run_id": run.id,
|
"run_id": run.id,
|
||||||
@@ -329,11 +335,20 @@ class TranslationOrchestrator:
|
|||||||
"dialect": job.database_dialect or job.target_dialect,
|
"dialect": job.database_dialect or job.target_dialect,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Build rows for SQL generation
|
# Determine effective target column for INSERT (defaults to translation_column)
|
||||||
|
effective_target = job.target_column or job.translation_column
|
||||||
|
|
||||||
|
# Build columns for SQL generation
|
||||||
columns = job.context_columns or []
|
columns = job.context_columns or []
|
||||||
|
|
||||||
|
# Always include translation_column (original text) if it's different from target
|
||||||
if job.translation_column and job.translation_column not in columns:
|
if job.translation_column and job.translation_column not in columns:
|
||||||
columns.append(job.translation_column)
|
columns.append(job.translation_column)
|
||||||
|
|
||||||
|
# Add target_column separately if it differs from translation_column
|
||||||
|
if effective_target and effective_target != job.translation_column and effective_target not in columns:
|
||||||
|
columns.append(effective_target)
|
||||||
|
|
||||||
# Also include key columns if used for upsert
|
# Also include key columns if used for upsert
|
||||||
if job.target_key_cols:
|
if job.target_key_cols:
|
||||||
for k in job.target_key_cols:
|
for k in job.target_key_cols:
|
||||||
@@ -343,27 +358,57 @@ class TranslationOrchestrator:
|
|||||||
rows_for_sql = []
|
rows_for_sql = []
|
||||||
for rec in records:
|
for rec in records:
|
||||||
row_data = {}
|
row_data = {}
|
||||||
|
source_data = rec.source_data or {}
|
||||||
|
|
||||||
|
# Context columns from source data
|
||||||
if job.context_columns:
|
if job.context_columns:
|
||||||
for col in job.context_columns:
|
for col in job.context_columns:
|
||||||
row_data[col] = ""
|
row_data[col] = source_data.get(col, "")
|
||||||
if job.translation_column:
|
|
||||||
|
# Original text column (translation_column)
|
||||||
|
if job.translation_column and job.translation_column not in (job.target_key_cols or []):
|
||||||
|
# If target_column differs, keep the original value from source_data;
|
||||||
|
# otherwise use the translated value
|
||||||
|
if effective_target and effective_target != job.translation_column:
|
||||||
|
row_data[job.translation_column] = source_data.get(job.translation_column, "")
|
||||||
|
else:
|
||||||
row_data[job.translation_column] = rec.target_sql or ""
|
row_data[job.translation_column] = rec.target_sql or ""
|
||||||
|
|
||||||
|
# Translated text goes into target_column (may be same as translation_column)
|
||||||
|
if effective_target:
|
||||||
|
row_data[effective_target] = rec.target_sql or ""
|
||||||
|
|
||||||
|
# Key columns from source data
|
||||||
if job.target_key_cols:
|
if job.target_key_cols:
|
||||||
source_data = rec.source_data or {}
|
|
||||||
for k in job.target_key_cols:
|
for k in job.target_key_cols:
|
||||||
# Use source_data value if available, fall back to empty string
|
|
||||||
row_data[k] = source_data.get(k, "")
|
row_data[k] = source_data.get(k, "")
|
||||||
rows_for_sql.append(row_data)
|
rows_for_sql.append(row_data)
|
||||||
|
|
||||||
if not columns:
|
if not columns:
|
||||||
# Use target_sql as the sole column
|
# Use target_sql as the sole column
|
||||||
columns = [job.translation_column or "translated_text"]
|
columns = [effective_target or "translated_text"]
|
||||||
rows_for_sql = [{columns[0]: rec.target_sql or ""} for rec in records]
|
rows_for_sql = [{columns[0]: rec.target_sql or ""} for rec in records]
|
||||||
|
|
||||||
|
# Resolve the real database backend engine from Superset
|
||||||
|
try:
|
||||||
|
env_id = job.environment_id or job.source_dialect or ""
|
||||||
|
executor = SupersetSqlLabExecutor(self.config_manager, env_id)
|
||||||
|
executor.resolve_database_id(
|
||||||
|
target_database_id=job.target_database_id,
|
||||||
|
)
|
||||||
|
real_backend = executor.get_database_backend()
|
||||||
|
except Exception as e:
|
||||||
|
logger.explore("Failed to resolve database backend, falling back to job dialet", {
|
||||||
|
"error": str(e),
|
||||||
|
})
|
||||||
|
real_backend = None
|
||||||
|
|
||||||
|
dialect = real_backend or job.database_dialect or job.target_dialect or "postgresql"
|
||||||
|
|
||||||
# Generate SQL
|
# Generate SQL
|
||||||
try:
|
try:
|
||||||
sql, row_count = SQLGenerator.generate(
|
sql, row_count = SQLGenerator.generate(
|
||||||
dialect=job.database_dialect or job.target_dialect or "postgresql",
|
dialect=dialect,
|
||||||
target_schema=job.target_schema,
|
target_schema=job.target_schema,
|
||||||
target_table=job.target_table or "translated_data",
|
target_table=job.target_table or "translated_data",
|
||||||
columns=columns,
|
columns=columns,
|
||||||
@@ -375,19 +420,24 @@ class TranslationOrchestrator:
|
|||||||
logger.explore("SQL generation failed", {"error": str(e)})
|
logger.explore("SQL generation failed", {"error": str(e)})
|
||||||
return {"status": "failed", "error_message": str(e), "query_id": None}
|
return {"status": "failed", "error_message": str(e), "query_id": None}
|
||||||
|
|
||||||
|
logger.reason("SQL generated with dialect", {
|
||||||
|
"dialect": dialect,
|
||||||
|
"real_backend": real_backend,
|
||||||
|
"job_database_dialect": job.database_dialect,
|
||||||
|
"job_target_dialect": job.target_dialect,
|
||||||
|
})
|
||||||
|
|
||||||
# Log insert phase start
|
# Log insert phase start
|
||||||
self.event_log.log_event(
|
self.event_log.log_event(
|
||||||
job_id=job.id,
|
job_id=job.id,
|
||||||
run_id=run.id,
|
run_id=run.id,
|
||||||
event_type="INSERT_PHASE_STARTED",
|
event_type="INSERT_PHASE_STARTED",
|
||||||
payload={"sql_length": len(sql), "row_count": row_count},
|
payload={"sql_length": len(sql), "row_count": row_count, "dialect": dialect},
|
||||||
created_by=self.current_user,
|
created_by=self.current_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Submit to Superset
|
# Submit to Superset
|
||||||
try:
|
try:
|
||||||
env_id = job.environment_id or job.source_dialect or ""
|
|
||||||
executor = SupersetSqlLabExecutor(self.config_manager, env_id)
|
|
||||||
result = executor.execute_and_poll(
|
result = executor.execute_and_poll(
|
||||||
sql=sql,
|
sql=sql,
|
||||||
max_polls=30,
|
max_polls=30,
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ class TranslationPreview:
|
|||||||
self.current_user = current_user
|
self.current_user = current_user
|
||||||
|
|
||||||
# region preview_rows [TYPE Function]
|
# region preview_rows [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Fetch sample rows from Superset dataset, send to LLM for translation, create preview session with records.
|
# @PURPOSE: Fetch sample rows from Superset dataset, send to LLM for translation, create preview session with records.
|
||||||
# @PRE: job_id exists and job has source_datasource_id, translation_column configured.
|
# @PRE: job_id exists and job has source_datasource_id, translation_column configured.
|
||||||
# @POST: Returns TranslationPreviewResponse with records, cost estimation, and persistent session.
|
# @POST: Returns TranslationPreviewResponse with records, cost estimation, and persistent session.
|
||||||
@@ -290,6 +289,19 @@ class TranslationPreview:
|
|||||||
status = "PENDING"
|
status = "PENDING"
|
||||||
feedback = None
|
feedback = None
|
||||||
|
|
||||||
|
# Extract source_data: store original row key columns for upsert matching
|
||||||
|
source_row = meta.get("source_row", {})
|
||||||
|
source_data = None
|
||||||
|
if job.target_key_cols:
|
||||||
|
source_data = {
|
||||||
|
k: source_row.get(k)
|
||||||
|
for k in job.target_key_cols
|
||||||
|
if k in source_row
|
||||||
|
}
|
||||||
|
elif source_row:
|
||||||
|
# No key columns configured — store the full row as fallback
|
||||||
|
source_data = dict(source_row)
|
||||||
|
|
||||||
record = TranslationPreviewRecord(
|
record = TranslationPreviewRecord(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
session_id=session.id,
|
session_id=session.id,
|
||||||
@@ -298,6 +310,7 @@ class TranslationPreview:
|
|||||||
source_object_type="table_row",
|
source_object_type="table_row",
|
||||||
source_object_id=str(idx),
|
source_object_id=str(idx),
|
||||||
source_object_name=f"Row {idx + 1}",
|
source_object_name=f"Row {idx + 1}",
|
||||||
|
source_data=source_data,
|
||||||
status=status,
|
status=status,
|
||||||
feedback=feedback,
|
feedback=feedback,
|
||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
@@ -516,7 +529,6 @@ class TranslationPreview:
|
|||||||
# endregion get_preview_session
|
# endregion get_preview_session
|
||||||
|
|
||||||
# region _fetch_sample_rows [TYPE Function]
|
# region _fetch_sample_rows [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Fetch sample rows from the Superset dataset for preview.
|
# @PURPOSE: Fetch sample rows from the Superset dataset for preview.
|
||||||
# @PRE: job has source_datasource_id and translation_column.
|
# @PRE: job has source_datasource_id and translation_column.
|
||||||
# @POST: Returns list of dicts with row data.
|
# @POST: Returns list of dicts with row data.
|
||||||
@@ -636,7 +648,6 @@ class TranslationPreview:
|
|||||||
# endregion _extract_data_rows
|
# endregion _extract_data_rows
|
||||||
|
|
||||||
# region _call_llm [TYPE Function]
|
# region _call_llm [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Call the configured LLM provider with the preview prompt.
|
# @PURPOSE: Call the configured LLM provider with the preview prompt.
|
||||||
# @PRE: job has a valid provider_id.
|
# @PRE: job has a valid provider_id.
|
||||||
# @POST: Returns raw LLM response string.
|
# @POST: Returns raw LLM response string.
|
||||||
|
|||||||
@@ -241,12 +241,14 @@ class TranslateJobService:
|
|||||||
source_key_cols=payload.source_key_cols or [],
|
source_key_cols=payload.source_key_cols or [],
|
||||||
target_key_cols=payload.target_key_cols or [],
|
target_key_cols=payload.target_key_cols or [],
|
||||||
translation_column=payload.translation_column,
|
translation_column=payload.translation_column,
|
||||||
|
target_column=payload.target_column,
|
||||||
context_columns=payload.context_columns or [],
|
context_columns=payload.context_columns or [],
|
||||||
target_language=payload.target_language,
|
target_language=payload.target_language,
|
||||||
provider_id=payload.provider_id,
|
provider_id=payload.provider_id,
|
||||||
batch_size=payload.batch_size,
|
batch_size=payload.batch_size,
|
||||||
upsert_strategy=payload.upsert_strategy,
|
upsert_strategy=payload.upsert_strategy,
|
||||||
environment_id=payload.environment_id,
|
environment_id=payload.environment_id,
|
||||||
|
target_database_id=payload.target_database_id,
|
||||||
status="DRAFT",
|
status="DRAFT",
|
||||||
created_by=self.current_user,
|
created_by=self.current_user,
|
||||||
)
|
)
|
||||||
@@ -360,6 +362,7 @@ class TranslateJobService:
|
|||||||
source_key_cols=source.source_key_cols,
|
source_key_cols=source.source_key_cols,
|
||||||
target_key_cols=source.target_key_cols,
|
target_key_cols=source.target_key_cols,
|
||||||
translation_column=source.translation_column,
|
translation_column=source.translation_column,
|
||||||
|
target_column=source.target_column,
|
||||||
context_columns=source.context_columns,
|
context_columns=source.context_columns,
|
||||||
target_language=source.target_language,
|
target_language=source.target_language,
|
||||||
provider_id=source.provider_id,
|
provider_id=source.provider_id,
|
||||||
@@ -461,6 +464,7 @@ def job_to_response(job: TranslationJob, dict_ids: Optional[List[str]] = None) -
|
|||||||
source_key_cols=job.source_key_cols or [],
|
source_key_cols=job.source_key_cols or [],
|
||||||
target_key_cols=job.target_key_cols or [],
|
target_key_cols=job.target_key_cols or [],
|
||||||
translation_column=job.translation_column,
|
translation_column=job.translation_column,
|
||||||
|
target_column=job.target_column,
|
||||||
context_columns=job.context_columns or [],
|
context_columns=job.context_columns or [],
|
||||||
target_language=job.target_language,
|
target_language=job.target_language,
|
||||||
provider_id=job.provider_id,
|
provider_id=job.provider_id,
|
||||||
@@ -472,6 +476,7 @@ def job_to_response(job: TranslationJob, dict_ids: Optional[List[str]] = None) -
|
|||||||
updated_at=job.updated_at,
|
updated_at=job.updated_at,
|
||||||
dictionary_ids=dict_ids or [],
|
dictionary_ids=dict_ids or [],
|
||||||
environment_id=job.environment_id,
|
environment_id=job.environment_id,
|
||||||
|
target_database_id=job.target_database_id,
|
||||||
)
|
)
|
||||||
# #endregion job_to_response
|
# #endregion job_to_response
|
||||||
|
|
||||||
|
|||||||
@@ -90,8 +90,13 @@ def _encode_sql_value(value: Any, dialect: Optional[str] = None) -> str:
|
|||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
return "TRUE" if value else "FALSE"
|
return "TRUE" if value else "FALSE"
|
||||||
|
|
||||||
# For ClickHouse: try to normalize timestamp-like string values
|
# For ClickHouse: try to normalize timestamp-like values (both string and numeric)
|
||||||
if dialect in CLICKHOUSE_DIALECTS and isinstance(value, str) and value:
|
if dialect in CLICKHOUSE_DIALECTS:
|
||||||
|
if isinstance(value, str) and value:
|
||||||
|
normalized = _normalize_timestamp_value(value)
|
||||||
|
if normalized:
|
||||||
|
return f"'{normalized}'"
|
||||||
|
elif isinstance(value, (int, float)):
|
||||||
normalized = _normalize_timestamp_value(value)
|
normalized = _normalize_timestamp_value(value)
|
||||||
if normalized:
|
if normalized:
|
||||||
return f"'{normalized}'"
|
return f"'{normalized}'"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class SupersetSqlLabExecutor:
|
|||||||
self.env_id = env_id
|
self.env_id = env_id
|
||||||
self._client: Optional[SupersetClient] = None
|
self._client: Optional[SupersetClient] = None
|
||||||
self._database_id: Optional[int] = None
|
self._database_id: Optional[int] = None
|
||||||
|
self._database_backend: Optional[str] = None
|
||||||
|
|
||||||
# region _get_client [TYPE Function]
|
# region _get_client [TYPE Function]
|
||||||
# @PURPOSE: Lazy-initialize SupersetClient for the configured environment.
|
# @PURPOSE: Lazy-initialize SupersetClient for the configured environment.
|
||||||
@@ -53,15 +54,43 @@ class SupersetSqlLabExecutor:
|
|||||||
# endregion _get_client
|
# endregion _get_client
|
||||||
|
|
||||||
# region resolve_database_id [TYPE Function]
|
# region resolve_database_id [TYPE Function]
|
||||||
# @PURPOSE: Resolve the target database ID from the environment.
|
# @PURPOSE: Resolve the target database ID from the environment and fetch its backend engine.
|
||||||
# @PRE: database_name or database_id should be known.
|
# @PRE: database_name or database_id should be known.
|
||||||
# @POST: Returns database_id integer or raises ValueError.
|
# @POST: Returns database_id integer or raises ValueError. Also sets self._database_backend.
|
||||||
# @SIDE_EFFECT: Fetches databases list from Superset.
|
# @SIDE_EFFECT: Fetches databases list from Superset; optionally fetches single DB for backend.
|
||||||
def resolve_database_id(self, database_name: Optional[str] = None) -> int:
|
def resolve_database_id(
|
||||||
|
self,
|
||||||
|
database_name: Optional[str] = None,
|
||||||
|
target_database_id: Optional[str] = None,
|
||||||
|
) -> int:
|
||||||
with belief_scope("SupersetSqlLabExecutor.resolve_database_id"):
|
with belief_scope("SupersetSqlLabExecutor.resolve_database_id"):
|
||||||
client = self._get_client()
|
client = self._get_client()
|
||||||
|
|
||||||
|
# If a specific target_database_id is provided, use it directly
|
||||||
|
if target_database_id:
|
||||||
|
try:
|
||||||
|
db_id = int(target_database_id)
|
||||||
|
# Fetch full DB info to get backend
|
||||||
|
db_info = client.get_database(db_id)
|
||||||
|
result = db_info.get("result", db_info)
|
||||||
|
self._database_id = result.get("id", db_id)
|
||||||
|
self._database_backend = result.get("backend") or result.get("engine")
|
||||||
|
logger.reason("Resolved database ID by target_database_id", {
|
||||||
|
"target_database_id": target_database_id,
|
||||||
|
"database_id": self._database_id,
|
||||||
|
"backend": self._database_backend,
|
||||||
|
})
|
||||||
|
return self._database_id
|
||||||
|
except Exception as e:
|
||||||
|
logger.explore("Failed to resolve by target_database_id, falling back", {
|
||||||
|
"target_database_id": target_database_id,
|
||||||
|
"error": str(e),
|
||||||
|
})
|
||||||
|
# Fall through to default resolution
|
||||||
|
|
||||||
|
# Fetch full database list with backend info
|
||||||
_, databases = client.get_databases(
|
_, databases = client.get_databases(
|
||||||
query={"columns": ["id", "database_name"]}
|
query={"columns": ["id", "database_name", "backend"]}
|
||||||
)
|
)
|
||||||
if not databases:
|
if not databases:
|
||||||
raise ValueError("No databases found in Superset environment")
|
raise ValueError("No databases found in Superset environment")
|
||||||
@@ -70,9 +99,11 @@ class SupersetSqlLabExecutor:
|
|||||||
for db in databases:
|
for db in databases:
|
||||||
if db.get("database_name", "").lower() == database_name.lower():
|
if db.get("database_name", "").lower() == database_name.lower():
|
||||||
self._database_id = db["id"]
|
self._database_id = db["id"]
|
||||||
|
self._database_backend = db.get("backend")
|
||||||
logger.reason("Resolved database ID by name", {
|
logger.reason("Resolved database ID by name", {
|
||||||
"database_name": database_name,
|
"database_name": database_name,
|
||||||
"database_id": self._database_id,
|
"database_id": self._database_id,
|
||||||
|
"backend": self._database_backend,
|
||||||
})
|
})
|
||||||
return self._database_id
|
return self._database_id
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -81,13 +112,22 @@ class SupersetSqlLabExecutor:
|
|||||||
|
|
||||||
# Default: use first database
|
# Default: use first database
|
||||||
self._database_id = databases[0]["id"]
|
self._database_id = databases[0]["id"]
|
||||||
|
self._database_backend = databases[0].get("backend")
|
||||||
logger.reason("Using default database", {
|
logger.reason("Using default database", {
|
||||||
"database_name": databases[0].get("database_name"),
|
"database_name": databases[0].get("database_name"),
|
||||||
"database_id": self._database_id,
|
"database_id": self._database_id,
|
||||||
|
"backend": self._database_backend,
|
||||||
})
|
})
|
||||||
return self._database_id
|
return self._database_id
|
||||||
# endregion resolve_database_id
|
# endregion resolve_database_id
|
||||||
|
|
||||||
|
# region get_database_backend [TYPE Function]
|
||||||
|
# @PURPOSE: Return the cached database backend/engine string.
|
||||||
|
# @POST: Returns backend string or None if not yet resolved.
|
||||||
|
def get_database_backend(self) -> Optional[str]:
|
||||||
|
return self._database_backend
|
||||||
|
# endregion get_database_backend
|
||||||
|
|
||||||
# region execute_sql [TYPE Function]
|
# region execute_sql [TYPE Function]
|
||||||
# @PURPOSE: Submit SQL to Superset SQL Lab and return execution tracking info.
|
# @PURPOSE: Submit SQL to Superset SQL Lab and return execution tracking info.
|
||||||
# @PRE: sql is valid SQL string. database_id is a valid Superset DB ID.
|
# @PRE: sql is valid SQL string. database_id is a valid Superset DB ID.
|
||||||
@@ -117,27 +157,75 @@ class SupersetSqlLabExecutor:
|
|||||||
"runAsync": run_async,
|
"runAsync": run_async,
|
||||||
"schema": None,
|
"schema": None,
|
||||||
"tab": "translation-insert",
|
"tab": "translation-insert",
|
||||||
|
"client_id": f"trl-{uuid.uuid4().hex[:7]}", # max 11 chars for Superset varchar(11)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Try multiple SQL Lab endpoints — some Superset versions use /sqllab/execute/,
|
||||||
|
# others use /sql_lab/execute/ (with underscore)
|
||||||
|
candidate_endpoints = ["/api/v1/sqllab/execute/", "/api/v1/sql_lab/execute/"]
|
||||||
|
response = None
|
||||||
|
last_error = None
|
||||||
|
for endpoint in candidate_endpoints:
|
||||||
try:
|
try:
|
||||||
response = client.network.request(
|
response = client.network.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
endpoint="/api/v1/sqllab/execute/",
|
endpoint=endpoint,
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
headers={"Content-Type": "application/json"},
|
headers={"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
if isinstance(response, dict) and response:
|
||||||
logger.explore("SQL Lab execute failed", {"error": str(e)})
|
logger.reason(f"SQL Lab endpoint succeeded", extra={
|
||||||
raise ValueError(f"Superset SQL Lab execute failed: {e}")
|
"endpoint": endpoint,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
except Exception as ep_err:
|
||||||
|
last_error = ep_err
|
||||||
|
logger.explore(f"SQL Lab endpoint failed, trying next", extra={
|
||||||
|
"error": str(ep_err),
|
||||||
|
"endpoint": endpoint,
|
||||||
|
})
|
||||||
|
response = None
|
||||||
|
|
||||||
# Parse response
|
if response is None:
|
||||||
|
error_msg = f"All SQL Lab endpoints failed. Last error: {last_error}"
|
||||||
|
logger.explore("SQL Lab execute failed", extra={"error": error_msg})
|
||||||
|
raise ValueError(f"Superset SQL Lab execute failed: {error_msg}")
|
||||||
|
|
||||||
|
# Parse response — try multiple known Superset response formats
|
||||||
result = response if isinstance(response, dict) else {}
|
result = response if isinstance(response, dict) else {}
|
||||||
query_id = result.get("query_id") or result.get("id")
|
|
||||||
|
# Superset may return query_id at top level, nested in "result", as "id", or in "query" block
|
||||||
|
query_id = (
|
||||||
|
result.get("query_id")
|
||||||
|
or result.get("id")
|
||||||
|
or (result.get("result") or {}).get("query_id")
|
||||||
|
or (result.get("result") or {}).get("id")
|
||||||
|
or (result.get("query") or {}).get("query_id")
|
||||||
|
or (result.get("query") or {}).get("id")
|
||||||
|
)
|
||||||
status = result.get("status", "unknown")
|
status = result.get("status", "unknown")
|
||||||
|
|
||||||
|
# Log full response for debugging if query_id is missing
|
||||||
|
if not query_id:
|
||||||
|
logger.explore("No query_id from SQL Lab execute", extra={
|
||||||
|
"database_id": db_id,
|
||||||
|
"status": status,
|
||||||
|
"raw_response_keys": list(result.keys()),
|
||||||
|
"raw_response_preview": json.dumps(result)[:2000],
|
||||||
|
})
|
||||||
|
logger.reason("SQL Lab execute response (no query_id)", extra={
|
||||||
|
"database_id": db_id,
|
||||||
|
"status": status,
|
||||||
|
"response_keys": list(result.keys()),
|
||||||
|
"has_result": "result" in result,
|
||||||
|
"has_query": "query" in result,
|
||||||
|
"raw_preview": json.dumps(result)[:2000],
|
||||||
|
})
|
||||||
|
|
||||||
logger.reason("SQL Lab execute response", {
|
logger.reason("SQL Lab execute response", {
|
||||||
"query_id": query_id,
|
"query_id": query_id,
|
||||||
"status": status,
|
"status": status,
|
||||||
|
"response_keys": list(result.keys()),
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -174,7 +262,9 @@ class SupersetSqlLabExecutor:
|
|||||||
method="GET",
|
method="GET",
|
||||||
endpoint=f"/api/v1/query/{query_id}",
|
endpoint=f"/api/v1/query/{query_id}",
|
||||||
)
|
)
|
||||||
result = response if isinstance(response, dict) else {}
|
raw = response if isinstance(response, dict) else {}
|
||||||
|
# Superset wraps query data in a nested "result" field
|
||||||
|
result = raw.get("result", raw)
|
||||||
status = result.get("status", "unknown")
|
status = result.get("status", "unknown")
|
||||||
state = result.get("state", result.get("status", ""))
|
state = result.get("state", result.get("status", ""))
|
||||||
|
|
||||||
@@ -255,9 +345,27 @@ class SupersetSqlLabExecutor:
|
|||||||
exec_result = self.execute_sql(
|
exec_result = self.execute_sql(
|
||||||
sql=sql,
|
sql=sql,
|
||||||
database_id=database_id,
|
database_id=database_id,
|
||||||
run_async=True,
|
run_async=False, # Sync mode — Superset returns result directly
|
||||||
)
|
)
|
||||||
|
|
||||||
|
status = exec_result.get("status", "unknown")
|
||||||
|
|
||||||
|
# Sync mode: response already has the final status and data
|
||||||
|
if status == "success":
|
||||||
|
logger.reason("SQL execution completed (sync)", {
|
||||||
|
"query_id": exec_result.get("query_id"),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"query_id": exec_result.get("query_id"),
|
||||||
|
"status": "success",
|
||||||
|
"state": "success",
|
||||||
|
"rows_affected": exec_result.get("raw_response", {}).get("query", {}).get("rows"),
|
||||||
|
"error_message": None,
|
||||||
|
"results": exec_result.get("raw_response", {}).get("data"),
|
||||||
|
"completed_on": exec_result.get("raw_response", {}).get("query", {}).get("endDttm"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# For async mode (fallback): poll for completion
|
||||||
query_id = exec_result.get("query_id")
|
query_id = exec_result.get("query_id")
|
||||||
if not query_id:
|
if not query_id:
|
||||||
logger.explore("No query_id from SQL Lab execute", {
|
logger.explore("No query_id from SQL Lab execute", {
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class TranslateJobCreate(BaseModel):
|
|||||||
target_table: Optional[str] = Field(None, description="Target table name")
|
target_table: Optional[str] = Field(None, description="Target table name")
|
||||||
source_key_cols: Optional[List[str]] = Field(default_factory=list, description="Source key column names")
|
source_key_cols: Optional[List[str]] = Field(default_factory=list, description="Source key column names")
|
||||||
target_key_cols: Optional[List[str]] = Field(default_factory=list, description="Target key column names")
|
target_key_cols: Optional[List[str]] = Field(default_factory=list, description="Target key column names")
|
||||||
translation_column: Optional[str] = Field(None, description="Column to translate")
|
translation_column: Optional[str] = Field(None, description="Source column to translate")
|
||||||
|
target_column: Optional[str] = Field(None, description="Target column for translated output (defaults to translation_column)")
|
||||||
context_columns: Optional[List[str]] = Field(default_factory=list, description="Context column names")
|
context_columns: Optional[List[str]] = Field(default_factory=list, description="Context column names")
|
||||||
target_language: Optional[str] = Field(None, description="Target language code")
|
target_language: Optional[str] = Field(None, description="Target language code")
|
||||||
provider_id: Optional[str] = Field(None, description="LLM provider ID")
|
provider_id: Optional[str] = Field(None, description="LLM provider ID")
|
||||||
@@ -31,6 +32,7 @@ class TranslateJobCreate(BaseModel):
|
|||||||
upsert_strategy: str = Field("MERGE", description="UPSERT strategy: MERGE, INSERT, UPDATE")
|
upsert_strategy: str = Field("MERGE", description="UPSERT strategy: MERGE, INSERT, UPDATE")
|
||||||
dictionary_ids: Optional[List[str]] = Field(default_factory=list, description="Associated terminology dictionary IDs")
|
dictionary_ids: Optional[List[str]] = Field(default_factory=list, description="Associated terminology dictionary IDs")
|
||||||
environment_id: Optional[str] = Field(None, description="Superset environment ID")
|
environment_id: Optional[str] = Field(None, description="Superset environment ID")
|
||||||
|
target_database_id: Optional[str] = Field(None, description="Superset database ID for SQL Lab insert target")
|
||||||
# #endregion TranslateJobCreate
|
# #endregion TranslateJobCreate
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@ class TranslateJobUpdate(BaseModel):
|
|||||||
source_key_cols: Optional[List[str]] = None
|
source_key_cols: Optional[List[str]] = None
|
||||||
target_key_cols: Optional[List[str]] = None
|
target_key_cols: Optional[List[str]] = None
|
||||||
translation_column: Optional[str] = None
|
translation_column: Optional[str] = None
|
||||||
|
target_column: Optional[str] = None
|
||||||
context_columns: Optional[List[str]] = None
|
context_columns: Optional[List[str]] = None
|
||||||
target_language: Optional[str] = None
|
target_language: Optional[str] = None
|
||||||
provider_id: Optional[str] = None
|
provider_id: Optional[str] = None
|
||||||
@@ -57,6 +60,7 @@ class TranslateJobUpdate(BaseModel):
|
|||||||
status: Optional[str] = None
|
status: Optional[str] = None
|
||||||
dictionary_ids: Optional[List[str]] = None
|
dictionary_ids: Optional[List[str]] = None
|
||||||
environment_id: Optional[str] = None
|
environment_id: Optional[str] = None
|
||||||
|
target_database_id: Optional[str] = None
|
||||||
# #endregion TranslateJobUpdate
|
# #endregion TranslateJobUpdate
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +80,7 @@ class TranslateJobResponse(BaseModel):
|
|||||||
source_key_cols: Optional[List[str]] = None
|
source_key_cols: Optional[List[str]] = None
|
||||||
target_key_cols: Optional[List[str]] = None
|
target_key_cols: Optional[List[str]] = None
|
||||||
translation_column: Optional[str] = None
|
translation_column: Optional[str] = None
|
||||||
|
target_column: Optional[str] = None
|
||||||
context_columns: Optional[List[str]] = None
|
context_columns: Optional[List[str]] = None
|
||||||
target_language: Optional[str] = None
|
target_language: Optional[str] = None
|
||||||
provider_id: Optional[str] = None
|
provider_id: Optional[str] = None
|
||||||
@@ -87,6 +92,7 @@ class TranslateJobResponse(BaseModel):
|
|||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
dictionary_ids: Optional[List[str]] = None
|
dictionary_ids: Optional[List[str]] = None
|
||||||
environment_id: Optional[str] = None
|
environment_id: Optional[str] = None
|
||||||
|
target_database_id: Optional[str] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region test_encryption_manager [TYPE Module]
|
# region test_encryption_manager [TYPE Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: encryption, security, fernet, api-keys, tests
|
# @SEMANTICS: encryption, security, fernet, api-keys, tests
|
||||||
# @PURPOSE: Unit tests for EncryptionManager encrypt/decrypt functionality.
|
# @PURPOSE: Unit tests for EncryptionManager encrypt/decrypt functionality.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from src.services.health_service import HealthService
|
|||||||
from src.models.llm import ValidationRecord
|
from src.models.llm import ValidationRecord
|
||||||
|
|
||||||
# region test_health_service [TYPE Module]
|
# region test_health_service [TYPE Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Unit tests for HealthService aggregation logic.
|
# @PURPOSE: Unit tests for HealthService aggregation logic.
|
||||||
# @RELATION: VERIFIES ->[src.services.health_service.HealthService]
|
# @RELATION: VERIFIES ->[src.services.health_service.HealthService]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region test_llm_plugin_persistence [TYPE Module]
|
# region test_llm_plugin_persistence [TYPE Module]
|
||||||
# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]
|
# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Regression test for ValidationRecord persistence fields populated from task context.
|
# @PURPOSE: Regression test for ValidationRecord persistence fields populated from task context.
|
||||||
|
|
||||||
import types
|
import types
|
||||||
@@ -11,7 +10,6 @@ from src.plugins.llm_analysis import plugin as plugin_module
|
|||||||
|
|
||||||
# region _DummyLogger [TYPE Class]
|
# region _DummyLogger [TYPE Class]
|
||||||
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
|
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Minimal logger shim for TaskContext-like objects used in tests.
|
# @PURPOSE: Minimal logger shim for TaskContext-like objects used in tests.
|
||||||
# @INVARIANT: Logging methods are no-ops and must not mutate test state.
|
# @INVARIANT: Logging methods are no-ops and must not mutate test state.
|
||||||
class _DummyLogger:
|
class _DummyLogger:
|
||||||
@@ -36,7 +34,6 @@ class _DummyLogger:
|
|||||||
|
|
||||||
# region _FakeDBSession [TYPE Class]
|
# region _FakeDBSession [TYPE Class]
|
||||||
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
|
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin.
|
# @PURPOSE: Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin.
|
||||||
# @INVARIANT: add/commit/close provide only persistence signals asserted by this test.
|
# @INVARIANT: add/commit/close provide only persistence signals asserted by this test.
|
||||||
class _FakeDBSession:
|
class _FakeDBSession:
|
||||||
@@ -61,7 +58,6 @@ class _FakeDBSession:
|
|||||||
# region test_dashboard_validation_plugin_persists_task_and_environment_ids [TYPE Function]
|
# region test_dashboard_validation_plugin_persists_task_and_environment_ids [TYPE Function]
|
||||||
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
|
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
|
||||||
# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]
|
# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Ensure db ValidationRecord includes context.task_id and params.environment_id.
|
# @PURPOSE: Ensure db ValidationRecord includes context.task_id and params.environment_id.
|
||||||
# @INVARIANT: Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals.
|
# @INVARIANT: Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals.
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -82,7 +78,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
|
|||||||
|
|
||||||
# region _FakeProviderService [TYPE Class]
|
# region _FakeProviderService [TYPE Class]
|
||||||
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: LLM provider service stub returning deterministic provider and decrypted API key for plugin tests.
|
# @PURPOSE: LLM provider service stub returning deterministic provider and decrypted API key for plugin tests.
|
||||||
# @INVARIANT: Returns same provider and key regardless of provider_id argument; no lookup logic.
|
# @INVARIANT: Returns same provider and key regardless of provider_id argument; no lookup logic.
|
||||||
class _FakeProviderService:
|
class _FakeProviderService:
|
||||||
@@ -99,7 +94,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
|
|||||||
|
|
||||||
# region _FakeScreenshotService [TYPE Class]
|
# region _FakeScreenshotService [TYPE Class]
|
||||||
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Screenshot service stub that accepts capture_dashboard calls without side effects.
|
# @PURPOSE: Screenshot service stub that accepts capture_dashboard calls without side effects.
|
||||||
# @INVARIANT: capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values.
|
# @INVARIANT: capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values.
|
||||||
class _FakeScreenshotService:
|
class _FakeScreenshotService:
|
||||||
@@ -113,7 +107,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
|
|||||||
|
|
||||||
# region _FakeLLMClient [TYPE Class]
|
# region _FakeLLMClient [TYPE Class]
|
||||||
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Deterministic LLM client double returning canonical analysis payload for persistence-path assertions.
|
# @PURPOSE: Deterministic LLM client double returning canonical analysis payload for persistence-path assertions.
|
||||||
# @INVARIANT: analyze_dashboard is side-effect free and returns schema-compatible PASS result.
|
# @INVARIANT: analyze_dashboard is side-effect free and returns schema-compatible PASS result.
|
||||||
class _FakeLLMClient:
|
class _FakeLLMClient:
|
||||||
@@ -137,7 +130,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
|
|||||||
|
|
||||||
# region _FakeNotificationService [TYPE Class]
|
# region _FakeNotificationService [TYPE Class]
|
||||||
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Notification service stub that accepts plugin dispatch_report payload without introducing side effects.
|
# @PURPOSE: Notification service stub that accepts plugin dispatch_report payload without introducing side effects.
|
||||||
# @INVARIANT: dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema.
|
# @INVARIANT: dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema.
|
||||||
class _FakeNotificationService:
|
class _FakeNotificationService:
|
||||||
@@ -151,7 +143,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
|
|||||||
|
|
||||||
# region _FakeConfigManager [TYPE Class]
|
# region _FakeConfigManager [TYPE Class]
|
||||||
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Config manager stub providing storage root path and minimal settings for plugin execution path.
|
# @PURPOSE: Config manager stub providing storage root path and minimal settings for plugin execution path.
|
||||||
# @INVARIANT: Only storage.root_path and llm fields are safe to access; all other settings fields are absent.
|
# @INVARIANT: Only storage.root_path and llm fields are safe to access; all other settings fields are absent.
|
||||||
class _FakeConfigManager:
|
class _FakeConfigManager:
|
||||||
@@ -170,7 +161,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
|
|||||||
|
|
||||||
# region _FakeSupersetClient [TYPE Class]
|
# region _FakeSupersetClient [TYPE Class]
|
||||||
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Superset client stub exposing network.request as a lambda that returns empty result list.
|
# @PURPOSE: Superset client stub exposing network.request as a lambda that returns empty result list.
|
||||||
# @INVARIANT: network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency.
|
# @INVARIANT: network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency.
|
||||||
class _FakeSupersetClient:
|
class _FakeSupersetClient:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region test_llm_prompt_templates [TYPE Module]
|
# region test_llm_prompt_templates [TYPE Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, llm, prompts, templates, settings
|
# @SEMANTICS: tests, llm, prompts, templates, settings
|
||||||
# @PURPOSE: Validate normalization and rendering behavior for configurable LLM prompt templates.
|
# @PURPOSE: Validate normalization and rendering behavior for configurable LLM prompt templates.
|
||||||
# @LAYER: Domain Tests
|
# @LAYER: Domain Tests
|
||||||
@@ -19,7 +18,6 @@ from src.services.llm_prompt_templates import (
|
|||||||
|
|
||||||
# region test_normalize_llm_settings_adds_default_prompts [TYPE Function]
|
# region test_normalize_llm_settings_adds_default_prompts [TYPE Function]
|
||||||
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Ensure legacy/partial llm settings are expanded with all prompt defaults.
|
# @PURPOSE: Ensure legacy/partial llm settings are expanded with all prompt defaults.
|
||||||
# @PRE: Input llm settings do not contain complete prompts object.
|
# @PRE: Input llm settings do not contain complete prompts object.
|
||||||
# @POST: Returned structure includes required prompt templates with fallback defaults.
|
# @POST: Returned structure includes required prompt templates with fallback defaults.
|
||||||
@@ -43,7 +41,6 @@ def test_normalize_llm_settings_adds_default_prompts():
|
|||||||
|
|
||||||
# region test_normalize_llm_settings_keeps_custom_prompt_values [TYPE Function]
|
# region test_normalize_llm_settings_keeps_custom_prompt_values [TYPE Function]
|
||||||
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Ensure user-customized prompt values are preserved during normalization.
|
# @PURPOSE: Ensure user-customized prompt values are preserved during normalization.
|
||||||
# @PRE: Input llm settings contain custom prompt override.
|
# @PRE: Input llm settings contain custom prompt override.
|
||||||
# @POST: Custom prompt value remains unchanged in normalized output.
|
# @POST: Custom prompt value remains unchanged in normalized output.
|
||||||
@@ -59,7 +56,6 @@ def test_normalize_llm_settings_keeps_custom_prompt_values():
|
|||||||
|
|
||||||
# region test_render_prompt_replaces_known_placeholders [TYPE Function]
|
# region test_render_prompt_replaces_known_placeholders [TYPE Function]
|
||||||
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Ensure template placeholders are deterministically replaced.
|
# @PURPOSE: Ensure template placeholders are deterministically replaced.
|
||||||
# @PRE: Template contains placeholders matching provided variables.
|
# @PRE: Template contains placeholders matching provided variables.
|
||||||
# @POST: Rendered prompt string contains substituted values.
|
# @POST: Rendered prompt string contains substituted values.
|
||||||
@@ -77,7 +73,6 @@ def test_render_prompt_replaces_known_placeholders():
|
|||||||
|
|
||||||
# region test_is_multimodal_model_detects_known_vision_models [TYPE Function]
|
# region test_is_multimodal_model_detects_known_vision_models [TYPE Function]
|
||||||
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Ensure multimodal model detection recognizes common vision-capable model names.
|
# @PURPOSE: Ensure multimodal model detection recognizes common vision-capable model names.
|
||||||
def test_is_multimodal_model_detects_known_vision_models():
|
def test_is_multimodal_model_detects_known_vision_models():
|
||||||
assert is_multimodal_model("gpt-4o") is True
|
assert is_multimodal_model("gpt-4o") is True
|
||||||
@@ -91,7 +86,6 @@ def test_is_multimodal_model_detects_known_vision_models():
|
|||||||
|
|
||||||
# region test_resolve_bound_provider_id_prefers_binding_then_default [TYPE Function]
|
# region test_resolve_bound_provider_id_prefers_binding_then_default [TYPE Function]
|
||||||
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Verify provider binding resolution priority.
|
# @PURPOSE: Verify provider binding resolution priority.
|
||||||
def test_resolve_bound_provider_id_prefers_binding_then_default():
|
def test_resolve_bound_provider_id_prefers_binding_then_default():
|
||||||
settings = {
|
settings = {
|
||||||
@@ -107,7 +101,6 @@ def test_resolve_bound_provider_id_prefers_binding_then_default():
|
|||||||
|
|
||||||
# region test_normalize_llm_settings_keeps_assistant_planner_settings [TYPE Function]
|
# region test_normalize_llm_settings_keeps_assistant_planner_settings [TYPE Function]
|
||||||
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
# @RELATION: BINDS_TO -> test_llm_prompt_templates
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Ensure assistant planner provider/model fields are preserved and normalized.
|
# @PURPOSE: Ensure assistant planner provider/model fields are preserved and normalized.
|
||||||
def test_normalize_llm_settings_keeps_assistant_planner_settings():
|
def test_normalize_llm_settings_keeps_assistant_planner_settings():
|
||||||
normalized = normalize_llm_settings(
|
normalized = normalize_llm_settings(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region test_llm_provider [TYPE Module]
|
# region test_llm_provider [TYPE Module]
|
||||||
# @RELATION: VERIFIES -> [src.services.llm_provider:Module]
|
# @RELATION: VERIFIES -> [src.services.llm_provider:Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, llm-provider, encryption, contract
|
# @SEMANTICS: tests, llm-provider, encryption, contract
|
||||||
# @PURPOSE: Contract testing for LLMProviderService and EncryptionManager
|
# @PURPOSE: Contract testing for LLMProviderService and EncryptionManager
|
||||||
# endregion test_llm_provider
|
# endregion test_llm_provider
|
||||||
@@ -68,7 +67,6 @@ def test_decrypt_invalid_data():
|
|||||||
|
|
||||||
# region mock_db [TYPE Fixture]
|
# region mock_db [TYPE Fixture]
|
||||||
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
|
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests.
|
# @PURPOSE: MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests.
|
||||||
# @INVARIANT: Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced.
|
# @INVARIANT: Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced.
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -82,7 +80,6 @@ def mock_db():
|
|||||||
|
|
||||||
# region service [TYPE Fixture]
|
# region service [TYPE Fixture]
|
||||||
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
|
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: LLMProviderService fixture wired to mock_db for provider CRUD tests.
|
# @PURPOSE: LLMProviderService fixture wired to mock_db for provider CRUD tests.
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def service(mock_db):
|
def service(mock_db):
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region test_rbac_permission_catalog [TYPE Module]
|
# region test_rbac_permission_catalog [TYPE Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, rbac, permissions, catalog, discovery, sync
|
# @SEMANTICS: tests, rbac, permissions, catalog, discovery, sync
|
||||||
# @PURPOSE: Verifies RBAC permission catalog discovery and idempotent synchronization behavior.
|
# @PURPOSE: Verifies RBAC permission catalog discovery and idempotent synchronization behavior.
|
||||||
# @LAYER: Service Tests
|
# @LAYER: Service Tests
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region TestResourceService [TYPE Module]
|
# region TestResourceService [TYPE Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: resource-service, tests, dashboards, datasets, activity
|
# @SEMANTICS: resource-service, tests, dashboards, datasets, activity
|
||||||
# @PURPOSE: Unit tests for ResourceService
|
# @PURPOSE: Unit tests for ResourceService
|
||||||
# @LAYER: Service
|
# @LAYER: Service
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ from ..core.logger import belief_scope
|
|||||||
# @RELATION DEPENDS_ON -> [Role]
|
# @RELATION DEPENDS_ON -> [Role]
|
||||||
class AuthService:
|
class AuthService:
|
||||||
# region AuthService_init [TYPE Function]
|
# region AuthService_init [TYPE Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Initializes the authentication service with repository access over an active DB session.
|
# @PURPOSE: Initializes the authentication service with repository access over an active DB session.
|
||||||
# @PRE: db is a valid SQLAlchemy Session instance bound to the auth persistence context.
|
# @PRE: db is a valid SQLAlchemy Session instance bound to the auth persistence context.
|
||||||
# @POST: self.repo is initialized and ready for auth user/role CRUD operations.
|
# @POST: self.repo is initialized and ready for auth user/role CRUD operations.
|
||||||
@@ -45,7 +44,6 @@ class AuthService:
|
|||||||
# endregion AuthService_init
|
# endregion AuthService_init
|
||||||
|
|
||||||
# region AuthService.authenticate_user [TYPE Function]
|
# region AuthService.authenticate_user [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Validates credentials and account state for local username/password authentication.
|
# @PURPOSE: Validates credentials and account state for local username/password authentication.
|
||||||
# @PRE: username and password are non-empty credential inputs.
|
# @PRE: username and password are non-empty credential inputs.
|
||||||
# @POST: Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.
|
# @POST: Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.
|
||||||
@@ -76,7 +74,6 @@ class AuthService:
|
|||||||
# endregion AuthService.authenticate_user
|
# endregion AuthService.authenticate_user
|
||||||
|
|
||||||
# region AuthService.create_session [TYPE Function]
|
# region AuthService.create_session [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Issues an access token payload for an already authenticated user.
|
# @PURPOSE: Issues an access token payload for an already authenticated user.
|
||||||
# @PRE: user is a valid User entity containing username and iterable roles with role.name values.
|
# @PRE: user is a valid User entity containing username and iterable roles with role.name values.
|
||||||
# @POST: Returns session dict with non-empty access_token and token_type='bearer'.
|
# @POST: Returns session dict with non-empty access_token and token_type='bearer'.
|
||||||
@@ -98,7 +95,6 @@ class AuthService:
|
|||||||
# endregion AuthService.create_session
|
# endregion AuthService.create_session
|
||||||
|
|
||||||
# region AuthService.provision_adfs_user [TYPE Function]
|
# region AuthService.provision_adfs_user [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.
|
# @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.
|
||||||
# @PRE: user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.
|
# @PRE: user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.
|
||||||
# @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.
|
# @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region TestAuditService [TYPE Module]
|
# region TestAuditService [TYPE Module]
|
||||||
# @RELATION: [DEPENDS_ON] ->[AuditService]
|
# @RELATION: [DEPENDS_ON] ->[AuditService]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, clean-release, audit, logging
|
# @SEMANTICS: tests, clean-release, audit, logging
|
||||||
# @PURPOSE: Validate audit hooks emit expected log patterns for clean release lifecycle.
|
# @PURPOSE: Validate audit hooks emit expected log patterns for clean release lifecycle.
|
||||||
# @LAYER: Infra
|
# @LAYER: Infra
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region TestComplianceOrchestrator [TYPE Module]
|
# region TestComplianceOrchestrator [TYPE Module]
|
||||||
# @RELATION: [DEPENDS_ON] ->[ComplianceOrchestrator]
|
# @RELATION: [DEPENDS_ON] ->[ComplianceOrchestrator]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, clean-release, orchestrator, stage-state-machine
|
# @SEMANTICS: tests, clean-release, orchestrator, stage-state-machine
|
||||||
# @PURPOSE: Validate compliance orchestrator stage transitions and final status derivation.
|
# @PURPOSE: Validate compliance orchestrator stage transitions and final status derivation.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region TestManifestBuilder [TYPE Module]
|
# region TestManifestBuilder [TYPE Module]
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @SEMANTICS: tests, clean-release, manifest, deterministic
|
# @SEMANTICS: tests, clean-release, manifest, deterministic
|
||||||
# @PURPOSE: Validate deterministic manifest generation behavior for US1.
|
# @PURPOSE: Validate deterministic manifest generation behavior for US1.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region TestPreparationService [TYPE Module]
|
# region TestPreparationService [TYPE Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, clean-release, preparation, flow
|
# @SEMANTICS: tests, clean-release, preparation, flow
|
||||||
# @PURPOSE: Validate release candidate preparation flow, including policy evaluation and manifest persisting.
|
# @PURPOSE: Validate release candidate preparation flow, including policy evaluation and manifest persisting.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region TestReportBuilder [TYPE Module]
|
# region TestReportBuilder [TYPE Module]
|
||||||
# @RELATION: [DEPENDS_ON] ->[ReportBuilder]
|
# @RELATION: [DEPENDS_ON] ->[ReportBuilder]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, clean-release, report-builder, counters
|
# @SEMANTICS: tests, clean-release, report-builder, counters
|
||||||
# @PURPOSE: Validate compliance report builder counter integrity and blocked-run constraints.
|
# @PURPOSE: Validate compliance report builder counter integrity and blocked-run constraints.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region TestSourceIsolation [TYPE Module]
|
# region TestSourceIsolation [TYPE Module]
|
||||||
# @RELATION: [DEPENDS_ON] ->[SourceIsolation]
|
# @RELATION: [DEPENDS_ON] ->[SourceIsolation]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, clean-release, source-isolation, internal-only
|
# @SEMANTICS: tests, clean-release, source-isolation, internal-only
|
||||||
# @PURPOSE: Verify internal source registry validation behavior.
|
# @PURPOSE: Verify internal source registry validation behavior.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# region TestStages [TYPE Module]
|
# region TestStages [TYPE Module]
|
||||||
# @RELATION: [DEPENDS_ON] ->[ComplianceStages]
|
# @RELATION: [DEPENDS_ON] ->[ComplianceStages]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, clean-release, compliance, stages
|
# @SEMANTICS: tests, clean-release, compliance, stages
|
||||||
# @PURPOSE: Validate final status derivation logic from stage results.
|
# @PURPOSE: Validate final status derivation logic from stage results.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ class ManifestRepository:
|
|||||||
"""Repository for distribution manifest persistence."""
|
"""Repository for distribution manifest persistence."""
|
||||||
|
|
||||||
# region ManifestRepository.__init__ [TYPE Function]
|
# region ManifestRepository.__init__ [TYPE Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Initialize repository with an active SQLAlchemy session.
|
# @PURPOSE: Initialize repository with an active SQLAlchemy session.
|
||||||
# @PRE: db is a valid SQLAlchemy Session instance.
|
# @PRE: db is a valid SQLAlchemy Session instance.
|
||||||
# @POST: Repository is ready for database operations.
|
# @POST: Repository is ready for database operations.
|
||||||
@@ -28,7 +27,6 @@ class ManifestRepository:
|
|||||||
# endregion ManifestRepository.__init__
|
# endregion ManifestRepository.__init__
|
||||||
|
|
||||||
# region ManifestRepository.save [TYPE Function]
|
# region ManifestRepository.save [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Persist a DistributionManifest to the database.
|
# @PURPOSE: Persist a DistributionManifest to the database.
|
||||||
# @PRE: manifest is a valid DistributionManifest instance with required fields populated.
|
# @PRE: manifest is a valid DistributionManifest instance with required fields populated.
|
||||||
# @POST: Manifest is committed to database and refreshed with generated ID.
|
# @POST: Manifest is committed to database and refreshed with generated ID.
|
||||||
@@ -43,7 +41,6 @@ class ManifestRepository:
|
|||||||
# endregion ManifestRepository.save
|
# endregion ManifestRepository.save
|
||||||
|
|
||||||
# region ManifestRepository.get_by_id [TYPE Function]
|
# region ManifestRepository.get_by_id [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Retrieve a single DistributionManifest by its primary key.
|
# @PURPOSE: Retrieve a single DistributionManifest by its primary key.
|
||||||
# @PRE: manifest_id is a valid string identifier.
|
# @PRE: manifest_id is a valid string identifier.
|
||||||
# @POST: Returns DistributionManifest if found, None otherwise.
|
# @POST: Returns DistributionManifest if found, None otherwise.
|
||||||
@@ -56,7 +53,6 @@ class ManifestRepository:
|
|||||||
# endregion ManifestRepository.get_by_id
|
# endregion ManifestRepository.get_by_id
|
||||||
|
|
||||||
# region ManifestRepository.get_latest_for_candidate [TYPE Function]
|
# region ManifestRepository.get_latest_for_candidate [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Retrieve the most recent manifest version for a given candidate.
|
# @PURPOSE: Retrieve the most recent manifest version for a given candidate.
|
||||||
# @PRE: candidate_id is a valid string identifier.
|
# @PRE: candidate_id is a valid string identifier.
|
||||||
# @POST: Returns the highest manifest_version manifest for the candidate, or None.
|
# @POST: Returns the highest manifest_version manifest for the candidate, or None.
|
||||||
@@ -72,7 +68,6 @@ class ManifestRepository:
|
|||||||
# endregion ManifestRepository.get_latest_for_candidate
|
# endregion ManifestRepository.get_latest_for_candidate
|
||||||
|
|
||||||
# region ManifestRepository.list_by_candidate [TYPE Function]
|
# region ManifestRepository.list_by_candidate [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: List all manifests for a specific candidate, ordered by version.
|
# @PURPOSE: List all manifests for a specific candidate, ordered by version.
|
||||||
# @PRE: candidate_id is a valid string identifier.
|
# @PRE: candidate_id is a valid string identifier.
|
||||||
# @POST: Returns a list of DistributionManifest instances (may be empty).
|
# @POST: Returns a list of DistributionManifest instances (may be empty).
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ class ClarificationAnswerCommand:
|
|||||||
# @SIDE_EFFECT: Mutates clarification answers, session flags, and related clarification findings.
|
# @SIDE_EFFECT: Mutates clarification answers, session flags, and related clarification findings.
|
||||||
class ClarificationEngine:
|
class ClarificationEngine:
|
||||||
# region ClarificationEngine_init [TYPE Function]
|
# region ClarificationEngine_init [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Bind repository dependency for clarification persistence operations.
|
# @PURPOSE: Bind repository dependency for clarification persistence operations.
|
||||||
def __init__(self, repository: DatasetReviewSessionRepository) -> None:
|
def __init__(self, repository: DatasetReviewSessionRepository) -> None:
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
@@ -113,7 +112,6 @@ class ClarificationEngine:
|
|||||||
# endregion ClarificationEngine_init
|
# endregion ClarificationEngine_init
|
||||||
|
|
||||||
# region build_question_payload [TYPE Function]
|
# region build_question_payload [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Return the one active highest-priority clarification question payload.
|
# @PURPOSE: Return the one active highest-priority clarification question payload.
|
||||||
# @PRE: Session contains unresolved clarification state or a resumable clarification session.
|
# @PRE: Session contains unresolved clarification state or a resumable clarification session.
|
||||||
# @POST: Returns exactly one active/open question payload or None when no unresolved question remains.
|
# @POST: Returns exactly one active/open question payload or None when no unresolved question remains.
|
||||||
@@ -173,7 +171,6 @@ class ClarificationEngine:
|
|||||||
# endregion build_question_payload
|
# endregion build_question_payload
|
||||||
|
|
||||||
# region record_answer [TYPE Function]
|
# region record_answer [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Persist one clarification answer before any pointer/readiness mutation.
|
# @PURPOSE: Persist one clarification answer before any pointer/readiness mutation.
|
||||||
# @PRE: Target question belongs to the session's active clarification session and is still open.
|
# @PRE: Target question belongs to the session's active clarification session and is still open.
|
||||||
# @POST: Answer row is persisted before current-question pointer advances.
|
# @POST: Answer row is persisted before current-question pointer advances.
|
||||||
@@ -260,7 +257,6 @@ class ClarificationEngine:
|
|||||||
# endregion record_answer
|
# endregion record_answer
|
||||||
|
|
||||||
# region summarize_progress [TYPE Function]
|
# region summarize_progress [TYPE Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Produce a compact progress summary for pause/resume and completion UX.
|
# @PURPOSE: Produce a compact progress summary for pause/resume and completion UX.
|
||||||
def summarize_progress(self, clarification_session: ClarificationSession) -> str:
|
def summarize_progress(self, clarification_session: ClarificationSession) -> str:
|
||||||
resolved = count_resolved_questions(clarification_session)
|
resolved = count_resolved_questions(clarification_session)
|
||||||
@@ -270,7 +266,6 @@ class ClarificationEngine:
|
|||||||
# endregion summarize_progress
|
# endregion summarize_progress
|
||||||
|
|
||||||
# region _get_latest_clarification_session [TYPE Function]
|
# region _get_latest_clarification_session [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Select the latest clarification session for the current dataset review aggregate.
|
# @PURPOSE: Select the latest clarification session for the current dataset review aggregate.
|
||||||
def _get_latest_clarification_session(self, session: DatasetReviewSession) -> Optional[ClarificationSession]:
|
def _get_latest_clarification_session(self, session: DatasetReviewSession) -> Optional[ClarificationSession]:
|
||||||
if not session.clarification_sessions:
|
if not session.clarification_sessions:
|
||||||
@@ -281,7 +276,6 @@ class ClarificationEngine:
|
|||||||
# endregion _get_latest_clarification_session
|
# endregion _get_latest_clarification_session
|
||||||
|
|
||||||
# region _find_question [TYPE Function]
|
# region _find_question [TYPE Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Resolve a clarification question from the active clarification aggregate.
|
# @PURPOSE: Resolve a clarification question from the active clarification aggregate.
|
||||||
def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> Optional[ClarificationQuestion]:
|
def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> Optional[ClarificationQuestion]:
|
||||||
for q in clarification_session.questions:
|
for q in clarification_session.questions:
|
||||||
|
|||||||
@@ -45,14 +45,12 @@ class SessionEventPayload:
|
|||||||
# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]
|
# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]
|
||||||
class SessionEventLogger:
|
class SessionEventLogger:
|
||||||
# region SessionEventLogger_init [TYPE Function]
|
# region SessionEventLogger_init [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Bind a live SQLAlchemy session to the session-event logger.
|
# @PURPOSE: Bind a live SQLAlchemy session to the session-event logger.
|
||||||
def __init__(self, db: Session) -> None:
|
def __init__(self, db: Session) -> None:
|
||||||
self.db = db
|
self.db = db
|
||||||
# endregion SessionEventLogger_init
|
# endregion SessionEventLogger_init
|
||||||
|
|
||||||
# region log_event [TYPE Function]
|
# region log_event [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation.
|
# @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation.
|
||||||
# @RELATION: [DEPENDS_ON] ->[SessionEvent]
|
# @RELATION: [DEPENDS_ON] ->[SessionEvent]
|
||||||
# @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty.
|
# @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty.
|
||||||
@@ -125,7 +123,6 @@ class SessionEventLogger:
|
|||||||
# endregion log_event
|
# endregion log_event
|
||||||
|
|
||||||
# region log_for_session [TYPE Function]
|
# region log_for_session [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root.
|
# @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root.
|
||||||
# @RELATION: [CALLS] ->[SessionEventLogger.log_event]
|
# @RELATION: [CALLS] ->[SessionEventLogger.log_event]
|
||||||
def log_for_session(
|
def log_for_session(
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ logger = cast(Any, logger)
|
|||||||
# @INVARIANT: session ownership is preserved on every mutation and recovery remains explicit when partial.
|
# @INVARIANT: session ownership is preserved on every mutation and recovery remains explicit when partial.
|
||||||
class DatasetReviewOrchestrator:
|
class DatasetReviewOrchestrator:
|
||||||
# region DatasetReviewOrchestrator_init [TYPE Function]
|
# region DatasetReviewOrchestrator_init [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary.
|
# @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary.
|
||||||
# @PRE: repository/config_manager are valid collaborators for the current request scope.
|
# @PRE: repository/config_manager are valid collaborators for the current request scope.
|
||||||
# @POST: Instance holds collaborator references used by start/preview/launch orchestration methods.
|
# @POST: Instance holds collaborator references used by start/preview/launch orchestration methods.
|
||||||
@@ -124,7 +123,6 @@ class DatasetReviewOrchestrator:
|
|||||||
# endregion DatasetReviewOrchestrator_init
|
# endregion DatasetReviewOrchestrator_init
|
||||||
|
|
||||||
# region start_session [TYPE Function]
|
# region start_session [TYPE Function]
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @PURPOSE: Initialize a new session from a Superset link or dataset selection and trigger context recovery.
|
# @PURPOSE: Initialize a new session from a Superset link or dataset selection and trigger context recovery.
|
||||||
# @RELATION: CALLS -> [SupersetContextExtractor.parse_superset_link]
|
# @RELATION: CALLS -> [SupersetContextExtractor.parse_superset_link]
|
||||||
# @RELATION: CALLS -> [TaskManager.create_task]
|
# @RELATION: CALLS -> [TaskManager.create_task]
|
||||||
@@ -279,7 +277,6 @@ class DatasetReviewOrchestrator:
|
|||||||
# endregion start_session
|
# endregion start_session
|
||||||
|
|
||||||
# region prepare_launch_preview [TYPE Function]
|
# region prepare_launch_preview [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation.
|
# @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation.
|
||||||
# @RELATION: CALLS -> [SupersetCompilationAdapter.compile_preview]
|
# @RELATION: CALLS -> [SupersetCompilationAdapter.compile_preview]
|
||||||
# @PRE: all required variables have candidate values or explicitly accepted defaults.
|
# @PRE: all required variables have candidate values or explicitly accepted defaults.
|
||||||
@@ -359,7 +356,6 @@ class DatasetReviewOrchestrator:
|
|||||||
# endregion prepare_launch_preview
|
# endregion prepare_launch_preview
|
||||||
|
|
||||||
# region launch_dataset [TYPE Function]
|
# region launch_dataset [TYPE Function]
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @PURPOSE: Start the approved dataset execution through SQL Lab and persist run context for audit/replay.
|
# @PURPOSE: Start the approved dataset execution through SQL Lab and persist run context for audit/replay.
|
||||||
# @RELATION: CALLS -> [SupersetCompilationAdapter.create_sql_lab_session]
|
# @RELATION: CALLS -> [SupersetCompilationAdapter.create_sql_lab_session]
|
||||||
# @PRE: session is run-ready and compiled preview is current.
|
# @PRE: session is run-ready and compiled preview is current.
|
||||||
@@ -459,7 +455,6 @@ class DatasetReviewOrchestrator:
|
|||||||
# endregion launch_dataset
|
# endregion launch_dataset
|
||||||
|
|
||||||
# region _build_recovery_bootstrap [TYPE Function]
|
# region _build_recovery_bootstrap [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation.
|
# @PURPOSE: Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation.
|
||||||
# @PRE: session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope.
|
# @PRE: session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope.
|
||||||
# @POST: Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.
|
# @POST: Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.
|
||||||
@@ -559,7 +554,6 @@ class DatasetReviewOrchestrator:
|
|||||||
# endregion _build_recovery_bootstrap
|
# endregion _build_recovery_bootstrap
|
||||||
|
|
||||||
# region _enqueue_recovery_task [TYPE Function]
|
# region _enqueue_recovery_task [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Link session start to observable async recovery when task infrastructure is available.
|
# @PURPOSE: Link session start to observable async recovery when task infrastructure is available.
|
||||||
# @PRE: session is already persisted.
|
# @PRE: session is already persisted.
|
||||||
# @POST: returns task identifier when a task could be enqueued, otherwise None.
|
# @POST: returns task identifier when a task could be enqueued, otherwise None.
|
||||||
|
|||||||
@@ -28,14 +28,12 @@ from src.services.dataset_review.repositories.session_repository import (
|
|||||||
|
|
||||||
# region SessionRepositoryTests [TYPE Module]
|
# region SessionRepositoryTests [TYPE Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Unit tests for DatasetReviewSessionRepository.
|
# @PURPOSE: Unit tests for DatasetReviewSessionRepository.
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def db_session():
|
def db_session():
|
||||||
# region db_session [TYPE Function]
|
# region db_session [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows.
|
# @PURPOSE: Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows.
|
||||||
# @RELATION: BINDS_TO -> [SessionRepositoryTests]
|
# @RELATION: BINDS_TO -> [SessionRepositoryTests]
|
||||||
engine = create_engine("sqlite:///:memory:")
|
engine = create_engine("sqlite:///:memory:")
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ class DatasetReviewSessionVersionConflictError(ValueError):
|
|||||||
# @SIDE_EFFECT: mutates and queries the persistence layer through the injected database session.
|
# @SIDE_EFFECT: mutates and queries the persistence layer through the injected database session.
|
||||||
class DatasetReviewSessionRepository:
|
class DatasetReviewSessionRepository:
|
||||||
# region init_repo [TYPE Function]
|
# region init_repo [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Bind one live SQLAlchemy session to the repository instance.
|
# @PURPOSE: Bind one live SQLAlchemy session to the repository instance.
|
||||||
# @PRE: db_session is not None
|
# @PRE: db_session is not None
|
||||||
# @POST: Repository instance initialized with valid session
|
# @POST: Repository instance initialized with valid session
|
||||||
@@ -75,7 +74,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion init_repo
|
# endregion init_repo
|
||||||
|
|
||||||
# region get_owned_session [TYPE Function]
|
# region get_owned_session [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths.
|
# @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths.
|
||||||
# @PRE: session_id and user_id are non-empty identifiers from the authenticated ownership scope.
|
# @PRE: session_id and user_id are non-empty identifiers from the authenticated ownership scope.
|
||||||
# @POST: returns the owned session or raises a deterministic access error.
|
# @POST: returns the owned session or raises a deterministic access error.
|
||||||
@@ -96,7 +94,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion get_owned_session
|
# endregion get_owned_session
|
||||||
|
|
||||||
# region create_sess [TYPE Function]
|
# region create_sess [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Persist an initial dataset review session shell.
|
# @PURPOSE: Persist an initial dataset review session shell.
|
||||||
# @POST: session is committed, refreshed, and returned with persisted identifiers.
|
# @POST: session is committed, refreshed, and returned with persisted identifiers.
|
||||||
def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession:
|
def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession:
|
||||||
@@ -111,7 +108,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion create_sess
|
# endregion create_sess
|
||||||
|
|
||||||
# region require_session_version [TYPE Function]
|
# region require_session_version [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted.
|
# @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted.
|
||||||
# @POST: returns the same session when versions match; otherwise raises deterministic conflict error.
|
# @POST: returns the same session when versions match; otherwise raises deterministic conflict error.
|
||||||
def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession:
|
def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession:
|
||||||
@@ -127,7 +123,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion require_session_version
|
# endregion require_session_version
|
||||||
|
|
||||||
# region bump_session_version [TYPE Function]
|
# region bump_session_version [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled.
|
# @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled.
|
||||||
# @POST: session version increments monotonically.
|
# @POST: session version increments monotonically.
|
||||||
def bump_session_version(self, session: DatasetReviewSession) -> int:
|
def bump_session_version(self, session: DatasetReviewSession) -> int:
|
||||||
@@ -141,7 +136,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion bump_session_version
|
# endregion bump_session_version
|
||||||
|
|
||||||
# region commit_session_mutation [TYPE Function]
|
# region commit_session_mutation [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts.
|
# @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts.
|
||||||
# @POST: session mutation is committed with one version increment or a deterministic conflict error is raised.
|
# @POST: session mutation is committed with one version increment or a deterministic conflict error is raised.
|
||||||
def commit_session_mutation(
|
def commit_session_mutation(
|
||||||
@@ -168,7 +162,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion commit_session_mutation
|
# endregion commit_session_mutation
|
||||||
|
|
||||||
# region load_detail [TYPE Function]
|
# region load_detail [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Return the full session aggregate for API and frontend resume flows.
|
# @PURPOSE: Return the full session aggregate for API and frontend resume flows.
|
||||||
# @POST: Returns SessionDetail with all fields populated or None.
|
# @POST: Returns SessionDetail with all fields populated or None.
|
||||||
def load_session_detail(self, session_id: str, user_id: str) -> Optional[DatasetReviewSession]:
|
def load_session_detail(self, session_id: str, user_id: str) -> Optional[DatasetReviewSession]:
|
||||||
@@ -202,7 +195,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion load_detail
|
# endregion load_detail
|
||||||
|
|
||||||
# region save_profile_and_findings [TYPE Function]
|
# region save_profile_and_findings [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Persist profile state and replace validation findings for an owned session.
|
# @PURPOSE: Persist profile state and replace validation findings for an owned session.
|
||||||
# @POST: stored profile matches the current session and findings are replaced.
|
# @POST: stored profile matches the current session and findings are replaced.
|
||||||
def save_profile_and_findings(
|
def save_profile_and_findings(
|
||||||
@@ -217,7 +209,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion save_profile_and_findings
|
# endregion save_profile_and_findings
|
||||||
|
|
||||||
# region save_recovery_state [TYPE Function]
|
# region save_recovery_state [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Persist imported filters, template variables, and initial execution mappings.
|
# @PURPOSE: Persist imported filters, template variables, and initial execution mappings.
|
||||||
def save_recovery_state(
|
def save_recovery_state(
|
||||||
self, session_id: str, user_id: str, imported_filters: List[ImportedFilter],
|
self, session_id: str, user_id: str, imported_filters: List[ImportedFilter],
|
||||||
@@ -234,7 +225,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion save_recovery_state
|
# endregion save_recovery_state
|
||||||
|
|
||||||
# region save_preview [TYPE Function]
|
# region save_preview [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Persist a preview snapshot and mark prior session previews stale.
|
# @PURPOSE: Persist a preview snapshot and mark prior session previews stale.
|
||||||
def save_preview(
|
def save_preview(
|
||||||
self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: Optional[int] = None,
|
self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: Optional[int] = None,
|
||||||
@@ -248,7 +238,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion save_preview
|
# endregion save_preview
|
||||||
|
|
||||||
# region save_run_context [TYPE Function]
|
# region save_run_context [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Persist an immutable launch audit snapshot for an owned session.
|
# @PURPOSE: Persist an immutable launch audit snapshot for an owned session.
|
||||||
def save_run_context(
|
def save_run_context(
|
||||||
self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: Optional[int] = None,
|
self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: Optional[int] = None,
|
||||||
@@ -262,7 +251,6 @@ class DatasetReviewSessionRepository:
|
|||||||
# endregion save_run_context
|
# endregion save_run_context
|
||||||
|
|
||||||
# region list_user_sess [TYPE Function]
|
# region list_user_sess [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: List review sessions owned by a specific user ordered by most recent update.
|
# @PURPOSE: List review sessions owned by a specific user ordered by most recent update.
|
||||||
def list_sessions_for_user(self, user_id: str) -> List[DatasetReviewSession]:
|
def list_sessions_for_user(self, user_id: str) -> List[DatasetReviewSession]:
|
||||||
with belief_scope("DatasetReviewSessionRepository.list_sessions_for_user"):
|
with belief_scope("DatasetReviewSessionRepository.list_sessions_for_user"):
|
||||||
|
|||||||
@@ -47,14 +47,12 @@ class DictionaryResolutionResult:
|
|||||||
# @SIDE_EFFECT: emits semantic trace logs for ranking and fallback decisions.
|
# @SIDE_EFFECT: emits semantic trace logs for ranking and fallback decisions.
|
||||||
class SemanticSourceResolver:
|
class SemanticSourceResolver:
|
||||||
# region resolve_from_file [TYPE Function]
|
# region resolve_from_file [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Normalize uploaded semantic file records into field-level candidates.
|
# @PURPOSE: Normalize uploaded semantic file records into field-level candidates.
|
||||||
def resolve_from_file(self, source_payload: Mapping[str, Any], fields: Iterable[Mapping[str, Any]]) -> DictionaryResolutionResult:
|
def resolve_from_file(self, source_payload: Mapping[str, Any], fields: Iterable[Mapping[str, Any]]) -> DictionaryResolutionResult:
|
||||||
return DictionaryResolutionResult(source_ref=str(source_payload.get("source_ref") or "uploaded_file"))
|
return DictionaryResolutionResult(source_ref=str(source_payload.get("source_ref") or "uploaded_file"))
|
||||||
# endregion resolve_from_file
|
# endregion resolve_from_file
|
||||||
|
|
||||||
# region resolve_from_dictionary [TYPE Function]
|
# region resolve_from_dictionary [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Resolve candidates from connected tabular dictionary sources.
|
# @PURPOSE: Resolve candidates from connected tabular dictionary sources.
|
||||||
# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]
|
# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]
|
||||||
# @RELATION: [DEPENDS_ON] ->[SemanticCandidate]
|
# @RELATION: [DEPENDS_ON] ->[SemanticCandidate]
|
||||||
@@ -212,7 +210,6 @@ class SemanticSourceResolver:
|
|||||||
# endregion resolve_from_dictionary
|
# endregion resolve_from_dictionary
|
||||||
|
|
||||||
# region resolve_from_reference_dataset [TYPE Function]
|
# region resolve_from_reference_dataset [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Reuse semantic metadata from trusted Superset datasets.
|
# @PURPOSE: Reuse semantic metadata from trusted Superset datasets.
|
||||||
def resolve_from_reference_dataset(
|
def resolve_from_reference_dataset(
|
||||||
self,
|
self,
|
||||||
@@ -223,7 +220,6 @@ class SemanticSourceResolver:
|
|||||||
# endregion resolve_from_reference_dataset
|
# endregion resolve_from_reference_dataset
|
||||||
|
|
||||||
# region rank_candidates [TYPE Function]
|
# region rank_candidates [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Apply confidence ordering and determine best candidate per field.
|
# @PURPOSE: Apply confidence ordering and determine best candidate per field.
|
||||||
# @RELATION: [DEPENDS_ON] ->[SemanticCandidate]
|
# @RELATION: [DEPENDS_ON] ->[SemanticCandidate]
|
||||||
def rank_candidates(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def rank_candidates(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
@@ -241,14 +237,12 @@ class SemanticSourceResolver:
|
|||||||
# endregion rank_candidates
|
# endregion rank_candidates
|
||||||
|
|
||||||
# region detect_conflicts [TYPE Function]
|
# region detect_conflicts [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Mark competing candidate sets that require explicit user review.
|
# @PURPOSE: Mark competing candidate sets that require explicit user review.
|
||||||
def detect_conflicts(self, candidates: List[Dict[str, Any]]) -> bool:
|
def detect_conflicts(self, candidates: List[Dict[str, Any]]) -> bool:
|
||||||
return len(candidates) > 1
|
return len(candidates) > 1
|
||||||
# endregion detect_conflicts
|
# endregion detect_conflicts
|
||||||
|
|
||||||
# region apply_field_decision [TYPE Function]
|
# region apply_field_decision [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Accept, reject, or manually override a field-level semantic value.
|
# @PURPOSE: Accept, reject, or manually override a field-level semantic value.
|
||||||
def apply_field_decision(self, field_state: Mapping[str, Any], decision: Mapping[str, Any]) -> Dict[str, Any]:
|
def apply_field_decision(self, field_state: Mapping[str, Any], decision: Mapping[str, Any]) -> Dict[str, Any]:
|
||||||
merged = dict(field_state)
|
merged = dict(field_state)
|
||||||
@@ -257,7 +251,6 @@ class SemanticSourceResolver:
|
|||||||
# endregion apply_field_decision
|
# endregion apply_field_decision
|
||||||
|
|
||||||
# region propagate_source_version_update [TYPE Function]
|
# region propagate_source_version_update [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Propagate a semantic source version change to unlocked field entries without silently overwriting manual or locked values.
|
# @PURPOSE: Propagate a semantic source version change to unlocked field entries without silently overwriting manual or locked values.
|
||||||
# @RELATION: [DEPENDS_ON] ->[SemanticSource]
|
# @RELATION: [DEPENDS_ON] ->[SemanticSource]
|
||||||
# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]
|
# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]
|
||||||
@@ -314,7 +307,6 @@ class SemanticSourceResolver:
|
|||||||
# endregion propagate_source_version_update
|
# endregion propagate_source_version_update
|
||||||
|
|
||||||
# region _normalize_dictionary_row [TYPE Function]
|
# region _normalize_dictionary_row [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Normalize one dictionary row into a consistent lookup structure.
|
# @PURPOSE: Normalize one dictionary row into a consistent lookup structure.
|
||||||
def _normalize_dictionary_row(self, row: Mapping[str, Any]) -> Dict[str, Any]:
|
def _normalize_dictionary_row(self, row: Mapping[str, Any]) -> Dict[str, Any]:
|
||||||
field_name = (
|
field_name = (
|
||||||
@@ -334,7 +326,6 @@ class SemanticSourceResolver:
|
|||||||
# endregion _normalize_dictionary_row
|
# endregion _normalize_dictionary_row
|
||||||
|
|
||||||
# region _find_fuzzy_matches [TYPE Function]
|
# region _find_fuzzy_matches [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Produce confidence-scored fuzzy matches while keeping them reviewable.
|
# @PURPOSE: Produce confidence-scored fuzzy matches while keeping them reviewable.
|
||||||
def _find_fuzzy_matches(self, field_name: str, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def _find_fuzzy_matches(self, field_name: str, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
normalized_target = self._normalize_key(field_name)
|
normalized_target = self._normalize_key(field_name)
|
||||||
@@ -352,7 +343,6 @@ class SemanticSourceResolver:
|
|||||||
# endregion _find_fuzzy_matches
|
# endregion _find_fuzzy_matches
|
||||||
|
|
||||||
# region _build_candidate_payload [TYPE Function]
|
# region _build_candidate_payload [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Project normalized dictionary rows into semantic candidate payloads.
|
# @PURPOSE: Project normalized dictionary rows into semantic candidate payloads.
|
||||||
def _build_candidate_payload(
|
def _build_candidate_payload(
|
||||||
self,
|
self,
|
||||||
@@ -373,7 +363,6 @@ class SemanticSourceResolver:
|
|||||||
# endregion _build_candidate_payload
|
# endregion _build_candidate_payload
|
||||||
|
|
||||||
# region _match_priority [TYPE Function]
|
# region _match_priority [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention.
|
# @PURPOSE: Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention.
|
||||||
def _match_priority(self, match_type: Optional[str]) -> int:
|
def _match_priority(self, match_type: Optional[str]) -> int:
|
||||||
priority = {
|
priority = {
|
||||||
@@ -386,7 +375,6 @@ class SemanticSourceResolver:
|
|||||||
# endregion _match_priority
|
# endregion _match_priority
|
||||||
|
|
||||||
# region _normalize_key [TYPE Function]
|
# region _normalize_key [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Normalize field identifiers for stable exact/fuzzy comparisons.
|
# @PURPOSE: Normalize field identifiers for stable exact/fuzzy comparisons.
|
||||||
def _normalize_key(self, value: str) -> str:
|
def _normalize_key(self, value: str) -> str:
|
||||||
return "".join(ch for ch in str(value or "").strip().lower() if ch.isalnum() or ch == "_")
|
return "".join(ch for ch in str(value or "").strip().lower() if ch.isalnum() or ch == "_")
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from src.core.logger import logger, belief_scope
|
|||||||
# @BRIEF Mixin providing repository status, diff, and commit history for GitService.
|
# @BRIEF Mixin providing repository status, diff, and commit history for GitService.
|
||||||
class GitServiceStatusMixin:
|
class GitServiceStatusMixin:
|
||||||
# region _parse_status_porcelain [TYPE Function]
|
# region _parse_status_porcelain [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Parse git status --porcelain output into staged, modified, and untracked file lists.
|
# @PURPOSE: Parse git status --porcelain output into staged, modified, and untracked file lists.
|
||||||
# @PRE: `repo` is an open GitPython Repo instance.
|
# @PRE: `repo` is an open GitPython Repo instance.
|
||||||
# @POST: Returns (staged, modified, untracked) tuple of file path lists.
|
# @POST: Returns (staged, modified, untracked) tuple of file path lists.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# #region git_service [C:1] [TYPE Module:Tombstone] [SEMANTICS git, service, shim, re-export, decomissioned]
|
# #region git_service [C:1] [TYPE Module:Tombstone] [SEMANTICS git, service, shim, re-export, decomissioned]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @BRIEF Re-export shim — GitService has been decomposed into services/git/ package.
|
# @BRIEF Re-export shim — GitService has been decomposed into services/git/ package.
|
||||||
# All consumers continue to import from this same path without changes.
|
# All consumers continue to import from this same path without changes.
|
||||||
# @RELATION REDIRECTS_TO -> [GitServiceModule]
|
# @RELATION REDIRECTS_TO -> [GitServiceModule]
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ class HealthService:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# region HealthService_init [TYPE Function]
|
# region HealthService_init [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution.
|
# @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution.
|
||||||
# @PRE: db is a valid SQLAlchemy session.
|
# @PRE: db is a valid SQLAlchemy session.
|
||||||
# @POST: Service is ready to aggregate summaries and delete health reports.
|
# @POST: Service is ready to aggregate summaries and delete health reports.
|
||||||
@@ -61,7 +60,6 @@ class HealthService:
|
|||||||
# endregion HealthService_init
|
# endregion HealthService_init
|
||||||
|
|
||||||
# region _prime_dashboard_meta_cache [TYPE Function]
|
# region _prime_dashboard_meta_cache [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment.
|
# @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment.
|
||||||
# @PRE: records may contain mixed numeric and slug dashboard identifiers.
|
# @PRE: records may contain mixed numeric and slug dashboard identifiers.
|
||||||
# @POST: Numeric dashboard ids for known environments are cached when discoverable.
|
# @POST: Numeric dashboard ids for known environments are cached when discoverable.
|
||||||
@@ -153,7 +151,6 @@ class HealthService:
|
|||||||
# endregion _prime_dashboard_meta_cache
|
# endregion _prime_dashboard_meta_cache
|
||||||
|
|
||||||
# region _resolve_dashboard_meta [TYPE Function]
|
# region _resolve_dashboard_meta [TYPE Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Resolve slug/title for a dashboard referenced by persisted validation record.
|
# @PURPOSE: Resolve slug/title for a dashboard referenced by persisted validation record.
|
||||||
# @PRE: dashboard_id may be numeric or slug-like; environment_id may be empty.
|
# @PRE: dashboard_id may be numeric or slug-like; environment_id may be empty.
|
||||||
# @POST: Returns dict with `slug` and `title` keys, using cache when possible.
|
# @POST: Returns dict with `slug` and `title` keys, using cache when possible.
|
||||||
@@ -184,7 +181,6 @@ class HealthService:
|
|||||||
# endregion _resolve_dashboard_meta
|
# endregion _resolve_dashboard_meta
|
||||||
|
|
||||||
# region get_health_summary [TYPE Function]
|
# region get_health_summary [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Aggregate latest validation status per dashboard and enrich rows with dashboard slug/title.
|
# @PURPOSE: Aggregate latest validation status per dashboard and enrich rows with dashboard slug/title.
|
||||||
# @PRE: environment_id may be omitted to aggregate across all environments.
|
# @PRE: environment_id may be omitted to aggregate across all environments.
|
||||||
# @POST: Returns HealthSummaryResponse with counts and latest record row per dashboard.
|
# @POST: Returns HealthSummaryResponse with counts and latest record row per dashboard.
|
||||||
@@ -288,7 +284,6 @@ class HealthService:
|
|||||||
# endregion get_health_summary
|
# endregion get_health_summary
|
||||||
|
|
||||||
# region delete_validation_report [TYPE Function]
|
# region delete_validation_report [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts.
|
# @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts.
|
||||||
# @PRE: record_id is a validation record identifier.
|
# @PRE: record_id is a validation record identifier.
|
||||||
# @POST: Returns True only when a matching record was deleted.
|
# @POST: Returns True only when a matching record was deleted.
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ class LLMProviderService:
|
|||||||
# endregion LLMProviderService_init
|
# endregion LLMProviderService_init
|
||||||
|
|
||||||
# region get_all_providers [TYPE Function]
|
# region get_all_providers [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Returns all configured LLM providers.
|
# @PURPOSE: Returns all configured LLM providers.
|
||||||
# @PRE: Database connection must be active.
|
# @PRE: Database connection must be active.
|
||||||
# @POST: Returns list of all LLMProvider records.
|
# @POST: Returns list of all LLMProvider records.
|
||||||
@@ -135,7 +134,6 @@ class LLMProviderService:
|
|||||||
# endregion get_all_providers
|
# endregion get_all_providers
|
||||||
|
|
||||||
# region get_provider [TYPE Function]
|
# region get_provider [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Returns a single LLM provider by ID.
|
# @PURPOSE: Returns a single LLM provider by ID.
|
||||||
# @PRE: provider_id must be a valid string.
|
# @PRE: provider_id must be a valid string.
|
||||||
# @POST: Returns LLMProvider or None if not found.
|
# @POST: Returns LLMProvider or None if not found.
|
||||||
@@ -149,7 +147,6 @@ class LLMProviderService:
|
|||||||
# endregion get_provider
|
# endregion get_provider
|
||||||
|
|
||||||
# region create_provider [TYPE Function]
|
# region create_provider [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Creates a new LLM provider with encrypted API key.
|
# @PURPOSE: Creates a new LLM provider with encrypted API key.
|
||||||
# @PRE: config must contain valid provider configuration.
|
# @PRE: config must contain valid provider configuration.
|
||||||
# @POST: New provider created and persisted to database.
|
# @POST: New provider created and persisted to database.
|
||||||
@@ -175,7 +172,6 @@ class LLMProviderService:
|
|||||||
# endregion create_provider
|
# endregion create_provider
|
||||||
|
|
||||||
# region update_provider [TYPE Function]
|
# region update_provider [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Updates an existing LLM provider.
|
# @PURPOSE: Updates an existing LLM provider.
|
||||||
# @PRE: provider_id must exist, config must be valid.
|
# @PRE: provider_id must exist, config must be valid.
|
||||||
# @POST: Provider updated and persisted to database.
|
# @POST: Provider updated and persisted to database.
|
||||||
@@ -210,7 +206,6 @@ class LLMProviderService:
|
|||||||
# endregion update_provider
|
# endregion update_provider
|
||||||
|
|
||||||
# region delete_provider [TYPE Function]
|
# region delete_provider [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Deletes an LLM provider.
|
# @PURPOSE: Deletes an LLM provider.
|
||||||
# @PRE: provider_id must exist.
|
# @PRE: provider_id must exist.
|
||||||
# @POST: Provider removed from database.
|
# @POST: Provider removed from database.
|
||||||
@@ -227,7 +222,6 @@ class LLMProviderService:
|
|||||||
# endregion delete_provider
|
# endregion delete_provider
|
||||||
|
|
||||||
# region get_decrypted_api_key [TYPE Function]
|
# region get_decrypted_api_key [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Returns the decrypted API key for a provider.
|
# @PURPOSE: Returns the decrypted API key for a provider.
|
||||||
# @PRE: provider_id must exist with valid encrypted key.
|
# @PRE: provider_id must exist with valid encrypted key.
|
||||||
# @POST: Returns decrypted API key or None on failure.
|
# @POST: Returns decrypted API key or None on failure.
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from ..core.utils.matching import suggest_mappings
|
|||||||
class MappingService:
|
class MappingService:
|
||||||
# region init [TYPE Function]
|
# region init [TYPE Function]
|
||||||
# @PURPOSE: Initializes the mapping service with a config manager.
|
# @PURPOSE: Initializes the mapping service with a config manager.
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PRE: config_manager is provided.
|
# @PRE: config_manager is provided.
|
||||||
# @PARAM: config_manager (ConfigManager) - The configuration manager.
|
# @PARAM: config_manager (ConfigManager) - The configuration manager.
|
||||||
# @POST: Service is initialized.
|
# @POST: Service is initialized.
|
||||||
@@ -41,7 +40,6 @@ class MappingService:
|
|||||||
|
|
||||||
# region _get_client [TYPE Function]
|
# region _get_client [TYPE Function]
|
||||||
# @PURPOSE: Helper to get an initialized SupersetClient for an environment.
|
# @PURPOSE: Helper to get an initialized SupersetClient for an environment.
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PARAM: env_id (str) - The ID of the environment.
|
# @PARAM: env_id (str) - The ID of the environment.
|
||||||
# @PRE: environment must exist in config.
|
# @PRE: environment must exist in config.
|
||||||
# @POST: Returns an initialized SupersetClient.
|
# @POST: Returns an initialized SupersetClient.
|
||||||
@@ -60,7 +58,6 @@ class MappingService:
|
|||||||
|
|
||||||
# region get_suggestions [TYPE Function]
|
# region get_suggestions [TYPE Function]
|
||||||
# @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions.
|
# @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions.
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PARAM: source_env_id (str) - Source environment ID.
|
# @PARAM: source_env_id (str) - Source environment ID.
|
||||||
# @PARAM: target_env_id (str) - Target environment ID.
|
# @PARAM: target_env_id (str) - Target environment ID.
|
||||||
# @PRE: Both environments must be accessible.
|
# @PRE: Both environments must be accessible.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region test_notification_service [TYPE Module]
|
# region test_notification_service [TYPE Module]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Unit tests for NotificationService routing and dispatch logic.
|
# @PURPOSE: Unit tests for NotificationService routing and dispatch logic.
|
||||||
# @RELATION: TESTS ->[NotificationService:Class]
|
# @RELATION: TESTS ->[NotificationService:Class]
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ from .providers import (
|
|||||||
# @DATA_CONTRACT: Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]
|
# @DATA_CONTRACT: Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]
|
||||||
class NotificationService:
|
class NotificationService:
|
||||||
# region NotificationService_init [TYPE Function]
|
# region NotificationService_init [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Bind DB and configuration collaborators used for provider initialization and routing.
|
# @PURPOSE: Bind DB and configuration collaborators used for provider initialization and routing.
|
||||||
# @RELATION: [BINDS_TO] ->[NotificationService]
|
# @RELATION: [BINDS_TO] ->[NotificationService]
|
||||||
# @RELATION: [DEPENDS_ON] ->[ValidationPolicy]
|
# @RELATION: [DEPENDS_ON] ->[ValidationPolicy]
|
||||||
@@ -57,7 +56,6 @@ class NotificationService:
|
|||||||
# endregion NotificationService_init
|
# endregion NotificationService_init
|
||||||
|
|
||||||
# region _initialize_providers [TYPE Function]
|
# region _initialize_providers [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Materialize configured notification channel adapters once per service lifetime.
|
# @PURPOSE: Materialize configured notification channel adapters once per service lifetime.
|
||||||
# @RELATION: [DEPENDS_ON] ->[SMTPProvider]
|
# @RELATION: [DEPENDS_ON] ->[SMTPProvider]
|
||||||
# @RELATION: [DEPENDS_ON] ->[TelegramProvider]
|
# @RELATION: [DEPENDS_ON] ->[TelegramProvider]
|
||||||
@@ -83,7 +81,6 @@ class NotificationService:
|
|||||||
# endregion _initialize_providers
|
# endregion _initialize_providers
|
||||||
|
|
||||||
# region dispatch_report [TYPE Function]
|
# region dispatch_report [TYPE Function]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @PURPOSE: Route one validation record to resolved owners and configured custom channels.
|
# @PURPOSE: Route one validation record to resolved owners and configured custom channels.
|
||||||
# @RELATION: [CALLS] ->[_initialize_providers]
|
# @RELATION: [CALLS] ->[_initialize_providers]
|
||||||
# @RELATION: [CALLS] ->[_should_notify]
|
# @RELATION: [CALLS] ->[_should_notify]
|
||||||
@@ -143,7 +140,6 @@ class NotificationService:
|
|||||||
# endregion dispatch_report
|
# endregion dispatch_report
|
||||||
|
|
||||||
# region _should_notify [TYPE Function]
|
# region _should_notify [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Evaluate record status against effective alert policy.
|
# @PURPOSE: Evaluate record status against effective alert policy.
|
||||||
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
|
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
|
||||||
# @RELATION: [DEPENDS_ON] ->[ValidationPolicy]
|
# @RELATION: [DEPENDS_ON] ->[ValidationPolicy]
|
||||||
@@ -161,7 +157,6 @@ class NotificationService:
|
|||||||
# endregion _should_notify
|
# endregion _should_notify
|
||||||
|
|
||||||
# region _resolve_targets [TYPE Function]
|
# region _resolve_targets [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Resolve owner and policy-defined delivery targets for one validation record.
|
# @PURPOSE: Resolve owner and policy-defined delivery targets for one validation record.
|
||||||
# @RELATION: [CALLS] ->[_find_dashboard_owners]
|
# @RELATION: [CALLS] ->[_find_dashboard_owners]
|
||||||
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
|
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
|
||||||
@@ -198,7 +193,6 @@ class NotificationService:
|
|||||||
# endregion _resolve_targets
|
# endregion _resolve_targets
|
||||||
|
|
||||||
# region _find_dashboard_owners [TYPE Function]
|
# region _find_dashboard_owners [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Load candidate dashboard owners from persisted profile preferences.
|
# @PURPOSE: Load candidate dashboard owners from persisted profile preferences.
|
||||||
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
|
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
|
||||||
# @RELATION: [DEPENDS_ON] ->[UserDashboardPreference]
|
# @RELATION: [DEPENDS_ON] ->[UserDashboardPreference]
|
||||||
@@ -220,7 +214,6 @@ class NotificationService:
|
|||||||
# endregion _find_dashboard_owners
|
# endregion _find_dashboard_owners
|
||||||
|
|
||||||
# region _build_body [TYPE Function]
|
# region _build_body [TYPE Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Format one validation record into provider-ready body text.
|
# @PURPOSE: Format one validation record into provider-ready body text.
|
||||||
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
|
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
|
||||||
def _build_body(self, record: ValidationRecord) -> str:
|
def _build_body(self, record: ValidationRecord) -> str:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region test_report_normalizer [TYPE Module]
|
# region test_report_normalizer [TYPE Module]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @SEMANTICS: tests, reports, normalizer, fallback
|
# @SEMANTICS: tests, reports, normalizer, fallback
|
||||||
# @PURPOSE: Validate unknown task type fallback and partial payload normalization behavior.
|
# @PURPOSE: Validate unknown task type fallback and partial payload normalization behavior.
|
||||||
# @RELATION: TESTS ->[normalize_report:Function]
|
# @RELATION: TESTS ->[normalize_report:Function]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# region test_report_service [TYPE Module]
|
# region test_report_service [TYPE Module]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Unit tests for ReportsService list/detail operations
|
# @PURPOSE: Unit tests for ReportsService list/detail operations
|
||||||
# @RELATION: TESTS ->[ReportsService:Class]
|
# @RELATION: TESTS ->[ReportsService:Class]
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ from .normalizer import normalize_task_report
|
|||||||
# @TEST_INVARIANT: consistent_pagination -> verifies: [valid_service]
|
# @TEST_INVARIANT: consistent_pagination -> verifies: [valid_service]
|
||||||
class ReportsService:
|
class ReportsService:
|
||||||
# region init [TYPE Function]
|
# region init [TYPE Function]
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @PURPOSE: Initialize service with TaskManager dependency.
|
# @PURPOSE: Initialize service with TaskManager dependency.
|
||||||
# @PRE: task_manager is a live TaskManager instance.
|
# @PRE: task_manager is a live TaskManager instance.
|
||||||
# @POST: self.task_manager is assigned and ready for read operations.
|
# @POST: self.task_manager is assigned and ready for read operations.
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from ..core.logger import logger, belief_scope
|
|||||||
class ResourceService:
|
class ResourceService:
|
||||||
|
|
||||||
# region ResourceService_init [TYPE Function]
|
# region ResourceService_init [TYPE Function]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Initialize the resource service with dependencies
|
# @PURPOSE: Initialize the resource service with dependencies
|
||||||
# @PRE: None
|
# @PRE: None
|
||||||
# @POST: ResourceService is ready to fetch resources
|
# @POST: ResourceService is ready to fetch resources
|
||||||
@@ -32,7 +31,6 @@ class ResourceService:
|
|||||||
# endregion ResourceService_init
|
# endregion ResourceService_init
|
||||||
|
|
||||||
# region get_dashboards_with_status [TYPE Function]
|
# region get_dashboards_with_status [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Fetch dashboards from environment with Git status and last task status
|
# @PURPOSE: Fetch dashboards from environment with Git status and last task status
|
||||||
# @PRE: env is a valid Environment object
|
# @PRE: env is a valid Environment object
|
||||||
# @POST: Returns list of dashboards with enhanced metadata
|
# @POST: Returns list of dashboards with enhanced metadata
|
||||||
@@ -82,7 +80,6 @@ class ResourceService:
|
|||||||
# endregion get_dashboards_with_status
|
# endregion get_dashboards_with_status
|
||||||
|
|
||||||
# region get_dashboards_page_with_status [TYPE Function]
|
# region get_dashboards_page_with_status [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.
|
# @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.
|
||||||
# @PRE: env is valid; page >= 1; page_size > 0.
|
# @PRE: env is valid; page >= 1; page_size > 0.
|
||||||
# @POST: Returns page items plus total counters without scanning all pages locally.
|
# @POST: Returns page items plus total counters without scanning all pages locally.
|
||||||
@@ -149,7 +146,6 @@ class ResourceService:
|
|||||||
# endregion get_dashboards_page_with_status
|
# endregion get_dashboards_page_with_status
|
||||||
|
|
||||||
# region _get_last_llm_task_for_dashboard [TYPE Function]
|
# region _get_last_llm_task_for_dashboard [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Get most recent LLM validation task for a dashboard in an environment
|
# @PURPOSE: Get most recent LLM validation task for a dashboard in an environment
|
||||||
# @PRE: dashboard_id is a valid integer identifier
|
# @PRE: dashboard_id is a valid integer identifier
|
||||||
# @POST: Returns the newest llm_dashboard_validation task summary or None
|
# @POST: Returns the newest llm_dashboard_validation task summary or None
|
||||||
@@ -233,7 +229,6 @@ class ResourceService:
|
|||||||
# endregion _get_last_llm_task_for_dashboard
|
# endregion _get_last_llm_task_for_dashboard
|
||||||
|
|
||||||
# region _normalize_task_status [TYPE Function]
|
# region _normalize_task_status [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Normalize task status to stable uppercase values for UI/API projections
|
# @PURPOSE: Normalize task status to stable uppercase values for UI/API projections
|
||||||
# @PRE: raw_status can be enum or string
|
# @PRE: raw_status can be enum or string
|
||||||
# @POST: Returns uppercase status without enum class prefix
|
# @POST: Returns uppercase status without enum class prefix
|
||||||
@@ -251,7 +246,6 @@ class ResourceService:
|
|||||||
# endregion _normalize_task_status
|
# endregion _normalize_task_status
|
||||||
|
|
||||||
# region _normalize_validation_status [TYPE Function]
|
# region _normalize_validation_status [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN
|
# @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN
|
||||||
# @PRE: raw_status can be any scalar type
|
# @PRE: raw_status can be any scalar type
|
||||||
# @POST: Returns normalized validation status token or None
|
# @POST: Returns normalized validation status token or None
|
||||||
@@ -268,7 +262,6 @@ class ResourceService:
|
|||||||
# endregion _normalize_validation_status
|
# endregion _normalize_validation_status
|
||||||
|
|
||||||
# region _normalize_datetime_for_compare [TYPE Function]
|
# region _normalize_datetime_for_compare [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.
|
# @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.
|
||||||
# @PRE: value may be datetime or any scalar.
|
# @PRE: value may be datetime or any scalar.
|
||||||
# @POST: Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.
|
# @POST: Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.
|
||||||
@@ -285,7 +278,6 @@ class ResourceService:
|
|||||||
# endregion _normalize_datetime_for_compare
|
# endregion _normalize_datetime_for_compare
|
||||||
|
|
||||||
# region get_datasets_with_status [TYPE Function]
|
# region get_datasets_with_status [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Fetch datasets from environment with mapping progress and last task status
|
# @PURPOSE: Fetch datasets from environment with mapping progress and last task status
|
||||||
# @PRE: env is a valid Environment object
|
# @PRE: env is a valid Environment object
|
||||||
# @POST: Returns list of datasets with enhanced metadata
|
# @POST: Returns list of datasets with enhanced metadata
|
||||||
@@ -324,7 +316,6 @@ class ResourceService:
|
|||||||
# endregion get_datasets_with_status
|
# endregion get_datasets_with_status
|
||||||
|
|
||||||
# region get_activity_summary [TYPE Function]
|
# region get_activity_summary [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Get summary of active and recent tasks for the activity indicator
|
# @PURPOSE: Get summary of active and recent tasks for the activity indicator
|
||||||
# @PRE: tasks is a list of Task objects
|
# @PRE: tasks is a list of Task objects
|
||||||
# @POST: Returns summary with active_count and recent_tasks
|
# @POST: Returns summary with active_count and recent_tasks
|
||||||
@@ -366,7 +357,6 @@ class ResourceService:
|
|||||||
# endregion get_activity_summary
|
# endregion get_activity_summary
|
||||||
|
|
||||||
# region _get_git_status_for_dashboard [TYPE Function]
|
# region _get_git_status_for_dashboard [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Get Git sync status for a dashboard
|
# @PURPOSE: Get Git sync status for a dashboard
|
||||||
# @PRE: dashboard_id is a valid integer
|
# @PRE: dashboard_id is a valid integer
|
||||||
# @POST: Returns git status or None if no repo exists
|
# @POST: Returns git status or None if no repo exists
|
||||||
@@ -426,7 +416,6 @@ class ResourceService:
|
|||||||
# endregion _get_git_status_for_dashboard
|
# endregion _get_git_status_for_dashboard
|
||||||
|
|
||||||
# region _get_last_task_for_resource [TYPE Function]
|
# region _get_last_task_for_resource [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Get the most recent task for a specific resource
|
# @PURPOSE: Get the most recent task for a specific resource
|
||||||
# @PRE: resource_id is a valid string
|
# @PRE: resource_id is a valid string
|
||||||
# @POST: Returns task summary or None if no tasks found
|
# @POST: Returns task summary or None if no tasks found
|
||||||
@@ -465,7 +454,6 @@ class ResourceService:
|
|||||||
# endregion _get_last_task_for_resource
|
# endregion _get_last_task_for_resource
|
||||||
|
|
||||||
# region _extract_resource_name_from_task [TYPE Function]
|
# region _extract_resource_name_from_task [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Extract resource name from task params
|
# @PURPOSE: Extract resource name from task params
|
||||||
# @PRE: task is a valid Task object
|
# @PRE: task is a valid Task object
|
||||||
# @POST: Returns resource name or task ID
|
# @POST: Returns resource name or task ID
|
||||||
@@ -478,7 +466,6 @@ class ResourceService:
|
|||||||
# endregion _extract_resource_name_from_task
|
# endregion _extract_resource_name_from_task
|
||||||
|
|
||||||
# region _extract_resource_type_from_task [TYPE Function]
|
# region _extract_resource_type_from_task [TYPE Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Extract resource type from task params
|
# @PURPOSE: Extract resource type from task params
|
||||||
# @PRE: task is a valid Task object
|
# @PRE: task is a valid Task object
|
||||||
# @POST: Returns resource type or 'unknown'
|
# @POST: Returns resource type or 'unknown'
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestArchiveParser:Module]
|
# [DEF:TestArchiveParser:Module]
|
||||||
#
|
#
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Unit tests for MigrationArchiveParser ZIP extraction contract.
|
# @PURPOSE: Unit tests for MigrationArchiveParser ZIP extraction contract.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
# @RELATION: DEPENDS_ON -> [MigrationArchiveParserModule]
|
# @RELATION: DEPENDS_ON -> [MigrationArchiveParserModule]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestDryRunOrchestrator:Module]
|
# [DEF:TestDryRunOrchestrator:Module]
|
||||||
#
|
#
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Unit tests for MigrationDryRunService diff and risk computation contracts.
|
# @PURPOSE: Unit tests for MigrationDryRunService diff and risk computation contracts.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
# @RELATION: DEPENDS_ON -> [MigrationDryRunOrchestratorModule]
|
# @RELATION: DEPENDS_ON -> [MigrationDryRunOrchestratorModule]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestGitServiceGiteaPr:Module]
|
# [DEF:TestGitServiceGiteaPr:Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, git, gitea, pull_request, fallback
|
# @SEMANTICS: tests, git, gitea, pull_request, fallback
|
||||||
# @PURPOSE: Validate Gitea PR creation fallback behavior when configured server URL is stale.
|
# @PURPOSE: Validate Gitea PR creation fallback behavior when configured server URL is stale.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestMappingService:Module]
|
# [DEF:TestMappingService:Module]
|
||||||
#
|
#
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs.
|
# @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
# @RELATION: VERIFIES ->[src.core.mapping_service.IdMappingService]
|
# @RELATION: VERIFIES ->[src.core.mapping_service.IdMappingService]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestMigrationEngine:Module]
|
# [DEF:TestMigrationEngine:Module]
|
||||||
#
|
#
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Unit tests for MigrationEngine's cross-filter patching algorithms.
|
# @PURPOSE: Unit tests for MigrationEngine's cross-filter patching algorithms.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
# @RELATION: VERIFIES -> [src.core.migration_engine:Module]
|
# @RELATION: VERIFIES -> [src.core.migration_engine:Module]
|
||||||
@@ -29,7 +28,6 @@ from src.models.mapping import ResourceType
|
|||||||
|
|
||||||
# [DEF:MockMappingService:Class]
|
# [DEF:MockMappingService:Class]
|
||||||
# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]
|
# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios.
|
# @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios.
|
||||||
# @INVARIANT: Returns mappings only for requested UUID keys present in seeded map.
|
# @INVARIANT: Returns mappings only for requested UUID keys present in seeded map.
|
||||||
class MockMappingService:
|
class MockMappingService:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:test_clean_release_cli:Module]
|
# [DEF:test_clean_release_cli:Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Smoke tests for the redesigned clean release CLI.
|
# @PURPOSE: Smoke tests for the redesigned clean release CLI.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestCleanReleaseTui:Module]
|
# [DEF:TestCleanReleaseTui:Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, tui, clean-release, curses
|
# @SEMANTICS: tests, tui, clean-release, curses
|
||||||
# @PURPOSE: Unit tests for the interactive curses TUI of the clean release process.
|
# @PURPOSE: Unit tests for the interactive curses TUI of the clean release process.
|
||||||
# @LAYER: Scripts
|
# @LAYER: Scripts
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:test_clean_release_tui_v2:Module]
|
# [DEF:test_clean_release_tui_v2:Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Smoke tests for thin-client TUI action dispatch and blocked transition behavior.
|
# @PURPOSE: Smoke tests for thin-client TUI action dispatch and blocked transition behavior.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestApprovalService:Module]
|
# [DEF:TestApprovalService:Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @SEMANTICS: tests, clean-release, approval, lifecycle, gate
|
# @SEMANTICS: tests, clean-release, approval, lifecycle, gate
|
||||||
# @PURPOSE: Define approval gate contracts for approve/reject operations over immutable compliance evidence.
|
# @PURPOSE: Define approval gate contracts for approve/reject operations over immutable compliance evidence.
|
||||||
# @LAYER: Tests
|
# @LAYER: Tests
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:test_candidate_manifest_services:Module]
|
# [DEF:test_candidate_manifest_services:Module]
|
||||||
# @RELATION: BELONGS_TO -> [SrcRoot:Module]
|
# @RELATION: BELONGS_TO -> [SrcRoot:Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Test lifecycle and manifest versioning for release candidates.
|
# @PURPOSE: Test lifecycle and manifest versioning for release candidates.
|
||||||
# @LAYER: Tests
|
# @LAYER: Tests
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestComplianceExecutionService:Module]
|
# [DEF:TestComplianceExecutionService:Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @SEMANTICS: tests, clean-release, compliance, pipeline, run-finalization
|
# @SEMANTICS: tests, clean-release, compliance, pipeline, run-finalization
|
||||||
# @PURPOSE: Validate stage pipeline and run finalization contracts for compliance execution.
|
# @PURPOSE: Validate stage pipeline and run finalization contracts for compliance execution.
|
||||||
# @LAYER: Tests
|
# @LAYER: Tests
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestComplianceTaskIntegration:Module]
|
# [DEF:TestComplianceTaskIntegration:Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @SEMANTICS: tests, clean-release, compliance, task-manager, integration
|
# @SEMANTICS: tests, clean-release, compliance, task-manager, integration
|
||||||
# @PURPOSE: Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes.
|
# @PURPOSE: Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes.
|
||||||
# @LAYER: Tests
|
# @LAYER: Tests
|
||||||
@@ -144,7 +143,6 @@ class CleanReleaseCompliancePlugin:
|
|||||||
|
|
||||||
# [DEF:_PluginLoaderStub:Class]
|
# [DEF:_PluginLoaderStub:Class]
|
||||||
# @RELATION: BINDS_TO -> TestComplianceTaskIntegration
|
# @RELATION: BINDS_TO -> TestComplianceTaskIntegration
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests.
|
# @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests.
|
||||||
# @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id.
|
# @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id.
|
||||||
class _PluginLoaderStub:
|
class _PluginLoaderStub:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# [DEF:TestDemoModeIsolation:Module]
|
# [DEF:TestDemoModeIsolation:Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository
|
# @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository
|
||||||
# @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration.
|
# @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration.
|
||||||
# @LAYER: Tests
|
# @LAYER: Tests
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# [DEF:TestPolicyResolutionService:Module]
|
# [DEF:TestPolicyResolutionService:Module]
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @SEMANTICS: clean-release, policy-resolution, trusted-snapshots, contracts
|
# @SEMANTICS: clean-release, policy-resolution, trusted-snapshots, contracts
|
||||||
# @PURPOSE: Verify trusted policy snapshot resolution contract and error guards.
|
# @PURPOSE: Verify trusted policy snapshot resolution contract and error guards.
|
||||||
# @LAYER: Tests
|
# @LAYER: Tests
|
||||||
@@ -24,7 +23,6 @@ from src.services.clean_release.repository import CleanReleaseRepository
|
|||||||
|
|
||||||
# [DEF:_config_manager:Function]
|
# [DEF:_config_manager:Function]
|
||||||
# @RELATION: BINDS_TO -> [TestPolicyResolutionService]
|
# @RELATION: BINDS_TO -> [TestPolicyResolutionService]
|
||||||
# @COMPLEXITY: 1
|
|
||||||
# @PURPOSE: Build deterministic ConfigManager-like stub for tests.
|
# @PURPOSE: Build deterministic ConfigManager-like stub for tests.
|
||||||
# @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.
|
# @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.
|
||||||
# @PRE: policy_id and registry_id may be None or non-empty strings.
|
# @PRE: policy_id and registry_id may be None or non-empty strings.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestPublicationService:Module]
|
# [DEF:TestPublicationService:Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @SEMANTICS: tests, clean-release, publication, revoke, gate
|
# @SEMANTICS: tests, clean-release, publication, revoke, gate
|
||||||
# @PURPOSE: Define publication gate contracts over approved candidates and immutable publication records.
|
# @PURPOSE: Define publication gate contracts over approved candidates and immutable publication records.
|
||||||
# @LAYER: Tests
|
# @LAYER: Tests
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestReportAuditImmutability:Module]
|
# [DEF:TestReportAuditImmutability:Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @SEMANTICS: tests, clean-release, report, audit, immutability, append-only
|
# @SEMANTICS: tests, clean-release, report, audit, immutability, append-only
|
||||||
# @PURPOSE: Validate report snapshot immutability expectations and append-only audit hook behavior for US2.
|
# @PURPOSE: Validate report snapshot immutability expectations and append-only audit hook behavior for US2.
|
||||||
# @LAYER: Tests
|
# @LAYER: Tests
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# [DEF:SupersetCompatibilityMatrixTests:Module]
|
# [DEF:SupersetCompatibilityMatrixTests:Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: dataset_review, superset, compatibility_matrix, preview, sql_lab, tests
|
# @SEMANTICS: dataset_review, superset, compatibility_matrix, preview, sql_lab, tests
|
||||||
# @PURPOSE: Verifies Superset preview and SQL Lab endpoint fallback strategy used by dataset-review orchestration.
|
# @PURPOSE: Verifies Superset preview and SQL Lab endpoint fallback strategy used by dataset-review orchestration.
|
||||||
# @LAYER: Tests
|
# @LAYER: Tests
|
||||||
@@ -21,7 +20,6 @@ from src.core.utils.superset_compilation_adapter import (
|
|||||||
|
|
||||||
|
|
||||||
# [DEF:make_adapter:Function]
|
# [DEF:make_adapter:Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Build an adapter with a mock Superset client and deterministic environment for compatibility tests.
|
# @PURPOSE: Build an adapter with a mock Superset client and deterministic environment for compatibility tests.
|
||||||
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
|
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
|
||||||
def make_adapter():
|
def make_adapter():
|
||||||
@@ -43,7 +41,6 @@ def make_adapter():
|
|||||||
|
|
||||||
|
|
||||||
# [DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]
|
# [DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @PURPOSE: Confirms preview compilation uses a supported client method first when the capability exists.
|
# @PURPOSE: Confirms preview compilation uses a supported client method first when the capability exists.
|
||||||
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
|
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
|
||||||
def test_preview_prefers_supported_client_method_before_network_fallback():
|
def test_preview_prefers_supported_client_method_before_network_fallback():
|
||||||
@@ -69,7 +66,6 @@ def test_preview_prefers_supported_client_method_before_network_fallback():
|
|||||||
|
|
||||||
|
|
||||||
# [DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]
|
# [DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Confirms preview fallback walks the compatibility matrix from preferred to legacy endpoints until one returns compiled SQL.
|
# @PURPOSE: Confirms preview fallback walks the compatibility matrix from preferred to legacy endpoints until one returns compiled SQL.
|
||||||
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
|
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
|
||||||
def test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql():
|
def test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql():
|
||||||
@@ -104,7 +100,6 @@ def test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql()
|
|||||||
|
|
||||||
|
|
||||||
# [DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]
|
# [DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction.
|
# @PURPOSE: Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction.
|
||||||
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
|
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
|
||||||
def test_sql_lab_launch_falls_back_to_legacy_execute_endpoint():
|
def test_sql_lab_launch_falls_back_to_legacy_execute_endpoint():
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# [DEF:TestAuth:Module]
|
# [DEF:TestAuth:Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Covers authentication service/repository behavior and auth bootstrap helpers.
|
# @PURPOSE: Covers authentication service/repository behavior and auth bootstrap helpers.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
# @RELATION: TESTS -> AuthService
|
# @RELATION: TESTS -> AuthService
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:TestDashboardsApi:Module]
|
# [DEF:TestDashboardsApi:Module]
|
||||||
# @RELATION: VERIFIES ->[src.api.routes.dashboards]
|
# @RELATION: VERIFIES ->[src.api.routes.dashboards]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @PURPOSE: Comprehensive contract-driven tests for Dashboard Hub API
|
# @PURPOSE: Comprehensive contract-driven tests for Dashboard Hub API
|
||||||
# @LAYER: Domain (Tests)
|
# @LAYER: Domain (Tests)
|
||||||
# @SEMANTICS: tests, dashboards, api, contract, remediation
|
# @SEMANTICS: tests, dashboards, api, contract, remediation
|
||||||
@@ -561,7 +560,7 @@ def test_backup_dashboards_with_schedule(mock_deps):
|
|||||||
# --- 8. Internal logic: _task_matches_dashboard ---
|
# --- 8. Internal logic: _task_matches_dashboard ---
|
||||||
# [/DEF:test_backup_dashboards_with_schedule:Function]
|
# [/DEF:test_backup_dashboards_with_schedule:Function]
|
||||||
|
|
||||||
from src.api.routes.dashboards import _task_matches_dashboard
|
from src.api.routes.dashboards._projection import _task_matches_dashboard
|
||||||
|
|
||||||
|
|
||||||
# [DEF:test_task_matches_dashboard_logic:Function]
|
# [DEF:test_task_matches_dashboard_logic:Function]
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
# @SEMANTICS: test, log, persistence, unit_test
|
# @SEMANTICS: test, log, persistence, unit_test
|
||||||
# @PURPOSE: Unit tests for TaskLogPersistenceService.
|
# @PURPOSE: Unit tests for TaskLogPersistenceService.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
# @COMPLEXITY: 5
|
|
||||||
|
|
||||||
# [SECTION: IMPORTS]
|
# [SECTION: IMPORTS]
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -20,7 +19,6 @@ from src.core.task_manager.models import LogEntry, LogFilter
|
|||||||
# [DEF:TestLogPersistence:Class]
|
# [DEF:TestLogPersistence:Class]
|
||||||
# @RELATION: BINDS_TO -> test_log_persistence
|
# @RELATION: BINDS_TO -> test_log_persistence
|
||||||
# @PURPOSE: Test suite for TaskLogPersistenceService.
|
# @PURPOSE: Test suite for TaskLogPersistenceService.
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @TEST_DATA: log_entry -> {"task_id": "test-task-1", "level": "INFO", "source": "test_source", "message": "Test message"}
|
# @TEST_DATA: log_entry -> {"task_id": "test-task-1", "level": "INFO", "source": "test_source", "message": "Test message"}
|
||||||
class TestLogPersistence:
|
class TestLogPersistence:
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# [DEF:TestLogger:Module]
|
# [DEF:TestLogger:Module]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: logging, tests, belief_state, cot, json
|
# @SEMANTICS: logging, tests, belief_state, cot, json
|
||||||
# @PURPOSE: Unit tests for the custom logger CoT JSON formatters and configuration context manager.
|
# @PURPOSE: Unit tests for the custom logger CoT JSON formatters and configuration context manager.
|
||||||
# @LAYER: Logging (Tests)
|
# @LAYER: Logging (Tests)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# [DEF:TestResourceHubs:Module]
|
# [DEF:TestResourceHubs:Module]
|
||||||
# @RELATION: DEPENDS_ON -> [DashboardsApi]
|
# @RELATION: DEPENDS_ON -> [DashboardsApi]
|
||||||
# @RELATION: DEPENDS_ON -> [DatasetsApi]
|
# @RELATION: DEPENDS_ON -> [DatasetsApi]
|
||||||
# @COMPLEXITY: 3
|
|
||||||
# @SEMANTICS: tests, resource-hubs, dashboards, datasets, pagination, api
|
# @SEMANTICS: tests, resource-hubs, dashboards, datasets, pagination, api
|
||||||
# @PURPOSE: Contract tests for resource hub dashboards/datasets listing and pagination boundary validation.
|
# @PURPOSE: Contract tests for resource hub dashboards/datasets listing and pagination boundary validation.
|
||||||
# @LAYER: Domain (Tests)
|
# @LAYER: Domain (Tests)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# [DEF:TestSmokeApp:Module]
|
# [DEF:TestSmokeApp:Module]
|
||||||
# @COMPLEXITY: 2
|
|
||||||
# @SEMANTICS: tests, smoke, app, imports, fastapi
|
# @SEMANTICS: tests, smoke, app, imports, fastapi
|
||||||
# @PURPOSE: Minimal smoke tests that verify the full application import chain
|
# @PURPOSE: Minimal smoke tests that verify the full application import chain
|
||||||
# succeeds without SyntaxError, IndentationError, ImportError, or
|
# succeeds without SyntaxError, IndentationError, ImportError, or
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# [DEF:test_task_manager:Module]
|
# [DEF:test_task_manager:Module]
|
||||||
# @RELATION: BELONGS_TO -> SrcRoot
|
# @RELATION: BELONGS_TO -> SrcRoot
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @SEMANTICS: task-manager, lifecycle, CRUD, log-buffer, filtering, tests
|
# @SEMANTICS: task-manager, lifecycle, CRUD, log-buffer, filtering, tests
|
||||||
# @PURPOSE: Unit tests for TaskManager lifecycle, CRUD, log buffering, and filtering.
|
# @PURPOSE: Unit tests for TaskManager lifecycle, CRUD, log buffering, and filtering.
|
||||||
# @LAYER: Core
|
# @LAYER: Core
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
# @SEMANTICS: test, task, persistence, unit_test
|
# @SEMANTICS: test, task, persistence, unit_test
|
||||||
# @PURPOSE: Unit tests for TaskPersistenceService.
|
# @PURPOSE: Unit tests for TaskPersistenceService.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"}
|
# @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"}
|
||||||
|
|
||||||
# [SECTION: IMPORTS]
|
# [SECTION: IMPORTS]
|
||||||
@@ -23,7 +22,6 @@ from src.core.task_manager.models import Task, TaskStatus, LogEntry
|
|||||||
# [DEF:TestTaskPersistenceHelpers:Class]
|
# [DEF:TestTaskPersistenceHelpers:Class]
|
||||||
# @RELATION: BINDS_TO -> test_task_persistence
|
# @RELATION: BINDS_TO -> test_task_persistence
|
||||||
# @PURPOSE: Test suite for TaskPersistenceService static helper methods.
|
# @PURPOSE: Test suite for TaskPersistenceService static helper methods.
|
||||||
# @COMPLEXITY: 5
|
|
||||||
class TestTaskPersistenceHelpers:
|
class TestTaskPersistenceHelpers:
|
||||||
|
|
||||||
# [DEF:test_json_load_if_needed_none:Function]
|
# [DEF:test_json_load_if_needed_none:Function]
|
||||||
@@ -113,7 +111,6 @@ class TestTaskPersistenceHelpers:
|
|||||||
# [DEF:TestTaskPersistenceService:Class]
|
# [DEF:TestTaskPersistenceService:Class]
|
||||||
# @RELATION: BINDS_TO -> test_task_persistence
|
# @RELATION: BINDS_TO -> test_task_persistence
|
||||||
# @PURPOSE: Test suite for TaskPersistenceService CRUD operations.
|
# @PURPOSE: Test suite for TaskPersistenceService CRUD operations.
|
||||||
# @COMPLEXITY: 5
|
|
||||||
# @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"}
|
# @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"}
|
||||||
class TestTaskPersistenceService:
|
class TestTaskPersistenceService:
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# [DEF:TranslateCorrectionTests:Module]
|
# [DEF:TranslateCorrectionTests:Module]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @SEMANTICS: tests, translate, corrections, dictionary
|
# @SEMANTICS: tests, translate, corrections, dictionary
|
||||||
# @PURPOSE: Tests for term correction API endpoints and DictionaryManager correction methods.
|
# @PURPOSE: Tests for term correction API endpoints and DictionaryManager correction methods.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# [DEF:TranslateHistoryTests:Module]
|
# [DEF:TranslateHistoryTests:Module]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @SEMANTICS: tests, translate, history, metrics
|
# @SEMANTICS: tests, translate, history, metrics
|
||||||
# @PURPOSE: Tests for run history list/detail endpoints and metrics aggregation.
|
# @PURPOSE: Tests for run history list/detail endpoints and metrics aggregation.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# [DEF:TranslateJobTests:Module]
|
# [DEF:TranslateJobTests:Module]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @SEMANTICS: tests, translate, jobs, crud, validation
|
# @SEMANTICS: tests, translate, jobs, crud, validation
|
||||||
# @PURPOSE: Tests for translation job CRUD endpoints and service layer with column validation.
|
# @PURPOSE: Tests for translation job CRUD endpoints and service layer with column validation.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# [DEF:TranslateSchedulerTests:Module]
|
# [DEF:TranslateSchedulerTests:Module]
|
||||||
# @COMPLEXITY: 4
|
|
||||||
# @SEMANTICS: tests, translate, scheduler
|
# @SEMANTICS: tests, translate, scheduler
|
||||||
# @PURPOSE: Tests for TranslationScheduler CRUD and APScheduler integration.
|
# @PURPOSE: Tests for TranslationScheduler CRUD and APScheduler integration.
|
||||||
# @LAYER: Test
|
# @LAYER: Test
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
<!--
|
<!--
|
||||||
@UX_STATE: Loading -> Default
|
@UX_STATE: Loading -> Default
|
||||||
|
|
||||||
@COMPLEXITY: 3
|
|
||||||
@SEMANTICS: dashboard, grid, selection, pagination
|
@SEMANTICS: dashboard, grid, selection, pagination
|
||||||
@PURPOSE: Displays a grid of dashboards with selection and pagination.
|
@PURPOSE: Displays a grid of dashboards with selection and pagination.
|
||||||
@LAYER: Component
|
@LAYER: Component
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
let validatingIds: Set<number> = $state(new Set());
|
let validatingIds: Set<number> = $state(new Set());
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [DEF:handleValidate:Function]
|
// #region handleValidate:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Triggers dashboard validation task.
|
* @purpose Triggers dashboard validation task.
|
||||||
*/
|
*/
|
||||||
@@ -70,7 +69,7 @@
|
|||||||
validatingIds = new Set(validatingIds);
|
validatingIds = new Set(validatingIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [/DEF:handleValidate:Function]
|
// #endregion handleValidate:Function
|
||||||
|
|
||||||
// [SECTION: DERIVED]
|
// [SECTION: DERIVED]
|
||||||
let filteredDashboards = $derived(
|
let filteredDashboards = $derived(
|
||||||
@@ -111,7 +110,7 @@
|
|||||||
);
|
);
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [DEF:handleSort:Function]
|
// #region handleSort:Function [TYPE Function]
|
||||||
// @PURPOSE: Toggles sort direction or changes sort column.
|
// @PURPOSE: Toggles sort direction or changes sort column.
|
||||||
// @PRE: column name is provided.
|
// @PRE: column name is provided.
|
||||||
// @POST: sortColumn and sortDirection state updated.
|
// @POST: sortColumn and sortDirection state updated.
|
||||||
@@ -123,9 +122,9 @@
|
|||||||
sortDirection = "asc";
|
sortDirection = "asc";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [/DEF:handleSort:Function]
|
// #endregion handleSort:Function
|
||||||
|
|
||||||
// [DEF:handleSelectionChange:Function]
|
// #region handleSelectionChange:Function [TYPE Function]
|
||||||
// @PURPOSE: Handles individual checkbox changes.
|
// @PURPOSE: Handles individual checkbox changes.
|
||||||
// @PRE: dashboard ID and checked status provided.
|
// @PRE: dashboard ID and checked status provided.
|
||||||
// @POST: selectedIds array updated.
|
// @POST: selectedIds array updated.
|
||||||
@@ -138,9 +137,9 @@
|
|||||||
}
|
}
|
||||||
selectedIds = newSelected;
|
selectedIds = newSelected;
|
||||||
}
|
}
|
||||||
// [/DEF:handleSelectionChange:Function]
|
// #endregion handleSelectionChange:Function
|
||||||
|
|
||||||
// [DEF:handleSelectAll:Function]
|
// #region handleSelectAll:Function [TYPE Function]
|
||||||
// @PURPOSE: Handles select all checkbox.
|
// @PURPOSE: Handles select all checkbox.
|
||||||
// @PRE: checked status provided.
|
// @PRE: checked status provided.
|
||||||
// @POST: selectedIds array updated for all paginated items.
|
// @POST: selectedIds array updated for all paginated items.
|
||||||
@@ -157,9 +156,9 @@
|
|||||||
}
|
}
|
||||||
selectedIds = newSelected;
|
selectedIds = newSelected;
|
||||||
}
|
}
|
||||||
// [/DEF:handleSelectAll:Function]
|
// #endregion handleSelectAll:Function
|
||||||
|
|
||||||
// [DEF:goToPage:Function]
|
// #region goToPage:Function [TYPE Function]
|
||||||
// @PURPOSE: Changes current page.
|
// @PURPOSE: Changes current page.
|
||||||
// @PRE: page index is provided.
|
// @PRE: page index is provided.
|
||||||
// @POST: currentPage state updated if within valid range.
|
// @POST: currentPage state updated if within valid range.
|
||||||
@@ -168,9 +167,9 @@
|
|||||||
currentPage = page;
|
currentPage = page;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [/DEF:goToPage:Function]
|
// #endregion goToPage:Function
|
||||||
|
|
||||||
// [DEF:openGit:Function]
|
// #region openGit:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Opens the Git management modal for a dashboard.
|
* @purpose Opens the Git management modal for a dashboard.
|
||||||
*/
|
*/
|
||||||
@@ -179,7 +178,7 @@
|
|||||||
gitDashboardTitle = dashboard.title;
|
gitDashboardTitle = dashboard.title;
|
||||||
showGitManager = true;
|
showGitManager = true;
|
||||||
}
|
}
|
||||||
// [/DEF:openGit:Function]
|
// #endregion openGit:Function
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
let formData = {};
|
let formData = {};
|
||||||
|
|
||||||
// [DEF:handleSubmit:Function]
|
// #region handleSubmit:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Emits submitted form data via callback prop.
|
* @purpose Emits submitted form data via callback prop.
|
||||||
* @pre formData contains user input.
|
* @pre formData contains user input.
|
||||||
@@ -30,9 +30,9 @@
|
|||||||
console.log("[DynamicForm][Action] Submitting form data.", { formData });
|
console.log("[DynamicForm][Action] Submitting form data.", { formData });
|
||||||
onsubmit(formData);
|
onsubmit(formData);
|
||||||
}
|
}
|
||||||
// [/DEF:handleSubmit:Function]
|
// #endregion handleSubmit:Function
|
||||||
|
|
||||||
// [DEF:initializeForm:Function]
|
// #region initializeForm:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Initialize form data with default values from the schema.
|
* @purpose Initialize form data with default values from the schema.
|
||||||
* @pre schema is provided and contains properties.
|
* @pre schema is provided and contains properties.
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [/DEF:initializeForm:Function]
|
// #endregion initializeForm:Function
|
||||||
|
|
||||||
initializeForm();
|
initializeForm();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [DEF:handleSelect:Function]
|
// #region handleSelect:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Propagates the selection change through a callback prop.
|
* @purpose Propagates the selection change through a callback prop.
|
||||||
* @pre event.target must be an HTMLSelectElement.
|
* @pre event.target must be an HTMLSelectElement.
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
selectedId = target.value;
|
selectedId = target.value;
|
||||||
onchange({ id: selectedId });
|
onchange({ id: selectedId });
|
||||||
}
|
}
|
||||||
// [/DEF:handleSelect:Function]
|
// #endregion handleSelect:Function
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<!-- #region Footer [C:3] [TYPE Component] [SEMANTICS footer, layout, copyright] -->
|
<!-- #region Footer [C:3] [TYPE Component] [SEMANTICS footer, layout, copyright] -->
|
||||||
<!-- @BRIEF Component component: components/Footer.svelte -->
|
<!-- @BRIEF Application footer displaying copyright information. -->
|
||||||
<!-- @LAYER UI -->
|
<!-- @LAYER UI -->
|
||||||
|
<!-- @RELATION DEPENDS_ON -> [RootLayout] -->
|
||||||
<!--
|
<!--
|
||||||
@COMPLEXITY: 1
|
|
||||||
@SEMANTICS: footer, layout, copyright
|
@SEMANTICS: footer, layout, copyright
|
||||||
@PURPOSE: Displays the application footer with copyright information.
|
@PURPOSE: Displays the application footer with copyright information.
|
||||||
@LAYER: UI
|
@LAYER: UI
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [DEF:updateMapping:Function]
|
// #region updateMapping:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Updates a mapping for a specific source database.
|
* @purpose Updates a mapping for a specific source database.
|
||||||
* @pre sourceUuid and targetUuid are provided.
|
* @pre sourceUuid and targetUuid are provided.
|
||||||
@@ -44,9 +44,9 @@
|
|||||||
engine: sDb?.engine || ""
|
engine: sDb?.engine || ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// [/DEF:updateMapping:Function]
|
// #endregion updateMapping:Function
|
||||||
|
|
||||||
// [DEF:getSuggestion:Function]
|
// #region getSuggestion:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Finds a suggestion for a source database.
|
* @purpose Finds a suggestion for a source database.
|
||||||
* @pre sourceUuid is provided.
|
* @pre sourceUuid is provided.
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
function getSuggestion(sourceUuid: string) {
|
function getSuggestion(sourceUuid: string) {
|
||||||
return suggestions.find(s => s.source_db_uuid === sourceUuid);
|
return suggestions.find(s => s.source_db_uuid === sourceUuid);
|
||||||
}
|
}
|
||||||
// [/DEF:getSuggestion:Function]
|
// #endregion getSuggestion:Function
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
let selectedTargetUuid = $state("");
|
let selectedTargetUuid = $state("");
|
||||||
|
|
||||||
// [DEF:resolve:Function]
|
// #region resolve:Function [TYPE Function]
|
||||||
// @PURPOSE: Resolves the missing mapping via callback prop.
|
// @PURPOSE: Resolves the missing mapping via callback prop.
|
||||||
// @PRE: selectedTargetUuid must be set.
|
// @PRE: selectedTargetUuid must be set.
|
||||||
// @POST: Parent callback receives mapping payload and modal closes.
|
// @POST: Parent callback receives mapping payload and modal closes.
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
});
|
});
|
||||||
show = false;
|
show = false;
|
||||||
}
|
}
|
||||||
// [/DEF:resolve:Function]
|
// #endregion resolve:Function
|
||||||
|
|
||||||
// [DEF:cancel:Function]
|
// #region cancel:Function [TYPE Function]
|
||||||
// @PURPOSE: Cancels the mapping resolution modal.
|
// @PURPOSE: Cancels the mapping resolution modal.
|
||||||
// @PRE: Modal is open.
|
// @PRE: Modal is open.
|
||||||
// @POST: Parent cancel callback is invoked and modal is hidden.
|
// @POST: Parent cancel callback is invoked and modal is hidden.
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
oncancel();
|
oncancel();
|
||||||
show = false;
|
show = false;
|
||||||
}
|
}
|
||||||
// [/DEF:cancel:Function]
|
// #endregion cancel:Function
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<!-- @BRIEF Component component: components/Navbar.svelte -->
|
<!-- @BRIEF Component component: components/Navbar.svelte -->
|
||||||
<!-- @LAYER UI -->
|
<!-- @LAYER UI -->
|
||||||
<!--
|
<!--
|
||||||
@COMPLEXITY: 3
|
|
||||||
@SEMANTICS: navbar, navigation, header, layout
|
@SEMANTICS: navbar, navigation, header, layout
|
||||||
@PURPOSE: Main navigation bar for the application.
|
@PURPOSE: Main navigation bar for the application.
|
||||||
@LAYER: UI
|
@LAYER: UI
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
let passwords = $state({});
|
let passwords = $state({});
|
||||||
let submitting = $state(false);
|
let submitting = $state(false);
|
||||||
|
|
||||||
// [DEF:handleSubmit:Function]
|
// #region handleSubmit:Function [TYPE Function]
|
||||||
// @PURPOSE: Validates and forwards passwords to resume the task.
|
// @PURPOSE: Validates and forwards passwords to resume the task.
|
||||||
// @PRE: All database passwords must be entered.
|
// @PRE: All database passwords must be entered.
|
||||||
// @POST: Parent resume callback receives passwords payload.
|
// @POST: Parent resume callback receives passwords payload.
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
onresume({ passwords });
|
onresume({ passwords });
|
||||||
// Reset submitting state is handled by parent or on close
|
// Reset submitting state is handled by parent or on close
|
||||||
}
|
}
|
||||||
// [/DEF:handleSubmit:Function]
|
// #endregion handleSubmit:Function
|
||||||
|
|
||||||
// [DEF:handleCancel:Function]
|
// #region handleCancel:Function [TYPE Function]
|
||||||
// @PURPOSE: Cancels the password prompt.
|
// @PURPOSE: Cancels the password prompt.
|
||||||
// @PRE: Modal is open.
|
// @PRE: Modal is open.
|
||||||
// @POST: Parent cancel callback is invoked and show is set to false.
|
// @POST: Parent cancel callback is invoked and show is set to false.
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
oncancel();
|
oncancel();
|
||||||
show = false;
|
show = false;
|
||||||
}
|
}
|
||||||
// [/DEF:handleCancel:Function]
|
// #endregion handleCancel:Function
|
||||||
|
|
||||||
// Reset passwords when modal opens/closes
|
// Reset passwords when modal opens/closes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<!-- @BRIEF Component component: components/RepositoryDashboardGrid.svelte -->
|
<!-- @BRIEF Component component: components/RepositoryDashboardGrid.svelte -->
|
||||||
<!-- @LAYER UI -->
|
<!-- @LAYER UI -->
|
||||||
<!--
|
<!--
|
||||||
@COMPLEXITY: 3
|
|
||||||
@SEMANTICS: dashboard, grid, selection, pagination
|
@SEMANTICS: dashboard, grid, selection, pagination
|
||||||
@PURPOSE: Displays a grid of dashboards with selection and pagination.
|
@PURPOSE: Displays a grid of dashboards with selection and pagination.
|
||||||
@LAYER: Component
|
@LAYER: Component
|
||||||
@@ -98,7 +97,7 @@
|
|||||||
);
|
);
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [DEF:handleSort:Function]
|
// #region handleSort:Function [TYPE Function]
|
||||||
// @PURPOSE: Toggles sort direction or changes sort column.
|
// @PURPOSE: Toggles sort direction or changes sort column.
|
||||||
// @PRE: column name is provided.
|
// @PRE: column name is provided.
|
||||||
// @POST: sortColumn and sortDirection state updated.
|
// @POST: sortColumn and sortDirection state updated.
|
||||||
@@ -110,9 +109,9 @@
|
|||||||
sortDirection = "asc";
|
sortDirection = "asc";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [/DEF:handleSort:Function]
|
// #endregion handleSort:Function
|
||||||
|
|
||||||
// [DEF:handleSelectionChange:Function]
|
// #region handleSelectionChange:Function [TYPE Function]
|
||||||
// @PURPOSE: Handles individual checkbox changes.
|
// @PURPOSE: Handles individual checkbox changes.
|
||||||
// @PRE: dashboard ID and checked status provided.
|
// @PRE: dashboard ID and checked status provided.
|
||||||
// @POST: selectedIds array updated.
|
// @POST: selectedIds array updated.
|
||||||
@@ -125,9 +124,9 @@
|
|||||||
}
|
}
|
||||||
selectedIds = newSelected;
|
selectedIds = newSelected;
|
||||||
}
|
}
|
||||||
// [/DEF:handleSelectionChange:Function]
|
// #endregion handleSelectionChange:Function
|
||||||
|
|
||||||
// [DEF:handleSelectAll:Function]
|
// #region handleSelectAll:Function [TYPE Function]
|
||||||
// @PURPOSE: Handles select all checkbox.
|
// @PURPOSE: Handles select all checkbox.
|
||||||
// @PRE: checked status provided.
|
// @PRE: checked status provided.
|
||||||
// @POST: selectedIds array updated for all paginated items.
|
// @POST: selectedIds array updated for all paginated items.
|
||||||
@@ -144,9 +143,9 @@
|
|||||||
}
|
}
|
||||||
selectedIds = newSelected;
|
selectedIds = newSelected;
|
||||||
}
|
}
|
||||||
// [/DEF:handleSelectAll:Function]
|
// #endregion handleSelectAll:Function
|
||||||
|
|
||||||
// [DEF:goToPage:Function]
|
// #region goToPage:Function [TYPE Function]
|
||||||
// @PURPOSE: Changes current page.
|
// @PURPOSE: Changes current page.
|
||||||
// @PRE: page index is provided.
|
// @PRE: page index is provided.
|
||||||
// @POST: currentPage state updated if within valid range.
|
// @POST: currentPage state updated if within valid range.
|
||||||
@@ -155,9 +154,9 @@
|
|||||||
currentPage = page;
|
currentPage = page;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [/DEF:goToPage:Function]
|
// #endregion goToPage:Function
|
||||||
|
|
||||||
// [DEF:getRepositoryStatusToken:Function]
|
// #region getRepositoryStatusToken:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Returns normalized repository status token for a dashboard.
|
* @purpose Returns normalized repository status token for a dashboard.
|
||||||
* @pre Dashboard exists.
|
* @pre Dashboard exists.
|
||||||
@@ -166,17 +165,17 @@
|
|||||||
function getRepositoryStatusToken(dashboardId: number): string {
|
function getRepositoryStatusToken(dashboardId: number): string {
|
||||||
return repositoryStatusByDashboardId[dashboardId] || "loading";
|
return repositoryStatusByDashboardId[dashboardId] || "loading";
|
||||||
}
|
}
|
||||||
// [/DEF:getRepositoryStatusToken:Function]
|
// #endregion getRepositoryStatusToken:Function
|
||||||
|
|
||||||
// [DEF:isRepositoryReady:Function]
|
// #region isRepositoryReady:Function [TYPE Function]
|
||||||
// @PURPOSE: Determines whether git actions can run for a dashboard.
|
// @PURPOSE: Determines whether git actions can run for a dashboard.
|
||||||
function isRepositoryReady(dashboardId: number): boolean {
|
function isRepositoryReady(dashboardId: number): boolean {
|
||||||
const token = getRepositoryStatusToken(dashboardId);
|
const token = getRepositoryStatusToken(dashboardId);
|
||||||
return token !== "loading" && token !== "no_repo" && token !== "error";
|
return token !== "loading" && token !== "no_repo" && token !== "error";
|
||||||
}
|
}
|
||||||
// [/DEF:isRepositoryReady:Function]
|
// #endregion isRepositoryReady:Function
|
||||||
|
|
||||||
// [DEF:invalidateRepositoryStatuses:Function]
|
// #region invalidateRepositoryStatuses:Function [TYPE Function]
|
||||||
// @PURPOSE: Marks dashboard statuses as loading so they are refetched.
|
// @PURPOSE: Marks dashboard statuses as loading so they are refetched.
|
||||||
function invalidateRepositoryStatuses(dashboardIds: number[]): void {
|
function invalidateRepositoryStatuses(dashboardIds: number[]): void {
|
||||||
if (dashboardIds.length === 0) return;
|
if (dashboardIds.length === 0) return;
|
||||||
@@ -186,9 +185,9 @@
|
|||||||
});
|
});
|
||||||
repositoryStatusByDashboardId = nextStatuses;
|
repositoryStatusByDashboardId = nextStatuses;
|
||||||
}
|
}
|
||||||
// [/DEF:invalidateRepositoryStatuses:Function]
|
// #endregion invalidateRepositoryStatuses:Function
|
||||||
|
|
||||||
// [DEF:resolveRepositoryStatusToken:Function]
|
// #region resolveRepositoryStatusToken:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Converts git status payload into a stable UI status token.
|
* @purpose Converts git status payload into a stable UI status token.
|
||||||
*/
|
*/
|
||||||
@@ -220,9 +219,9 @@
|
|||||||
|
|
||||||
return hasChanges ? "changes" : "synced";
|
return hasChanges ? "changes" : "synced";
|
||||||
}
|
}
|
||||||
// [/DEF:resolveRepositoryStatusToken:Function]
|
// #endregion resolveRepositoryStatusToken:Function
|
||||||
|
|
||||||
// [DEF:loadRepositoryStatuses:Function]
|
// #region loadRepositoryStatuses:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Hydrates repository status map for dashboards in repository mode.
|
* @purpose Hydrates repository status map for dashboards in repository mode.
|
||||||
*/
|
*/
|
||||||
@@ -279,9 +278,9 @@
|
|||||||
...Object.fromEntries(entries),
|
...Object.fromEntries(entries),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// [/DEF:loadRepositoryStatuses:Function]
|
// #endregion loadRepositoryStatuses:Function
|
||||||
|
|
||||||
// [DEF:runBulkGitAction:Function]
|
// #region runBulkGitAction:Function [TYPE Function]
|
||||||
// @PURPOSE: Executes git action for selected dashboards with limited parallelism.
|
// @PURPOSE: Executes git action for selected dashboards with limited parallelism.
|
||||||
async function runBulkGitAction(
|
async function runBulkGitAction(
|
||||||
actionToken: string,
|
actionToken: string,
|
||||||
@@ -333,15 +332,15 @@
|
|||||||
bulkActionRunning = false;
|
bulkActionRunning = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [/DEF:runBulkGitAction:Function]
|
// #endregion runBulkGitAction:Function
|
||||||
|
|
||||||
// [DEF:handleBulkSync:Function]
|
// #region handleBulkSync:Function [TYPE Function]
|
||||||
async function handleBulkSync(): Promise<void> {
|
async function handleBulkSync(): Promise<void> {
|
||||||
await runBulkGitAction("sync", (dashboardId) => gitService.sync(dashboardId, null, envId));
|
await runBulkGitAction("sync", (dashboardId) => gitService.sync(dashboardId, null, envId));
|
||||||
}
|
}
|
||||||
// [/DEF:handleBulkSync:Function]
|
// #endregion handleBulkSync:Function
|
||||||
|
|
||||||
// [DEF:handleBulkCommit:Function]
|
// #region handleBulkCommit:Function [TYPE Function]
|
||||||
async function handleBulkCommit(): Promise<void> {
|
async function handleBulkCommit(): Promise<void> {
|
||||||
const message = prompt($t.git?.commit_message);
|
const message = prompt($t.git?.commit_message);
|
||||||
if (!message?.trim()) return;
|
if (!message?.trim()) return;
|
||||||
@@ -349,21 +348,21 @@
|
|||||||
gitService.commit(dashboardId, message.trim(), [], envId),
|
gitService.commit(dashboardId, message.trim(), [], envId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// [/DEF:handleBulkCommit:Function]
|
// #endregion handleBulkCommit:Function
|
||||||
|
|
||||||
// [DEF:handleBulkPull:Function]
|
// #region handleBulkPull:Function [TYPE Function]
|
||||||
async function handleBulkPull(): Promise<void> {
|
async function handleBulkPull(): Promise<void> {
|
||||||
await runBulkGitAction("pull", (dashboardId) => gitService.pull(dashboardId, envId));
|
await runBulkGitAction("pull", (dashboardId) => gitService.pull(dashboardId, envId));
|
||||||
}
|
}
|
||||||
// [/DEF:handleBulkPull:Function]
|
// #endregion handleBulkPull:Function
|
||||||
|
|
||||||
// [DEF:handleBulkPush:Function]
|
// #region handleBulkPush:Function [TYPE Function]
|
||||||
async function handleBulkPush(): Promise<void> {
|
async function handleBulkPush(): Promise<void> {
|
||||||
await runBulkGitAction("push", (dashboardId) => gitService.push(dashboardId, envId));
|
await runBulkGitAction("push", (dashboardId) => gitService.push(dashboardId, envId));
|
||||||
}
|
}
|
||||||
// [/DEF:handleBulkPush:Function]
|
// #endregion handleBulkPush:Function
|
||||||
|
|
||||||
// [DEF:handleBulkDelete:Function]
|
// #region handleBulkDelete:Function [TYPE Function]
|
||||||
// @PURPOSE: Removes selected repositories from storage and binding table.
|
// @PURPOSE: Removes selected repositories from storage and binding table.
|
||||||
async function handleBulkDelete(): Promise<void> {
|
async function handleBulkDelete(): Promise<void> {
|
||||||
if (!confirm($t.git?.confirm_delete_repo || "Удалить выбранные репозитории?")) return;
|
if (!confirm($t.git?.confirm_delete_repo || "Удалить выбранные репозитории?")) return;
|
||||||
@@ -374,9 +373,9 @@
|
|||||||
dashboards = dashboards.filter((dashboard) => !idsToDelete.includes(dashboard.id));
|
dashboards = dashboards.filter((dashboard) => !idsToDelete.includes(dashboard.id));
|
||||||
selectedIds = [];
|
selectedIds = [];
|
||||||
}
|
}
|
||||||
// [/DEF:handleBulkDelete:Function]
|
// #endregion handleBulkDelete:Function
|
||||||
|
|
||||||
// [DEF:handleManageSelected:Function]
|
// #region handleManageSelected:Function [TYPE Function]
|
||||||
// @PURPOSE: Opens Git manager for exactly one selected dashboard.
|
// @PURPOSE: Opens Git manager for exactly one selected dashboard.
|
||||||
async function handleManageSelected(): Promise<void> {
|
async function handleManageSelected(): Promise<void> {
|
||||||
if (selectedIds.length !== 1) {
|
if (selectedIds.length !== 1) {
|
||||||
@@ -390,9 +389,9 @@
|
|||||||
);
|
);
|
||||||
openGitManagerForDashboard(selectedDashboard || null);
|
openGitManagerForDashboard(selectedDashboard || null);
|
||||||
}
|
}
|
||||||
// [/DEF:handleManageSelected:Function]
|
// #endregion handleManageSelected:Function
|
||||||
|
|
||||||
// [DEF:resolveDashboardRef:Function]
|
// #region resolveDashboardRef:Function [TYPE Function]
|
||||||
// @PURPOSE: Resolves dashboard slug from payload fields.
|
// @PURPOSE: Resolves dashboard slug from payload fields.
|
||||||
// @PRE: Dashboard metadata is provided.
|
// @PRE: Dashboard metadata is provided.
|
||||||
// @POST: Returns slug string or null if unavailable.
|
// @POST: Returns slug string or null if unavailable.
|
||||||
@@ -411,9 +410,9 @@
|
|||||||
if (!slugMatch?.[1]) return null;
|
if (!slugMatch?.[1]) return null;
|
||||||
return decodeURIComponent(slugMatch[1]);
|
return decodeURIComponent(slugMatch[1]);
|
||||||
}
|
}
|
||||||
// [/DEF:resolveDashboardRef:Function]
|
// #endregion resolveDashboardRef:Function
|
||||||
|
|
||||||
// [DEF:openGitManagerForDashboard:Function]
|
// #region openGitManagerForDashboard:Function [TYPE Function]
|
||||||
// @PURPOSE: Opens Git manager for provided dashboard metadata.
|
// @PURPOSE: Opens Git manager for provided dashboard metadata.
|
||||||
function openGitManagerForDashboard(dashboard: DashboardMetadata | null): void {
|
function openGitManagerForDashboard(dashboard: DashboardMetadata | null): void {
|
||||||
if (!dashboard) return;
|
if (!dashboard) return;
|
||||||
@@ -426,9 +425,9 @@
|
|||||||
gitDashboardTitle = dashboard.title || "";
|
gitDashboardTitle = dashboard.title || "";
|
||||||
showGitManager = true;
|
showGitManager = true;
|
||||||
}
|
}
|
||||||
// [/DEF:openGitManagerForDashboard:Function]
|
// #endregion openGitManagerForDashboard:Function
|
||||||
|
|
||||||
// [DEF:handleInitializeRepositories:Function]
|
// #region handleInitializeRepositories:Function [TYPE Function]
|
||||||
// @PURPOSE: Opens Git manager from bulk actions to initialize selected repository.
|
// @PURPOSE: Opens Git manager from bulk actions to initialize selected repository.
|
||||||
async function handleInitializeRepositories(): Promise<void> {
|
async function handleInitializeRepositories(): Promise<void> {
|
||||||
if (selectedIds.length !== 1) {
|
if (selectedIds.length !== 1) {
|
||||||
@@ -439,9 +438,9 @@
|
|||||||
const selectedDashboard = dashboards.find((dashboard) => dashboard.id === selectedDashboardId) || null;
|
const selectedDashboard = dashboards.find((dashboard) => dashboard.id === selectedDashboardId) || null;
|
||||||
openGitManagerForDashboard(selectedDashboard);
|
openGitManagerForDashboard(selectedDashboard);
|
||||||
}
|
}
|
||||||
// [/DEF:handleInitializeRepositories:Function]
|
// #endregion handleInitializeRepositories:Function
|
||||||
|
|
||||||
// [DEF:getSortStatusValue:Function]
|
// #region getSortStatusValue:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Returns sort value for status column based on mode.
|
* @purpose Returns sort value for status column based on mode.
|
||||||
*/
|
*/
|
||||||
@@ -451,9 +450,9 @@
|
|||||||
}
|
}
|
||||||
return String(dashboard.status || "").toLowerCase();
|
return String(dashboard.status || "").toLowerCase();
|
||||||
}
|
}
|
||||||
// [/DEF:getSortStatusValue:Function]
|
// #endregion getSortStatusValue:Function
|
||||||
|
|
||||||
// [DEF:getStatusLabel:Function]
|
// #region getStatusLabel:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Returns localized label for status column.
|
* @purpose Returns localized label for status column.
|
||||||
*/
|
*/
|
||||||
@@ -464,9 +463,9 @@
|
|||||||
const token = getRepositoryStatusToken(dashboard.id);
|
const token = getRepositoryStatusToken(dashboard.id);
|
||||||
return $t.git?.repo_status?.[token] || token;
|
return $t.git?.repo_status?.[token] || token;
|
||||||
}
|
}
|
||||||
// [/DEF:getStatusLabel:Function]
|
// #endregion getStatusLabel:Function
|
||||||
|
|
||||||
// [DEF:getStatusBadgeClass:Function]
|
// #region getStatusBadgeClass:Function [TYPE Function]
|
||||||
/**
|
/**
|
||||||
* @purpose Returns badge style for status column.
|
* @purpose Returns badge style for status column.
|
||||||
*/
|
*/
|
||||||
@@ -487,7 +486,7 @@
|
|||||||
if (token === "diverged") return "bg-purple-100 text-purple-800";
|
if (token === "diverged") return "bg-purple-100 text-purple-800";
|
||||||
return "bg-rose-100 text-rose-800";
|
return "bg-rose-100 text-rose-800";
|
||||||
}
|
}
|
||||||
// [/DEF:getStatusBadgeClass:Function]
|
// #endregion getStatusBadgeClass:Function
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
dashboards;
|
dashboards;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<!-- @LAYER UI -->
|
<!-- @LAYER UI -->
|
||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* @COMPLEXITY: 3
|
|
||||||
* @PURPOSE: Blocking startup wizard for creating the first Superset environment from zero-state screens.
|
* @PURPOSE: Blocking startup wizard for creating the first Superset environment from zero-state screens.
|
||||||
* @LAYER: UI
|
* @LAYER: UI
|
||||||
* @RELATION: CALLS -> api
|
* @RELATION: CALLS -> api
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user