chore(lint): apply ruff --fix (4443 auto-fixes)

Auto-fixed categories:
- F401: unused imports removed
- I001: import blocks sorted
- W293: trailing whitespace stripped
- UP035: deprecated typing imports replaced
- SIM: simplify suggestions applied
- ARG: unused args prefixed with underscore
- T201: print statements removed
- F841: unused variables removed
- RUF059: unpacked variables prefixed

Remaining ~1500 unfixable errors (C901, B904, N806, E402) require manual work.

Backend smoke tests: 13/13 passed.
This commit is contained in:
2026-05-14 11:20:17 +03:00
parent a9a5eff518
commit c6189876b3
337 changed files with 4677 additions and 4515 deletions

View File

@@ -8,17 +8,19 @@
# @RELATION DEPENDS_ON -> [is_adfs_configured]
# @INVARIANT: All auth endpoints must return consistent error codes.
import starlette.requests
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from ..core.database import get_auth_db
from ..services.auth_service import AuthService
from ..schemas.auth import Token, User as UserSchema
from ..dependencies import get_current_user
from ..core.auth.oauth import oauth, is_adfs_configured
from ..core.auth.logger import log_security_event
from ..core.auth.oauth import is_adfs_configured, oauth
from ..core.database import get_auth_db
from ..core.logger import belief_scope
import starlette.requests
from ..dependencies import get_current_user
from ..schemas.auth import Token
from ..schemas.auth import User as UserSchema
from ..services.auth_service import AuthService
# #region router [C:1] [TYPE Variable]
# @RELATION DEPENDS_ON -> [fastapi.APIRouter]

View File

@@ -14,26 +14,26 @@
# @RELATION DEPENDS_ON -> [ReportsRouter]
# @RELATION DEPENDS_ON -> [LlmRoutes]
__all__ = [
"plugins",
"tasks",
"settings",
"connections",
"environments",
"mappings",
"migration",
"git",
"storage",
"admin",
"reports",
"assistant",
"clean_release",
"clean_release_v2",
"profile",
"dataset_review",
"llm",
"connections",
"dashboards",
"dataset_review",
"datasets",
"environments",
"git",
"health",
"llm",
"mappings",
"migration",
"plugins",
"profile",
"reports",
"settings",
"storage",
"tasks",
"translate",
]
# #endregion Route_Group_Contracts

View File

@@ -28,4 +28,4 @@ class FakeQuery:
return list(self._rows)
def count(self):
return len(self._rows)
# #endregion RoutesTestsConftest
# #endregion RoutesTestsConftest

View File

@@ -1,4 +1,5 @@
import os
os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w="
# #region AssistantApiTests [TYPE Module] [C:3] [SEMANTICS tests, assistant, api]
# @BRIEF Validate assistant API endpoint logic via direct async handler invocation.
@@ -6,14 +7,10 @@ os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w="
# @INVARIANT: Every test clears assistant in-memory state before execution.
import asyncio
import uuid
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple
from datetime import datetime
from unittest.mock import MagicMock
import pytest
from fastapi import HTTPException
from pydantic import BaseModel
from src.api.routes import assistant as assistant_routes
from src.schemas.auth import User
from src.models.assistant import AssistantMessageRecord
from src.models.dataset_review import (
ApprovalState,
@@ -22,13 +19,16 @@ from src.models.dataset_review import (
ExecutionMapping,
ImportedFilter,
MappingMethod,
SemanticCandidate,
SemanticFieldEntry,
ReadinessState,
RecommendedAction,
SemanticCandidate,
SemanticFieldEntry,
SessionPhase,
SessionStatus,
)
from src.schemas.auth import User
# #region _run_async [TYPE Function]
# @RELATION BINDS_TO -> [AssistantApiTests]
def _run_async(coro):
@@ -437,4 +437,4 @@ def test_dataset_review_scoped_command_routes_field_semantics_update():
assert resp.intent["entities"]["description"] == "Approved semantic wording"
assert resp.intent["entities"]["lock_field"] is True
# #endregion test_dataset_review_scoped_command_routes_field_semantics_update
# #endregion AssistantApiTests
# #endregion AssistantApiTests

View File

@@ -1,16 +1,19 @@
import os
os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w="
# #region TestAssistantAuthz [TYPE Module] [C:3] [SEMANTICS tests, assistant, authz, confirmation, rbac]
# @BRIEF Verify assistant confirmation ownership, expiration, and deny behavior for restricted users.
# @LAYER: UI (API Tests)
# @RELATION DEPENDS_ON -> AssistantApi
# @INVARIANT: Security-sensitive flows fail closed for unauthorized actors.
import os
import asyncio
import os
from datetime import datetime, timedelta
from types import SimpleNamespace
import pytest
from fastapi import HTTPException
# Force isolated sqlite databases for test module before dependencies import.
os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_authz.db")
os.environ.setdefault(
@@ -25,6 +28,8 @@ from src.models.assistant import (
AssistantConfirmationRecord,
AssistantMessageRecord,
)
# #region _run_async [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Execute async endpoint handler in synchronous test context.
@@ -279,4 +284,4 @@ def test_limited_user_cannot_launch_restricted_operation():
)
assert response.state == "denied"
# #endregion test_limited_user_cannot_launch_restricted_operation
# #endregion TestAssistantAuthz
# #endregion TestAssistantAuthz

View File

@@ -3,21 +3,25 @@
# @BRIEF Contract tests for clean release checks and reports endpoints.
# @LAYER: Domain
# @INVARIANT: API returns deterministic payload shapes for checks and reports.
from datetime import datetime, timezone
from datetime import UTC, datetime
from fastapi.testclient import TestClient
from src.app import app
from src.dependencies import get_clean_release_repository
from src.models.clean_release import (
CheckFinalStatus,
CleanProfilePolicy,
ComplianceReport,
ProfileType,
ReleaseCandidate,
ReleaseCandidateStatus,
ResourceSourceEntry,
ResourceSourceRegistry,
ComplianceReport,
CheckFinalStatus,
)
from src.services.clean_release.repository import CleanReleaseRepository
# #region _repo_with_seed_data [TYPE Function]
# @RELATION BINDS_TO -> TestCleanReleaseApi
def _repo_with_seed_data() -> CleanReleaseRepository:
@@ -27,7 +31,7 @@ def _repo_with_seed_data() -> CleanReleaseRepository:
candidate_id="2026.03.03-rc1",
version="2026.03.03",
profile=ProfileType.ENTERPRISE_CLEAN,
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
created_by="tester",
source_snapshot_ref="git:abc123",
status=ReleaseCandidateStatus.PREPARED,
@@ -46,7 +50,7 @@ def _repo_with_seed_data() -> CleanReleaseRepository:
enabled=True,
)
],
updated_at=datetime.now(timezone.utc),
updated_at=datetime.now(UTC),
updated_by="tester",
status="active",
)
@@ -60,7 +64,7 @@ def _repo_with_seed_data() -> CleanReleaseRepository:
required_system_categories=["system-init"],
external_source_forbidden=True,
internal_source_registry_ref="registry-internal-v1",
effective_from=datetime.now(timezone.utc),
effective_from=datetime.now(UTC),
profile=ProfileType.ENTERPRISE_CLEAN,
)
)
@@ -120,7 +124,7 @@ def test_get_report_success():
report_id="rep-1",
check_run_id="run-1",
candidate_id="2026.03.03-rc1",
generated_at=datetime.now(timezone.utc),
generated_at=datetime.now(UTC),
final_status=CheckFinalStatus.COMPLIANT,
operator_summary="all systems go",
structured_payload_ref="manifest-1",
@@ -163,4 +167,4 @@ def test_prepare_candidate_api_success():
finally:
app.dependency_overrides.clear()
# #endregion test_prepare_candidate_api_success
# #endregion TestCleanReleaseApi
# #endregion TestCleanReleaseApi

View File

@@ -3,9 +3,12 @@
# @BRIEF Compatibility tests for legacy clean-release API paths retained during v2 migration.
# @LAYER: Tests
from __future__ import annotations
import os
from datetime import datetime, timezone
from datetime import UTC, datetime
from fastapi.testclient import TestClient
os.environ.setdefault("DATABASE_URL", "sqlite:///./test_clean_release_legacy_compat.db")
os.environ.setdefault(
"AUTH_DATABASE_URL", "sqlite:///./test_clean_release_legacy_auth.db"
@@ -22,6 +25,8 @@ from src.models.clean_release import (
ResourceSourceRegistry,
)
from src.services.clean_release.repository import CleanReleaseRepository
# #region _seed_legacy_repo [TYPE Function]
# @RELATION BINDS_TO -> TestCleanReleaseLegacyCompat
# @BRIEF Seed in-memory repository with minimum trusted data for legacy endpoint contracts.
@@ -29,7 +34,7 @@ from src.services.clean_release.repository import CleanReleaseRepository
# @POST: Candidate, policy, registry and manifest are available for legacy checks flow.
def _seed_legacy_repo() -> CleanReleaseRepository:
repo = CleanReleaseRepository()
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
repo.save_candidate(
ReleaseCandidate(
id="legacy-rc-001",
@@ -56,10 +61,10 @@ def _seed_legacy_repo() -> CleanReleaseRepository:
updated_by="compat-tester",
status="ACTIVE",
)
setattr(registry, "immutable", True)
setattr(registry, "allowed_hosts", ["repo.intra.company.local"])
setattr(registry, "allowed_schemes", ["https"])
setattr(registry, "allowed_source_types", ["artifact-repo"])
registry.immutable = True
registry.allowed_hosts = ["repo.intra.company.local"]
registry.allowed_schemes = ["https"]
registry.allowed_source_types = ["artifact-repo"]
repo.save_registry(registry)
policy = CleanProfilePolicy(
policy_id="legacy-pol-1",
@@ -71,17 +76,8 @@ def _seed_legacy_repo() -> CleanReleaseRepository:
required_system_categories=["core"],
effective_from=now,
)
setattr(policy, "immutable", True)
setattr(
policy,
"content_json",
{
"profile": "enterprise-clean",
"prohibited_artifact_categories": ["test-data"],
"required_system_categories": ["core"],
"external_source_forbidden": True,
},
)
policy.immutable = True
policy.content_json = {"profile": "enterprise-clean", "prohibited_artifact_categories": ["test-data"], "required_system_categories": ["core"], "external_source_forbidden": True}
repo.save_policy(policy)
repo.save_manifest(
DistributionManifest(
@@ -160,4 +156,4 @@ def test_legacy_checks_endpoints_still_available() -> None:
finally:
app.dependency_overrides.clear()
# #endregion test_legacy_checks_endpoints_still_available
# #endregion TestCleanReleaseLegacyCompat
# #endregion TestCleanReleaseLegacyCompat

View File

@@ -3,8 +3,10 @@
# @BRIEF Validate API behavior for source isolation violations in clean release preparation.
# @LAYER: Domain
# @INVARIANT: External endpoints must produce blocking violation entries.
from datetime import datetime, timezone
from datetime import UTC, datetime
from fastapi.testclient import TestClient
from src.app import app
from src.dependencies import get_clean_release_repository
from src.models.clean_release import (
@@ -16,6 +18,8 @@ from src.models.clean_release import (
ResourceSourceRegistry,
)
from src.services.clean_release.repository import CleanReleaseRepository
# #region _repo_with_seed_data [TYPE Function]
# @RELATION BINDS_TO -> TestCleanReleaseSourcePolicy
# @BRIEF Seed repository with candidate, registry, and active policy for source isolation test flow.
@@ -26,7 +30,7 @@ def _repo_with_seed_data() -> CleanReleaseRepository:
candidate_id="2026.03.03-rc1",
version="2026.03.03",
profile=ProfileType.ENTERPRISE_CLEAN,
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
created_by="tester",
source_snapshot_ref="git:abc123",
status=ReleaseCandidateStatus.DRAFT,
@@ -45,7 +49,7 @@ def _repo_with_seed_data() -> CleanReleaseRepository:
enabled=True,
)
],
updated_at=datetime.now(timezone.utc),
updated_at=datetime.now(UTC),
updated_by="tester",
status="active",
)
@@ -59,7 +63,7 @@ def _repo_with_seed_data() -> CleanReleaseRepository:
required_system_categories=["system-init"],
external_source_forbidden=True,
internal_source_registry_ref="registry-internal-v1",
effective_from=datetime.now(timezone.utc),
effective_from=datetime.now(UTC),
profile=ProfileType.ENTERPRISE_CLEAN,
)
)
@@ -95,4 +99,4 @@ def test_prepare_candidate_blocks_external_source():
finally:
app.dependency_overrides.clear()
# #endregion test_prepare_candidate_blocks_external_source
# #endregion TestCleanReleaseSourcePolicy
# #endregion TestCleanReleaseSourcePolicy

View File

@@ -2,20 +2,11 @@
# @BRIEF API contract tests for redesigned clean release endpoints.
# @LAYER: Domain
# @RELATION DEPENDS_ON -> [CleanReleaseV2Api]
from datetime import datetime, timezone
from types import SimpleNamespace
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from src.app import app
from src.dependencies import get_clean_release_repository, get_config_manager
from src.models.clean_release import (
CleanPolicySnapshot,
DistributionManifest,
ReleaseCandidate,
SourceRegistrySnapshot,
)
from src.services.clean_release.enums import CandidateStatus
client = TestClient(app)
# [REASON] Implementing API contract tests for candidate/artifact/manifest endpoints (T012).
# #region test_candidate_registration_contract [TYPE Function]
@@ -93,4 +84,4 @@ def test_manifest_build_contract():
assert "manifest_digest" in data
assert data["candidate_id"] == candidate_id
# #endregion test_manifest_build_contract
# #endregion CleanReleaseV2ApiTests
# #endregion CleanReleaseV2ApiTests

View File

@@ -3,14 +3,17 @@
# @LAYER: Domain
# @RELATION DEPENDS_ON -> [CleanReleaseV2Api]
"""Contract tests for redesigned approval/publication API endpoints."""
from datetime import datetime, timezone
from datetime import UTC, datetime
from uuid import uuid4
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.routes.clean_release_v2 import router as clean_release_v2_router
from src.dependencies import get_clean_release_repository
from src.models.clean_release import ComplianceReport, ReleaseCandidate
from src.services.clean_release.enums import CandidateStatus, ComplianceDecision
test_app = FastAPI()
test_app.include_router(clean_release_v2_router)
client = TestClient(test_app)
@@ -27,7 +30,7 @@ def _seed_candidate_and_passed_report() -> tuple[str, str]:
version="1.0.0",
source_snapshot_ref="git:sha-api-release",
created_by="api-test",
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
status=CandidateStatus.CHECK_PASSED.value,
)
)
@@ -42,7 +45,7 @@ def _seed_candidate_and_passed_report() -> tuple[str, str]:
"violations_count": 0,
"blocking_violations_count": 0,
},
generated_at=datetime.now(timezone.utc),
generated_at=datetime.now(UTC),
immutable=True,
)
)
@@ -100,4 +103,4 @@ def test_release_reject_contract() -> None:
assert payload["status"] == "ok"
assert payload["decision"] == "REJECTED"
# #endregion test_release_reject_contract
# #endregion CleanReleaseV2ReleaseApiTests
# #endregion CleanReleaseV2ReleaseApiTests

View File

@@ -2,14 +2,16 @@
# @BRIEF Verifies connection routes bootstrap their table before CRUD access.
# @LAYER: API
# @RELATION DEPENDS_ON -> ConnectionsRouter
import asyncio
import os
import sys
import asyncio
from pathlib import Path
import pytest
from sqlalchemy import create_engine, inspect
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
# Force SQLite in-memory for database module imports.
# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
@@ -64,4 +66,4 @@ def test_create_connection_bootstraps_missing_table(db_session):
assert created.host == "warehouse.internal"
assert "connection_configs" in inspector.get_table_names()
# #endregion test_create_connection_bootstraps_missing_table
# #endregion ConnectionsRoutesTests
# #endregion ConnectionsRoutesTests

View File

@@ -2,22 +2,25 @@
# @BRIEF Unit tests for dashboards API endpoints.
# @LAYER: API
# @RELATION DEPENDS_ON -> [DashboardsApi]
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
from datetime import datetime, timezone
from fastapi.testclient import TestClient
from src.app import app
from src.api.routes.dashboards import DashboardsResponse
from src.dependencies import (
get_current_user,
has_permission,
get_config_manager,
get_task_manager,
get_resource_service,
get_mapping_service,
)
from src.app import app
from src.core.database import get_db
from src.dependencies import (
get_config_manager,
get_current_user,
get_mapping_service,
get_resource_service,
get_task_manager,
has_permission,
)
from src.services.profile_service import ProfileService as DomainProfileService
# Global mock user for get_current_user dependency overrides
mock_user = MagicMock()
mock_user.id = "u-1"
@@ -404,7 +407,7 @@ def test_get_database_mappings_env_not_found(mock_deps):
# @BRIEF Validate dashboard task history returns only related backup and LLM tasks.
# @TEST: GET /api/dashboards/{id}/tasks returns backup and llm tasks for dashboard
def test_get_dashboard_tasks_history_filters_success(mock_deps):
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
llm_task = MagicMock()
llm_task.id = "task-llm-1"
llm_task.plugin_id = "llm_dashboard_validation"
@@ -871,4 +874,4 @@ def test_get_dashboards_profile_filter_matches_owner_object_payload_contract(moc
assert {item["id"] for item in payload["dashboards"]} == {701}
assert payload["dashboards"][0]["title"] == "Featured Charts"
# #endregion test_get_dashboards_profile_filter_matches_owner_object_payload_contract
# #endregion DashboardsApiTests
# #endregion DashboardsApiTests

View File

@@ -3,23 +3,22 @@
# @LAYER: API
# @RELATION [BINDS_TO] ->[DatasetReviewApi]
# @RELATION [BINDS_TO] ->[DatasetReviewOrchestrator]
from datetime import datetime, timezone
import json
from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import MagicMock, patch
import pytest
from fastapi.testclient import TestClient
from src.app import app
from src.api.routes.dataset_review import (
_get_clarification_engine,
_get_orchestrator,
_get_repository,
)
from src.core.config_models import Environment, GlobalSettings, AppConfig
from src.app import app
from src.core.config_models import AppConfig, Environment, GlobalSettings
from src.core.utils.superset_context_extractor import SupersetContextExtractor
from src.dependencies import get_config_manager, get_current_user, get_task_manager
from src.models.dataset_review import (
AnswerKind,
ApprovalState,
BusinessSummarySource,
CandidateMatchType,
@@ -31,12 +30,12 @@ from src.models.dataset_review import (
CompiledPreview,
ConfidenceState,
DatasetReviewSession,
LaunchStatus,
ExecutionMapping,
FieldKind,
FieldProvenance,
FindingArea,
FindingSeverity,
LaunchStatus,
MappingMethod,
PreviewStatus,
QuestionState,
@@ -46,12 +45,13 @@ from src.models.dataset_review import (
SemanticCandidate,
SemanticFieldEntry,
SemanticSource,
SessionPhase,
SessionStatus,
SemanticSourceStatus,
SemanticSourceType,
SessionPhase,
SessionStatus,
TrustLevel,
)
from src.services.dataset_review.event_logger import SessionEventLogger
from src.services.dataset_review.orchestrator import (
DatasetReviewOrchestrator,
LaunchDatasetResult,
@@ -61,11 +61,11 @@ from src.services.dataset_review.orchestrator import (
from src.services.dataset_review.orchestrator_pkg._helpers import (
build_execution_snapshot,
)
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
from src.services.dataset_review.event_logger import SessionEventLogger
from src.services.dataset_review.repositories.session_repository import (
DatasetReviewSessionVersionConflictError,
)
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
client = TestClient(app)
# #region _make_user [TYPE Function]
# @RELATION BINDS_TO -> DatasetReviewApiTests
@@ -101,7 +101,7 @@ def _make_config_manager():
# #region _make_session [TYPE Function]
# @RELATION BINDS_TO -> DatasetReviewApiTests
def _make_session():
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
return DatasetReviewSession(
session_id="sess-1",
user_id="user-1",
@@ -124,7 +124,7 @@ def _make_session():
# #region _make_us2_session [TYPE Function]
# @RELATION BINDS_TO -> DatasetReviewApiTests
def _make_us2_session():
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
session = _make_session()
session.readiness_state = ReadinessState.CLARIFICATION_NEEDED
session.recommended_action = RecommendedAction.START_CLARIFICATION
@@ -239,7 +239,7 @@ def _make_us3_session():
`imported_filter` and `template_variable` are bare MagicMocks without spec;
ORM attribute access is unchecked.
"""
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
session = _make_session()
session.readiness_state = ReadinessState.MAPPING_REVIEW_NEEDED
session.recommended_action = RecommendedAction.APPROVE_MAPPING
@@ -695,7 +695,7 @@ def test_start_session_endpoint_returns_created_summary(
def test_get_session_detail_export_and_lifecycle_endpoints(
dataset_review_api_dependencies,
):
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
session = MagicMock(spec=DatasetReviewSession)
session.session_id = "sess-1"
session.user_id = "user-1"
@@ -957,8 +957,8 @@ def test_us3_mapping_patch_approval_preview_and_launch_endpoints(
compiled_by="superset",
error_code=None,
error_details=None,
compiled_at=datetime.now(timezone.utc),
created_at=datetime.now(timezone.utc),
compiled_at=datetime.now(UTC),
created_at=datetime.now(UTC),
)
session.previews = [latest_preview]
repository = MagicMock()
@@ -983,8 +983,8 @@ def test_us3_mapping_patch_approval_preview_and_launch_endpoints(
compiled_by="superset",
error_code=None,
error_details=None,
compiled_at=datetime.now(timezone.utc),
created_at=datetime.now(timezone.utc),
compiled_at=datetime.now(UTC),
created_at=datetime.now(UTC),
)
run_context = SimpleNamespace(
run_context_id="run-1",
@@ -1000,7 +1000,7 @@ def test_us3_mapping_patch_approval_preview_and_launch_endpoints(
open_warning_refs=[],
launch_status=LaunchStatus.STARTED,
launch_error=None,
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
)
orchestrator = MagicMock()
orchestrator.prepare_launch_preview.return_value = PreparePreviewResult(
@@ -1099,7 +1099,7 @@ def test_us3_mapping_patch_approval_preview_and_launch_endpoints(
error_code=None,
error_details=None,
compiled_at=None,
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
),
blocked_reasons=[],
)
@@ -1161,8 +1161,8 @@ def test_us3_preview_response_propagates_refreshed_session_version_for_launch_fo
compiled_by="superset",
error_code=None,
error_details=None,
compiled_at=datetime.now(timezone.utc),
created_at=datetime.now(timezone.utc),
compiled_at=datetime.now(UTC),
created_at=datetime.now(UTC),
)
run_context = SimpleNamespace(
run_context_id="run-2",
@@ -1178,7 +1178,7 @@ def test_us3_preview_response_propagates_refreshed_session_version_for_launch_fo
open_warning_refs=[],
launch_status=LaunchStatus.STARTED,
launch_error=None,
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
)
orchestrator = MagicMock()
def _prepare_preview(command):
@@ -1235,7 +1235,7 @@ def test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not
error_code="superset_preview_failed",
error_details="RuntimeError: [API_FAILURE] API resource not found at endpoint '/chart/data' | Context: {'status_code': 404, 'endpoint': '/chart/data', 'subtype': 'not_found'}",
compiled_at=None,
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
)
orchestrator = MagicMock()
orchestrator.prepare_launch_preview.return_value = PreparePreviewResult(
@@ -1479,7 +1479,7 @@ def test_us3_launch_endpoint_requires_launch_permission(
open_warning_refs=[],
launch_status=LaunchStatus.STARTED,
launch_error=None,
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
)
orchestrator = MagicMock()
orchestrator.launch_dataset.return_value = LaunchDatasetResult(
@@ -1534,4 +1534,4 @@ def test_semantic_source_version_propagation_preserves_locked_fields():
assert locked_field.source_version == "2026.03"
assert locked_field.needs_review is False
# #endregion test_semantic_source_version_propagation_preserves_locked_fields
# #endregion DatasetReviewApiTests
# #endregion DatasetReviewApiTests

View File

@@ -3,19 +3,22 @@
# @LAYER: API
# @RELATION DEPENDS_ON -> [DatasetsApi]
# @INVARIANT: Endpoint contracts remain stable for success and validation failure paths.
from unittest.mock import AsyncMock, MagicMock
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
from fastapi.testclient import TestClient
from src.api.routes.datasets import DatasetsResponse
from src.app import app
from src.api.routes.datasets import DatasetsResponse, DatasetDetailResponse
from src.dependencies import (
get_current_user,
has_permission,
get_config_manager,
get_task_manager,
get_resource_service,
get_current_user,
get_mapping_service,
get_resource_service,
get_task_manager,
has_permission,
)
# Global mock user for get_current_user dependency overrides
mock_user = MagicMock()
mock_user.username = "testuser"
@@ -253,4 +256,4 @@ def test_get_datasets_superset_failure(mock_deps):
assert response.status_code == 503
assert "Failed to fetch datasets" in response.json()["detail"]
# #endregion test_get_datasets_superset_failure
# #endregion DatasetsApiTests
# #endregion DatasetsApiTests

View File

@@ -1,12 +1,16 @@
# #region TestGitApi [TYPE Module] [C:3] [SEMANTICS test, git, api, config, repository]
# @RELATION VERIFIES -> [GitApi]
# @BRIEF API tests for Git configurations and repository operations.
import pytest
import asyncio
from unittest.mock import MagicMock
import pytest
from fastapi import HTTPException
from src.api.routes import git as git_routes
from src.models.git import GitServerConfig, GitProvider, GitStatus, GitRepository
from src.models.git import GitProvider, GitRepository, GitServerConfig, GitStatus
# #region DbMock [TYPE Class] [C:2]
# @RELATION BINDS_TO -> [TestGitApi]
# @BRIEF In-memory session double for git route tests with minimal query/filter persistence semantics.
@@ -123,6 +127,8 @@ def test_create_git_config_persists_config():
) # Note: route returns unmasked until serialized by FastAPI usually, but in tests schema might catch it or not.
# #endregion test_create_git_config_persists_config
from src.api.routes.git_schemas import GitServerConfigUpdate
# #region test_update_git_config_modifies_record [TYPE Function]
# @RELATION BINDS_TO -> [TestGitApi]
# @BRIEF Validate updating git config modifies mutable fields while preserving masked PAT semantics.
@@ -406,4 +412,4 @@ def test_init_repository_initializes_and_saves_binding(monkeypatch):
assert isinstance(db._added[0], GitRepository)
assert db._added[0].dashboard_id == 123
# #endregion test_init_repository_initializes_and_saves_binding
# #endregion TestGitApi
# #endregion TestGitApi

View File

@@ -2,11 +2,15 @@
# @BRIEF Validate status endpoint behavior for missing and error repository states.
# @LAYER: Domain (Tests)
# @RELATION VERIFIES -> [GitApi]
from fastapi import HTTPException
import pytest
import asyncio
from unittest.mock import MagicMock
import pytest
from fastapi import HTTPException
from src.api.routes import git as git_routes
# #region test_get_repository_status_returns_no_repo_payload_for_missing_repo [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure missing local repository is represented as NO_REPO payload instead of an API error.
@@ -347,4 +351,4 @@ def test_continue_merge_passes_message_and_returns_commit(monkeypatch):
assert response["status"] == "committed"
assert response["commit_hash"] == "abc123"
# #endregion test_continue_merge_passes_message_and_returns_commit
# #endregion TestGitStatusRoute
# #endregion TestGitStatusRoute

View File

@@ -4,16 +4,19 @@
# @LAYER: API
# @RELATION VERIFIES -> backend.src.api.routes.migration
#
import pytest
import sys
from datetime import UTC, datetime
from pathlib import Path
from unittest.mock import MagicMock, AsyncMock, patch
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# Add backend directory to sys.path
backend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())
if backend_dir not in sys.path:
sys.path.insert(0, backend_dir)
import os
# Force SQLite in-memory for all database connections BEFORE importing any application code
# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
@@ -26,9 +29,10 @@ os.environ["ENVIRONMENT"] = "testing"
from fastapi import HTTPException
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.models.mapping import Base, ResourceMapping, ResourceType
# Patch the get_db dependency if `src.api.routes.migration` imports it
from unittest.mock import patch
patch("src.core.database.get_db").start()
# --- Fixtures ---
@pytest.fixture
@@ -111,7 +115,7 @@ async def test_get_resource_mappings_returns_formatted_list(db_session):
uuid="uuid-1",
remote_integer_id="42",
resource_name="Sales Chart",
last_synced_at=datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc),
last_synced_at=datetime(2026, 1, 15, 12, 0, 0, tzinfo=UTC),
)
db_session.add(m1)
db_session.commit()
@@ -517,4 +521,4 @@ async def test_dry_run_migration_rejects_same_environment(db_session):
selection=selection, config_manager=cm, db=db_session, _=None
)
assert exc.value.status_code == 400
# #endregion TestMigrationRoutes
# #endregion TestMigrationRoutes

View File

@@ -3,9 +3,11 @@
# @BRIEF Verifies profile API route contracts for preference read/update and Superset account lookup.
# @LAYER: API
# [SECTION: IMPORTS]
from datetime import datetime, timezone
from datetime import UTC, datetime
from unittest.mock import MagicMock, patch
from fastapi.testclient import TestClient
from src.app import app
from src.core.database import get_db
from src.dependencies import get_config_manager, get_current_user
@@ -22,6 +24,7 @@ from src.services.profile_service import (
ProfileAuthorizationError,
ProfileValidationError,
)
# [/SECTION]
client = TestClient(app)
# #region mock_profile_route_dependencies [TYPE Function]
@@ -46,6 +49,8 @@ def mock_profile_route_dependencies():
# @PRE: None.
# @POST: Yields overridden dependencies and clears overrides after test.
import pytest
@pytest.fixture(autouse=True)
def profile_route_deps_fixture():
yielded = mock_profile_route_dependencies()
@@ -58,7 +63,7 @@ def profile_route_deps_fixture():
# @PRE: user_id is provided.
# @POST: Returns ProfilePreferenceResponse object with deterministic timestamps.
def _build_preference_response(user_id: str = "u-1") -> ProfilePreferenceResponse:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
return ProfilePreferenceResponse(
status="success",
message="Preference loaded",
@@ -260,4 +265,4 @@ def test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture):
payload = response.json()
assert payload["detail"] == "Environment 'missing-env' not found"
# #endregion test_lookup_superset_accounts_env_not_found
# #endregion TestProfileApi
# #endregion TestProfileApi

View File

@@ -3,12 +3,16 @@
# @BRIEF Contract tests for GET /api/reports defaults, pagination, and filtering behavior.
# @LAYER: Domain (Tests)
# @INVARIANT: API response contract contains {items,total,page,page_size,has_next,applied_filters}.
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from types import SimpleNamespace
from fastapi.testclient import TestClient
from src.app import app
from src.core.task_manager.models import Task, TaskStatus
from src.dependencies import get_current_user, get_task_manager
# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().
# #region _FakeTaskManager [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestReportsApi]
@@ -147,7 +151,7 @@ def test_get_reports_filter_and_pagination():
# @BRIEF Validate reports sorting remains stable when task timestamps mix naive and timezone-aware datetimes.
def test_get_reports_handles_mixed_naive_and_aware_datetimes():
naive_now = datetime.utcnow()
aware_now = datetime.now(timezone.utc)
aware_now = datetime.now(UTC)
tasks = [
_make_task(
"t-naive",
@@ -201,4 +205,4 @@ def test_get_reports_invalid_filter_returns_400():
finally:
app.dependency_overrides.clear()
# #endregion test_get_reports_invalid_filter_returns_400
# #endregion TestReportsApi
# #endregion TestReportsApi

View File

@@ -5,10 +5,14 @@
# @INVARIANT: Detail endpoint tests must keep deterministic assertions for success and not-found contracts.
from datetime import datetime, timedelta
from types import SimpleNamespace
from fastapi.testclient import TestClient
from src.app import app
from src.core.task_manager.models import Task, TaskStatus
from src.dependencies import get_current_user, get_task_manager
# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().
# #region _FakeTaskManager [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestReportsDetailApi]
@@ -87,4 +91,4 @@ def test_get_report_detail_not_found():
finally:
app.dependency_overrides.clear()
# #endregion test_get_report_detail_not_found
# #endregion TestReportsDetailApi
# #endregion TestReportsDetailApi

View File

@@ -5,10 +5,14 @@
# @INVARIANT: List and detail payloads include required contract keys.
from datetime import datetime
from types import SimpleNamespace
from fastapi.testclient import TestClient
from src.app import app
from src.core.task_manager.models import Task, TaskStatus
from src.dependencies import get_current_user, get_task_manager
# #region _FakeTaskManager [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestReportsOpenapiConformance]
# @BRIEF Minimal task-manager fake exposing static task list for OpenAPI conformance checks.
@@ -84,4 +88,4 @@ def test_reports_detail_openapi_required_keys():
finally:
app.dependency_overrides.clear()
# #endregion test_reports_detail_openapi_required_keys
# #endregion TestReportsOpenapiConformance
# #endregion TestReportsOpenapiConformance

View File

@@ -2,12 +2,16 @@
# @RELATION VERIFIES -> [src.api.routes.tasks:Module]
# @BRIEF Contract testing for task logs API endpoints.
# @LAYER: Domain (Tests)
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from unittest.mock import MagicMock
from src.dependencies import get_task_manager, has_permission
from src.api.routes.tasks import router
from src.dependencies import get_task_manager, has_permission
# @TEST_FIXTURE: mock_app
@pytest.fixture
def client():
@@ -74,4 +78,4 @@ def test_get_task_log_stats_success(client):
# response_model=LogStats might wrap this, but let's check basic structure
# assuming tm.get_task_log_stats returns something compatible with LogStats
# #endregion test_get_task_log_stats_success
# #endregion test_tasks_logs_module
# #endregion test_tasks_logs_module

View File

@@ -8,26 +8,29 @@
#
# @INVARIANT: All endpoints in this module require 'Admin' role or 'admin' scope.
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ...core.database import get_auth_db
from ...core.auth.repository import AuthRepository
from ...core.auth.security import get_password_hash
from ...core.database import get_auth_db
from ...core.logger import belief_scope, logger
from ...dependencies import get_plugin_loader, has_permission
from ...models.auth import ADGroupMapping, Role, User
from ...schemas.auth import (
User as UserSchema,
ADGroupMappingCreate,
ADGroupMappingSchema,
PermissionSchema,
RoleCreate,
RoleSchema,
RoleUpdate,
UserCreate,
UserUpdate,
RoleSchema,
RoleCreate,
RoleUpdate,
PermissionSchema,
ADGroupMappingSchema,
ADGroupMappingCreate,
)
from ...models.auth import User, Role, ADGroupMapping
from ...dependencies import has_permission, get_plugin_loader
from ...core.logger import logger, belief_scope
from ...schemas.auth import (
User as UserSchema,
)
from ...services.rbac_permission_catalog import (
discover_declared_permissions,
sync_permission_catalog,
@@ -45,7 +48,7 @@ router = APIRouter(prefix="/api/admin", tags=["admin"])
# @PRE: Current user has 'Admin' role.
# @POST: Returns a list of UserSchema objects.
# @RELATION CALLS -> User
@router.get("/users", response_model=List[UserSchema])
@router.get("/users", response_model=list[UserSchema])
async def list_users(
db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:users", "READ"))
):
@@ -175,7 +178,7 @@ async def delete_user(
# #region list_roles [C:3] [TYPE Function]
# @BRIEF Lists all available roles.
# @RELATION CALLS -> [Role:Class]
@router.get("/roles", response_model=List[RoleSchema])
@router.get("/roles", response_model=list[RoleSchema])
async def list_roles(
db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:roles", "READ"))
):
@@ -296,7 +299,7 @@ async def delete_role(
# @BRIEF Lists all available system permissions for assignment.
# @POST: Returns a list of all PermissionSchema objects.
# @RELATION CALLS -> backend.src.core.auth.repository.AuthRepository.list_permissions
@router.get("/permissions", response_model=List[PermissionSchema])
@router.get("/permissions", response_model=list[PermissionSchema])
async def list_permissions(
db: Session = Depends(get_auth_db),
plugin_loader=Depends(get_plugin_loader),
@@ -325,7 +328,7 @@ async def list_permissions(
# #region list_ad_mappings [C:3] [TYPE Function]
# @BRIEF Lists all AD Group to Role mappings.
# @RELATION CALLS -> ADGroupMapping
@router.get("/ad-mappings", response_model=List[ADGroupMappingSchema])
@router.get("/ad-mappings", response_model=list[ADGroupMappingSchema])
async def list_ad_mappings(
db: Session = Depends(get_auth_db),
_=Depends(has_permission("admin:settings", "READ")),

View File

@@ -8,107 +8,107 @@
# @INVARIANT: Risky operations are never executed without valid confirmation token.
# Re-export public API for backward compatibility.
from ._routes import router, send_message, confirm_operation, cancel_operation
from ._admin_routes import list_conversations, delete_conversation, get_history, get_assistant_audit
from ._schemas import (
AssistantMessageRequest,
AssistantMessageResponse,
AssistantAction,
ConfirmationRecord,
CONVERSATIONS,
USER_ACTIVE_CONVERSATION,
CONFIRMATIONS,
ASSISTANT_AUDIT,
INTENT_PERMISSION_CHECKS,
_SAFE_OPS,
_DATASET_REVIEW_OPS,
)
from ._admin_routes import delete_conversation, get_assistant_audit, get_history, list_conversations
from ._command_parser import _parse_command
from ._llm_planner import _build_tool_catalog
from ._llm_planner_intent import _plan_intent_with_llm, _authorize_intent
from ._dispatch import _dispatch_intent, _async_confirmation_summary, _clarification_text_for_intent
from ._dataset_review import (
_load_dataset_review_context,
_plan_dataset_review_intent,
)
from ._dataset_review_dispatch import _dispatch_dataset_review_intent
from ._dispatch import _async_confirmation_summary, _clarification_text_for_intent, _dispatch_intent
from ._history import (
_append_history,
_persist_message,
_audit,
_cleanup_history_ttl,
_coerce_query_bool,
_is_conversation_archived,
_load_confirmation_from_db,
_persist_audit,
_persist_confirmation,
_update_confirmation_state,
_load_confirmation_from_db,
_persist_message,
_resolve_or_create_conversation,
_cleanup_history_ttl,
_is_conversation_archived,
_coerce_query_bool,
_update_confirmation_state,
)
from ._llm_planner import _build_tool_catalog
from ._llm_planner_intent import _authorize_intent, _plan_intent_with_llm
from ._resolvers import (
_build_task_observability_summary,
_extract_id,
_resolve_env_id,
_is_production_env,
_resolve_provider_id,
_extract_result_deep_links,
_get_default_environment_id,
_get_environment_name_by_id,
_is_production_env,
_resolve_dashboard_id_by_ref,
_resolve_dashboard_id_entity,
_get_environment_name_by_id,
_extract_result_deep_links,
_build_task_observability_summary,
_resolve_env_id,
_resolve_provider_id,
)
from ._routes import cancel_operation, confirm_operation, router, send_message
from ._schemas import (
_DATASET_REVIEW_OPS,
_SAFE_OPS,
ASSISTANT_AUDIT,
CONFIRMATIONS,
CONVERSATIONS,
INTENT_PERMISSION_CHECKS,
USER_ACTIVE_CONVERSATION,
AssistantAction,
AssistantMessageRequest,
AssistantMessageResponse,
ConfirmationRecord,
)
__all__ = [
"router",
"send_message",
"confirm_operation",
"cancel_operation",
"list_conversations",
"delete_conversation",
"get_history",
"get_assistant_audit",
"ASSISTANT_AUDIT",
"CONFIRMATIONS",
"CONVERSATIONS",
"INTENT_PERMISSION_CHECKS",
"USER_ACTIVE_CONVERSATION",
"_DATASET_REVIEW_OPS",
"_SAFE_OPS",
"AssistantAction",
"AssistantMessageRequest",
"AssistantMessageResponse",
"AssistantAction",
"ConfirmationRecord",
"CONVERSATIONS",
"USER_ACTIVE_CONVERSATION",
"CONFIRMATIONS",
"ASSISTANT_AUDIT",
"INTENT_PERMISSION_CHECKS",
"_SAFE_OPS",
"_DATASET_REVIEW_OPS",
"_parse_command",
"_plan_intent_with_llm",
"_build_tool_catalog",
"_authorize_intent",
"_dispatch_intent",
"_async_confirmation_summary",
"_clarification_text_for_intent",
"_load_dataset_review_context",
"_plan_dataset_review_intent",
"_dispatch_dataset_review_intent",
"_append_history",
"_persist_message",
"_async_confirmation_summary",
"_audit",
"_authorize_intent",
"_build_task_observability_summary",
"_build_tool_catalog",
"_clarification_text_for_intent",
"_cleanup_history_ttl",
"_coerce_query_bool",
"_dispatch_dataset_review_intent",
"_dispatch_intent",
"_extract_id",
"_extract_result_deep_links",
"_get_default_environment_id",
"_get_environment_name_by_id",
"_is_conversation_archived",
"_is_production_env",
"_load_confirmation_from_db",
"_load_dataset_review_context",
"_parse_command",
"_persist_audit",
"_persist_confirmation",
"_update_confirmation_state",
"_load_confirmation_from_db",
"_resolve_or_create_conversation",
"_cleanup_history_ttl",
"_is_conversation_archived",
"_coerce_query_bool",
"_extract_id",
"_resolve_env_id",
"_is_production_env",
"_resolve_provider_id",
"_get_default_environment_id",
"_persist_message",
"_plan_dataset_review_intent",
"_plan_intent_with_llm",
"_resolve_dashboard_id_by_ref",
"_resolve_dashboard_id_entity",
"_get_environment_name_by_id",
"_extract_result_deep_links",
"_build_task_observability_summary",
"_resolve_env_id",
"_resolve_or_create_conversation",
"_resolve_provider_id",
"_update_confirmation_state",
"cancel_operation",
"confirm_operation",
"delete_conversation",
"get_assistant_audit",
"get_history",
"list_conversations",
"router",
"send_message",
]
# #endregion AssistantApi

View File

@@ -8,36 +8,33 @@
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any, Dict, Optional
from typing import Any
from fastapi import Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import desc
from sqlalchemy.orm import Session
from src.core.logger import belief_scope, logger
from src.dependencies import get_current_user, has_permission
from src.core.database import get_db
from src.core.logger import belief_scope
from src.dependencies import get_current_user, has_permission
from src.models.assistant import (
AssistantAuditRecord,
AssistantMessageRecord,
)
from src.schemas.auth import User
from ._schemas import (
ASSISTANT_AUDIT,
CONVERSATIONS,
AssistantAction,
AssistantMessageResponse,
)
from ._history import (
_cleanup_history_ttl,
_coerce_query_bool,
_is_conversation_archived,
_persist_audit,
_resolve_or_create_conversation,
)
from ._routes import router
from ._schemas import (
ASSISTANT_AUDIT,
CONVERSATIONS,
)
# #region list_conversations [C:2] [TYPE Function]
@@ -50,7 +47,7 @@ async def list_conversations(
page_size: int = Query(20, ge=1, le=100),
include_archived: bool = Query(False),
archived_only: bool = Query(False),
search: Optional[str] = Query(None),
search: str | None = Query(None),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
@@ -67,7 +64,7 @@ async def list_conversations(
.all()
)
summary: Dict[str, Dict[str, Any]] = {}
summary: dict[str, dict[str, Any]] = {}
for row in rows:
conv_id = row.conversation_id
if not conv_id:
@@ -189,7 +186,7 @@ async def delete_conversation(
async def get_history(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
conversation_id: Optional[str] = Query(None),
conversation_id: str | None = Query(None),
from_latest: bool = Query(False),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),

View File

@@ -7,10 +7,11 @@
from __future__ import annotations
import re
from typing import Any, Dict, List, Optional
from typing import Any
from src.core.logger import belief_scope, logger
from src.core.config_manager import ConfigManager
from src.core.logger import belief_scope, logger
from ._resolvers import _extract_id, _is_production_env
@@ -23,36 +24,36 @@ from ._resolvers import _extract_id, _is_production_env
# @PRE: message contains raw user text and config manager resolves environments.
# @POST: Returns intent dict with domain/operation/entities/confidence/risk fields.
# @INVARIANT: every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.
def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any]:
def _parse_command(message: str, config_manager: ConfigManager) -> dict[str, Any]:
with belief_scope('_parse_command'):
logger.reason('Belief protocol reasoning checkpoint for _parse_command')
text = message.strip()
lower = text.lower()
if any((phrase in lower for phrase in ['что ты умеешь', 'что умеешь', 'что ты можешь', 'help', 'помощь', 'доступные команды', 'какие команды'])):
if any(phrase in lower for phrase in ['что ты умеешь', 'что умеешь', 'что ты можешь', 'help', 'помощь', 'доступные команды', 'какие команды']):
logger.reflect('Belief protocol postcondition checkpoint for _parse_command')
return {'domain': 'assistant', 'operation': 'show_capabilities', 'entities': {}, 'confidence': 0.98, 'risk_level': 'safe', 'requires_confirmation': False}
dashboard_id = _extract_id(lower, ['(?:дашборд\\w*|dashboard)\\s*(?:id\\s*)?(\\d+)'])
dashboard_ref = _extract_id(lower, ['(?:дашборд\\w*|dashboard)\\s*(?:id\\s*)?([a-zа-я0-9._-]+)'])
dataset_id = _extract_id(lower, ['(?:датасет\\w*|dataset)\\s*(?:id\\s*)?(\\d+)'])
task_id = _extract_id(lower, ['(task[-_a-z0-9]{1,}|[0-9a-f]{8}-[0-9a-f-]{27,})'])
if any((k in lower for k in ['статус', 'status', 'state', 'проверь задачу'])):
if any(k in lower for k in ['статус', 'status', 'state', 'проверь задачу']):
logger.reflect('Belief protocol postcondition checkpoint for _parse_command')
return {'domain': 'status', 'operation': 'get_task_status', 'entities': {'task_id': task_id}, 'confidence': 0.92 if task_id else 0.66, 'risk_level': 'safe', 'requires_confirmation': False}
if any((k in lower for k in ['ветк', 'branch'])) and any((k in lower for k in ['созд', 'сделай', 'create'])):
if any(k in lower for k in ['ветк', 'branch']) and any(k in lower for k in ['созд', 'сделай', 'create']):
branch = _extract_id(lower, ['(?:ветк\\w*|branch)\\s+([a-z0-9._/-]+)'])
logger.reflect('Belief protocol postcondition checkpoint for _parse_command')
return {'domain': 'git', 'operation': 'create_branch', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'branch_name': branch}, 'confidence': 0.95 if branch and dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}
if any((k in lower for k in ['коммит', 'commit'])):
if any(k in lower for k in ['коммит', 'commit']):
quoted = re.search('"([^"]{3,120})"', text)
message_text = quoted.group(1) if quoted else 'assistant: update dashboard changes'
logger.reflect('Belief protocol postcondition checkpoint for _parse_command')
return {'domain': 'git', 'operation': 'commit_changes', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'message': message_text}, 'confidence': 0.9 if dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}
if any((k in lower for k in ['деплой', 'deploy', 'разверн'])):
if any(k in lower for k in ['деплой', 'deploy', 'разверн']):
env_match = _extract_id(lower, ['(?:в|to)\\s+([a-z0-9_-]+)'])
is_dangerous = _is_production_env(env_match, config_manager)
logger.reflect('Belief protocol postcondition checkpoint for _parse_command')
return {'domain': 'git', 'operation': 'deploy_dashboard', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.92 if dashboard_id and env_match else 0.7, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous}
if any((k in lower for k in ['миграц', 'migration', 'migrate'])):
if any(k in lower for k in ['миграц', 'migration', 'migrate']):
src = _extract_id(lower, ['(?:с|from)\\s+([a-z0-9_-]+)'])
tgt = _extract_id(lower, ['(?:на|to)\\s+([a-z0-9_-]+)'])
dry_run = '--dry-run' in lower or 'dry run' in lower
@@ -61,20 +62,20 @@ def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any
is_dangerous = _is_production_env(tgt, config_manager)
logger.reflect('Belief protocol postcondition checkpoint for _parse_command')
return {'domain': 'migration', 'operation': 'execute_migration', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'source_env': src, 'target_env': tgt, 'dry_run': dry_run, 'replace_db_config': replace_db_config, 'fix_cross_filters': fix_cross_filters}, 'confidence': 0.95 if dashboard_id and src and tgt else 0.72, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous or dry_run}
if any((k in lower for k in ['бэкап', 'backup', 'резерв'])):
if any(k in lower for k in ['бэкап', 'backup', 'резерв']):
env_match = _extract_id(lower, ['(?:в|for|из|from)\\s+([a-z0-9_-]+)'])
logger.reflect('Belief protocol postcondition checkpoint for _parse_command')
return {'domain': 'backup', 'operation': 'run_backup', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.9 if env_match else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}
if any((k in lower for k in ['здоровье', 'health', 'ошибки', 'failing', 'проблемы'])):
if any(k in lower for k in ['здоровье', 'health', 'ошибки', 'failing', 'проблемы']):
env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\s+([a-z0-9_-]+)'])
logger.reflect('Belief protocol postcondition checkpoint for _parse_command')
return {'domain': 'health', 'operation': 'get_health_summary', 'entities': {'environment': env_match}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}
if any((k in lower for k in ['валидац', 'validate', 'провер'])):
if any(k in lower for k in ['валидац', 'validate', 'провер']):
env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\s+([a-z0-9_-]+)'])
provider_match = _extract_id(lower, ['(?:provider|провайдер)\\s+([a-z0-9_-]+)'])
logger.reflect('Belief protocol postcondition checkpoint for _parse_command')
return {'domain': 'llm', 'operation': 'run_llm_validation', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'dashboard_ref': dashboard_ref if dashboard_ref and (not dashboard_ref.isdigit()) else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dashboard_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}
if any((k in lower for k in ['документац', 'documentation', 'generate docs', 'сгенерируй док'])):
if any(k in lower for k in ['документац', 'documentation', 'generate docs', 'сгенерируй док']):
env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\s+([a-z0-9_-]+)'])
provider_match = _extract_id(lower, ['(?:provider|провайдер)\\s+([a-z0-9_-]+)'])
logger.reflect('Belief protocol postcondition checkpoint for _parse_command')

View File

@@ -9,10 +9,9 @@
from __future__ import annotations
import re
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from typing import Any
from fastapi import HTTPException, status
from fastapi import HTTPException
from sqlalchemy.orm import Session
from src.core.logger import belief_scope, logger
@@ -22,16 +21,10 @@ from src.core.utils.superset_context_extractor import (
from src.models.dataset_review import (
ApprovalState,
DatasetReviewSession,
ReadinessState,
RecommendedAction,
)
from src.services.dataset_review.repositories.session_repository import (
DatasetReviewSessionRepository,
DatasetReviewSessionVersionConflictError,
)
from src.schemas.auth import User
from ._schemas import (
AssistantAction,
from src.services.dataset_review.repositories.session_repository import (
DatasetReviewSessionRepository,
)
@@ -41,7 +34,7 @@ from ._schemas import (
# @PRE: session_id is a valid active review session identifier.
# @POST: Returns a serializable dictionary containing the complete review context.
# @SIDE_EFFECT: Reads session data from the database.
def _serialize_dataset_review_context(session: DatasetReviewSession) -> Dict[str, Any]:
def _serialize_dataset_review_context(session: DatasetReviewSession) -> dict[str, Any]:
with belief_scope('_serialize_dataset_review_context'):
logger.reason('Belief protocol reasoning checkpoint for _serialize_dataset_review_context')
latest_preview = None
@@ -61,7 +54,7 @@ def _serialize_dataset_review_context(session: DatasetReviewSession) -> Dict[str
# @PRE: session_id is a valid active review session identifier.
# @POST: Returns a loaded context object with session data and findings.
# @SIDE_EFFECT: Reads session data from the database.
def _load_dataset_review_context(dataset_review_session_id: Optional[str], current_user: User, db: Session) -> Optional[Dict[str, Any]]:
def _load_dataset_review_context(dataset_review_session_id: str | None, current_user: User, db: Session) -> dict[str, Any] | None:
with belief_scope('_load_dataset_review_context'):
if not dataset_review_session_id:
return None
@@ -79,7 +72,7 @@ def _load_dataset_review_context(dataset_review_session_id: Optional[str], curre
# #region _extract_dataset_review_target [C:2] [TYPE Function]
# @BRIEF Extract structured dataset-review focus target hints embedded in assistant prompts.
def _extract_dataset_review_target(message: str) -> Tuple[Optional[str], Optional[str]]:
def _extract_dataset_review_target(message: str) -> tuple[str | None, str | None]:
match = re.search(
r"(?:target|focus)\s*[:=]\s*(field|mapping|finding|filter)[:=]([A-Za-z0-9._-]+)",
str(message or ""),
@@ -96,9 +89,9 @@ def _extract_dataset_review_target(message: str) -> Tuple[Optional[str], Optiona
# #region _match_dataset_review_field [C:2] [TYPE Function]
# @BRIEF Resolve one semantic field from assistant-visible context by id or user-visible label.
def _match_dataset_review_field(
dataset_context: Dict[str, Any],
dataset_context: dict[str, Any],
message: str,
) -> Optional[Dict[str, Any]]:
) -> dict[str, Any] | None:
target_kind, target_id = _extract_dataset_review_target(message)
fields = dataset_context.get("semantic_fields", []) or []
if target_kind == "field" and target_id:
@@ -125,7 +118,7 @@ def _match_dataset_review_field(
# #region _extract_quoted_segment [C:2] [TYPE Function]
# @BRIEF Extract one quoted assistant command segment after a label token.
def _extract_quoted_segment(message: str, label: str) -> Optional[str]:
def _extract_quoted_segment(message: str, label: str) -> str | None:
pattern = rf"{label}\s*[=:]?\s*[\"']([^\"']+)[\"']"
match = re.search(pattern, str(message or ""), re.IGNORECASE)
return match.group(1).strip() if match else None
@@ -139,8 +132,8 @@ def _extract_quoted_segment(message: str, label: str) -> Optional[str]:
# @RELATION CALLS -> DatasetReviewOrchestrator
def _plan_dataset_review_intent(
message: str,
dataset_context: Dict[str, Any],
) -> Optional[Dict[str, Any]]:
dataset_context: dict[str, Any],
) -> dict[str, Any] | None:
lower = message.strip().lower()
session_id = dataset_context["session_id"]
session_version = int(dataset_context.get("version", 0) or 0)

View File

@@ -9,18 +9,20 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from src.core.logger import belief_scope, logger
from src.api.routes.dataset_review import FieldSemanticUpdateRequest, _update_semantic_field_state
from src.core.config_manager import ConfigManager
from src.core.logger import belief_scope, logger
from src.models.dataset_review import (
ApprovalState,
ReadinessState,
RecommendedAction,
)
from src.schemas.auth import User
from src.services.dataset_review.orchestrator import (
DatasetReviewOrchestrator,
PreparePreviewCommand,
@@ -29,8 +31,7 @@ from src.services.dataset_review.repositories.session_repository import (
DatasetReviewSessionRepository,
DatasetReviewSessionVersionConflictError,
)
from src.schemas.auth import User
from src.api.routes.dataset_review import FieldSemanticUpdateRequest, _update_semantic_field_state
from ._schemas import (
AssistantAction,
)
@@ -63,11 +64,11 @@ def _dataset_review_conflict_http_exception(
# @POST: Returns a structured response with planned actions and confirmations.
# @SIDE_EFFECT: May update session state and enqueue tasks.
async def _dispatch_dataset_review_intent(
intent: Dict[str, Any],
intent: dict[str, Any],
current_user: User,
config_manager: ConfigManager,
db: Session,
) -> Tuple[str, Optional[str], List[AssistantAction]]:
) -> tuple[str, str | None, list[AssistantAction]]:
with belief_scope("_dispatch_dataset_review_intent"):
logger.reason(
"Dispatching assistant dataset-review intent",

View File

@@ -9,22 +9,22 @@
from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple
from typing import Any
from fastapi import HTTPException
from sqlalchemy.orm import Session
from src.core.logger import belief_scope, logger
from src.core.config_manager import ConfigManager
from src.core.logger import belief_scope, logger
from src.core.task_manager import TaskManager
from src.schemas.auth import User
from src.services.git_service import GitService
from src.services.llm_provider import LLMProviderService
from src.services.llm_prompt_templates import is_multimodal_model
from ._schemas import (
_DATASET_REVIEW_OPS,
AssistantAction,
)
from src.services.llm_provider import LLMProviderService
from ._dataset_review_dispatch import _dispatch_dataset_review_intent
from ._history import _coerce_query_bool
from ._llm_planner import _check_any_permission
from ._resolvers import (
_build_task_observability_summary,
_extract_result_deep_links,
@@ -33,9 +33,10 @@ from ._resolvers import (
_resolve_env_id,
_resolve_provider_id,
)
from ._history import _coerce_query_bool
from ._llm_planner import _check_any_permission
from ._dataset_review_dispatch import _dispatch_dataset_review_intent
from ._schemas import (
_DATASET_REVIEW_OPS,
AssistantAction,
)
git_service = GitService()
@@ -45,10 +46,10 @@ git_service = GitService()
# @PRE: state was classified as needs_clarification for current intent/error combination.
# @POST: Returned text is human-readable and actionable for target operation.
def _clarification_text_for_intent(
intent: Optional[Dict[str, Any]], detail_text: str
intent: dict[str, Any] | None, detail_text: str
) -> str:
operation = (intent or {}).get("operation")
guidance_by_operation: Dict[str, str] = {
guidance_by_operation: dict[str, str] = {
"run_llm_validation": (
"Нужно уточнение для запуска LLM-валидации: Укажите дашборд (id или slug), окружение и провайдер LLM."
),
@@ -72,12 +73,12 @@ def _clarification_text_for_intent(
# @PRE: actions is a non-empty list of planned review actions.
# @POST: Returns a formatted summary string suitable for display to the user.
# @SIDE_EFFECT: None - pure formatting function.
async def _async_confirmation_summary(intent: Dict[str, Any], config_manager: ConfigManager, db: Session) -> str:
async def _async_confirmation_summary(intent: dict[str, Any], config_manager: ConfigManager, db: Session) -> str:
with belief_scope('_confirmation_summary'):
logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')
operation = intent.get('operation', '')
entities = intent.get('entities', {})
descriptions: Dict[str, str] = {'create_branch': 'создание ветки{branch} для дашборда{dashboard}', 'commit_changes': 'коммит изменений для дашборда{dashboard}', 'deploy_dashboard': 'деплой дашборда{dashboard} в окружение{env}', 'execute_migration': 'миграция дашборда{dashboard} с{src} на{tgt}', 'run_backup': 'бэкап окружения{env}{dashboard}', 'run_llm_validation': 'LLM-валидация дашборда{dashboard}{env}', 'run_llm_documentation': 'генерация документации для датасета{dataset}{env}'}
descriptions: dict[str, str] = {'create_branch': 'создание ветки{branch} для дашборда{dashboard}', 'commit_changes': 'коммит изменений для дашборда{dashboard}', 'deploy_dashboard': 'деплой дашборда{dashboard} в окружение{env}', 'execute_migration': 'миграция дашборда{dashboard} с{src} на{tgt}', 'run_backup': 'бэкап окружения{env}{dashboard}', 'run_llm_validation': 'LLM-валидация дашборда{dashboard}{env}', 'run_llm_documentation': 'генерация документации для датасета{dataset}{env}'}
template = descriptions.get(operation)
if not template:
logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')
@@ -98,8 +99,8 @@ async def _async_confirmation_summary(intent: Dict[str, Any], config_manager: Co
if dry_run_enabled:
try:
from src.core.migration.dry_run_orchestrator import MigrationDryRunService
from src.models.dashboard import DashboardSelection
from src.core.superset_client import SupersetClient
from src.models.dashboard import DashboardSelection
src_token = entities.get('source_env')
tgt_token = entities.get('target_env')
dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)
@@ -148,7 +149,7 @@ async def _async_confirmation_summary(intent: Dict[str, Any], config_manager: Co
# @PRE: intent operation is known and actor permissions are validated per operation.
# @POST: Returns response text, optional task id, and UI actions for follow-up.
# @INVARIANT: unsupported operations are rejected via HTTPException(400).
async def _dispatch_intent(intent: Dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> Tuple[str, Optional[str], List[AssistantAction]]:
async def _dispatch_intent(intent: dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> tuple[str, str | None, list[AssistantAction]]:
with belief_scope('_dispatch_intent'):
logger.reason('Belief protocol reasoning checkpoint for _dispatch_intent')
operation = intent.get('operation')
@@ -164,7 +165,7 @@ async def _dispatch_intent(intent: Dict[str, Any], current_user: User, task_mana
if not available:
logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')
return ('Сейчас нет доступных для вас операций ассистента.', None, [])
commands = '\n'.join((f'- {item}' for item in available))
commands = '\n'.join(f'- {item}' for item in available)
text = f'Вот что я могу сделать для вас:\n{commands}\n\nПример: `запусти миграцию с dev на prod для дашборда 42`.'
logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')
return (text, None, [])
@@ -248,7 +249,7 @@ async def _dispatch_intent(intent: Dict[str, Any], current_user: User, task_mana
raise HTTPException(status_code=422, detail='Missing source_env/target_env')
if not dashboard_id and (not dashboard_ref):
raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')
migration_params: Dict[str, Any] = {'source_env_id': src, 'target_env_id': tgt, 'replace_db_config': _coerce_query_bool(entities.get('replace_db_config', False)), 'fix_cross_filters': _coerce_query_bool(entities.get('fix_cross_filters', True))}
migration_params: dict[str, Any] = {'source_env_id': src, 'target_env_id': tgt, 'replace_db_config': _coerce_query_bool(entities.get('replace_db_config', False)), 'fix_cross_filters': _coerce_query_bool(entities.get('fix_cross_filters', True))}
if dashboard_id:
migration_params['selected_ids'] = [dashboard_id]
else:
@@ -262,7 +263,7 @@ async def _dispatch_intent(intent: Dict[str, Any], current_user: User, task_mana
env_id = _resolve_env_id(env_token, config_manager)
if not env_id:
raise HTTPException(status_code=400, detail='Missing or unknown environment')
params: Dict[str, Any] = {'environment_id': env_id}
params: dict[str, Any] = {'environment_id': env_id}
if entities.get('dashboard_id') or entities.get('dashboard_ref'):
dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)
if not dashboard_id:

View File

@@ -8,21 +8,21 @@ from __future__ import annotations
import uuid
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple
from typing import Any
from sqlalchemy.orm import Session
from src.core.logger import belief_scope, logger
from src.core.logger import logger
from src.models.assistant import (
AssistantAuditRecord,
AssistantConfirmationRecord,
AssistantMessageRecord,
)
from ._schemas import (
ASSISTANT_ARCHIVE_AFTER_DAYS,
ASSISTANT_MESSAGE_TTL_DAYS,
ASSISTANT_AUDIT,
CONFIRMATIONS,
ASSISTANT_MESSAGE_TTL_DAYS,
CONVERSATIONS,
ConfirmationRecord,
)
@@ -43,9 +43,9 @@ def _append_history(
conversation_id: str,
role: str,
text: str,
state: Optional[str] = None,
task_id: Optional[str] = None,
confirmation_id: Optional[str] = None,
state: str | None = None,
task_id: str | None = None,
confirmation_id: str | None = None,
):
key = (user_id, conversation_id)
if key not in CONVERSATIONS:
@@ -81,10 +81,10 @@ def _persist_message(
conversation_id: str,
role: str,
text: str,
state: Optional[str] = None,
task_id: Optional[str] = None,
confirmation_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
state: str | None = None,
task_id: str | None = None,
confirmation_id: str | None = None,
metadata: dict[str, Any] | None = None,
):
try:
row = AssistantMessageRecord(
@@ -116,7 +116,7 @@ def _persist_message(
# @PRE: payload describes decision/outcome fields.
# @POST: ASSISTANT_AUDIT list for user contains new timestamped entry.
# @INVARIANT: persisted in-memory audit entry always contains created_at in ISO format.
def _audit(user_id: str, payload: Dict[str, Any]):
def _audit(user_id: str, payload: dict[str, Any]):
if user_id not in ASSISTANT_AUDIT:
ASSISTANT_AUDIT[user_id] = []
ASSISTANT_AUDIT[user_id].append(
@@ -133,7 +133,7 @@ def _audit(user_id: str, payload: Dict[str, Any]):
# @PRE: db session is writable and payload is JSON-serializable.
# @POST: Audit row is committed or failure is logged with rollback.
def _persist_audit(
db: Session, user_id: str, payload: Dict[str, Any], conversation_id: Optional[str]
db: Session, user_id: str, payload: dict[str, Any], conversation_id: str | None
):
try:
row = AssistantAuditRecord(
@@ -213,7 +213,7 @@ def _update_confirmation_state(db: Session, confirmation_id: str, state: str):
# @POST: Returns ConfirmationRecord when found, otherwise None.
def _load_confirmation_from_db(
db: Session, confirmation_id: str
) -> Optional[ConfirmationRecord]:
) -> ConfirmationRecord | None:
row = (
db.query(AssistantConfirmationRecord)
.filter(AssistantConfirmationRecord.id == confirmation_id)
@@ -240,7 +240,7 @@ def _load_confirmation_from_db(
# @BRIEF Resolve active conversation id in memory or create a new one.
# @PRE: user_id identifies current actor.
# @POST: Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.
def _ensure_conversation(user_id: str, conversation_id: Optional[str]) -> str:
def _ensure_conversation(user_id: str, conversation_id: str | None) -> str:
if conversation_id:
from ._schemas import USER_ACTIVE_CONVERSATION
USER_ACTIVE_CONVERSATION[user_id] = conversation_id
@@ -264,7 +264,7 @@ def _ensure_conversation(user_id: str, conversation_id: Optional[str]) -> str:
# @PRE: user_id and db session are available.
# @POST: Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.
def _resolve_or_create_conversation(
user_id: str, conversation_id: Optional[str], db: Session
user_id: str, conversation_id: str | None, db: Session
) -> str:
from ._schemas import USER_ACTIVE_CONVERSATION
@@ -316,7 +316,7 @@ def _cleanup_history_ttl(db: Session, user_id: str):
f"[assistant.history][ttl_cleanup_failed] user={user_id} error={exc}"
)
stale_keys: List[Tuple[str, str]] = []
stale_keys: list[tuple[str, str]] = []
for key, items in CONVERSATIONS.items():
if key[0] != user_id:
continue
@@ -341,7 +341,7 @@ def _cleanup_history_ttl(db: Session, user_id: str):
# @BRIEF Determine archived state for a conversation based on last update timestamp.
# @PRE: updated_at can be null for empty conversations.
# @POST: Returns True when conversation inactivity exceeds archive threshold.
def _is_conversation_archived(updated_at: Optional[datetime]) -> bool:
def _is_conversation_archived(updated_at: datetime | None) -> bool:
if not updated_at:
return False
cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_ARCHIVE_AFTER_DAYS)

View File

@@ -8,32 +8,31 @@
from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple
from typing import Any
from fastapi import HTTPException
from sqlalchemy.orm import Session
from src.core.logger import belief_scope, logger
from src.core.config_manager import ConfigManager
from src.dependencies import has_permission
from src.schemas.auth import User
from src.services.llm_provider import LLMProviderService
from src.services.llm_prompt_templates import resolve_bound_provider_id
from ._schemas import (
INTENT_PERMISSION_CHECKS,
AssistantAction,
)
from src.services.llm_provider import LLMProviderService
from ._resolvers import (
_get_default_environment_id,
)
from ._schemas import (
INTENT_PERMISSION_CHECKS,
)
# #region _check_any_permission [C:2] [TYPE Function]
# @BRIEF Validate user against alternative permission checks (logical OR).
# @PRE: checks list contains resource-action tuples.
# @POST: Returns on first successful permission; raises 403-like HTTPException otherwise.
def _check_any_permission(current_user: User, checks: List[Tuple[str, str]]):
errors: List[HTTPException] = []
def _check_any_permission(current_user: User, checks: list[tuple[str, str]]):
errors: list[HTTPException] = []
for resource, action in checks:
try:
has_permission(resource, action)(current_user)
@@ -55,7 +54,7 @@ def _check_any_permission(current_user: User, checks: List[Tuple[str, str]]):
# @BRIEF Check whether user has at least one permission tuple from the provided list.
# @PRE: current_user and checks list are valid.
# @POST: Returns True when at least one permission check passes.
def _has_any_permission(current_user: User, checks: List[Tuple[str, str]]) -> bool:
def _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bool:
try:
_check_any_permission(current_user, checks)
return True
@@ -75,8 +74,8 @@ def _build_tool_catalog(
current_user: User,
config_manager: ConfigManager,
db: Session,
dataset_review_context: Optional[Dict[str, Any]] = None,
) -> List[Dict[str, Any]]:
dataset_review_context: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
envs = config_manager.get_environments()
default_env_id = _get_default_environment_id(config_manager)
providers = LLMProviderService(db).get_all_providers()
@@ -95,7 +94,7 @@ def _build_tool_catalog(
resolve_bound_provider_id(llm_settings, "documentation") or fallback_provider
)
candidates: List[Dict[str, Any]] = [
candidates: list[dict[str, Any]] = [
{
"operation": "show_capabilities",
"domain": "assistant",
@@ -201,7 +200,7 @@ def _build_tool_catalog(
},
]
available: List[Dict[str, Any]] = []
available: list[dict[str, Any]] = []
for tool in candidates:
checks = INTENT_PERMISSION_CHECKS.get(tool["operation"], [])
if checks and not _has_any_permission(current_user, checks):
@@ -209,7 +208,7 @@ def _build_tool_catalog(
available.append(tool)
if dataset_review_context is not None:
dataset_tools: List[Dict[str, Any]] = [
dataset_tools: list[dict[str, Any]] = [
{
"operation": "dataset_review_answer_context",
"domain": "dataset_review",
@@ -272,7 +271,7 @@ def _build_tool_catalog(
# @BRIEF Normalize intent entity value types from LLM output to route-compatible values.
# @PRE: intent contains entities dict or missing entities.
# @POST: Returned intent has numeric ids coerced where possible and string values stripped.
def _coerce_intent_entities(intent: Dict[str, Any]) -> Dict[str, Any]:
def _coerce_intent_entities(intent: dict[str, Any]) -> dict[str, Any]:
entities = intent.get("entities")
if not isinstance(entities, dict):
intent["entities"] = {}

View File

@@ -8,29 +8,29 @@
from __future__ import annotations
import json
from typing import Any, Dict, List, Optional
from typing import Any
from sqlalchemy.orm import Session
from src.core.logger import belief_scope, logger
from src.core.config_manager import ConfigManager
from src.core.logger import logger
from src.plugins.llm_analysis.models import LLMProviderType
from src.plugins.llm_analysis.service import LLMClient
from src.schemas.auth import User
from src.services.llm_provider import LLMProviderService
from src.services.llm_prompt_templates import (
normalize_llm_settings,
resolve_bound_provider_id,
)
from src.plugins.llm_analysis.service import LLMClient
from src.plugins.llm_analysis.models import LLMProviderType
from ._schemas import INTENT_PERMISSION_CHECKS
from ._resolvers import (
_is_production_env,
_resolve_provider_id,
)
from src.services.llm_provider import LLMProviderService
from ._llm_planner import (
_check_any_permission,
_coerce_intent_entities,
)
from ._resolvers import (
_is_production_env,
_resolve_provider_id,
)
from ._schemas import INTENT_PERMISSION_CHECKS
# #region _plan_intent_with_llm [C:2] [TYPE Function]
@@ -39,10 +39,10 @@ from ._llm_planner import (
# @POST: Returns normalized intent dict when planning succeeds; otherwise None.
async def _plan_intent_with_llm(
message: str,
tools: List[Dict[str, Any]],
tools: list[dict[str, Any]],
db: Session,
config_manager: ConfigManager,
) -> Optional[Dict[str, Any]]:
) -> dict[str, Any] | None:
if not tools:
return None
@@ -158,7 +158,7 @@ async def _plan_intent_with_llm(
# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch.
# @PRE: intent.operation is present for known assistant command domains.
# @POST: Returns if authorized; raises HTTPException(403) when denied.
def _authorize_intent(intent: Dict[str, Any], current_user: User):
def _authorize_intent(intent: dict[str, Any], current_user: User):
operation = intent.get("operation")
if operation in INTENT_PERMISSION_CHECKS:
_check_any_permission(current_user, INTENT_PERMISSION_CHECKS[operation])

View File

@@ -8,16 +8,15 @@
from __future__ import annotations
import re
from typing import Any, Dict, List, Optional, Tuple, cast
from typing import Any, cast
from sqlalchemy.orm import Session
from src.core.logger import belief_scope, logger
from src.core.config_manager import ConfigManager
from src.core.logger import logger
from src.core.superset_client import SupersetClient
from src.services.llm_provider import LLMProviderService
from src.services.llm_prompt_templates import resolve_bound_provider_id
from src.schemas.auth import User
from src.services.llm_provider import LLMProviderService
logger = cast(Any, logger)
@@ -26,7 +25,7 @@ logger = cast(Any, logger)
# @BRIEF Extract first regex match group from text by ordered pattern list.
# @PRE: patterns contain at least one capture group.
# @POST: Returns first matched token or None.
def _extract_id(text: str, patterns: List[str]) -> Optional[str]:
def _extract_id(text: str, patterns: list[str]) -> str | None:
for p in patterns:
m = re.search(p, text, flags=re.IGNORECASE)
if m:
@@ -41,8 +40,8 @@ def _extract_id(text: str, patterns: List[str]) -> Optional[str]:
# @PRE: config_manager provides environment list.
# @POST: Returns matched environment id or None.
def _resolve_env_id(
token: Optional[str], config_manager: ConfigManager
) -> Optional[str]:
token: str | None, config_manager: ConfigManager
) -> str | None:
if not token:
return None
@@ -60,7 +59,7 @@ def _resolve_env_id(
# @BRIEF Determine whether environment token resolves to production-like target.
# @PRE: config_manager provides environments or token text is provided.
# @POST: Returns True for production/prod synonyms, else False.
def _is_production_env(token: Optional[str], config_manager: ConfigManager) -> bool:
def _is_production_env(token: str | None, config_manager: ConfigManager) -> bool:
env_id = _resolve_env_id(token, config_manager)
if not env_id:
return (token or "").strip().lower() in {"prod", "production", "прод"}
@@ -79,11 +78,11 @@ def _is_production_env(token: Optional[str], config_manager: ConfigManager) -> b
# @PRE: db session can load provider list through LLMProviderService.
# @POST: Returns provider id or None when no providers configured.
def _resolve_provider_id(
provider_token: Optional[str],
provider_token: str | None,
db: Session,
config_manager: Optional[ConfigManager] = None,
task_key: Optional[str] = None,
) -> Optional[str]:
config_manager: ConfigManager | None = None,
task_key: str | None = None,
) -> str | None:
service = LLMProviderService(db)
providers = service.get_all_providers()
if not providers:
@@ -114,7 +113,7 @@ def _resolve_provider_id(
# @BRIEF Resolve default environment id from settings or first configured environment.
# @PRE: config_manager returns environments list.
# @POST: Returns default environment id or None when environment list is empty.
def _get_default_environment_id(config_manager: ConfigManager) -> Optional[str]:
def _get_default_environment_id(config_manager: ConfigManager) -> str | None:
configured = config_manager.get_environments()
if not configured:
return None
@@ -139,10 +138,10 @@ def _get_default_environment_id(config_manager: ConfigManager) -> Optional[str]:
# @PRE: dashboard_ref is a non-empty string-like token.
# @POST: Returns dashboard id when uniquely matched, otherwise None.
def _resolve_dashboard_id_by_ref(
dashboard_ref: Optional[str],
env_id: Optional[str],
dashboard_ref: str | None,
env_id: str | None,
config_manager: ConfigManager,
) -> Optional[int]:
) -> int | None:
if not dashboard_ref or not env_id:
return None
env = next(
@@ -191,10 +190,10 @@ def _resolve_dashboard_id_by_ref(
# @PRE: entities may contain dashboard_id as int/str and optional dashboard_ref.
# @POST: Returns resolved dashboard id or None when ambiguous/unresolvable.
def _resolve_dashboard_id_entity(
entities: Dict[str, Any],
entities: dict[str, Any],
config_manager: ConfigManager,
env_hint: Optional[str] = None,
) -> Optional[int]:
env_hint: str | None = None,
) -> int | None:
raw_dashboard_id = entities.get("dashboard_id")
dashboard_ref = entities.get("dashboard_ref")
@@ -232,7 +231,7 @@ def _resolve_dashboard_id_entity(
# @PRE: environment id may be None.
# @POST: Returns matching environment name or fallback id.
def _get_environment_name_by_id(
env_id: Optional[str], config_manager: ConfigManager
env_id: str | None, config_manager: ConfigManager
) -> str:
if not env_id:
return "unknown"
@@ -250,15 +249,15 @@ def _get_environment_name_by_id(
# @POST: Returns zero or more assistant actions for dashboard open/diff.
def _extract_result_deep_links(
task: Any, config_manager: ConfigManager
) -> List:
) -> list:
from ._schemas import AssistantAction
plugin_id = getattr(task, "plugin_id", None)
params = getattr(task, "params", {}) or {}
result = getattr(task, "result", {}) or {}
actions: list = []
dashboard_id: Optional[int] = None
env_id: Optional[str] = None
dashboard_id: int | None = None
env_id: str | None = None
if plugin_id == "superset-migration":
migrated = (

View File

@@ -14,54 +14,22 @@ from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import desc
from src.core.config_manager import ConfigManager
from src.core.database import get_db
from src.core.logger import belief_scope, logger
from src.core.task_manager import TaskManager
from src.dependencies import (
get_config_manager,
get_current_user,
get_task_manager,
get_config_manager,
has_permission,
)
from src.core.config_manager import ConfigManager
from src.core.database import get_db
from src.models.assistant import (
AssistantAuditRecord,
AssistantMessageRecord,
)
from src.schemas.auth import User
from ._schemas import (
_SAFE_OPS,
ASSISTANT_AUDIT,
CONFIRMATIONS,
CONVERSATIONS,
USER_ACTIVE_CONVERSATION,
AssistantAction,
AssistantMessageRequest,
AssistantMessageResponse,
ConfirmationRecord,
)
from ._history import (
_append_history,
_audit,
_cleanup_history_ttl,
_coerce_query_bool,
_is_conversation_archived,
_load_confirmation_from_db,
_persist_audit,
_persist_confirmation,
_persist_message,
_resolve_or_create_conversation,
_update_confirmation_state,
)
from ._command_parser import _parse_command
from ._llm_planner import _build_tool_catalog
from ._llm_planner_intent import _authorize_intent, _plan_intent_with_llm
from ._dataset_review import (
_load_dataset_review_context,
_plan_dataset_review_intent,
@@ -71,6 +39,26 @@ from ._dispatch import (
_clarification_text_for_intent,
_dispatch_intent,
)
from ._history import (
_append_history,
_audit,
_load_confirmation_from_db,
_persist_audit,
_persist_confirmation,
_persist_message,
_resolve_or_create_conversation,
_update_confirmation_state,
)
from ._llm_planner import _build_tool_catalog
from ._llm_planner_intent import _authorize_intent, _plan_intent_with_llm
from ._schemas import (
_SAFE_OPS,
CONFIRMATIONS,
AssistantAction,
AssistantMessageRequest,
AssistantMessageResponse,
ConfirmationRecord,
)
router = APIRouter(tags=["Assistant"])

View File

@@ -8,12 +8,10 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from typing import Any
from pydantic import BaseModel, Field
from src.schemas.auth import User
# #region AssistantMessageRequest [C:1] [TYPE Class]
# @BRIEF Input payload for assistant message endpoint.
@@ -24,9 +22,9 @@ from src.schemas.auth import User
# @POST: Request object provides message text and optional conversation binding.
# @INVARIANT: message is always non-empty and no longer than 4000 characters.
class AssistantMessageRequest(BaseModel):
conversation_id: Optional[str] = None
conversation_id: str | None = None
message: str = Field(..., min_length=1, max_length=4000)
dataset_review_session_id: Optional[str] = None
dataset_review_session_id: str | None = None
# #endregion AssistantMessageRequest
@@ -43,7 +41,7 @@ class AssistantMessageRequest(BaseModel):
class AssistantAction(BaseModel):
type: str
label: str
target: Optional[str] = None
target: str | None = None
# #endregion AssistantAction
@@ -64,10 +62,10 @@ class AssistantMessageResponse(BaseModel):
response_id: str
state: str
text: str
intent: Optional[Dict[str, Any]] = None
confirmation_id: Optional[str] = None
task_id: Optional[str] = None
actions: List[AssistantAction] = Field(default_factory=list)
intent: dict[str, Any] | None = None
confirmation_id: str | None = None
task_id: str | None = None
actions: list[AssistantAction] = Field(default_factory=list)
created_at: datetime
@@ -88,8 +86,8 @@ class ConfirmationRecord(BaseModel):
id: str
user_id: str
conversation_id: str
intent: Dict[str, Any]
dispatch: Dict[str, Any]
intent: dict[str, Any]
dispatch: dict[str, Any]
expires_at: datetime
state: str = "pending"
created_at: datetime
@@ -100,10 +98,10 @@ class ConfirmationRecord(BaseModel):
# --- In-memory stores ---
CONVERSATIONS: Dict[Tuple[str, str], List[Dict[str, Any]]] = {}
USER_ACTIVE_CONVERSATION: Dict[str, str] = {}
CONFIRMATIONS: Dict[str, ConfirmationRecord] = {}
ASSISTANT_AUDIT: Dict[str, List[Dict[str, Any]]] = {}
CONVERSATIONS: dict[tuple[str, str], list[dict[str, Any]]] = {}
USER_ACTIVE_CONVERSATION: dict[str, str] = {}
CONFIRMATIONS: dict[str, ConfirmationRecord] = {}
ASSISTANT_AUDIT: dict[str, list[dict[str, Any]]] = {}
ASSISTANT_ARCHIVE_AFTER_DAYS = 14
ASSISTANT_MESSAGE_TTL_DAYS = 90
@@ -121,7 +119,7 @@ _DATASET_REVIEW_OPS = {
"dataset_review_generate_sql_preview",
}
INTENT_PERMISSION_CHECKS: Dict[str, List[Tuple[str, str]]] = {
INTENT_PERMISSION_CHECKS: dict[str, list[tuple[str, str]]] = {
"get_task_status": [("tasks", "READ")],
"create_branch": [("plugin:git", "EXECUTE")],
"commit_changes": [("plugin:git", "EXECUTE")],

View File

@@ -10,45 +10,38 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any, Dict, List
from datetime import UTC, datetime
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from ...core.logger import belief_scope, logger
from ...dependencies import get_clean_release_repository, get_config_manager
from ...services.clean_release.preparation_service import prepare_candidate
from ...services.clean_release.repository import CleanReleaseRepository
from ...dependencies import get_clean_release_repository
from ...models.clean_release import (
CandidateArtifact,
ComplianceStageRun,
ComplianceViolation,
ReleaseCandidate,
)
from ...services.clean_release.compliance_orchestrator import (
CleanComplianceOrchestrator,
)
from ...services.clean_release.report_builder import ComplianceReportBuilder
from ...services.clean_release.compliance_execution_service import (
ComplianceExecutionService,
ComplianceRunError,
)
from ...services.clean_release.dto import (
CandidateDTO,
ManifestDTO,
CandidateOverviewDTO,
ComplianceRunDTO,
ManifestDTO,
)
from ...services.clean_release.enums import (
CandidateStatus,
ComplianceDecision,
ComplianceStageName,
ViolationCategory,
ViolationSeverity,
RunStatus,
CandidateStatus,
)
from ...models.clean_release import (
ComplianceRun,
ComplianceStageRun,
ComplianceViolation,
CandidateArtifact,
ReleaseCandidate,
ViolationSeverity,
)
from ...services.clean_release.preparation_service import prepare_candidate
from ...services.clean_release.report_builder import ComplianceReportBuilder
from ...services.clean_release.repository import CleanReleaseRepository
router = APIRouter(prefix="/api/clean-release", tags=["Clean Release"])
@@ -57,8 +50,8 @@ router = APIRouter(prefix="/api/clean-release", tags=["Clean Release"])
# @BRIEF Request schema for candidate preparation endpoint.
class PrepareCandidateRequest(BaseModel):
candidate_id: str = Field(min_length=1)
artifacts: List[Dict[str, Any]] = Field(default_factory=list)
sources: List[str] = Field(default_factory=list)
artifacts: list[dict[str, Any]] = Field(default_factory=list)
sources: list[str] = Field(default_factory=list)
operator_id: str = Field(min_length=1)
@@ -92,7 +85,7 @@ class RegisterCandidateRequest(BaseModel):
# #region ImportArtifactsRequest [TYPE Class]
# @BRIEF Request schema for candidate artifact import endpoint.
class ImportArtifactsRequest(BaseModel):
artifacts: List[Dict[str, Any]] = Field(default_factory=list)
artifacts: list[dict[str, Any]] = Field(default_factory=list)
# #endregion ImportArtifactsRequest
@@ -140,7 +133,7 @@ async def register_candidate_v2_endpoint(
version=payload.version,
source_snapshot_ref=payload.source_snapshot_ref,
created_by=payload.created_by,
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
status=CandidateStatus.DRAFT.value,
)
repository.save_candidate(candidate)
@@ -292,7 +285,7 @@ async def get_candidate_overview_v2_endpoint(
sorted(
runs,
key=lambda run: run.requested_at
or datetime.min.replace(tzinfo=timezone.utc),
or datetime.min.replace(tzinfo=UTC),
reverse=True,
)[0]
if runs
@@ -317,7 +310,7 @@ async def get_candidate_overview_v2_endpoint(
sorted(
[item for item in approval_decisions if item.candidate_id == candidate_id],
key=lambda item: item.decided_at
or datetime.min.replace(tzinfo=timezone.utc),
or datetime.min.replace(tzinfo=UTC),
reverse=True,
)[0]
if approval_decisions
@@ -330,7 +323,7 @@ async def get_candidate_overview_v2_endpoint(
sorted(
[item for item in publication_records if item.candidate_id == candidate_id],
key=lambda item: item.published_at
or datetime.min.replace(tzinfo=timezone.utc),
or datetime.min.replace(tzinfo=UTC),
reverse=True,
)[0]
if publication_records

View File

@@ -8,26 +8,28 @@
# @POST: Candidate registration, approval, publication, and revocation routes are registered without behavior changes.
# @SIDE_EFFECT: Persists candidate lifecycle state through clean release services and repository adapters.
from datetime import UTC, datetime
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List, Dict, Any
from datetime import datetime, timezone
from ...dependencies import get_clean_release_repository
from ...models.clean_release import (
CandidateArtifact,
DistributionManifest,
ReleaseCandidate,
)
from ...services.clean_release.approval_service import (
approve_candidate,
reject_candidate,
)
from ...services.clean_release.dto import CandidateDTO, ManifestDTO
from ...services.clean_release.enums import CandidateStatus
from ...services.clean_release.publication_service import (
publish_candidate,
revoke_publication,
)
from ...services.clean_release.repository import CleanReleaseRepository
from ...dependencies import get_clean_release_repository
from ...services.clean_release.enums import CandidateStatus
from ...models.clean_release import (
ReleaseCandidate,
CandidateArtifact,
DistributionManifest,
)
from ...services.clean_release.dto import CandidateDTO, ManifestDTO
router = APIRouter(prefix="/api/v2/clean-release", tags=["Clean Release V2"])
@@ -69,7 +71,7 @@ class RevokeRequest(dict):
"/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED
)
async def register_candidate(
payload: Dict[str, Any],
payload: dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
candidate = ReleaseCandidate(
@@ -77,7 +79,7 @@ async def register_candidate(
version=payload["version"],
source_snapshot_ref=payload["source_snapshot_ref"],
created_by=payload["created_by"],
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
status=CandidateStatus.DRAFT.value,
)
repository.save_candidate(candidate)
@@ -102,7 +104,7 @@ async def register_candidate(
@router.post("/candidates/{candidate_id}/artifacts")
async def import_artifacts(
candidate_id: str,
payload: Dict[str, Any],
payload: dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
candidate = repository.get_candidate(candidate_id)
@@ -152,7 +154,7 @@ async def build_manifest(
manifest_digest="hash-123",
artifacts_digest="art-hash-123",
created_by="system",
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
source_snapshot_ref=candidate.source_snapshot_ref,
content_json={"items": [], "summary": {}},
)
@@ -180,7 +182,7 @@ async def build_manifest(
@router.post("/candidates/{candidate_id}/approve")
async def approve_candidate_endpoint(
candidate_id: str,
payload: Dict[str, Any],
payload: dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
try:
@@ -191,7 +193,7 @@ async def approve_candidate_endpoint(
decided_by=str(payload["decided_by"]),
comment=payload.get("comment"),
)
except Exception as exc: # noqa: BLE001
except Exception as exc:
raise HTTPException(
status_code=409, detail={"message": str(exc), "code": "APPROVAL_GATE_ERROR"}
)
@@ -208,7 +210,7 @@ async def approve_candidate_endpoint(
@router.post("/candidates/{candidate_id}/reject")
async def reject_candidate_endpoint(
candidate_id: str,
payload: Dict[str, Any],
payload: dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
try:
@@ -219,7 +221,7 @@ async def reject_candidate_endpoint(
decided_by=str(payload["decided_by"]),
comment=payload.get("comment"),
)
except Exception as exc: # noqa: BLE001
except Exception as exc:
raise HTTPException(
status_code=409, detail={"message": str(exc), "code": "APPROVAL_GATE_ERROR"}
)
@@ -236,7 +238,7 @@ async def reject_candidate_endpoint(
@router.post("/candidates/{candidate_id}/publish")
async def publish_candidate_endpoint(
candidate_id: str,
payload: Dict[str, Any],
payload: dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
try:
@@ -248,7 +250,7 @@ async def publish_candidate_endpoint(
target_channel=str(payload["target_channel"]),
publication_ref=payload.get("publication_ref"),
)
except Exception as exc: # noqa: BLE001
except Exception as exc:
raise HTTPException(
status_code=409,
detail={"message": str(exc), "code": "PUBLICATION_GATE_ERROR"},
@@ -280,7 +282,7 @@ async def publish_candidate_endpoint(
@router.post("/publications/{publication_id}/revoke")
async def revoke_publication_endpoint(
publication_id: str,
payload: Dict[str, Any],
payload: dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
try:
@@ -290,7 +292,7 @@ async def revoke_publication_endpoint(
revoked_by=str(payload["revoked_by"]),
comment=payload.get("comment"),
)
except Exception as exc: # noqa: BLE001
except Exception as exc:
raise HTTPException(
status_code=409,
detail={"message": str(exc), "code": "PUBLICATION_GATE_ERROR"},

View File

@@ -4,14 +4,15 @@
# @RELATION DEPENDS_ON -> [get_db]
# @RELATION DEPENDS_ON -> [ConnectionConfig]
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ...core.database import get_db, ensure_connection_configs_table
from ...models.connection import ConnectionConfig
from pydantic import BaseModel
from datetime import datetime
from ...core.logger import logger, belief_scope
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ...core.database import ensure_connection_configs_table, get_db
from ...core.logger import belief_scope, logger
from ...models.connection import ConnectionConfig
router = APIRouter()
@@ -36,10 +37,10 @@ class ConnectionSchema(BaseModel):
id: str
name: str
type: str
host: Optional[str] = None
port: Optional[int] = None
database: Optional[str] = None
username: Optional[str] = None
host: str | None = None
port: int | None = None
database: str | None = None
username: str | None = None
created_at: datetime
class Config:
@@ -55,11 +56,11 @@ class ConnectionSchema(BaseModel):
class ConnectionCreate(BaseModel):
name: str
type: str
host: Optional[str] = None
port: Optional[int] = None
database: Optional[str] = None
username: Optional[str] = None
password: Optional[str] = None
host: str | None = None
port: int | None = None
database: str | None = None
username: str | None = None
password: str | None = None
# #endregion ConnectionCreate
@@ -71,7 +72,7 @@ class ConnectionCreate(BaseModel):
# @POST: Returns list of connection configs.
# @RELATION CALLS -> _ensure_connections_schema
# @RELATION DEPENDS_ON -> ConnectionConfig
@router.get("", response_model=List[ConnectionSchema])
@router.get("", response_model=list[ConnectionSchema])
async def list_connections(db: Session = Depends(get_db)):
with belief_scope("ConnectionsRouter.list_connections"):
_ensure_connections_schema(db)

View File

@@ -33,10 +33,10 @@
#
# @TEST_INVARIANT: metadata_consistency -> verifies: [dashboard_list_happy, empty_dashboards]
from ._action_routes import *
from ._detail_routes import *
from ._listing_routes import *
from ._router import router
from ._schemas import *
from ._listing_routes import *
from ._detail_routes import *
from ._action_routes import *
# #endregion DashboardsApi

View File

@@ -6,18 +6,19 @@
from fastapi import Depends, HTTPException
from src.core.logger import belief_scope, logger
from src.dependencies import (
get_config_manager,
get_task_manager,
has_permission,
)
from src.core.logger import logger, belief_scope
from ._router import router
from ._schemas import (
MigrateRequest,
BackupRequest,
MigrateRequest,
TaskResponse,
)
from ._router import router
# #region migrate_dashboards [C:2] [TYPE Function]
@@ -91,7 +92,7 @@ async def migrate_dashboards(
f"[migrate_dashboards][Coherence:Failed] Failed to create migration task: {e}"
)
raise HTTPException(
status_code=503, detail=f"Failed to create migration task: {str(e)}"
status_code=503, detail=f"Failed to create migration task: {e!s}"
)
@@ -158,7 +159,7 @@ async def backup_dashboards(
f"[backup_dashboards][Coherence:Failed] Failed to create backup task: {e}"
)
raise HTTPException(
status_code=503, detail=f"Failed to create backup task: {str(e)}"
status_code=503, detail=f"Failed to create backup task: {e!s}"
)

View File

@@ -6,23 +6,24 @@
# @RELATION DEPENDS_ON -> [DashboardHelpers]
# @RELATION DEPENDS_ON -> [DashboardProjection]
from typing import Optional, List, Dict, Any
import re
from typing import Any
from urllib.parse import urlparse
from fastapi import Depends, HTTPException, Query, Response
from fastapi.responses import JSONResponse
from src.core.async_superset_client import AsyncSupersetClient
from src.core.logger import belief_scope, logger
from src.core.superset_client import SupersetClient
from src.core.utils.network import DashboardNotFoundError
from src.dependencies import (
get_config_manager,
get_task_manager,
get_mapping_service,
get_task_manager,
has_permission,
)
from src.core.async_superset_client import AsyncSupersetClient
from src.core.superset_client import SupersetClient
from src.core.logger import logger, belief_scope
from src.core.utils.network import DashboardNotFoundError
from ._helpers import (
_resolve_dashboard_id_from_ref,
_resolve_dashboard_id_from_ref_async,
@@ -30,6 +31,7 @@ from ._helpers import (
from ._projection import (
_task_matches_dashboard,
)
from ._router import router
from ._schemas import (
DashboardDetailResponse,
DashboardTaskHistoryItem,
@@ -37,7 +39,6 @@ from ._schemas import (
DatabaseMapping,
DatabaseMappingsResponse,
)
from ._router import router
# #region get_database_mappings [C:2] [TYPE Function]
@@ -99,7 +100,7 @@ async def get_database_mappings(
f"[get_database_mappings][Coherence:Failed] Failed to get database mappings: {e}"
)
raise HTTPException(
status_code=503, detail=f"Failed to get database mappings: {str(e)}"
status_code=503, detail=f"Failed to get database mappings: {e!s}"
)
@@ -144,7 +145,7 @@ async def get_dashboard_detail(
f"[get_dashboard_detail][Coherence:Failed] Failed to fetch dashboard detail: {e}"
)
raise HTTPException(
status_code=503, detail=f"Failed to fetch dashboard detail: {str(e)}"
status_code=503, detail=f"Failed to fetch dashboard detail: {e!s}"
)
@@ -158,7 +159,7 @@ async def get_dashboard_detail(
@router.get("/{dashboard_ref}/tasks", response_model=DashboardTaskHistoryResponse)
async def get_dashboard_tasks_history(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
limit: int = Query(20, ge=1, le=100),
config_manager=Depends(get_config_manager),
task_manager=Depends(get_task_manager),
@@ -168,8 +169,8 @@ async def get_dashboard_tasks_history(
"get_dashboard_tasks_history",
f"dashboard_ref={dashboard_ref}, env_id={env_id}, limit={limit}",
):
dashboard_id: Optional[int] = None
client: Optional[AsyncSupersetClient] = None
dashboard_id: int | None = None
client: AsyncSupersetClient | None = None
try:
if dashboard_ref.isdigit():
dashboard_id = int(dashboard_ref)
@@ -351,7 +352,7 @@ async def get_dashboard_thumbnail(
)
if thumb_response.status_code == 202:
payload_202: Dict[str, Any] = {}
payload_202: dict[str, Any] = {}
try:
payload_202 = thumb_response.json()
except Exception:
@@ -373,7 +374,7 @@ async def get_dashboard_thumbnail(
)
raise HTTPException(
status_code=503,
detail=f"Failed to fetch dashboard thumbnail: {str(e)}",
detail=f"Failed to fetch dashboard thumbnail: {e!s}",
)

View File

@@ -4,11 +4,12 @@
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [AsyncSupersetClient]
from typing import List, Optional, Dict, Any
from typing import Any
from fastapi import HTTPException
from src.core.superset_client import SupersetClient
from src.core.async_superset_client import AsyncSupersetClient
from src.core.logger import logger
from src.core.superset_client import SupersetClient
# #region _find_dashboard_id_by_slug [C:2] [TYPE Function]
@@ -18,7 +19,7 @@ from src.core.logger import logger
def _find_dashboard_id_by_slug(
client: SupersetClient,
dashboard_slug: str,
) -> Optional[int]:
) -> int | None:
query_variants = [
{
"filters": [{"col": "slug", "opr": "eq", "value": dashboard_slug}],
@@ -81,7 +82,7 @@ def _resolve_dashboard_id_from_ref(
async def _find_dashboard_id_by_slug_async(
client: AsyncSupersetClient,
dashboard_slug: str,
) -> Optional[int]:
) -> int | None:
query_variants = [
{
"filters": [{"col": "slug", "opr": "eq", "value": dashboard_slug}],
@@ -140,10 +141,10 @@ async def _resolve_dashboard_id_from_ref_async(
# @BRIEF Normalize query filter values to lower-cased non-empty tokens.
# @PRE: values may be None or list of strings.
# @POST: Returns trimmed normalized list preserving input order.
def _normalize_filter_values(values: Optional[List[str]]) -> List[str]:
def _normalize_filter_values(values: list[str] | None) -> list[str]:
if not values:
return []
normalized: List[str] = []
normalized: list[str] = []
for value in values:
token = str(value or "").strip().lower()
if token:
@@ -158,7 +159,7 @@ def _normalize_filter_values(values: Optional[List[str]]) -> List[str]:
# @BRIEF Build comparable git status token for dashboards filtering.
# @PRE: dashboard payload may contain git_status or None.
# @POST: Returns one of ok|diff|no_repo|error|pending.
def _dashboard_git_filter_value(dashboard: Dict[str, Any]) -> str:
def _dashboard_git_filter_value(dashboard: dict[str, Any]) -> str:
git_status = dashboard.get("git_status") or {}
sync_status = str(git_status.get("sync_status") or "").strip().upper()
has_repo = git_status.get("has_repo")

View File

@@ -6,25 +6,32 @@
# @RELATION DEPENDS_ON -> [DashboardHelpers]
# @RELATION DEPENDS_ON -> [DashboardProjection]
import os
from typing import List, Optional, Dict, Any, Literal
from typing import Any, Literal
from fastapi import Depends, HTTPException, Query
from sqlalchemy.orm import Session
from src.dependencies import (
get_config_manager, get_task_manager, get_resource_service,
get_current_user, has_permission,
)
from src.core.database import get_db
from src.core.logger import logger, belief_scope
from src.core.logger import belief_scope, logger
from src.dependencies import (
get_config_manager,
get_current_user,
get_resource_service,
get_task_manager,
has_permission,
)
from src.models.auth import User
from src.services.profile_service import ProfileService
from ._helpers import _normalize_filter_values, _dashboard_git_filter_value
from ._helpers import _dashboard_git_filter_value, _normalize_filter_values
from ._projection import (
_project_dashboard_response_items, _get_profile_filter_binding,
_resolve_profile_actor_aliases, _matches_dashboard_actor_aliases,
_get_profile_filter_binding,
_matches_dashboard_actor_aliases,
_project_dashboard_response_items,
_resolve_profile_actor_aliases,
)
from ._schemas import EffectiveProfileFilter, DashboardsResponse
from ._router import router
from ._schemas import DashboardsResponse, EffectiveProfileFilter
# #region get_dashboards [C:3] [TYPE Function]
@@ -39,17 +46,17 @@ from ._router import router
@router.get("", response_model=DashboardsResponse)
async def get_dashboards(
env_id: str,
search: Optional[str] = None,
search: str | None = None,
page: int = 1,
page_size: int = 10,
page_context: Literal["dashboards_main", "other"] = Query("dashboards_main"),
apply_profile_default: bool = Query(default=True),
override_show_all: bool = Query(default=False),
filter_title: Optional[List[str]] = Query(default=None),
filter_git_status: Optional[List[str]] = Query(default=None),
filter_llm_status: Optional[List[str]] = Query(default=None),
filter_changed_on: Optional[List[str]] = Query(default=None),
filter_actor: Optional[List[str]] = Query(default=None),
filter_title: list[str] | None = Query(default=None),
filter_git_status: list[str] | None = Query(default=None),
filter_llm_status: list[str] | None = Query(default=None),
filter_changed_on: list[str] | None = Query(default=None),
filter_actor: list[str] | None = Query(default=None),
config_manager=Depends(get_config_manager),
task_manager=Depends(get_task_manager),
resource_service=Depends(get_resource_service),
@@ -75,7 +82,7 @@ async def get_dashboards(
logger.error(f"[get_dashboards][Coherence:Failed] Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
bound_username: Optional[str] = None
bound_username: str | None = None
can_apply_profile_filter = False
can_apply_slug_filter = False
effective_profile_filter = EffectiveProfileFilter(
@@ -85,7 +92,7 @@ async def get_dashboards(
username=None,
match_logic=None,
)
profile_service: Optional[ProfileService] = None
profile_service: ProfileService | None = None
try:
profile_service_module = getattr(ProfileService, "__module__", "")
@@ -245,7 +252,7 @@ async def get_dashboards(
"Applying profile actor filter",
extra={"src": "get_dashboards", "payload": {"env": env_id, "bound_username": bound_username, "actor_aliases": actor_aliases, "dashboards_before": len(dashboards)}},
)
filtered_dashboards: List[Dict[str, Any]] = []
filtered_dashboards: list[dict[str, Any]] = []
max_actor_samples = 15
for index, dashboard in enumerate(dashboards):
owners_value = dashboard.get("owners")
@@ -287,7 +294,7 @@ async def get_dashboards(
or search_lower in d.get("slug", "").lower()
]
def _matches_dashboard_filters(dashboard: Dict[str, Any]) -> bool:
def _matches_dashboard_filters(dashboard: dict[str, Any]) -> bool:
title_value = str(dashboard.get("title") or "").strip().lower()
if title_filters and title_value not in title_filters:
return False
@@ -376,7 +383,7 @@ async def get_dashboards(
f"[get_dashboards][Coherence:Failed] Failed to fetch dashboards: {e}"
)
raise HTTPException(
status_code=503, detail=f"Failed to fetch dashboards: {str(e)}"
status_code=503, detail=f"Failed to fetch dashboards: {e!s}"
)

View File

@@ -4,9 +4,9 @@
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [ProfileService]
from typing import Any, Dict, List, Optional
from typing import Any
from src.core.logger import logger, belief_scope
from src.core.logger import logger
from src.core.superset_client import SupersetClient
from src.core.superset_profile_lookup import SupersetAccountLookupAdapter
from src.models.auth import User
@@ -15,7 +15,7 @@ from src.services.profile_service import ProfileService
# #region _normalize_actor_alias_token [C:2] [TYPE Function]
# @BRIEF Normalize actor alias token to comparable trim+lower text.
def _normalize_actor_alias_token(value: Any) -> Optional[str]:
def _normalize_actor_alias_token(value: Any) -> str | None:
if value is None:
return None
normalized = str(value).strip().lower()
@@ -27,7 +27,7 @@ def _normalize_actor_alias_token(value: Any) -> Optional[str]:
# #region _normalize_owner_display_token [C:2] [TYPE Function]
# @BRIEF Project owner payload value into stable display string for API response contracts.
def _normalize_owner_display_token(owner: Any) -> Optional[str]:
def _normalize_owner_display_token(owner: Any) -> str | None:
if owner is None:
return None
if isinstance(owner, dict):
@@ -46,15 +46,15 @@ def _normalize_owner_display_token(owner: Any) -> Optional[str]:
# #region _normalize_dashboard_owner_values [C:2] [TYPE Function]
# @BRIEF Normalize dashboard owners payload to optional list of display strings.
def _normalize_dashboard_owner_values(owners: Any) -> Optional[List[str]]:
def _normalize_dashboard_owner_values(owners: Any) -> list[str] | None:
if owners is None:
return None
raw_items: List[Any]
raw_items: list[Any]
if isinstance(owners, list):
raw_items = owners
else:
raw_items = [owners]
normalized: List[str] = []
normalized: list[str] = []
for owner in raw_items:
token = _normalize_owner_display_token(owner)
if token and token not in normalized:
@@ -68,9 +68,9 @@ def _normalize_dashboard_owner_values(owners: Any) -> Optional[List[str]]:
# #region _project_dashboard_response_items [C:2] [TYPE Function]
# @BRIEF Project dashboard payloads to response-contract-safe shape.
def _project_dashboard_response_items(
dashboards: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
projected: List[Dict[str, Any]] = []
dashboards: list[dict[str, Any]],
) -> list[dict[str, Any]]:
projected: list[dict[str, Any]] = []
for dashboard in dashboards:
projected_dashboard = dict(dashboard)
projected_dashboard["owners"] = _normalize_dashboard_owner_values(
@@ -87,9 +87,9 @@ def _project_dashboard_response_items(
# @BRIEF Resolve dashboard profile-filter binding through current or legacy profile service contracts.
def _get_profile_filter_binding(
profile_service: Any, current_user: User
) -> Dict[str, Any]:
) -> dict[str, Any]:
def _read_optional_string(value: Any) -> Optional[str]:
def _read_optional_string(value: Any) -> str | None:
return value if isinstance(value, str) else None
def _read_bool(value: Any, default: bool) -> bool:
@@ -143,11 +143,11 @@ def _get_profile_filter_binding(
# #region _resolve_profile_actor_aliases [C:2] [TYPE Function]
# @BRIEF Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out.
# @SIDE_EFFECT: Performs at most one Superset users-lookup request.
def _resolve_profile_actor_aliases(env: Any, bound_username: str) -> List[str]:
def _resolve_profile_actor_aliases(env: Any, bound_username: str) -> list[str]:
normalized_bound = _normalize_actor_alias_token(bound_username)
if not normalized_bound:
return []
aliases: List[str] = [normalized_bound]
aliases: list[str] = [normalized_bound]
try:
client = SupersetClient(env)
adapter = SupersetAccountLookupAdapter(
@@ -164,7 +164,7 @@ def _resolve_profile_actor_aliases(env: Any, bound_username: str) -> List[str]:
lookup_items = (
lookup_payload.get("items", []) if isinstance(lookup_payload, dict) else []
)
matched_item: Optional[Dict[str, Any]] = None
matched_item: dict[str, Any] | None = None
for item in lookup_items:
if not isinstance(item, dict):
continue
@@ -200,9 +200,9 @@ def _resolve_profile_actor_aliases(env: Any, bound_username: str) -> List[str]:
# @BRIEF Apply profile actor matching against multiple aliases (username + optional display name).
def _matches_dashboard_actor_aliases(
profile_service: ProfileService,
actor_aliases: List[str],
owners: Optional[Any],
modified_by: Optional[str],
actor_aliases: list[str],
owners: Any | None,
modified_by: str | None,
) -> bool:
for actor_alias in actor_aliases:
if profile_service.matches_dashboard_actor(
@@ -220,7 +220,7 @@ def _matches_dashboard_actor_aliases(
# #region _task_matches_dashboard [C:2] [TYPE Function]
# @BRIEF Checks whether task params are tied to a specific dashboard and environment.
def _task_matches_dashboard(
task: Any, dashboard_id: int, env_id: Optional[str]
task: Any, dashboard_id: int, env_id: str | None
) -> bool:
plugin_id = getattr(task, "plugin_id", None)
if plugin_id not in {"superset-backup", "llm_dashboard_validation"}:

View File

@@ -3,17 +3,18 @@
# @LAYER: Infra
# @RELATION DEPENDS_ON -> [Pydantic]
from typing import List, Optional, Dict, Any, Literal
from typing import Literal
from pydantic import BaseModel, Field
# #region GitStatus [C:1] [TYPE DataClass]
# @BRIEF DTO for dashboard Git synchronization status.
class GitStatus(BaseModel):
branch: Optional[str] = None
sync_status: Optional[str] = Field(None, pattern="^OK|DIFF|NO_REPO|ERROR$")
has_repo: Optional[bool] = None
has_changes_for_commit: Optional[bool] = None
branch: str | None = None
sync_status: str | None = Field(None, pattern="^OK|DIFF|NO_REPO|ERROR$")
has_repo: bool | None = None
has_changes_for_commit: bool | None = None
# #endregion GitStatus
@@ -22,12 +23,12 @@ class GitStatus(BaseModel):
# #region LastTask [C:2] [TYPE DataClass]
# @BRIEF DTO for the most recent background task associated with a dashboard.
class LastTask(BaseModel):
task_id: Optional[str] = None
status: Optional[str] = Field(
task_id: str | None = None
status: str | None = Field(
None,
pattern="^PENDING|RUNNING|SUCCESS|FAILED|ERROR|AWAITING_INPUT|WAITING_INPUT|AWAITING_MAPPING$",
)
validation_status: Optional[str] = Field(None, pattern="^PASS|FAIL|WARN|UNKNOWN$")
validation_status: str | None = Field(None, pattern="^PASS|FAIL|WARN|UNKNOWN$")
# #endregion LastTask
@@ -38,14 +39,14 @@ class LastTask(BaseModel):
class DashboardItem(BaseModel):
id: int
title: str
slug: Optional[str] = None
url: Optional[str] = None
last_modified: Optional[str] = None
created_by: Optional[str] = None
modified_by: Optional[str] = None
owners: Optional[List[str]] = None
git_status: Optional[GitStatus] = None
last_task: Optional[LastTask] = None
slug: str | None = None
url: str | None = None
last_modified: str | None = None
created_by: str | None = None
modified_by: str | None = None
owners: list[str] | None = None
git_status: GitStatus | None = None
last_task: LastTask | None = None
# #endregion DashboardItem
@@ -57,10 +58,8 @@ class EffectiveProfileFilter(BaseModel):
applied: bool
source_page: Literal["dashboards_main", "other"] = "dashboards_main"
override_show_all: bool = False
username: Optional[str] = None
match_logic: Optional[
Literal["owners_or_modified_by", "slug_only", "owners_or_modified_by+slug_only"]
] = None
username: str | None = None
match_logic: Literal["owners_or_modified_by", "slug_only", "owners_or_modified_by+slug_only"] | None = None
# #endregion EffectiveProfileFilter
@@ -69,12 +68,12 @@ class EffectiveProfileFilter(BaseModel):
# #region DashboardsResponse [C:1] [TYPE DataClass]
# @BRIEF Envelope DTO for paginated dashboards list.
class DashboardsResponse(BaseModel):
dashboards: List[DashboardItem]
dashboards: list[DashboardItem]
total: int
page: int
page_size: int
total_pages: int
effective_profile_filter: Optional[EffectiveProfileFilter] = None
effective_profile_filter: EffectiveProfileFilter | None = None
# #endregion DashboardsResponse
@@ -85,10 +84,10 @@ class DashboardsResponse(BaseModel):
class DashboardChartItem(BaseModel):
id: int
title: str
viz_type: Optional[str] = None
dataset_id: Optional[int] = None
last_modified: Optional[str] = None
overview: Optional[str] = None
viz_type: str | None = None
dataset_id: int | None = None
last_modified: str | None = None
overview: str | None = None
# #endregion DashboardChartItem
@@ -99,10 +98,10 @@ class DashboardChartItem(BaseModel):
class DashboardDatasetItem(BaseModel):
id: int
table_name: str
schema: Optional[str] = None
schema: str | None = None
database: str
last_modified: Optional[str] = None
overview: Optional[str] = None
last_modified: str | None = None
overview: str | None = None
# #endregion DashboardDatasetItem
@@ -113,13 +112,13 @@ class DashboardDatasetItem(BaseModel):
class DashboardDetailResponse(BaseModel):
id: int
title: str
slug: Optional[str] = None
url: Optional[str] = None
description: Optional[str] = None
last_modified: Optional[str] = None
published: Optional[bool] = None
charts: List[DashboardChartItem]
datasets: List[DashboardDatasetItem]
slug: str | None = None
url: str | None = None
description: str | None = None
last_modified: str | None = None
published: bool | None = None
charts: list[DashboardChartItem]
datasets: list[DashboardDatasetItem]
chart_count: int
dataset_count: int
@@ -133,11 +132,11 @@ class DashboardTaskHistoryItem(BaseModel):
id: str
plugin_id: str
status: str
validation_status: Optional[str] = None
started_at: Optional[str] = None
finished_at: Optional[str] = None
env_id: Optional[str] = None
summary: Optional[str] = None
validation_status: str | None = None
started_at: str | None = None
finished_at: str | None = None
env_id: str | None = None
summary: str | None = None
# #endregion DashboardTaskHistoryItem
@@ -147,7 +146,7 @@ class DashboardTaskHistoryItem(BaseModel):
# @BRIEF Collection DTO for task history.
class DashboardTaskHistoryResponse(BaseModel):
dashboard_id: int
items: List[DashboardTaskHistoryItem]
items: list[DashboardTaskHistoryItem]
# #endregion DashboardTaskHistoryResponse
@@ -158,8 +157,8 @@ class DashboardTaskHistoryResponse(BaseModel):
class DatabaseMapping(BaseModel):
source_db: str
target_db: str
source_db_uuid: Optional[str] = None
target_db_uuid: Optional[str] = None
source_db_uuid: str | None = None
target_db_uuid: str | None = None
confidence: float
@@ -169,7 +168,7 @@ class DatabaseMapping(BaseModel):
# #region DatabaseMappingsResponse [C:1] [TYPE DataClass]
# @BRIEF Wrapper for database mappings.
class DatabaseMappingsResponse(BaseModel):
mappings: List[DatabaseMapping]
mappings: list[DatabaseMapping]
# #endregion DatabaseMappingsResponse
@@ -180,10 +179,10 @@ class DatabaseMappingsResponse(BaseModel):
class MigrateRequest(BaseModel):
source_env_id: str = Field(..., description="Source environment ID")
target_env_id: str = Field(..., description="Target environment ID")
dashboard_ids: List[int] = Field(
dashboard_ids: list[int] = Field(
..., description="List of dashboard IDs to migrate"
)
db_mappings: Optional[Dict[str, str]] = Field(
db_mappings: dict[str, str] | None = Field(
None, description="Database mappings for migration"
)
replace_db_config: bool = Field(False, description="Replace database configuration")
@@ -205,8 +204,8 @@ class TaskResponse(BaseModel):
# @BRIEF DTO for dashboard backup requests.
class BackupRequest(BaseModel):
env_id: str = Field(..., description="Environment ID")
dashboard_ids: List[int] = Field(..., description="List of dashboard IDs to backup")
schedule: Optional[str] = Field(
dashboard_ids: list[int] = Field(..., description="List of dashboard IDs to backup")
schedule: str | None = Field(
None, description="Cron schedule for recurring backups (e.g., '0 0 * * *')"
)

View File

@@ -5,31 +5,31 @@
# @REJECTED: Keeping all routes in one file because it exceeded the fractal limit by 6x and accumulated severe structural erosion risk.
from src.api.routes.dataset_review_pkg._dependencies import ( # noqa: F401
StartSessionRequest,
UpdateSessionRequest,
SessionCollectionResponse,
ExportArtifactResponse,
FieldSemanticUpdateRequest,
FeedbackRequest,
ClarificationAnswerRequest,
ClarificationSessionSummaryResponse,
ClarificationStateResponse,
ClarificationAnswerResultResponse,
FeedbackResponse,
ApproveMappingRequest,
BatchApproveMappingRequest,
BatchApproveSemanticItemRequest,
BatchApproveSemanticRequest,
BatchApproveMappingRequest,
PreviewEnqueueResultResponse,
MappingCollectionResponse,
UpdateExecutionMappingRequest,
ClarificationAnswerRequest,
ClarificationAnswerResultResponse,
ClarificationSessionSummaryResponse,
ClarificationStateResponse,
ExportArtifactResponse,
FeedbackRequest,
FeedbackResponse,
FieldSemanticUpdateRequest,
LaunchDatasetResponse,
MappingCollectionResponse,
PreviewEnqueueResultResponse,
SessionCollectionResponse,
StartSessionRequest,
UpdateExecutionMappingRequest,
UpdateSessionRequest,
_get_clarification_engine,
_get_orchestrator,
_get_repository,
_require_auto_review_flag,
_require_clarification_flag,
_require_execution_flag,
_get_repository,
_get_orchestrator,
_get_clarification_engine,
_update_semantic_field_state,
)
from src.api.routes.dataset_review_pkg._routes import router # noqa: F401

View File

@@ -4,11 +4,10 @@
from __future__ import annotations
import json
from datetime import datetime
from typing import Any, Dict, List, Optional, Union, cast
from typing import Any
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Response, status
from fastapi import Depends, Header, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
@@ -16,33 +15,18 @@ from src.core.database import get_db
from src.core.logger import belief_scope, logger
from src.dependencies import (
get_config_manager,
get_current_user,
get_task_manager,
has_permission,
)
from src.models.auth import User
from src.models.dataset_review import (
AnswerKind,
ApprovalState,
ArtifactFormat,
CandidateStatus,
ClarificationSession,
DatasetReviewSession,
ExecutionMapping,
FieldProvenance,
MappingMethod,
PreviewStatus,
QuestionState,
ReadinessState,
RecommendedAction,
SemanticCandidate,
SemanticFieldEntry,
SessionStatus,
)
from src.schemas.dataset_review import (
ClarificationAnswerDto,
ClarificationQuestionDto,
ClarificationSessionDto,
CompiledPreviewDto,
DatasetRunContextDto,
ExecutionMappingDto,
@@ -52,16 +36,10 @@ from src.schemas.dataset_review import (
ValidationFindingDto,
)
from src.services.dataset_review.clarification_engine import (
ClarificationAnswerCommand,
ClarificationEngine,
ClarificationQuestionPayload,
ClarificationStateResult,
)
from src.services.dataset_review.orchestrator import (
DatasetReviewOrchestrator,
LaunchDatasetCommand,
PreparePreviewCommand,
StartSessionCommand,
)
from src.services.dataset_review.repositories.session_repository import (
DatasetReviewSessionRepository,
@@ -84,7 +62,7 @@ class StartSessionRequest(BaseModel):
# @BRIEF Request DTO for lifecycle state updates on an existing session.
class UpdateSessionRequest(BaseModel):
status: SessionStatus
note: Optional[str] = None
note: str | None = None
# #endregion UpdateSessionRequest
@@ -93,7 +71,7 @@ class UpdateSessionRequest(BaseModel):
# #region SessionCollectionResponse [C:1] [TYPE Class]
# @BRIEF Paginated session collection response.
class SessionCollectionResponse(BaseModel):
items: List[SessionSummary]
items: list[SessionSummary]
total: int
page: int
page_size: int
@@ -112,8 +90,8 @@ class ExportArtifactResponse(BaseModel):
format: str
storage_ref: str
created_by_user_id: str
created_at: Optional[str] = None
content: Dict[str, Any]
created_at: str | None = None
content: dict[str, Any]
# #endregion ExportArtifactResponse
@@ -122,12 +100,12 @@ class ExportArtifactResponse(BaseModel):
# #region FieldSemanticUpdateRequest [C:1] [TYPE Class]
# @BRIEF Request DTO for field-level semantic candidate acceptance or manual override.
class FieldSemanticUpdateRequest(BaseModel):
candidate_id: Optional[str] = None
verbose_name: Optional[str] = None
description: Optional[str] = None
display_format: Optional[str] = None
candidate_id: str | None = None
verbose_name: str | None = None
description: str | None = None
display_format: str | None = None
lock_field: bool = False
resolution_note: Optional[str] = None
resolution_note: str | None = None
# #endregion FieldSemanticUpdateRequest
@@ -147,7 +125,7 @@ class FeedbackRequest(BaseModel):
class ClarificationAnswerRequest(BaseModel):
question_id: str = Field(..., min_length=1)
answer_kind: AnswerKind
answer_value: Optional[str] = None
answer_value: str | None = None
# #endregion ClarificationAnswerRequest
@@ -159,10 +137,10 @@ class ClarificationSessionSummaryResponse(BaseModel):
clarification_session_id: str
session_id: str
status: str
current_question_id: Optional[str] = None
current_question_id: str | None = None
resolved_count: int
remaining_count: int
summary_delta: Optional[str] = None
summary_delta: str | None = None
# #endregion ClarificationSessionSummaryResponse
@@ -171,8 +149,8 @@ class ClarificationSessionSummaryResponse(BaseModel):
# #region ClarificationStateResponse [C:1] [TYPE Class]
# @BRIEF Response DTO for current clarification state and active question payload.
class ClarificationStateResponse(BaseModel):
clarification_session: Optional[ClarificationSessionSummaryResponse] = None
current_question: Optional[ClarificationQuestionDto] = None
clarification_session: ClarificationSessionSummaryResponse | None = None
current_question: ClarificationQuestionDto | None = None
# #endregion ClarificationStateResponse
@@ -183,7 +161,7 @@ class ClarificationStateResponse(BaseModel):
class ClarificationAnswerResultResponse(BaseModel):
clarification_state: ClarificationStateResponse
session: SessionSummary
changed_findings: List[ValidationFindingDto]
changed_findings: list[ValidationFindingDto]
# #endregion ClarificationAnswerResultResponse
@@ -202,7 +180,7 @@ class FeedbackResponse(BaseModel):
# #region ApproveMappingRequest [C:1] [TYPE Class]
# @BRIEF Optional request DTO for explicit mapping approval audit notes.
class ApproveMappingRequest(BaseModel):
approval_note: Optional[str] = None
approval_note: str | None = None
# #endregion ApproveMappingRequest
@@ -222,7 +200,7 @@ class BatchApproveSemanticItemRequest(BaseModel):
# #region BatchApproveSemanticRequest [C:1] [TYPE Class]
# @BRIEF Request DTO for explicit batch semantic approvals.
class BatchApproveSemanticRequest(BaseModel):
items: List[BatchApproveSemanticItemRequest] = Field(..., min_length=1)
items: list[BatchApproveSemanticItemRequest] = Field(..., min_length=1)
# #endregion BatchApproveSemanticRequest
@@ -231,8 +209,8 @@ class BatchApproveSemanticRequest(BaseModel):
# #region BatchApproveMappingRequest [C:1] [TYPE Class]
# @BRIEF Request DTO for explicit batch mapping approvals.
class BatchApproveMappingRequest(BaseModel):
mapping_ids: List[str] = Field(..., min_length=1)
approval_note: Optional[str] = None
mapping_ids: list[str] = Field(..., min_length=1)
approval_note: str | None = None
# #endregion BatchApproveMappingRequest
@@ -242,9 +220,9 @@ class BatchApproveMappingRequest(BaseModel):
# @BRIEF Async preview trigger response exposing only enqueue state.
class PreviewEnqueueResultResponse(BaseModel):
session_id: str
session_version: Optional[int] = None
session_version: int | None = None
preview_status: str
task_id: Optional[str] = None
task_id: str | None = None
# #endregion PreviewEnqueueResultResponse
@@ -253,7 +231,7 @@ class PreviewEnqueueResultResponse(BaseModel):
# #region MappingCollectionResponse [C:1] [TYPE Class]
# @BRIEF Wrapper for execution mapping list responses.
class MappingCollectionResponse(BaseModel):
items: List[ExecutionMappingDto]
items: list[ExecutionMappingDto]
# #endregion MappingCollectionResponse
@@ -262,9 +240,9 @@ class MappingCollectionResponse(BaseModel):
# #region UpdateExecutionMappingRequest [C:1] [TYPE Class]
# @BRIEF Request DTO for one manual execution-mapping override update.
class UpdateExecutionMappingRequest(BaseModel):
effective_value: Optional[Any] = None
mapping_method: Optional[str] = Field(default=None, pattern="^(manual_override|direct_match|heuristic_match|semantic_match)$")
transformation_note: Optional[str] = None
effective_value: Any | None = None
mapping_method: str | None = Field(default=None, pattern="^(manual_override|direct_match|heuristic_match|semantic_match)$")
transformation_note: str | None = None
# #endregion UpdateExecutionMappingRequest

View File

@@ -5,43 +5,10 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, List, Optional, Union, cast
from typing import Any, Union, cast
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from src.core.logger import belief_scope, logger
from src.dependencies import get_config_manager, get_current_user, has_permission
from src.models.auth import User
from src.models.dataset_review import (
ApprovalState,
ArtifactFormat,
FieldProvenance,
MappingMethod,
PreviewStatus,
ReadinessState,
RecommendedAction,
SessionStatus,
)
from src.schemas.dataset_review import (
ClarificationAnswerDto,
CompiledPreviewDto,
ExecutionMappingDto,
SemanticFieldEntryDto,
SessionSummary,
ValidationFindingDto,
)
from src.services.dataset_review.clarification_engine import (
ClarificationAnswerCommand,
ClarificationStateResult,
)
from src.services.dataset_review.orchestrator import (
LaunchDatasetCommand,
PreparePreviewCommand,
StartSessionCommand,
)
from src.services.dataset_review.repositories.session_repository import (
DatasetReviewSessionVersionConflictError,
)
from src.api.routes.dataset_review_pkg._dependencies import (
BatchApproveMappingRequest,
BatchApproveSemanticRequest,
@@ -60,15 +27,16 @@ from src.api.routes.dataset_review_pkg._dependencies import (
UpdateExecutionMappingRequest,
UpdateSessionRequest,
_build_documentation_export,
_build_session_version_conflict_http_exception,
_build_sql_lab_redirect_url,
_build_validation_export,
_commit_owned_session_mutation,
_get_clarification_engine,
_get_latest_clarification_session_or_404,
_get_orchestrator,
_get_owned_field_or_404,
_get_owned_mapping_or_404,
_get_owned_session_or_404,
_get_orchestrator,
_get_repository,
_prepare_owned_session_mutation,
_record_session_event,
@@ -85,7 +53,38 @@ from src.api.routes.dataset_review_pkg._dependencies import (
_serialize_session_detail,
_serialize_session_summary,
_update_semantic_field_state,
_build_session_version_conflict_http_exception,
)
from src.core.logger import belief_scope, logger
from src.dependencies import get_config_manager, get_current_user, has_permission
from src.models.auth import User
from src.models.dataset_review import (
ApprovalState,
ArtifactFormat,
FieldProvenance,
MappingMethod,
PreviewStatus,
ReadinessState,
RecommendedAction,
SessionStatus,
)
from src.schemas.dataset_review import (
CompiledPreviewDto,
ExecutionMappingDto,
SemanticFieldEntryDto,
SessionSummary,
ValidationFindingDto,
)
from src.services.dataset_review.clarification_engine import (
ClarificationAnswerCommand,
ClarificationStateResult,
)
from src.services.dataset_review.orchestrator import (
LaunchDatasetCommand,
PreparePreviewCommand,
StartSessionCommand,
)
from src.services.dataset_review.repositories.session_repository import (
DatasetReviewSessionVersionConflictError,
)
router = APIRouter(prefix="/api/dataset-orchestration", tags=["Dataset Orchestration"])
@@ -639,7 +638,7 @@ async def unlock_field_semantic(
# @BRIEF Approve multiple semantic candidate decisions in one batch.
@router.post(
"/sessions/{session_id}/fields/semantic/approve-batch",
response_model=List[SemanticFieldEntryDto],
response_model=list[SemanticFieldEntryDto],
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "MANAGE")),
@@ -828,7 +827,7 @@ async def approve_execution_mapping(
# @BRIEF Approve multiple warning-sensitive execution mappings in one batch.
@router.post(
"/sessions/{session_id}/mappings/approve-batch",
response_model=List[ExecutionMappingDto],
response_model=list[ExecutionMappingDto],
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_execution_flag),

View File

@@ -8,12 +8,13 @@
#
# @INVARIANT: All dataset responses include last_task metadata
from fastapi import APIRouter, Depends, HTTPException, Query
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from ...dependencies import get_config_manager, get_task_manager, get_resource_service, has_permission
from ...core.logger import logger, belief_scope
from ...core.logger import belief_scope, logger
from ...core.superset_client import SupersetClient
from ...dependencies import get_config_manager, get_resource_service, get_task_manager, has_permission
router = APIRouter(prefix="/api/datasets", tags=["Datasets"])
@@ -27,8 +28,8 @@ class MappedFields(BaseModel):
# #region LastTask [C:1] [TYPE DataClass]
# @BRIEF DTO for the most recent task associated with a dataset
class LastTask(BaseModel):
task_id: Optional[str] = None
status: Optional[str] = Field(None, pattern="^RUNNING|SUCCESS|ERROR|WAITING_INPUT$")
task_id: str | None = None
status: str | None = Field(None, pattern="^RUNNING|SUCCESS|ERROR|WAITING_INPUT$")
# #endregion LastTask
# #region DatasetItem [C:1] [TYPE DataClass]
@@ -38,8 +39,8 @@ class DatasetItem(BaseModel):
table_name: str
schema_name: str = Field(..., alias="schema")
database: str
mapped_fields: Optional[MappedFields] = None
last_task: Optional[LastTask] = None
mapped_fields: MappedFields | None = None
last_task: LastTask | None = None
class Config:
allow_population_by_field_name = True
@@ -50,7 +51,7 @@ class DatasetItem(BaseModel):
class LinkedDashboard(BaseModel):
id: int
title: str
slug: Optional[str] = None
slug: str | None = None
# #endregion LinkedDashboard
# #region DatasetColumn [C:1] [TYPE DataClass]
@@ -58,28 +59,28 @@ class LinkedDashboard(BaseModel):
class DatasetColumn(BaseModel):
id: int
name: str
type: Optional[str] = None
type: str | None = None
is_dttm: bool = False
is_active: bool = True
description: Optional[str] = None
description: str | None = None
# #endregion DatasetColumn
# #region DatasetDetailResponse [C:1] [TYPE DataClass]
# @BRIEF Detailed DTO for a dataset including columns and links
class DatasetDetailResponse(BaseModel):
id: int
table_name: Optional[str] = None
schema_name: Optional[str] = Field(None, alias="schema")
table_name: str | None = None
schema_name: str | None = Field(None, alias="schema")
database: str
description: Optional[str] = None
columns: List[DatasetColumn]
description: str | None = None
columns: list[DatasetColumn]
column_count: int
sql: Optional[str] = None
linked_dashboards: List[LinkedDashboard]
sql: str | None = None
linked_dashboards: list[LinkedDashboard]
linked_dashboard_count: int
is_sqllab_view: bool = False
created_on: Optional[str] = None
changed_on: Optional[str] = None
created_on: str | None = None
changed_on: str | None = None
class Config:
allow_population_by_field_name = True
@@ -88,7 +89,7 @@ class DatasetDetailResponse(BaseModel):
# #region DatasetsResponse [C:1] [TYPE DataClass]
# @BRIEF Paginated response DTO for dataset listings
class DatasetsResponse(BaseModel):
datasets: List[DatasetItem]
datasets: list[DatasetItem]
total: int
page: int
page_size: int
@@ -109,7 +110,7 @@ class TaskResponse(BaseModel):
@router.get("/ids")
async def get_dataset_ids(
env_id: str,
search: Optional[str] = None,
search: str | None = None,
config_manager=Depends(get_config_manager),
task_manager=Depends(get_task_manager),
resource_service=Depends(get_resource_service),
@@ -122,31 +123,31 @@ async def get_dataset_ids(
if not env:
logger.error(f"[get_dataset_ids][Coherence:Failed] Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
# Get all tasks for status lookup
all_tasks = task_manager.get_all_tasks()
# Fetch datasets with status using ResourceService
datasets = await resource_service.get_datasets_with_status(env, all_tasks)
# Apply search filter if provided
if search:
search_lower = search.lower()
datasets = [
d for d in datasets
d for d in datasets
if search_lower in d.get('table_name', '').lower()
]
# Extract and return just the IDs
dataset_ids = [d['id'] for d in datasets]
logger.info(f"[get_dataset_ids][Coherence:OK] Returning {len(dataset_ids)} dataset IDs")
return {"dataset_ids": dataset_ids}
except Exception as e:
logger.error(f"[get_dataset_ids][Coherence:Failed] Failed to fetch dataset IDs: {e}")
raise HTTPException(status_code=503, detail=f"Failed to fetch dataset IDs: {str(e)}")
raise HTTPException(status_code=503, detail=f"Failed to fetch dataset IDs: {e!s}")
# #endregion get_dataset_ids
# #region get_datasets [C:3] [TYPE Function]
@@ -160,7 +161,7 @@ async def get_dataset_ids(
@router.get("", response_model=DatasetsResponse)
async def get_datasets(
env_id: str,
search: Optional[str] = None,
search: str | None = None,
page: int = 1,
page_size: int = 10,
config_manager=Depends(get_config_manager),
@@ -176,40 +177,40 @@ async def get_datasets(
if page_size < 1 or page_size > 100:
logger.error(f"[get_datasets][Coherence:Failed] Invalid page_size: {page_size}")
raise HTTPException(status_code=400, detail="Page size must be between 1 and 100")
# Validate environment exists
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == env_id), None)
if not env:
logger.error(f"[get_datasets][Coherence:Failed] Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
# Get all tasks for status lookup
all_tasks = task_manager.get_all_tasks()
# Fetch datasets with status using ResourceService
datasets = await resource_service.get_datasets_with_status(env, all_tasks)
# Apply search filter if provided
if search:
search_lower = search.lower()
datasets = [
d for d in datasets
d for d in datasets
if search_lower in d.get('table_name', '').lower()
]
# Calculate pagination
total = len(datasets)
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
# Slice datasets for current page
paginated_datasets = datasets[start_idx:end_idx]
logger.info(f"[get_datasets][Coherence:OK] Returning {len(paginated_datasets)} datasets (page {page}/{total_pages}, total: {total})")
return DatasetsResponse(
datasets=paginated_datasets,
total=total,
@@ -217,20 +218,20 @@ async def get_datasets(
page_size=page_size,
total_pages=total_pages
)
except Exception as e:
logger.error(f"[get_datasets][Coherence:Failed] Failed to fetch datasets: {e}")
raise HTTPException(status_code=503, detail=f"Failed to fetch datasets: {str(e)}")
raise HTTPException(status_code=503, detail=f"Failed to fetch datasets: {e!s}")
# #endregion get_datasets
# #region MapColumnsRequest [C:1] [TYPE DataClass]
# @BRIEF Request DTO for initiating column mapping
class MapColumnsRequest(BaseModel):
env_id: str = Field(..., description="Environment ID")
dataset_ids: List[int] = Field(..., description="List of dataset IDs to map")
dataset_ids: list[int] = Field(..., description="List of dataset IDs to map")
source_type: str = Field(..., description="Source type: 'postgresql' or 'xlsx'")
connection_id: Optional[str] = Field(None, description="Connection ID for PostgreSQL source")
file_data: Optional[str] = Field(None, description="File path or data for XLSX source")
connection_id: str | None = Field(None, description="Connection ID for PostgreSQL source")
file_data: str | None = Field(None, description="File path or data for XLSX source")
# #endregion MapColumnsRequest
# #region map_columns [C:3] [TYPE Function]
@@ -254,20 +255,20 @@ async def map_columns(
if not request.dataset_ids:
logger.error("[map_columns][Coherence:Failed] No dataset IDs provided")
raise HTTPException(status_code=400, detail="At least one dataset ID must be provided")
# Validate source type
if request.source_type not in ['postgresql', 'xlsx']:
logger.error(f"[map_columns][Coherence:Failed] Invalid source type: {request.source_type}")
raise HTTPException(status_code=400, detail="Source type must be 'postgresql' or 'xlsx'")
# Validate environment exists
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == request.env_id), None)
if not env:
logger.error(f"[map_columns][Coherence:Failed] Environment not found: {request.env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
# Create mapping task
task_params = {
@@ -277,28 +278,28 @@ async def map_columns(
'connection_id': request.connection_id,
'file_data': request.file_data
}
task_obj = await task_manager.create_task(
plugin_id='dataset-mapper',
params=task_params
)
logger.info(f"[map_columns][Coherence:OK] Mapping task created: {task_obj.id} for {len(request.dataset_ids)} datasets")
return TaskResponse(task_id=str(task_obj.id))
except Exception as e:
logger.error(f"[map_columns][Coherence:Failed] Failed to create mapping task: {e}")
raise HTTPException(status_code=503, detail=f"Failed to create mapping task: {str(e)}")
raise HTTPException(status_code=503, detail=f"Failed to create mapping task: {e!s}")
# #endregion map_columns
# #region GenerateDocsRequest [C:1] [TYPE DataClass]
# @BRIEF Request DTO for initiating documentation generation
class GenerateDocsRequest(BaseModel):
env_id: str = Field(..., description="Environment ID")
dataset_ids: List[int] = Field(..., description="List of dataset IDs to generate docs for")
dataset_ids: list[int] = Field(..., description="List of dataset IDs to generate docs for")
llm_provider: str = Field(..., description="LLM provider to use")
options: Optional[dict] = Field(None, description="Additional options for documentation generation")
options: dict | None = Field(None, description="Additional options for documentation generation")
# #endregion GenerateDocsRequest
# #region generate_docs [C:3] [TYPE Function]
@@ -322,15 +323,15 @@ async def generate_docs(
if not request.dataset_ids:
logger.error("[generate_docs][Coherence:Failed] No dataset IDs provided")
raise HTTPException(status_code=400, detail="At least one dataset ID must be provided")
# Validate environment exists
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == request.env_id), None)
if not env:
logger.error(f"[generate_docs][Coherence:Failed] Environment not found: {request.env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
# Create documentation generation task
task_params = {
@@ -339,19 +340,19 @@ async def generate_docs(
'provider_id': request.llm_provider,
'options': request.options or {}
}
task_obj = await task_manager.create_task(
plugin_id='llm_documentation',
params=task_params
)
logger.info(f"[generate_docs][Coherence:OK] Documentation generation task created: {task_obj.id} for {len(request.dataset_ids)} datasets")
return TaskResponse(task_id=str(task_obj.id))
except Exception as e:
logger.error(f"[generate_docs][Coherence:Failed] Failed to create documentation generation task: {e}")
raise HTTPException(status_code=503, detail=f"Failed to create documentation generation task: {str(e)}")
raise HTTPException(status_code=503, detail=f"Failed to create documentation generation task: {e!s}")
# #endregion generate_docs
# #region get_dataset_detail [C:3] [TYPE Function]
@@ -374,19 +375,19 @@ async def get_dataset_detail(
if not env:
logger.error(f"[get_dataset_detail][Coherence:Failed] Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
try:
# Fetch detailed dataset info using SupersetClient
client = SupersetClient(env)
dataset_detail = client.get_dataset_detail(dataset_id)
logger.info(f"[get_dataset_detail][Coherence:OK] Retrieved dataset {dataset_id} with {dataset_detail['column_count']} columns and {dataset_detail['linked_dashboard_count']} linked dashboards")
return DatasetDetailResponse(**dataset_detail)
except Exception as e:
logger.error(f"[get_dataset_detail][Coherence:Failed] Failed to fetch dataset detail: {e}")
raise HTTPException(status_code=503, detail=f"Failed to fetch dataset detail: {str(e)}")
raise HTTPException(status_code=503, detail=f"Failed to fetch dataset detail: {e!s}")
# #endregion get_dataset_detail
# #endregion DatasetsApi

View File

@@ -7,12 +7,13 @@
#
# @INVARIANT: Environment IDs must exist in the configuration.
from fastapi import APIRouter, Depends, HTTPException
from typing import List, Optional
from ...dependencies import get_config_manager, get_scheduler_service, has_permission
from ...core.superset_client import SupersetClient
from pydantic import BaseModel, Field
from ...core.logger import belief_scope
from ...core.superset_client import SupersetClient
from ...dependencies import get_config_manager, get_scheduler_service, has_permission
router = APIRouter(prefix="/api/environments", tags=["Environments"])
@@ -41,14 +42,14 @@ class EnvironmentResponse(BaseModel):
url: str
stage: str = "DEV"
is_production: bool = False
backup_schedule: Optional[ScheduleSchema] = None
backup_schedule: ScheduleSchema | None = None
# #endregion EnvironmentResponse
# #region DatabaseResponse [TYPE DataClass]
class DatabaseResponse(BaseModel):
uuid: str
database_name: str
engine: Optional[str]
engine: str | None
# #endregion DatabaseResponse
# #region get_environments [TYPE Function] [SEMANTICS list, environments, config]
@@ -56,7 +57,7 @@ class DatabaseResponse(BaseModel):
# @LAYER: API
# @PRE: config_manager is injected via Depends.
# @POST: Returns a list of EnvironmentResponse objects.
@router.get("", response_model=List[EnvironmentResponse])
@router.get("", response_model=list[EnvironmentResponse])
async def get_environments(
config_manager=Depends(get_config_manager),
_ = Depends(has_permission("environments", "READ"))
@@ -106,16 +107,16 @@ async def update_environment_schedule(
env = next((e for e in envs if e.id == id), None)
if not env:
raise HTTPException(status_code=404, detail="Environment not found")
# Update environment config
env.backup_schedule.enabled = schedule.enabled
env.backup_schedule.cron_expression = schedule.cron_expression
config_manager.update_environment(id, env)
# Refresh scheduler
scheduler_service.load_schedules()
return {"message": "Schedule updated successfully"}
# #endregion update_environment_schedule
@@ -135,13 +136,13 @@ async def get_environment_databases(
env = next((e for e in envs if e.id == id), None)
if not env:
raise HTTPException(status_code=404, detail="Environment not found")
try:
# Initialize SupersetClient from environment config
client = SupersetClient(env)
return client.get_databases_summary()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to fetch databases: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to fetch databases: {e!s}")
# #endregion get_environment_databases
# #endregion EnvironmentsApi

View File

@@ -11,32 +11,32 @@ import os
from src.services.git_service import GitService
from ._router import router
from ._deps import MAX_REPOSITORY_STATUS_BATCH
from ._router import router
# -- git_service singleton (canonical instance; tests may monkeypatch git_routes.git_service) --
git_service = GitService()
# -- Config routes --
from ._config_routes import get_git_configs, create_git_config, update_git_config, delete_git_config, test_git_config # noqa: E501, F401
# -- Gitea routes --
from ._gitea_routes import list_gitea_repositories, create_gitea_repository, delete_gitea_repository, create_remote_repository # noqa: E501, F401
# -- Repo routes (core) --
from ._repo_routes import init_repository, get_repository_binding, delete_repository, get_branches, create_branch, checkout_branch # noqa: E501, F401
from ._helpers import _resolve_dashboard_id_from_ref, _resolve_dashboard_id_from_ref_async, _resolve_repo_key_from_ref # noqa: E501, F401 — re-exported for test monkeypatch compatibility
# -- Repo operations routes (commit, push, pull, status, diff, history, generate-message) --
from ._repo_operations_routes import commit_changes, push_changes, pull_changes, get_repository_status, get_repository_status_batch, get_repository_diff, get_history, generate_commit_message # noqa: E501, F401
# -- Repo lifecycle routes (sync, promote, deploy) --
from ._repo_lifecycle_routes import sync_dashboard, promote_dashboard, deploy_dashboard # noqa: E501, F401
# -- Merge routes --
from ._merge_routes import get_merge_status, get_merge_conflicts, resolve_merge_conflicts, abort_merge, continue_merge # noqa: E501, F401
from ._config_routes import create_git_config, delete_git_config, get_git_configs, test_git_config, update_git_config # noqa: F401
# -- Environment routes --
from ._environment_routes import get_environments # noqa: E501, F401
from ._environment_routes import get_environments # noqa: F401
# -- Gitea routes --
from ._gitea_routes import create_gitea_repository, create_remote_repository, delete_gitea_repository, list_gitea_repositories # noqa: F401
from ._helpers import _resolve_dashboard_id_from_ref, _resolve_dashboard_id_from_ref_async, _resolve_repo_key_from_ref # noqa: F401 — re-exported for test monkeypatch compatibility
# -- Merge routes --
from ._merge_routes import abort_merge, continue_merge, get_merge_conflicts, get_merge_status, resolve_merge_conflicts # noqa: F401
# -- Repo lifecycle routes (sync, promote, deploy) --
from ._repo_lifecycle_routes import deploy_dashboard, promote_dashboard, sync_dashboard # noqa: F401
# -- Repo operations routes (commit, push, pull, status, diff, history, generate-message) --
from ._repo_operations_routes import commit_changes, generate_commit_message, get_history, get_repository_diff, get_repository_status, get_repository_status_batch, pull_changes, push_changes # noqa: F401
# -- Repo routes (core) --
from ._repo_routes import checkout_branch, create_branch, delete_repository, get_branches, get_repository_binding, init_repository # noqa: F401
# #endregion GitPackage

View File

@@ -2,30 +2,27 @@
# @BRIEF FastAPI endpoints for Git server configuration CRUD and connection testing.
# @LAYER: API
from typing import List
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from src.core.database import get_db
from src.core.logger import belief_scope, logger
from src.dependencies import has_permission
from src.models.git import GitServerConfig
from src.api.routes.git_schemas import (
GitServerConfigCreate,
GitServerConfigSchema,
GitServerConfigUpdate,
)
from src.core.database import get_db
from src.core.logger import belief_scope
from src.dependencies import has_permission
from src.models.git import GitServerConfig
from ._router import router
from ._helpers import _get_git_config_or_404
from ._deps import get_git_service
from ._router import router
# #region get_git_configs [C:2] [TYPE Function]
# @BRIEF List all configured Git servers.
@router.get("/config", response_model=List[GitServerConfigSchema])
@router.get("/config", response_model=list[GitServerConfigSchema])
async def get_git_configs(
db: Session = Depends(get_db),
_=Depends(has_permission("git_config", "READ")),

View File

@@ -2,21 +2,19 @@
# @BRIEF FastAPI endpoint for listing deployment environments.
# @LAYER: API
from typing import List
from fastapi import Depends
from src.api.routes.git_schemas import DeploymentEnvironmentSchema
from src.core.logger import belief_scope
from src.dependencies import get_config_manager, has_permission
from src.api.routes.git_schemas import DeploymentEnvironmentSchema
from ._router import router
# #region get_environments [C:2] [TYPE Function]
# @BRIEF List all deployment environments.
@router.get("/environments", response_model=List[DeploymentEnvironmentSchema])
@router.get("/environments", response_model=list[DeploymentEnvironmentSchema])
async def get_environments(
config_manager=Depends(get_config_manager),
_=Depends(has_permission("environments", "READ")),

View File

@@ -2,31 +2,29 @@
# @BRIEF FastAPI endpoints for Gitea-specific repository operations.
# @LAYER: API
from typing import List
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from src.core.database import get_db
from src.core.logger import belief_scope
from src.dependencies import has_permission
from src.models.git import GitProvider
from src.api.routes.git_schemas import (
GiteaRepoCreateRequest,
GiteaRepoSchema,
RemoteRepoCreateRequest,
RemoteRepoSchema,
)
from src.core.database import get_db
from src.core.logger import belief_scope
from src.dependencies import has_permission
from src.models.git import GitProvider
from ._router import router
from ._helpers import _get_git_config_or_404
from ._deps import get_git_service
from ._helpers import _get_git_config_or_404
from ._router import router
# #region list_gitea_repositories [C:2] [TYPE Function]
# @BRIEF List repositories in Gitea for a saved Gitea config.
@router.get("/config/{config_id}/gitea/repos", response_model=List[GiteaRepoSchema])
@router.get("/config/{config_id}/gitea/repos", response_model=list[GiteaRepoSchema])
async def list_gitea_repositories(
config_id: str,
db: Session = Depends(get_db),

View File

@@ -6,19 +6,17 @@
# @RELATION USES -> [UserDashboardPreference]
import os
from typing import Optional
from fastapi import HTTPException
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.core.superset_client import SupersetClient
from src.dependencies import get_config_manager
from src.models.auth import User
from src.models.git import GitServerConfig
from src.models.profile import UserDashboardPreference
from ._deps import get_git_service, MAX_REPOSITORY_STATUS_BATCH
from ._deps import get_git_service
# #region _build_no_repo_status_payload [C:1] [TYPE Function]
@@ -49,7 +47,7 @@ def _build_no_repo_status_payload() -> dict:
# @POST: Raises HTTPException(500) with route-specific context.
def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None:
logger.error(f"[{route_name}][Coherence:Failed] {error}")
raise HTTPException(status_code=500, detail=f"{route_name} failed: {str(error)}")
raise HTTPException(status_code=500, detail=f"{route_name} failed: {error!s}")
# #endregion _handle_unexpected_git_route_error
@@ -95,7 +93,7 @@ def _get_git_config_or_404(db: Session, config_id: str) -> GitServerConfig:
def _find_dashboard_id_by_slug(
client: SupersetClient,
dashboard_slug: str,
) -> Optional[int]:
) -> int | None:
query_variants = [
{
"filters": [{"col": "slug", "opr": "eq", "value": dashboard_slug}],
@@ -127,7 +125,7 @@ def _find_dashboard_id_by_slug(
def _resolve_dashboard_id_from_ref(
dashboard_ref: str,
config_manager,
env_id: Optional[str] = None,
env_id: str | None = None,
) -> int:
normalized_ref = str(dashboard_ref or "").strip()
if not normalized_ref:
@@ -161,7 +159,7 @@ def _resolve_dashboard_id_from_ref(
async def _find_dashboard_id_by_slug_async(
client: "AsyncSupersetClient",
dashboard_slug: str,
) -> Optional[int]:
) -> int | None:
query_variants = [
{
"filters": [{"col": "slug", "opr": "eq", "value": dashboard_slug}],
@@ -193,7 +191,7 @@ async def _find_dashboard_id_by_slug_async(
async def _resolve_dashboard_id_from_ref_async(
dashboard_ref: str,
config_manager,
env_id: Optional[str] = None,
env_id: str | None = None,
) -> int:
normalized_ref = str(dashboard_ref or "").strip()
if not normalized_ref:
@@ -234,7 +232,7 @@ def _resolve_repo_key_from_ref(
dashboard_ref: str,
dashboard_id: int,
config_manager,
env_id: Optional[str] = None,
env_id: str | None = None,
) -> str:
normalized_ref = str(dashboard_ref or "").strip()
if normalized_ref and not normalized_ref.isdigit():
@@ -261,7 +259,7 @@ def _resolve_repo_key_from_ref(
# #region _sanitize_optional_identity_value [C:1] [TYPE Function]
# @BRIEF Normalize optional identity value into trimmed string or None.
def _sanitize_optional_identity_value(value: Optional[str]) -> Optional[str]:
def _sanitize_optional_identity_value(value: str | None) -> str | None:
normalized = str(value or "").strip()
if not normalized:
return None
@@ -273,8 +271,8 @@ def _sanitize_optional_identity_value(value: Optional[str]) -> Optional[str]:
# @BRIEF Resolve configured Git username/email from current user's profile preferences.
def _resolve_current_user_git_identity(
db: Session,
current_user: Optional[User],
) -> Optional[tuple[str, str]]:
current_user: User | None,
) -> tuple[str, str] | None:
if db is None or not hasattr(db, "query"):
return None
@@ -315,7 +313,7 @@ def _resolve_current_user_git_identity(
def _apply_git_identity_from_profile(
dashboard_id: int,
db: Session,
current_user: Optional[User],
current_user: User | None,
) -> None:
identity = _resolve_current_user_git_identity(db, current_user)
if not identity:

View File

@@ -2,25 +2,23 @@
# @BRIEF FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue).
# @LAYER: API
from typing import List, Optional
from fastapi import Depends, HTTPException
from src.core.logger import belief_scope
from src.dependencies import get_config_manager, has_permission
from src.api.routes.git_schemas import (
MergeConflictFileSchema,
MergeContinueRequest,
MergeResolveRequest,
MergeStatusSchema,
)
from src.core.logger import belief_scope
from src.dependencies import get_config_manager, has_permission
from ._router import router
from ._deps import get_git_service
from ._helpers import (
_handle_unexpected_git_route_error,
)
from ._deps import get_git_service
from ._router import router
# #region get_merge_status [C:2] [TYPE Function]
@@ -30,7 +28,7 @@ from ._deps import get_git_service
)
async def get_merge_status(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -54,11 +52,11 @@ async def get_merge_status(
# @BRIEF Return conflicted files with mine/theirs previews for web conflict resolver.
@router.get(
"/repositories/{dashboard_ref}/merge/conflicts",
response_model=List[MergeConflictFileSchema],
response_model=list[MergeConflictFileSchema],
)
async def get_merge_conflicts(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -85,7 +83,7 @@ async def get_merge_conflicts(
async def resolve_merge_conflicts(
dashboard_ref: str,
resolve_data: MergeResolveRequest,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -114,7 +112,7 @@ async def resolve_merge_conflicts(
@router.post("/repositories/{dashboard_ref}/merge/abort")
async def abort_merge(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -141,7 +139,7 @@ async def abort_merge(
async def continue_merge(
dashboard_ref: str,
continue_data: MergeContinueRequest,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):

View File

@@ -2,31 +2,28 @@
# @BRIEF FastAPI endpoints for Git lifecycle operations (sync, promote, deploy).
# @LAYER: API
import typing
from typing import Optional
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from src.core.database import get_db
from src.core.logger import belief_scope, logger
from src.dependencies import get_config_manager, get_current_user, has_permission
from src.models.auth import User
from src.models.git import GitProvider, GitRepository
from src.api.routes.git_schemas import (
DeployRequest,
PromoteRequest,
PromoteResponse,
)
from src.core.database import get_db
from src.core.logger import belief_scope, logger
from src.dependencies import get_config_manager, get_current_user, has_permission
from src.models.auth import User
from src.models.git import GitProvider, GitRepository
from ._router import router
from ._deps import get_git_service
from ._helpers import (
_apply_git_identity_from_profile,
_get_git_config_or_404,
_handle_unexpected_git_route_error,
)
from ._deps import get_git_service
from ._router import router
# #region sync_dashboard [C:3] [TYPE Function]
@@ -35,8 +32,8 @@ from ._deps import get_git_service
@router.post("/repositories/{dashboard_ref}/sync")
async def sync_dashboard(
dashboard_ref: str,
env_id: Optional[str] = None,
source_env_id: typing.Optional[str] = None,
env_id: str | None = None,
source_env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -71,7 +68,7 @@ async def sync_dashboard(
async def promote_dashboard(
dashboard_ref: str,
payload: PromoteRequest,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
@@ -194,7 +191,7 @@ async def promote_dashboard(
async def deploy_dashboard(
dashboard_ref: str,
deploy_data: DeployRequest,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):

View File

@@ -2,32 +2,30 @@
# @BRIEF FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).
# @LAYER: API
from typing import List, Optional
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from src.core.database import get_db
from src.core.logger import belief_scope, logger
from src.dependencies import get_config_manager, get_current_user, has_permission
from src.models.auth import User
from src.models.git import GitRepository, GitServerConfig
from src.api.routes.git_schemas import (
CommitCreate,
CommitSchema,
RepoStatusBatchRequest,
RepoStatusBatchResponse,
)
from src.core.database import get_db
from src.core.logger import belief_scope, logger
from src.dependencies import get_config_manager, get_current_user, has_permission
from src.models.auth import User
from src.models.git import GitRepository, GitServerConfig
from ._router import router
from ._deps import MAX_REPOSITORY_STATUS_BATCH, get_git_service
from ._helpers import (
_apply_git_identity_from_profile,
_build_no_repo_status_payload,
_handle_unexpected_git_route_error,
_resolve_repository_status,
)
from ._deps import get_git_service, MAX_REPOSITORY_STATUS_BATCH
from ._router import router
# #region commit_changes [C:2] [TYPE Function]
@@ -36,7 +34,7 @@ from ._deps import get_git_service, MAX_REPOSITORY_STATUS_BATCH
async def commit_changes(
dashboard_ref: str,
commit_data: CommitCreate,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
@@ -67,7 +65,7 @@ async def commit_changes(
@router.post("/repositories/{dashboard_ref}/push")
async def push_changes(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -94,7 +92,7 @@ async def push_changes(
@router.post("/repositories/{dashboard_ref}/pull")
async def pull_changes(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
@@ -155,7 +153,7 @@ async def pull_changes(
@router.get("/repositories/{dashboard_ref}/status")
async def get_repository_status(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -218,9 +216,9 @@ async def get_repository_status_batch(
@router.get("/repositories/{dashboard_ref}/diff")
async def get_repository_diff(
dashboard_ref: str,
file_path: Optional[str] = None,
file_path: str | None = None,
staged: bool = False,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -242,11 +240,11 @@ async def get_repository_diff(
# #region get_history [C:2] [TYPE Function]
# @BRIEF View commit history for a dashboard's repository.
@router.get("/repositories/{dashboard_ref}/history", response_model=List[CommitSchema])
@router.get("/repositories/{dashboard_ref}/history", response_model=list[CommitSchema])
async def get_history(
dashboard_ref: str,
limit: int = 50,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -272,7 +270,7 @@ async def get_history(
@router.post("/repositories/{dashboard_ref}/generate-message")
async def generate_commit_message(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
_=Depends(has_permission("plugin:git", "EXECUTE")),
@@ -295,14 +293,14 @@ async def generate_commit_message(
history_objs = _gs.get_commit_history(dashboard_id, limit=5)
history = [h.message for h in history_objs if hasattr(h, "message")]
from ...services.llm_provider import LLMProviderService
from ...plugins.llm_analysis.service import LLMClient
from ...plugins.llm_analysis.models import LLMProviderType
from ...plugins.llm_analysis.service import LLMClient
from ...services.llm_prompt_templates import (
DEFAULT_LLM_PROMPTS,
normalize_llm_settings,
resolve_bound_provider_id,
)
from ...services.llm_provider import LLMProviderService
llm_service = LLMProviderService(db)
providers = llm_service.get_all_providers()

View File

@@ -2,17 +2,10 @@
# @BRIEF FastAPI endpoints for core Git repository operations (init, binding, branches, checkout).
# @LAYER: API
from typing import List, Optional
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from src.core.database import get_db
from src.core.logger import belief_scope, logger
from src.dependencies import get_config_manager, get_current_user, has_permission
from src.models.auth import User
from src.models.git import GitRepository, GitServerConfig
from src.api.routes.git_schemas import (
BranchCheckout,
BranchCreate,
@@ -20,14 +13,19 @@ from src.api.routes.git_schemas import (
RepoInitRequest,
RepositoryBindingSchema,
)
from src.core.database import get_db
from src.core.logger import belief_scope, logger
from src.dependencies import get_config_manager, get_current_user, has_permission
from src.models.auth import User
from src.models.git import GitRepository, GitServerConfig
from ._router import router
from ._deps import get_git_service
from ._helpers import (
_apply_git_identity_from_profile,
_get_git_config_or_404,
_handle_unexpected_git_route_error,
)
from ._deps import get_git_service
from ._router import router
# #region init_repository [C:3] [TYPE Function]
@@ -37,7 +35,7 @@ from ._deps import get_git_service
async def init_repository(
dashboard_ref: str,
init_data: RepoInitRequest,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
_=Depends(has_permission("plugin:git", "EXECUTE")),
@@ -115,7 +113,7 @@ async def init_repository(
@router.get("/repositories/{dashboard_ref}", response_model=RepositoryBindingSchema)
async def get_repository_binding(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
_=Depends(has_permission("plugin:git", "EXECUTE")),
@@ -156,7 +154,7 @@ async def get_repository_binding(
@router.delete("/repositories/{dashboard_ref}")
async def delete_repository(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -179,10 +177,10 @@ async def delete_repository(
# #region get_branches [C:2] [TYPE Function]
# @BRIEF List all branches for a dashboard's repository.
@router.get("/repositories/{dashboard_ref}/branches", response_model=List[BranchSchema])
@router.get("/repositories/{dashboard_ref}/branches", response_model=list[BranchSchema])
async def get_branches(
dashboard_ref: str,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):
@@ -208,7 +206,7 @@ async def get_branches(
async def create_branch(
dashboard_ref: str,
branch_data: BranchCreate,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
@@ -240,7 +238,7 @@ async def create_branch(
async def checkout_branch(
dashboard_ref: str,
checkout_data: BranchCheckout,
env_id: Optional[str] = None,
env_id: str | None = None,
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:git", "EXECUTE")),
):

View File

@@ -6,11 +6,14 @@
#
# @INVARIANT: All schemas must be compatible with the FastAPI router.
from pydantic import BaseModel, Field
from typing import Any, Dict, List, Optional
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
from src.models.git import GitProvider, GitStatus, SyncStatus
# #region GitServerConfigBase [C:1] [TYPE Class]
# @BRIEF Base schema for Git server configuration attributes.
class GitServerConfigBase(BaseModel):
@@ -19,26 +22,26 @@ class GitServerConfigBase(BaseModel):
url: str = Field(..., description="Server base URL")
pat: str = Field(..., description="Personal Access Token")
pat: str = Field(..., description="Personal Access Token")
default_repository: Optional[str] = Field(None, description="Default repository path (org/repo)")
default_branch: Optional[str] = Field("main", description="Default branch logic/name")
default_repository: str | None = Field(None, description="Default repository path (org/repo)")
default_branch: str | None = Field("main", description="Default branch logic/name")
# #endregion GitServerConfigBase
# #region GitServerConfigUpdate [TYPE Class]
# @BRIEF Schema for updating an existing Git server configuration.
class GitServerConfigUpdate(BaseModel):
name: Optional[str] = Field(None, description="Display name for the Git server")
provider: Optional[GitProvider] = Field(None, description="Git provider (GITHUB, GITLAB, GITEA)")
url: Optional[str] = Field(None, description="Server base URL")
pat: Optional[str] = Field(None, description="Personal Access Token")
default_repository: Optional[str] = Field(None, description="Default repository path (org/repo)")
default_branch: Optional[str] = Field(None, description="Default branch logic/name")
name: str | None = Field(None, description="Display name for the Git server")
provider: GitProvider | None = Field(None, description="Git provider (GITHUB, GITLAB, GITEA)")
url: str | None = Field(None, description="Server base URL")
pat: str | None = Field(None, description="Personal Access Token")
default_repository: str | None = Field(None, description="Default repository path (org/repo)")
default_branch: str | None = Field(None, description="Default branch logic/name")
# #endregion GitServerConfigUpdate
# #region GitServerConfigCreate [TYPE Class]
# @BRIEF Schema for creating a new Git server configuration.
class GitServerConfigCreate(GitServerConfigBase):
"""Schema for creating a new Git server configuration."""
config_id: Optional[str] = Field(None, description="Optional config ID, useful for testing an existing config without sending its full PAT")
config_id: str | None = Field(None, description="Optional config ID, useful for testing an existing config without sending its full PAT")
# #endregion GitServerConfigCreate
# #region GitServerConfigSchema [TYPE Class]
@@ -88,7 +91,7 @@ class CommitSchema(BaseModel):
email: str
timestamp: datetime
message: str
files_changed: List[str]
files_changed: list[str]
# #endregion CommitSchema
# #region BranchCreate [TYPE Class]
@@ -111,7 +114,7 @@ class BranchCheckout(BaseModel):
class CommitCreate(BaseModel):
"""Schema for staging and committing changes."""
message: str
files: List[str]
files: list[str]
# #endregion CommitCreate
# #region ConflictResolution [TYPE Class]
@@ -120,7 +123,7 @@ class ConflictResolution(BaseModel):
"""Schema for resolving merge conflicts."""
file_path: str
resolution: str = Field(pattern="^(mine|theirs|manual)$")
content: Optional[str] = None
content: str | None = None
# #endregion ConflictResolution
@@ -131,8 +134,8 @@ class MergeStatusSchema(BaseModel):
repository_path: str
git_dir: str
current_branch: str
merge_head: Optional[str] = None
merge_message_preview: Optional[str] = None
merge_head: str | None = None
merge_message_preview: str | None = None
conflicts_count: int = 0
# #endregion MergeStatusSchema
@@ -141,22 +144,22 @@ class MergeStatusSchema(BaseModel):
# @BRIEF Schema describing one conflicted file with optional side snapshots.
class MergeConflictFileSchema(BaseModel):
file_path: str
mine: Optional[str] = None
theirs: Optional[str] = None
mine: str | None = None
theirs: str | None = None
# #endregion MergeConflictFileSchema
# #region MergeResolveRequest [TYPE Class]
# @BRIEF Request schema for resolving one or multiple merge conflicts.
class MergeResolveRequest(BaseModel):
resolutions: List[ConflictResolution] = Field(default_factory=list)
resolutions: list[ConflictResolution] = Field(default_factory=list)
# #endregion MergeResolveRequest
# #region MergeContinueRequest [TYPE Class]
# @BRIEF Request schema for finishing merge with optional explicit commit message.
class MergeContinueRequest(BaseModel):
message: Optional[str] = None
message: str | None = None
# #endregion MergeContinueRequest
# #region DeploymentEnvironmentSchema [TYPE Class]
@@ -201,14 +204,14 @@ class RepositoryBindingSchema(BaseModel):
# #region RepoStatusBatchRequest [TYPE Class]
# @BRIEF Schema for requesting repository statuses for multiple dashboards in a single call.
class RepoStatusBatchRequest(BaseModel):
dashboard_ids: List[int] = Field(default_factory=list, description="Dashboard IDs to resolve repository statuses for")
dashboard_ids: list[int] = Field(default_factory=list, description="Dashboard IDs to resolve repository statuses for")
# #endregion RepoStatusBatchRequest
# #region RepoStatusBatchResponse [TYPE Class]
# @BRIEF Schema for returning repository statuses keyed by dashboard ID.
class RepoStatusBatchResponse(BaseModel):
statuses: Dict[str, Dict[str, Any]]
statuses: dict[str, dict[str, Any]]
# #endregion RepoStatusBatchResponse
@@ -218,10 +221,10 @@ class GiteaRepoSchema(BaseModel):
name: str
full_name: str
private: bool = False
clone_url: Optional[str] = None
html_url: Optional[str] = None
ssh_url: Optional[str] = None
default_branch: Optional[str] = None
clone_url: str | None = None
html_url: str | None = None
ssh_url: str | None = None
default_branch: str | None = None
# #endregion GiteaRepoSchema
@@ -230,9 +233,9 @@ class GiteaRepoSchema(BaseModel):
class GiteaRepoCreateRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
private: bool = True
description: Optional[str] = None
description: str | None = None
auto_init: bool = True
default_branch: Optional[str] = "main"
default_branch: str | None = "main"
# #endregion GiteaRepoCreateRequest
@@ -243,10 +246,10 @@ class RemoteRepoSchema(BaseModel):
name: str
full_name: str
private: bool = False
clone_url: Optional[str] = None
html_url: Optional[str] = None
ssh_url: Optional[str] = None
default_branch: Optional[str] = None
clone_url: str | None = None
html_url: str | None = None
ssh_url: str | None = None
default_branch: str | None = None
# #endregion RemoteRepoSchema
@@ -255,9 +258,9 @@ class RemoteRepoSchema(BaseModel):
class RemoteRepoCreateRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
private: bool = True
description: Optional[str] = None
description: str | None = None
auto_init: bool = True
default_branch: Optional[str] = "main"
default_branch: str | None = "main"
# #endregion RemoteRepoCreateRequest
@@ -267,9 +270,9 @@ class PromoteRequest(BaseModel):
from_branch: str = Field(..., min_length=1, max_length=255)
to_branch: str = Field(..., min_length=1, max_length=255)
mode: str = Field(default="mr", pattern="^(mr|direct)$")
title: Optional[str] = None
description: Optional[str] = None
reason: Optional[str] = None
title: str | None = None
description: str | None = None
reason: str | None = None
draft: bool = False
remove_source_branch: bool = False
# #endregion PromoteRequest
@@ -282,8 +285,8 @@ class PromoteResponse(BaseModel):
from_branch: str
to_branch: str
status: str
url: Optional[str] = None
reference_id: Optional[str] = None
url: str | None = None
reference_id: str | None = None
policy_violation: bool = False
# #endregion PromoteResponse

View File

@@ -3,13 +3,14 @@
# @LAYER: UI/API
# @RELATION DEPENDS_ON -> health_service
from fastapi import APIRouter, Depends, Query, HTTPException, status
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from ...core.database import get_db
from ...services.health_service import HealthService
from ...dependencies import get_config_manager, get_task_manager, has_permission
from ...schemas.health import HealthSummaryResponse
from ...dependencies import has_permission, get_config_manager, get_task_manager
from ...services.health_service import HealthService
router = APIRouter(prefix="/api/health", tags=["Health"])
@@ -20,7 +21,7 @@ router = APIRouter(prefix="/api/health", tags=["Health"])
# @RELATION CALLS -> backend.src.services.health_service.HealthService
@router.get("/summary", response_model=HealthSummaryResponse)
async def get_health_summary(
environment_id: Optional[str] = Query(None),
environment_id: str | None = Query(None),
db: Session = Depends(get_db),
config_manager = Depends(get_config_manager),
_ = Depends(has_permission("plugin:migration", "READ"))

View File

@@ -6,25 +6,26 @@
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List, Optional
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from ...core.database import get_db
from ...core.logger import logger
from ...schemas.auth import User
from ...dependencies import get_current_user as get_current_active_user
from ...plugins.llm_analysis.models import LLMProviderConfig, LLMProviderType
from ...schemas.auth import User
from ...services.llm_provider import LLMProviderService
from ...core.database import get_db
from sqlalchemy.orm import Session
# #region FetchModelsRequest [C:1] [TYPE Class]
# @BRIEF Pydantic request model for the fetch-models endpoint.
class FetchModelsRequest(BaseModel):
base_url: Optional[str] = Field(None, description="LLM provider base URL")
provider_type: Optional[str] = Field(None, description="Provider type (openai, anthropic, etc)")
api_key: Optional[str] = Field(None, description="Direct API key (takes precedence over provider_id)")
provider_id: Optional[str] = Field(None, description="Saved provider ID to look up stored API key")
base_url: str | None = Field(None, description="LLM provider base URL")
provider_type: str | None = Field(None, description="Provider type (openai, anthropic, etc)")
api_key: str | None = Field(None, description="Direct API key (takes precedence over provider_id)")
provider_id: str | None = Field(None, description="Saved provider ID to look up stored API key")
# #endregion FetchModelsRequest
@@ -40,7 +41,7 @@ router = APIRouter(tags=["LLM"])
# @PRE: value can be None.
# @POST: Returns True only for non-placeholder key.
# @RELATION BINDS_TO -> [LlmRoutes]
def _is_valid_runtime_api_key(value: Optional[str]) -> bool:
def _is_valid_runtime_api_key(value: str | None) -> bool:
key = (value or "").strip()
if not key:
return False
@@ -58,7 +59,7 @@ def _is_valid_runtime_api_key(value: Optional[str]) -> bool:
# @POST: Returns list of LLMProviderConfig.
# @RELATION CALLS -> [LLMProviderService]
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
@router.get("/providers", response_model=List[LLMProviderConfig])
@router.get("/providers", response_model=list[LLMProviderConfig])
async def get_providers(
current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)
):
@@ -100,8 +101,8 @@ async def fetch_models(
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
from ...plugins.llm_analysis.service import LLMClient
base_url = (payload.base_url or "").strip()
provider_type_str = (payload.provider_type or "").strip()

View File

@@ -8,14 +8,15 @@
#
# @INVARIANT: Mappings are persisted in the SQLite database.
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
from typing import List, Optional
from ...core.database import get_db
from ...core.logger import belief_scope
from ...dependencies import get_config_manager, has_permission
from ...core.database import get_db
from ...models.mapping import DatabaseMapping
from pydantic import BaseModel
router = APIRouter(tags=["mappings"])
@@ -27,7 +28,7 @@ class MappingCreate(BaseModel):
target_db_uuid: str
source_db_name: str
target_db_name: str
engine: Optional[str] = None
engine: str | None = None
# #endregion MappingCreate
# #region MappingResponse [TYPE DataClass]
@@ -39,7 +40,7 @@ class MappingResponse(BaseModel):
target_db_uuid: str
source_db_name: str
target_db_name: str
engine: Optional[str] = None
engine: str | None = None
class Config:
from_attributes = True
@@ -55,10 +56,10 @@ class SuggestRequest(BaseModel):
# @BRIEF List all saved database mappings.
# @PRE: db session is injected.
# @POST: Returns filtered list of DatabaseMapping records.
@router.get("", response_model=List[MappingResponse])
@router.get("", response_model=list[MappingResponse])
async def get_mappings(
source_env_id: Optional[str] = None,
target_env_id: Optional[str] = None,
source_env_id: str | None = None,
target_env_id: str | None = None,
db: Session = Depends(get_db),
_ = Depends(has_permission("plugin:mapper", "EXECUTE"))
):
@@ -88,7 +89,7 @@ async def create_mapping(
DatabaseMapping.target_env_id == mapping.target_env_id,
DatabaseMapping.source_db_uuid == mapping.source_db_uuid
).first()
if existing:
existing.target_db_uuid = mapping.target_db_uuid
existing.target_db_name = mapping.target_db_name
@@ -96,7 +97,7 @@ async def create_mapping(
db.commit()
db.refresh(existing)
return existing
new_mapping = DatabaseMapping(**mapping.dict())
db.add(new_mapping)
db.commit()

View File

@@ -21,16 +21,18 @@
# @TEST_EDGE: [external_fail] ->[HTTP_500]
# @TEST_INVARIANT: [EnvironmentValidationBeforeAction] -> VERIFIED_BY: [invalid_environment, valid_execution]
from typing import Any, cast
from fastapi import APIRouter, Depends, HTTPException, Query
from typing import List, Dict, Any, Optional, cast
from sqlalchemy.orm import Session
from ...dependencies import get_config_manager, get_task_manager, has_permission
from ...core.database import get_db
from ...models.dashboard import DashboardMetadata, DashboardSelection
from ...core.superset_client import SupersetClient
from ...core.logger import logger, belief_scope
from ...core.migration.dry_run_orchestrator import MigrationDryRunService
from ...core.logger import belief_scope, logger
from ...core.mapping_service import IdMappingService
from ...core.migration.dry_run_orchestrator import MigrationDryRunService
from ...core.superset_client import SupersetClient
from ...dependencies import get_config_manager, get_task_manager, has_permission
from ...models.dashboard import DashboardMetadata, DashboardSelection
from ...models.mapping import ResourceMapping
logger = cast(Any, logger)
@@ -45,7 +47,7 @@ router = APIRouter(prefix="/api", tags=["migration"])
# @SIDE_EFFECT: Reads environment configuration and performs remote Superset metadata retrieval over network.
# @DATA_CONTRACT: Input[str env_id] -> Output[List[DashboardMetadata]]
# @RELATION CALLS -> [SupersetClient.get_dashboards_summary]
@router.get("/environments/{env_id}/dashboards", response_model=List[DashboardMetadata])
@router.get("/environments/{env_id}/dashboards", response_model=list[DashboardMetadata])
async def get_dashboards(
env_id: str,
config_manager=Depends(get_config_manager),
@@ -125,7 +127,7 @@ async def execute_migration(
except Exception as e:
logger.explore(f"Task creation failed: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to create migration task: {str(e)}"
status_code=500, detail=f"Failed to create migration task: {e!s}"
)
@@ -141,7 +143,7 @@ async def execute_migration(
# @RELATION DEPENDS_ON -> [DashboardSelection]
# @RELATION DEPENDS_ON -> [MigrationDryRunService]
# @INVARIANT: Dry-run flow remains read-only and rejects identical source/target environments before service execution.
@router.post("/migration/dry-run", response_model=Dict[str, Any])
@router.post("/migration/dry-run", response_model=dict[str, Any])
async def dry_run_migration(
selection: DashboardSelection,
config_manager=Depends(get_config_manager),
@@ -205,7 +207,7 @@ async def dry_run_migration(
# @SIDE_EFFECT: Reads configuration from config manager.
# @DATA_CONTRACT: Input[None] -> Output[Dict[str, str]]
# @RELATION DEPENDS_ON -> [AppDependencies]
@router.get("/migration/settings", response_model=Dict[str, str])
@router.get("/migration/settings", response_model=dict[str, str])
async def get_migration_settings(
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:migration", "READ")),
@@ -226,9 +228,9 @@ async def get_migration_settings(
# @SIDE_EFFECT: Mutates configuration and writes persisted config through config manager.
# @DATA_CONTRACT: Input[Dict[str, str]] -> Output[Dict[str, str]]
# @RELATION DEPENDS_ON -> [AppDependencies]
@router.put("/migration/settings", response_model=Dict[str, str])
@router.put("/migration/settings", response_model=dict[str, str])
async def update_migration_settings(
payload: Dict[str, str],
payload: dict[str, str],
config_manager=Depends(get_config_manager),
_=Depends(has_permission("plugin:migration", "WRITE")),
):
@@ -257,13 +259,13 @@ async def update_migration_settings(
# @SIDE_EFFECT: Executes database read queries against ResourceMapping table.
# @DATA_CONTRACT: Input[QueryParams] -> Output[Dict[str, Any]]
# @RELATION DEPENDS_ON -> [ResourceMapping]
@router.get("/migration/mappings-data", response_model=Dict[str, Any])
@router.get("/migration/mappings-data", response_model=dict[str, Any])
async def get_resource_mappings(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=500),
search: Optional[str] = Query(None, description="Search by resource name or UUID"),
env_id: Optional[str] = Query(None, description="Filter by environment ID"),
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
search: str | None = Query(None, description="Search by resource name or UUID"),
env_id: str | None = Query(None, description="Filter by environment ID"),
resource_type: str | None = Query(None, description="Filter by resource type"),
db: Session = Depends(get_db),
_=Depends(has_permission("plugin:migration", "READ")),
):
@@ -330,7 +332,7 @@ async def get_resource_mappings(
# @DATA_CONTRACT: Input[None] -> Output[Dict[str, Any]]
# @RELATION DEPENDS_ON -> [IdMappingService]
# @RELATION CALLS -> [sync_environment]
@router.post("/migration/sync-now", response_model=Dict[str, Any])
@router.post("/migration/sync-now", response_model=dict[str, Any])
async def trigger_sync_now(
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),

View File

@@ -4,12 +4,12 @@
# @RELATION DEPENDS_ON -> [PluginConfig]
# @RELATION DEPENDS_ON -> [get_plugin_loader]
# @RELATION BINDS_TO -> [API_Routes]
from typing import List
from fastapi import APIRouter, Depends
from ...core.logger import belief_scope
from ...core.plugin_base import PluginConfig
from ...dependencies import get_plugin_loader, has_permission
from ...core.logger import belief_scope
router = APIRouter()
@@ -20,7 +20,7 @@ router = APIRouter()
# @POST: Returns a list of PluginConfig objects.
# @RELATION CALLS -> [get_plugin_loader]
# @RELATION DEPENDS_ON -> [PluginConfig]
@router.get("", response_model=List[PluginConfig])
@router.get("", response_model=list[PluginConfig])
async def list_plugins(
plugin_loader=Depends(get_plugin_loader),
_=Depends(has_permission("plugins", "READ")),

View File

@@ -13,13 +13,12 @@
# @UX_FEEDBACK: Stable status/message/warning payloads support profile page feedback.
# @UX_RECOVERY: Lookup degradation keeps manual username save path available.
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from ...core.database import get_db
from ...core.logger import logger, belief_scope
from ...core.logger import belief_scope, logger
from ...dependencies import (
get_config_manager,
get_current_user,
@@ -110,7 +109,7 @@ async def update_preferences(
@router.get("/superset-accounts", response_model=SupersetAccountLookupResponse)
async def lookup_superset_accounts(
environment_id: str = Query(...),
search: Optional[str] = Query(default=None),
search: str | None = Query(default=None),
page_index: int = Query(default=0, ge=0),
page_size: int = Query(default=20, ge=1, le=100),
sort_column: str = Query(default="username"),
@@ -144,4 +143,4 @@ async def lookup_superset_accounts(
raise HTTPException(status_code=404, detail=str(exc)) from exc
# #endregion lookup_superset_accounts
# #endregion ProfileApiModule
# #endregion ProfileApiModule

View File

@@ -12,17 +12,16 @@
# @DATA_CONTRACT: [ReportQuery] -> [ReportCollection | ReportDetailView]
from datetime import datetime
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from ...core.logger import belief_scope
from ...core.task_manager import TaskManager
from ...dependencies import (
get_clean_release_repository,
get_task_manager,
has_permission,
get_clean_release_repository,
)
from ...core.task_manager import TaskManager
from ...core.logger import belief_scope
from ...models.report import (
ReportCollection,
ReportDetailView,
@@ -41,7 +40,7 @@ router = APIRouter(prefix="/api/reports", tags=["Reports"])
# @PRE: raw may be None/empty or comma-separated values.
# @POST: Returns enum list or raises HTTP 400 with deterministic machine-readable payload.
# @RELATION BINDS_TO -> [ReportsRouter]
def _parse_csv_enum_list(raw: Optional[str], enum_cls, field_name: str) -> List:
def _parse_csv_enum_list(raw: str | None, enum_cls, field_name: str) -> list:
with belief_scope("_parse_csv_enum_list"):
if raw is None or not raw.strip():
return []
@@ -95,11 +94,11 @@ def _parse_csv_enum_list(raw: Optional[str], enum_cls, field_name: str) -> List:
async def list_reports(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
task_types: Optional[str] = Query(None, description="Comma-separated task types"),
statuses: Optional[str] = Query(None, description="Comma-separated statuses"),
time_from: Optional[datetime] = Query(None),
time_to: Optional[datetime] = Query(None),
search: Optional[str] = Query(None, max_length=200),
task_types: str | None = Query(None, description="Comma-separated task types"),
statuses: str | None = Query(None, description="Comma-separated statuses"),
time_from: datetime | None = Query(None),
time_to: datetime | None = Query(None),
search: str | None = Query(None, max_length=200),
sort_by: str = Query("updated_at"),
sort_order: str = Query("desc"),
task_manager: TaskManager = Depends(get_task_manager),

View File

@@ -9,28 +9,29 @@
# @INVARIANT: All settings changes must be persisted via ConfigManager.
# @PUBLIC_API: router
from fastapi import APIRouter, Depends, HTTPException
from typing import List
from pydantic import BaseModel
from ...core.config_models import AppConfig, Environment, GlobalSettings, LoggingConfig
from ...models.storage import StorageConfig
from ...dependencies import get_config_manager, has_permission
from ...core.config_manager import ConfigManager
from ...core.logger import logger, belief_scope
from ...core.superset_client import SupersetClient
from ...services.llm_prompt_templates import normalize_llm_settings
from ...models.llm import ValidationPolicy
from ...models.config import AppConfigRecord
from ...schemas.settings import (
ValidationPolicyCreate,
ValidationPolicyUpdate,
ValidationPolicyResponse,
TranslationScheduleItem,
)
from ...models.translate import TranslationSchedule, TranslationJob
from ...core.database import get_db
from sqlalchemy.orm import Session
from ...core.config_manager import ConfigManager
from ...core.config_models import AppConfig, Environment, GlobalSettings, LoggingConfig
from ...core.database import get_db
from ...core.logger import belief_scope, logger
from ...core.superset_client import SupersetClient
from ...dependencies import get_config_manager, has_permission
from ...models.config import AppConfigRecord
from ...models.llm import ValidationPolicy
from ...models.storage import StorageConfig
from ...models.translate import TranslationJob, TranslationSchedule
from ...schemas.settings import (
TranslationScheduleItem,
ValidationPolicyCreate,
ValidationPolicyResponse,
ValidationPolicyUpdate,
)
from ...services.llm_prompt_templates import normalize_llm_settings
# #region LoggingConfigResponse [C:1] [TYPE Class] [SEMANTICS logging, config, response]
# @BRIEF Response model for logging configuration with current task log level.
@@ -179,7 +180,7 @@ async def update_storage_settings(
# @BRIEF Lists all configured Superset environments.
# @PRE: Config manager is available.
# @POST: Returns list of environments.
@router.get("/environments", response_model=List[Environment])
@router.get("/environments", response_model=list[Environment])
async def get_environments(
config_manager: ConfigManager = Depends(get_config_manager),
_=Depends(has_permission("admin:settings", "READ")),
@@ -375,10 +376,10 @@ async def update_logging_config(
# #region ConsolidatedSettingsResponse [C:1] [TYPE Class]
# @BRIEF Response model for consolidated application settings.
class ConsolidatedSettingsResponse(BaseModel):
environments: List[dict]
connections: List[dict]
environments: list[dict]
connections: list[dict]
llm: dict
llm_providers: List[dict]
llm_providers: list[dict]
logging: dict
storage: dict
notifications: dict = {}
@@ -410,8 +411,8 @@ async def get_consolidated_settings(
config = config_manager.get_config()
from ...services.llm_provider import LLMProviderService
from ...core.database import SessionLocal
from ...services.llm_provider import LLMProviderService
db = SessionLocal()
notifications_payload = {}
@@ -524,7 +525,7 @@ async def update_consolidated_settings(
# #region get_validation_policies [C:2] [TYPE Function]
# @BRIEF Lists all validation policies.
@router.get("/automation/policies", response_model=List[ValidationPolicyResponse])
@router.get("/automation/policies", response_model=list[ValidationPolicyResponse])
async def get_validation_policies(
db: Session = Depends(get_db), _=Depends(has_permission("admin:settings", "READ"))
):
@@ -603,7 +604,7 @@ async def delete_validation_policy(
# #region get_translation_schedules [C:2] [TYPE Function]
# @BRIEF Lists all translation schedules joined with translation job names.
@router.get("/automation/translation-schedules", response_model=List[TranslationScheduleItem])
@router.get("/automation/translation-schedules", response_model=list[TranslationScheduleItem])
async def get_translation_schedules(
db: Session = Depends(get_db),
_=Depends(has_permission("admin:settings", "READ")),

View File

@@ -8,13 +8,14 @@
# @INVARIANT: All paths must be validated against path traversal.
from pathlib import Path
from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from fastapi.responses import FileResponse
from typing import List, Optional
from ...models.storage import StoredFile, FileCategory
from ...dependencies import get_plugin_loader, has_permission
from ...plugins.storage.plugin import StoragePlugin
from ...core.logger import belief_scope
from ...dependencies import get_plugin_loader, has_permission
from ...models.storage import FileCategory, StoredFile
from ...plugins.storage.plugin import StoragePlugin
router = APIRouter(tags=["storage"])
@@ -25,10 +26,10 @@ router = APIRouter(tags=["storage"])
# @POST: Returns a list of StoredFile objects.
#
# @RELATION DEPENDS_ON -> [StoragePlugin]
@router.get("/files", response_model=List[StoredFile])
@router.get("/files", response_model=list[StoredFile])
async def list_files(
category: Optional[FileCategory] = None,
path: Optional[str] = None,
category: FileCategory | None = None,
path: str | None = None,
recursive: bool = False,
plugin_loader=Depends(get_plugin_loader),
_ = Depends(has_permission("plugin:storage", "READ"))
@@ -54,7 +55,7 @@ async def list_files(
@router.post("/upload", response_model=StoredFile, status_code=201)
async def upload_file(
category: FileCategory = Form(...),
path: Optional[str] = Form(None),
path: str | None = Form(None),
file: UploadFile = File(...),
plugin_loader=Depends(get_plugin_loader),
_ = Depends(has_permission("plugin:storage", "WRITE"))

View File

@@ -5,20 +5,21 @@
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [LLMProviderService]
from typing import List, Dict, Any, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from pydantic import BaseModel
from ...core.logger import belief_scope
from typing import Any
from ...core.task_manager import TaskManager, Task, TaskStatus, LogEntry
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel
from ...core.config_manager import ConfigManager
from ...core.logger import belief_scope
from ...core.task_manager import Task, TaskManager, TaskStatus
from ...core.task_manager.models import LogFilter, LogStats
from ...dependencies import (
get_config_manager,
get_current_user,
get_task_manager,
has_permission,
get_current_user,
get_config_manager,
)
from ...core.config_manager import ConfigManager
from ...services.llm_prompt_templates import (
is_multimodal_model,
normalize_llm_settings,
@@ -36,15 +37,15 @@ TASK_TYPE_PLUGIN_MAP = {
class CreateTaskRequest(BaseModel):
plugin_id: str
params: Dict[str, Any]
params: dict[str, Any]
class ResolveTaskRequest(BaseModel):
resolution_params: Dict[str, Any]
resolution_params: dict[str, Any]
class ResumeTaskRequest(BaseModel):
passwords: Dict[str, str]
passwords: dict[str, str]
# #region create_task [C:3] [TYPE Function]
@@ -132,15 +133,15 @@ async def create_task(
# @POST: Returns a list of tasks.
# @RELATION CALLS -> [TaskManager]
# @RELATION BINDS_TO -> [TASK_TYPE_PLUGIN_MAP]
@router.get("", response_model=List[Task])
@router.get("", response_model=list[Task])
async def list_tasks(
limit: int = 10,
offset: int = 0,
status_filter: Optional[TaskStatus] = Query(None, alias="status"),
task_type: Optional[str] = Query(
status_filter: TaskStatus | None = Query(None, alias="status"),
task_type: str | None = Query(
None, description="Task category: llm_validation, backup, migration"
),
plugin_id: Optional[List[str]] = Query(
plugin_id: list[str] | None = Query(
None, description="Filter by plugin_id (repeatable query param)"
),
completed_only: bool = Query(
@@ -210,11 +211,11 @@ async def get_task(
@router.get("/{task_id}/logs")
async def get_task_logs(
task_id: str,
level: Optional[str] = Query(
level: str | None = Query(
None, description="Filter by log level (DEBUG, INFO, WARNING, ERROR)"
),
source: Optional[str] = Query(None, description="Filter by source component"),
search: Optional[str] = Query(None, description="Text search in message"),
source: str | None = Query(None, description="Filter by source component"),
search: str | None = Query(None, description="Text search in message"),
offset: int = Query(0, ge=0, description="Number of logs to skip"),
limit: int = Query(
100, ge=1, le=1000, description="Maximum number of logs to return"
@@ -291,7 +292,7 @@ async def get_task_log_stats(
# @PRE: task_id must exist.
# @POST: Returns list of unique source names or raises 404.
# @RELATION CALLS -> [TaskManager]
@router.get("/{task_id}/logs/sources", response_model=List[str])
@router.get("/{task_id}/logs/sources", response_model=list[str])
async def get_task_log_sources(
task_id: str,
task_manager: TaskManager = Depends(get_task_manager),
@@ -361,7 +362,7 @@ async def resume_task(
# @RELATION CALLS -> [TaskManager]
@router.delete("", status_code=status.HTTP_204_NO_CONTENT)
async def clear_tasks(
status: Optional[TaskStatus] = None,
status: TaskStatus | None = None,
task_manager: TaskManager = Depends(get_task_manager),
_=Depends(has_permission("tasks", "WRITE")),
):

View File

@@ -18,15 +18,17 @@ Re-exports the shared router and all route handler modules.
All sub-modules register their handlers on the router at import time.
"""
from . import (
_correction_routes, # noqa: F401 — registers correction handlers
_dictionary_routes, # noqa: F401 — registers dictionary handlers
_job_routes, # noqa: F401 — registers job handlers
_metrics_routes, # noqa: F401 — registers metrics handlers
_preview_routes, # noqa: F401 — registers preview handlers
_run_list_routes, # noqa: F401 — registers run list/detail/csv handlers
_run_routes, # noqa: F401 — registers run handlers
_schedule_routes, # noqa: F401 — registers schedule handlers
)
from ._router import router
from . import _job_routes # noqa: F401 — registers job handlers
from . import _run_routes # noqa: F401 — registers run handlers
from . import _run_list_routes # noqa: F401 — registers run list/detail/csv handlers
from . import _preview_routes # noqa: F401 — registers preview handlers
from . import _dictionary_routes # noqa: F401 — registers dictionary handlers
from . import _schedule_routes # noqa: F401 — registers schedule handlers
from . import _metrics_routes # noqa: F401 — registers metrics handlers
from . import _correction_routes # noqa: F401 — registers correction handlers
__all__ = [
"router",

View File

@@ -2,24 +2,20 @@
# @BRIEF Term correction submission endpoints.
# @LAYER: API
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
from fastapi import Depends, HTTPException, status
from sqlalchemy.orm import Session
from ....core.database import get_db
from ....core.logger import logger, belief_scope
from ....schemas.auth import User
from ....core.logger import logger
from ....dependencies import get_current_user, has_permission
from ....plugins.translate.dictionary import DictionaryManager
from ....schemas.auth import User
from ....schemas.translate import (
TermCorrectionSubmit,
TermCorrectionBulkSubmit,
CorrectionSubmitResponse,
TermCorrectionSubmit,
)
from ._router import router
# ============================================================
# Corrections
# ============================================================

View File

@@ -2,26 +2,21 @@
# @BRIEF Terminology Dictionary CRUD, entries management, and import routes.
# @LAYER: API
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
from fastapi import Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from ....core.database import get_db
from ....core.logger import logger, belief_scope
from ....schemas.auth import User
from ....core.logger import belief_scope, logger
from ....dependencies import get_current_user, has_permission
from ....plugins.translate.dictionary import DictionaryManager
from ....schemas.auth import User
from ....schemas.translate import (
DictionaryCreate,
DictionaryEntryCreate,
DictionaryEntryResponse,
DictionaryImport,
DictionaryResponse,
)
from ._router import router
from ._helpers import _dict_to_response, _get_dictionary_entry_counts
from ._router import router
# ============================================================
# Terminology Dictionaries

View File

@@ -3,7 +3,8 @@
# @LAYER: API
# @RELATION DEPENDS_ON -> [models.translate]
from typing import Any, Dict, List
from typing import Any
from sqlalchemy.orm import Session
@@ -55,8 +56,9 @@ def _dict_to_response(d: Any, entry_count: int = 0) -> dict:
# #region _get_dictionary_entry_counts [C:2] [TYPE Function]
# @BRIEF Get entry counts for a list of dictionary IDs.
def _get_dictionary_entry_counts(db: Session, dict_ids: List[str]) -> Dict[str, int]:
def _get_dictionary_entry_counts(db: Session, dict_ids: list[str]) -> dict[str, int]:
from sqlalchemy import func
from ....models.translate import DictionaryEntry
counts = (
db.query(DictionaryEntry.dictionary_id, func.count(DictionaryEntry.id))

View File

@@ -2,31 +2,31 @@
# @BRIEF Translation Job CRUD and datasource column routes.
# @LAYER: API
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
from fastapi import Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from ....core.database import get_db
from ....core.logger import logger, belief_scope
from ....schemas.auth import User
from ....dependencies import get_current_user, has_permission, get_config_manager
from ....core.config_manager import ConfigManager
from ....core.database import get_db
from ....core.logger import belief_scope, logger
from ....dependencies import get_config_manager, get_current_user, has_permission
from ....plugins.translate.service import (
TranslateJobService,
job_to_response,
)
from ....plugins.translate.service import (
get_datasource_columns as fetch_datasource_columns_service,
)
from ....schemas.auth import User
from ....schemas.translate import (
TranslateJobCreate,
TranslateJobUpdate,
TranslateJobResponse,
DatasourceColumnsResponse,
DuplicateJobResponse,
TranslateJobCreate,
TranslateJobResponse,
TranslateJobUpdate,
)
from ._router import router
# ============================================================
# Translation Job CRUD
# ============================================================
@@ -35,11 +35,11 @@ from ._router import router
# @BRIEF List all translation jobs.
# @PRE: User has translate.job.view permission.
# @POST: Returns list of translation jobs.
@router.get("/jobs", response_model=List[TranslateJobResponse])
@router.get("/jobs", response_model=list[TranslateJobResponse])
async def list_jobs(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
status: Optional[str] = Query(None),
status: str | None = Query(None),
current_user: User = Depends(get_current_user),
_ = Depends(has_permission("translate.job", "VIEW")),
db: Session = Depends(get_db),
@@ -199,7 +199,7 @@ async def duplicate_job(
@router.get("/datasources")
async def list_datasources(
env_id: str = Query(..., description="Superset environment ID"),
search: Optional[str] = Query(None, description="Search by table name"),
search: str | None = Query(None, description="Search by table name"),
current_user: User = Depends(get_current_user),
_ = Depends(has_permission("translate.job", "VIEW")),
db: Session = Depends(get_db),
@@ -212,7 +212,7 @@ async def list_datasources(
datasets = service.fetch_available_datasources(env_id, search)
return datasets
except Exception as e:
logger.explore(f"list_datasources failed", extra={"src": "translate_routes", "error": str(e)})
logger.explore("list_datasources failed", extra={"src": "translate_routes", "error": str(e)})
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))
@router.get("/datasources/{datasource_id}/columns", response_model=DatasourceColumnsResponse)
@@ -238,7 +238,7 @@ async def get_datasource_columns(
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
logger.explore(f"get_datasource_columns failed", extra={"src": "translate_routes", "error": str(e)})
logger.explore("get_datasource_columns failed", extra={"src": "translate_routes", "error": str(e)})
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"Failed to fetch datasource columns from Superset: {e}",

View File

@@ -2,19 +2,17 @@
# @BRIEF Translation Metrics endpoints.
# @LAYER: API
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
from fastapi import Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from ....core.database import get_db
from ....core.logger import logger, belief_scope
from ....schemas.auth import User
from ....core.logger import logger
from ....dependencies import get_current_user, has_permission
from ....plugins.translate.metrics import TranslationMetrics
from ....schemas.auth import User
from ._router import router
# ============================================================
# Metrics
# ============================================================
@@ -25,7 +23,7 @@ from ._router import router
# @POST: Returns metrics data.
@router.get("/metrics")
async def get_metrics(
job_id: Optional[str] = Query(None),
job_id: str | None = Query(None),
current_user: User = Depends(get_current_user),
_ = Depends(has_permission("translate.metrics", "VIEW")),
db: Session = Depends(get_db),

View File

@@ -2,26 +2,23 @@
# @BRIEF Translation Preview session management routes.
# @LAYER: API
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
from fastapi import Depends, HTTPException, status
from sqlalchemy.orm import Session
from ....core.database import get_db
from ....core.logger import logger, belief_scope
from ....schemas.auth import User
from ....dependencies import get_current_user, has_permission, get_config_manager
from ....core.config_manager import ConfigManager
from ....core.database import get_db
from ....core.logger import logger
from ....dependencies import get_config_manager, get_current_user, has_permission
from ....plugins.translate.preview import TranslationPreview
from ....schemas.auth import User
from ....schemas.translate import (
PreviewRequest,
PreviewRowUpdate,
PreviewAcceptResponse,
PreviewRow,
PreviewRowUpdate,
)
from ._router import router
# ============================================================
# Preview
# ============================================================
@@ -56,7 +53,7 @@ async def preview_translation(
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
logger.explore(f"preview_translation failed", extra={"src": "translate_routes", "error": str(e)})
logger.explore("preview_translation failed", extra={"src": "translate_routes", "error": str(e)})
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Preview failed: {e}")
# #endregion preview_translation
@@ -151,7 +148,7 @@ async def apply_preview(
# @BRIEF Get records for a preview session.
# @PRE: User has translate.job.view permission.
# @POST: Returns list of preview records.
@router.get("/preview/{session_id}/records", response_model=List[PreviewRow])
@router.get("/preview/{session_id}/records", response_model=list[PreviewRow])
async def get_preview_records(
session_id: str,
current_user: User = Depends(get_current_user),
@@ -161,7 +158,7 @@ async def get_preview_records(
"""Get records for a preview session."""
logger.reason(f"get_preview_records — Session: {session_id}, User: {current_user.username}", extra={"src": "translate_routes"})
try:
from ....models.translate import TranslationPreviewSession, TranslationPreviewRecord
from ....models.translate import TranslationPreviewRecord, TranslationPreviewSession
session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()
if not session:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Preview session '{session_id}' not found")
@@ -186,7 +183,7 @@ async def get_preview_records(
except HTTPException:
raise
except Exception as e:
logger.explore(f"get_preview_records failed", extra={"src": "translate_routes", "error": str(e)})
logger.explore("get_preview_records failed", extra={"src": "translate_routes", "error": str(e)})
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# #endregion get_preview_records

View File

@@ -2,21 +2,19 @@
# @BRIEF Translation Run listing, detail, and CSV download routes (cross-job).
# @LAYER: API
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
from fastapi import Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from ....core.database import get_db
from ....core.logger import logger, belief_scope
from ....schemas.auth import User
from ....dependencies import get_current_user, has_permission, get_config_manager
from ....core.config_manager import ConfigManager
from ....plugins.translate.orchestrator import TranslationOrchestrator
from ....core.database import get_db
from ....core.logger import logger
from ....dependencies import get_config_manager, get_current_user, has_permission
from ....plugins.translate.events import TranslationEventLog
from ....plugins.translate.orchestrator import TranslationOrchestrator
from ....schemas.auth import User
from ._router import router
# ============================================================
# List Runs (cross-job)
# ============================================================
@@ -27,12 +25,12 @@ from ._router import router
# @POST: Returns paginated list of runs.
@router.get("/runs")
async def list_runs(
job_id: Optional[str] = Query(None),
run_status: Optional[str] = Query(None),
trigger_type: Optional[str] = Query(None),
created_by: Optional[str] = Query(None),
date_from: Optional[str] = Query(None),
date_to: Optional[str] = Query(None),
job_id: str | None = Query(None),
run_status: str | None = Query(None),
trigger_type: str | None = Query(None),
created_by: str | None = Query(None),
date_from: str | None = Query(None),
date_to: str | None = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user),
@@ -100,7 +98,7 @@ async def list_runs(
return {"items": items, "total": total, "page": page, "page_size": page_size}
except Exception as e:
logger.explore(f"list_runs failed", extra={"src": "translate_routes", "error": str(e)})
logger.explore("list_runs failed", extra={"src": "translate_routes", "error": str(e)})
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# #endregion list_runs
@@ -166,11 +164,13 @@ async def download_skipped_csv(
"""Download skipped translation records as CSV."""
logger.reason(f"download_skipped_csv — Run: {run_id}, User: {current_user.username}", extra={"src": "translate_routes"})
try:
from ....models.translate import TranslationRecord
from fastapi.responses import StreamingResponse
import csv
import io
from fastapi.responses import StreamingResponse
from ....models.translate import TranslationRecord
records = (
db.query(TranslationRecord)
.filter(
@@ -197,7 +197,7 @@ async def download_skipped_csv(
headers={"Content-Disposition": f"attachment; filename=skipped_{run_id}.csv"},
)
except Exception as e:
logger.explore(f"download_skipped_csv failed", extra={"src": "translate_routes", "error": str(e)})
logger.explore("download_skipped_csv failed", extra={"src": "translate_routes", "error": str(e)})
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# #endregion download_skipped_csv

View File

@@ -2,22 +2,19 @@
# @BRIEF Translation Run execution, history, status, records and batches routes.
# @LAYER: API
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
from datetime import UTC, datetime
from fastapi import Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from ....core.database import get_db, SessionLocal
from ....core.logger import logger, belief_scope
from ....schemas.auth import User
from ....dependencies import get_current_user, has_permission, get_config_manager
from ....core.config_manager import ConfigManager
from ....core.database import SessionLocal, get_db
from ....core.logger import logger
from ....dependencies import get_config_manager, get_current_user, has_permission
from ....plugins.translate.orchestrator import TranslationOrchestrator
from ....plugins.translate.events import TranslationEventLog
from ._router import router
from ....schemas.auth import User
from ._helpers import _run_to_response
from ._router import router
# ============================================================
# Translation Run / Execute
@@ -119,7 +116,7 @@ async def run_translation(
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_run.completed_at = datetime.now(UTC)
fb_db.commit()
logger.reason(
"Background execute: run marked FAILED",
@@ -147,7 +144,7 @@ async def run_translation(
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
logger.explore(f"run_translation failed", extra={"src": "translate_routes", "error": str(e)})
logger.explore("run_translation failed", extra={"src": "translate_routes", "error": str(e)})
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Run failed: {e}")
# #endregion run_translation
@@ -173,7 +170,7 @@ async def retry_run(
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
logger.explore(f"retry_run failed", extra={"src": "translate_routes", "error": str(e)})
logger.explore("retry_run failed", extra={"src": "translate_routes", "error": str(e)})
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Retry failed: {e}")
# #endregion retry_run
@@ -199,7 +196,7 @@ async def retry_insert(
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
logger.explore(f"retry_insert failed", extra={"src": "translate_routes", "error": str(e)})
logger.explore("retry_insert failed", extra={"src": "translate_routes", "error": str(e)})
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Retry insert failed: {e}")
# #endregion retry_insert
@@ -283,7 +280,7 @@ async def get_run_records(
run_id: str,
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=500),
status: Optional[str] = Query(None),
status: str | None = Query(None),
current_user: User = Depends(get_current_user),
_ = Depends(has_permission("translate.history", "VIEW")),
db: Session = Depends(get_db),
@@ -340,7 +337,7 @@ async def get_batches(
for b in batches
]
except Exception as e:
logger.explore(f"get_batches failed", extra={"src": "translate_routes", "error": str(e)})
logger.explore("get_batches failed", extra={"src": "translate_routes", "error": str(e)})
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# #endregion get_batches

View File

@@ -2,21 +2,18 @@
# @BRIEF Translation Schedule management routes.
# @LAYER: API
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
from fastapi import Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from ....core.database import get_db
from ....core.logger import logger, belief_scope
from ....schemas.auth import User
from ....dependencies import get_current_user, has_permission, get_config_manager
from ....core.config_manager import ConfigManager
from ....core.database import get_db
from ....core.logger import logger
from ....dependencies import get_config_manager, get_current_user, has_permission
from ....plugins.translate.scheduler import TranslationScheduler
from ....schemas.translate import ScheduleConfig, ScheduleResponse
from ....schemas.auth import User
from ....schemas.translate import ScheduleConfig
from ._router import router
# ============================================================
# Schedule
# ============================================================