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:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,4 +28,4 @@ class FakeQuery:
|
||||
return list(self._rows)
|
||||
def count(self):
|
||||
return len(self._rows)
|
||||
# #endregion RoutesTestsConftest
|
||||
# #endregion RoutesTestsConftest
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"] = {}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
@@ -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")],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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"}:
|
||||
|
||||
@@ -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 * * *')"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")),
|
||||
):
|
||||
|
||||
@@ -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")),
|
||||
):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")),
|
||||
):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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")),
|
||||
):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
# ============================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
# ============================================================
|
||||
|
||||
@@ -11,46 +11,50 @@
|
||||
# @DATA_CONTRACT: [HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream]
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# project_root is used for static files mounting
|
||||
project_root = Path(__file__).resolve().parent.parent.parent
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, HTTPException
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
import asyncio
|
||||
from .dependencies import get_task_manager, get_scheduler_service
|
||||
from .core.encryption_key import ensure_encryption_key
|
||||
from .core.utils.network import NetworkError
|
||||
from .core.logger import logger, belief_scope
|
||||
from .core.cot_logger import seed_trace_id
|
||||
from .core.database import AuthSessionLocal
|
||||
from .core.auth.security import get_password_hash
|
||||
from .models.auth import User, Role
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from .api import auth
|
||||
from .api.routes import (
|
||||
plugins,
|
||||
tasks,
|
||||
settings,
|
||||
environments,
|
||||
mappings,
|
||||
migration,
|
||||
connections,
|
||||
git,
|
||||
storage,
|
||||
admin,
|
||||
llm,
|
||||
dashboards,
|
||||
datasets,
|
||||
reports,
|
||||
assistant,
|
||||
clean_release,
|
||||
clean_release_v2,
|
||||
profile,
|
||||
health,
|
||||
connections,
|
||||
dashboards,
|
||||
dataset_review,
|
||||
datasets,
|
||||
environments,
|
||||
git,
|
||||
health,
|
||||
llm,
|
||||
mappings,
|
||||
migration,
|
||||
plugins,
|
||||
profile,
|
||||
reports,
|
||||
settings,
|
||||
storage,
|
||||
tasks,
|
||||
translate,
|
||||
)
|
||||
from .api import auth
|
||||
from .core.auth.security import get_password_hash
|
||||
from .core.cot_logger import seed_trace_id
|
||||
from .core.database import AuthSessionLocal
|
||||
from .core.encryption_key import ensure_encryption_key
|
||||
from .core.logger import belief_scope, logger
|
||||
from .core.utils.network import NetworkError
|
||||
from .dependencies import get_scheduler_service, get_task_manager
|
||||
from .models.auth import Role, User
|
||||
|
||||
# #region FastAPI_App [C:3] [TYPE Global] [SEMANTICS app, fastapi, instance, route-registry]
|
||||
# @BRIEF Canonical FastAPI application instance for route, middleware, and websocket registration.
|
||||
# @RELATION DEPENDS_ON -> [ApiRoutesModule]
|
||||
@@ -141,6 +145,7 @@ async def shutdown_event():
|
||||
# @BRIEF Configure application-wide middleware (Session, CORS).
|
||||
# Configure Session Middleware (required by Authlib for OAuth2 flow)
|
||||
from .core.auth.config import auth_config
|
||||
|
||||
app.add_middleware(SessionMiddleware, secret_key=auth_config.SECRET_KEY)
|
||||
# Configure CORS
|
||||
app.add_middleware(
|
||||
@@ -193,6 +198,7 @@ async def log_requests(request: Request, call_next):
|
||||
# Seed trace_id on every request — MUST be the outermost middleware so trace_id
|
||||
# is seeded before log_requests (or any other middleware) runs.
|
||||
from .core.middleware.trace import TraceContextMiddleware
|
||||
|
||||
app.add_middleware(TraceContextMiddleware)
|
||||
# #region API_Routes [C:3] [TYPE Block]
|
||||
# @BRIEF Register all FastAPI route groups exposed by the application entrypoint.
|
||||
@@ -425,4 +431,4 @@ else:
|
||||
}
|
||||
# #endregion read_root
|
||||
# #endregion StaticFiles
|
||||
# #endregion AppModule
|
||||
# #endregion AppModule
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
# @LAYER: Domain
|
||||
# @RELATION VERIFIES -> ConfigManager
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.core.config_manager import ConfigManager
|
||||
from src.core.config_models import AppConfig, Environment, GlobalSettings
|
||||
|
||||
|
||||
# #region test_get_payload_preserves_legacy_sections [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestConfigManagerCompat
|
||||
# @BRIEF Ensure get_payload merges typed config into raw payload without dropping legacy sections.
|
||||
@@ -207,4 +210,4 @@ def test_load_config_syncs_environment_records_from_existing_db_payload(monkeypa
|
||||
assert committed["value"] is True
|
||||
assert closed["value"] is True
|
||||
# #endregion test_load_config_syncs_environment_records_from_existing_db_payload
|
||||
# #endregion TestConfigManagerCompat
|
||||
# #endregion TestConfigManagerCompat
|
||||
|
||||
@@ -6,21 +6,22 @@
|
||||
# @RELATION [BINDS_TO] ->[FilterState, ParsedNativeFilters, ExtraFormDataMerge]
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
from src.core.superset_client import SupersetClient
|
||||
from src.core.async_superset_client import AsyncSupersetClient
|
||||
|
||||
from src.core.config_models import Environment
|
||||
from src.core.superset_client import SupersetClient
|
||||
from src.core.utils.superset_context_extractor import (
|
||||
SupersetContextExtractor,
|
||||
SupersetParsedContext,
|
||||
sanitize_imported_filter_for_assistant,
|
||||
)
|
||||
from src.models.filter_state import (
|
||||
ExtraFormDataMerge,
|
||||
FilterState,
|
||||
NativeFilterDataMask,
|
||||
ParsedNativeFilters,
|
||||
ExtraFormDataMerge,
|
||||
)
|
||||
|
||||
|
||||
# #region _make_environment [TYPE Function]
|
||||
# @RELATION BINDS_TO -> NativeFilterExtractionTests
|
||||
def _make_environment() -> Environment:
|
||||
@@ -568,4 +569,4 @@ def test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values():
|
||||
assert result["raw_value_masked"] is True
|
||||
assert result["normalized_value"] == {"filter_clauses": []}
|
||||
# #endregion test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values
|
||||
# #endregion NativeFilterExtractionTests
|
||||
# #endregion NativeFilterExtractionTests
|
||||
|
||||
@@ -4,13 +4,17 @@
|
||||
# @RELATION [BINDS_TO] ->[AsyncNetworkModule]
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from src.core.config_models import Environment
|
||||
from src.core.superset_client import SupersetClient
|
||||
from src.core.utils.async_network import AsyncAPIClient
|
||||
from src.core.utils.network import APIClient, DashboardNotFoundError, SupersetAPIError
|
||||
|
||||
|
||||
# #region _make_environment [TYPE Function]
|
||||
# @RELATION BINDS_TO -> SupersetPreviewPipelineTests
|
||||
def _make_environment() -> Environment:
|
||||
@@ -454,4 +458,4 @@ async def test_async_network_404_mapping_translates_dashboard_endpoints():
|
||||
finally:
|
||||
await client.aclose()
|
||||
# #endregion test_async_network_404_mapping_translates_dashboard_endpoints
|
||||
# #endregion SupersetPreviewPipelineTests
|
||||
# #endregion SupersetPreviewPipelineTests
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
backend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())
|
||||
if backend_dir not in sys.path:
|
||||
sys.path.insert(0, backend_dir)
|
||||
from src.core.superset_profile_lookup import SupersetAccountLookupAdapter
|
||||
from src.core.utils.network import AuthenticationError, SupersetAPIError
|
||||
|
||||
|
||||
# [/SECTION]
|
||||
# #region _RecordingNetworkClient [TYPE Class] [C:2]
|
||||
# @RELATION BINDS_TO -> TestSupersetProfileLookup
|
||||
@@ -23,9 +27,9 @@ class _RecordingNetworkClient:
|
||||
# @PURPOSE: Initializes scripted network responses.
|
||||
# @PRE: scripted_responses is ordered per expected request sequence.
|
||||
# @POST: Instance stores response script and captures subsequent request calls.
|
||||
def __init__(self, scripted_responses: List[Any]):
|
||||
def __init__(self, scripted_responses: list[Any]):
|
||||
self._scripted_responses = scripted_responses
|
||||
self.calls: List[Dict[str, Any]] = []
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
# #endregion __init__
|
||||
# #region request [TYPE Function]
|
||||
# @PURPOSE: Mimics APIClient.request while capturing call arguments.
|
||||
@@ -35,9 +39,9 @@ class _RecordingNetworkClient:
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
params: dict[str, Any] | None = None,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
self.calls.append(
|
||||
{
|
||||
"method": method,
|
||||
@@ -116,4 +120,4 @@ def test_get_users_page_uses_fallback_endpoint_when_primary_fails():
|
||||
"/security/users",
|
||||
]
|
||||
# #endregion test_get_users_page_uses_fallback_endpoint_when_primary_fails
|
||||
# #endregion TestSupersetProfileLookup
|
||||
# #endregion TestSupersetProfileLookup
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import pytest
|
||||
from datetime import time, date, datetime, timedelta
|
||||
from datetime import date, datetime, time
|
||||
|
||||
from src.core.scheduler import ThrottledSchedulerConfigurator
|
||||
|
||||
|
||||
# #region test_throttled_scheduler [TYPE Module] [C:3] [SEMANTICS test, scheduler, throttle, unit]
|
||||
# @RELATION BELONGS_TO -> SrcRoot
|
||||
# @BRIEF Unit tests for ThrottledSchedulerConfigurator distribution logic.
|
||||
@@ -112,4 +114,4 @@ def test_calculate_schedule_very_small_window():
|
||||
assert schedule[1] == datetime(2024, 1, 1, 1, 0, 0, 500000) # 0.5s
|
||||
assert schedule[2] == datetime(2024, 1, 1, 1, 0, 1)
|
||||
# #endregion test_calculate_schedule_very_small_window
|
||||
# #endregion test_throttled_scheduler
|
||||
# #endregion test_throttled_scheduler
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple, cast
|
||||
from typing import Any, cast
|
||||
|
||||
from .config_models import Environment
|
||||
from .logger import logger as app_logger, belief_scope
|
||||
from .logger import belief_scope
|
||||
from .logger import logger as app_logger
|
||||
from .superset_client import SupersetClient
|
||||
from .utils.async_network import AsyncAPIClient
|
||||
|
||||
|
||||
# #region AsyncSupersetClient [C:3] [TYPE Class]
|
||||
# @BRIEF Async sibling of SupersetClient for dashboard read paths.
|
||||
# @RELATION INHERITS -> [SupersetClient]
|
||||
@@ -50,8 +54,8 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
|
||||
# @RELATION: [CALLS] -> [AsyncAPIClient.request]
|
||||
async def get_dashboards_page_async(
|
||||
self, query: Optional[Dict] = None
|
||||
) -> Tuple[int, List[Dict]]:
|
||||
self, query: dict | None = None
|
||||
) -> tuple[int, list[dict]]:
|
||||
with belief_scope("AsyncSupersetClient.get_dashboards_page_async"):
|
||||
validated_query = self._validate_query_params(query or {})
|
||||
if "columns" not in validated_query:
|
||||
@@ -68,7 +72,7 @@ class AsyncSupersetClient(SupersetClient):
|
||||
"owners",
|
||||
]
|
||||
response_json = cast(
|
||||
Dict[str, Any],
|
||||
dict[str, Any],
|
||||
await self.network.request(
|
||||
method="GET",
|
||||
endpoint="/dashboard/",
|
||||
@@ -84,26 +88,26 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# @POST: Returns raw dashboard payload from Superset API.
|
||||
# @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]
|
||||
# @RELATION: [CALLS] ->[AsyncAPIClient.request]
|
||||
async def get_dashboard_async(self, dashboard_id: int) -> Dict:
|
||||
async def get_dashboard_async(self, dashboard_id: int) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.get_dashboard_async", f"id={dashboard_id}"
|
||||
):
|
||||
response = await self.network.request(
|
||||
method="GET", endpoint=f"/dashboard/{dashboard_id}"
|
||||
)
|
||||
return cast(Dict, response)
|
||||
return cast(dict, response)
|
||||
# #endregion get_dashboard_async
|
||||
# #region get_chart_async [TYPE Function] [C:3]
|
||||
# @PURPOSE: Fetch one chart payload asynchronously.
|
||||
# @POST: Returns raw chart payload from Superset API.
|
||||
# @DATA_CONTRACT: Input[chart_id: int] -> Output[Dict]
|
||||
# @RELATION: [CALLS] ->[AsyncAPIClient.request]
|
||||
async def get_chart_async(self, chart_id: int) -> Dict:
|
||||
async def get_chart_async(self, chart_id: int) -> dict:
|
||||
with belief_scope("AsyncSupersetClient.get_chart_async", f"id={chart_id}"):
|
||||
response = await self.network.request(
|
||||
method="GET", endpoint=f"/chart/{chart_id}"
|
||||
)
|
||||
return cast(Dict, response)
|
||||
return cast(dict, response)
|
||||
# #endregion get_chart_async
|
||||
# #region get_dashboard_detail_async [TYPE Function] [C:3]
|
||||
# @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.
|
||||
@@ -111,17 +115,17 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]
|
||||
# @RELATION: [CALLS] ->[get_dashboard_async]
|
||||
# @RELATION: [CALLS] ->[get_chart_async]
|
||||
async def get_dashboard_detail_async(self, dashboard_id: int) -> Dict:
|
||||
async def get_dashboard_detail_async(self, dashboard_id: int) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.get_dashboard_detail_async", f"id={dashboard_id}"
|
||||
):
|
||||
dashboard_response = await self.get_dashboard_async(dashboard_id)
|
||||
dashboard_data = dashboard_response.get("result", dashboard_response)
|
||||
charts: List[Dict] = []
|
||||
datasets: List[Dict] = []
|
||||
charts: list[dict] = []
|
||||
datasets: list[dict] = []
|
||||
def extract_dataset_id_from_form_data(
|
||||
form_data: Optional[Dict],
|
||||
) -> Optional[int]:
|
||||
form_data: dict | None,
|
||||
) -> int | None:
|
||||
if not isinstance(form_data, dict):
|
||||
return None
|
||||
datasource = form_data.get("datasource")
|
||||
@@ -394,7 +398,7 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# @PURPOSE: Fetch stored dashboard permalink state asynchronously.
|
||||
# @POST: Returns dashboard permalink state payload from Superset API.
|
||||
# @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]
|
||||
async def get_dashboard_permalink_state_async(self, permalink_key: str) -> Dict:
|
||||
async def get_dashboard_permalink_state_async(self, permalink_key: str) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.get_dashboard_permalink_state_async",
|
||||
f"key={permalink_key}",
|
||||
@@ -402,7 +406,7 @@ class AsyncSupersetClient(SupersetClient):
|
||||
response = await self.network.request(
|
||||
method="GET", endpoint=f"/dashboard/permalink/{permalink_key}"
|
||||
)
|
||||
return cast(Dict, response)
|
||||
return cast(dict, response)
|
||||
# #endregion get_dashboard_permalink_state_async
|
||||
# #region get_native_filter_state_async [TYPE Function] [C:2]
|
||||
# @PURPOSE: Fetch stored native filter state asynchronously.
|
||||
@@ -410,7 +414,7 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
|
||||
async def get_native_filter_state_async(
|
||||
self, dashboard_id: int, filter_state_key: str
|
||||
) -> Dict:
|
||||
) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.get_native_filter_state_async",
|
||||
f"dashboard={dashboard_id}, key={filter_state_key}",
|
||||
@@ -419,7 +423,7 @@ class AsyncSupersetClient(SupersetClient):
|
||||
method="GET",
|
||||
endpoint=f"/dashboard/{dashboard_id}/filter_state/{filter_state_key}",
|
||||
)
|
||||
return cast(Dict, response)
|
||||
return cast(dict, response)
|
||||
# #endregion get_native_filter_state_async
|
||||
# #region extract_native_filters_from_permalink_async [TYPE Function] [C:3]
|
||||
# @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.
|
||||
@@ -428,7 +432,7 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# @RELATION: [CALLS] ->[get_dashboard_permalink_state_async]
|
||||
async def extract_native_filters_from_permalink_async(
|
||||
self, permalink_key: str
|
||||
) -> Dict:
|
||||
) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.extract_native_filters_from_permalink_async",
|
||||
f"key={permalink_key}",
|
||||
@@ -463,7 +467,7 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# @RELATION: [CALLS] ->[get_native_filter_state_async]
|
||||
async def extract_native_filters_from_key_async(
|
||||
self, dashboard_id: int, filter_state_key: str
|
||||
) -> Dict:
|
||||
) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.extract_native_filters_from_key_async",
|
||||
f"dashboard={dashboard_id}, key={filter_state_key}",
|
||||
@@ -515,7 +519,7 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# @DATA_CONTRACT: Input[url: str] -> Output[Dict]
|
||||
# @RELATION: [CALLS] ->[extract_native_filters_from_permalink_async]
|
||||
# @RELATION: [CALLS] ->[extract_native_filters_from_key_async]
|
||||
async def parse_dashboard_url_for_filters_async(self, url: str) -> Dict:
|
||||
async def parse_dashboard_url_for_filters_async(self, url: str) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.parse_dashboard_url_for_filters_async", f"url={url}"
|
||||
):
|
||||
@@ -609,4 +613,4 @@ class AsyncSupersetClient(SupersetClient):
|
||||
return result
|
||||
# #endregion parse_dashboard_url_for_filters_async
|
||||
# #endregion AsyncSupersetClient
|
||||
# #endregion AsyncSupersetClientModule
|
||||
# #endregion AsyncSupersetClientModule
|
||||
|
||||
@@ -4,18 +4,21 @@
|
||||
# @RELATION VERIFIES -> AuthPackage
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add src to path
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from src.core.database import Base
|
||||
# Import all models to ensure they are registered with Base before create_all - must import both auth and mapping to ensure Base knows about all tables
|
||||
from src.models import mapping, auth, task, report
|
||||
from src.models.auth import User, Role, Permission, ADGroupMapping
|
||||
from src.services.auth_service import AuthService
|
||||
|
||||
from src.core.auth.repository import AuthRepository
|
||||
from src.core.auth.security import verify_password, get_password_hash
|
||||
from src.core.auth.security import get_password_hash, verify_password
|
||||
from src.core.database import Base
|
||||
|
||||
# Import all models to ensure they are registered with Base before create_all - must import both auth and mapping to ensure Base knows about all tables
|
||||
from src.models.auth import ADGroupMapping, Permission, Role, User
|
||||
from src.services.auth_service import AuthService
|
||||
|
||||
# Create in-memory SQLite database for testing
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
|
||||
engine = create_engine(
|
||||
@@ -252,4 +255,4 @@ def test_provision_adfs_user_existing(auth_service, auth_repo):
|
||||
assert user.username == "existingadfs@domain.com"
|
||||
assert len(user.roles) == 0 # No matching group mappings
|
||||
# #endregion test_auth
|
||||
# #endregion test_provision_adfs_user_existing
|
||||
# #endregion test_provision_adfs_user_existing
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
# #region AuthConfig [TYPE Class]
|
||||
# @BRIEF Holds authentication-related settings.
|
||||
# @PRE: Environment variables may be provided via .env file.
|
||||
@@ -31,7 +32,7 @@ class AuthConfig(BaseSettings):
|
||||
ADFS_CLIENT_ID: str = Field(default="", env="ADFS_CLIENT_ID")
|
||||
ADFS_CLIENT_SECRET: str = Field(default="", env="ADFS_CLIENT_SECRET")
|
||||
ADFS_METADATA_URL: str = Field(default="", env="ADFS_METADATA_URL")
|
||||
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
extra = "ignore"
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
# @INVARIANT: Tokens must include expiration time and user identifier.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from jose import jwt
|
||||
from .config import auth_config
|
||||
|
||||
from ..logger import belief_scope
|
||||
from .config import auth_config
|
||||
|
||||
|
||||
# #region create_access_token [TYPE Function]
|
||||
@@ -20,7 +21,7 @@ from ..logger import belief_scope
|
||||
# @POST: Returns a signed JWT string.
|
||||
# @RELATION DEPENDS_ON -> [auth_config]
|
||||
#
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
|
||||
with belief_scope("create_access_token"):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
#
|
||||
# @INVARIANT: Must not log sensitive data like passwords or full tokens.
|
||||
|
||||
from ..logger import logger, belief_scope
|
||||
from datetime import datetime
|
||||
|
||||
from ..logger import belief_scope, logger
|
||||
|
||||
|
||||
# #region log_security_event [TYPE Function]
|
||||
# @BRIEF Logs a security-related event for audit trails.
|
||||
# @PRE: event_type and username are strings.
|
||||
@@ -23,4 +25,4 @@ def log_security_event(event_type: str, username: str, details: dict = None):
|
||||
logger.info(msg)
|
||||
# #endregion log_security_event
|
||||
|
||||
# #endregion AuthLoggerModule
|
||||
# #endregion AuthLoggerModule
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
# @INVARIANT: Must use secure OIDC flows.
|
||||
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
|
||||
from .config import auth_config
|
||||
|
||||
# #region oauth [TYPE Variable]
|
||||
@@ -48,4 +49,4 @@ def is_adfs_configured() -> bool:
|
||||
# Initial registration
|
||||
register_adfs()
|
||||
|
||||
# #endregion AuthOauthModule
|
||||
# #endregion AuthOauthModule
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
# @PRE: Database connection is active.
|
||||
# @POST: Provides valid access to identity data.
|
||||
# @SIDE_EFFECT: Executes database read queries through the injected SQLAlchemy session boundary.
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
from ...models.auth import Permission, Role, User, ADGroupMapping
|
||||
|
||||
from ...models.auth import ADGroupMapping, Permission, Role, User
|
||||
from ...models.profile import UserDashboardPreference
|
||||
from ..logger import belief_scope, logger
|
||||
|
||||
|
||||
# #region AuthRepository [TYPE Class]
|
||||
# @BRIEF Provides low-level CRUD operations for identity and authorization records.
|
||||
# @PRE: Database session is bound.
|
||||
@@ -28,7 +31,7 @@ class AuthRepository:
|
||||
# @PRE: user_id is a valid UUID string.
|
||||
# @POST: Returns User object if found, else None.
|
||||
# @RELATION: DEPENDS_ON -> [User]
|
||||
def get_user_by_id(self, user_id: str) -> Optional[User]:
|
||||
def get_user_by_id(self, user_id: str) -> User | None:
|
||||
with belief_scope("AuthRepository.get_user_by_id"):
|
||||
logger.reason(f"Fetching user by id: {user_id}")
|
||||
result = self.db.query(User).filter(User.id == user_id).first()
|
||||
@@ -40,7 +43,7 @@ class AuthRepository:
|
||||
# @PRE: username is a non-empty string.
|
||||
# @POST: Returns User object if found, else None.
|
||||
# @RELATION: DEPENDS_ON -> [User]
|
||||
def get_user_by_username(self, username: str) -> Optional[User]:
|
||||
def get_user_by_username(self, username: str) -> User | None:
|
||||
with belief_scope("AuthRepository.get_user_by_username"):
|
||||
logger.reason(f"Fetching user by username: {username}")
|
||||
result = self.db.query(User).filter(User.username == username).first()
|
||||
@@ -51,7 +54,7 @@ class AuthRepository:
|
||||
# @PURPOSE: Retrieve role by UUID with permissions preloaded.
|
||||
# @RELATION: DEPENDS_ON -> [Role]
|
||||
# @RELATION: DEPENDS_ON -> [Permission]
|
||||
def get_role_by_id(self, role_id: str) -> Optional[Role]:
|
||||
def get_role_by_id(self, role_id: str) -> Role | None:
|
||||
with belief_scope("AuthRepository.get_role_by_id"):
|
||||
return (
|
||||
self.db.query(Role)
|
||||
@@ -63,14 +66,14 @@ class AuthRepository:
|
||||
# #region get_role_by_name [TYPE Function]
|
||||
# @PURPOSE: Retrieve role by unique name.
|
||||
# @RELATION: DEPENDS_ON -> [Role]
|
||||
def get_role_by_name(self, name: str) -> Optional[Role]:
|
||||
def get_role_by_name(self, name: str) -> Role | None:
|
||||
with belief_scope("AuthRepository.get_role_by_name"):
|
||||
return self.db.query(Role).filter(Role.name == name).first()
|
||||
# #endregion get_role_by_name
|
||||
# #region get_permission_by_id [TYPE Function]
|
||||
# @PURPOSE: Retrieve permission by UUID.
|
||||
# @RELATION: DEPENDS_ON -> [Permission]
|
||||
def get_permission_by_id(self, permission_id: str) -> Optional[Permission]:
|
||||
def get_permission_by_id(self, permission_id: str) -> Permission | None:
|
||||
with belief_scope("AuthRepository.get_permission_by_id"):
|
||||
return (
|
||||
self.db.query(Permission).filter(Permission.id == permission_id).first()
|
||||
@@ -81,7 +84,7 @@ class AuthRepository:
|
||||
# @RELATION: DEPENDS_ON -> [Permission]
|
||||
def get_permission_by_resource_action(
|
||||
self, resource: str, action: str
|
||||
) -> Optional[Permission]:
|
||||
) -> Permission | None:
|
||||
with belief_scope("AuthRepository.get_permission_by_resource_action"):
|
||||
return (
|
||||
self.db.query(Permission)
|
||||
@@ -92,7 +95,7 @@ class AuthRepository:
|
||||
# #region list_permissions [TYPE Function]
|
||||
# @PURPOSE: List all system permissions.
|
||||
# @RELATION: DEPENDS_ON -> [Permission]
|
||||
def list_permissions(self) -> List[Permission]:
|
||||
def list_permissions(self) -> list[Permission]:
|
||||
with belief_scope("AuthRepository.list_permissions"):
|
||||
return self.db.query(Permission).all()
|
||||
# #endregion list_permissions
|
||||
@@ -101,7 +104,7 @@ class AuthRepository:
|
||||
# @RELATION: DEPENDS_ON -> [UserDashboardPreference]
|
||||
def get_user_dashboard_preference(
|
||||
self, user_id: str
|
||||
) -> Optional[UserDashboardPreference]:
|
||||
) -> UserDashboardPreference | None:
|
||||
with belief_scope("AuthRepository.get_user_dashboard_preference"):
|
||||
return (
|
||||
self.db.query(UserDashboardPreference)
|
||||
@@ -115,7 +118,7 @@ class AuthRepository:
|
||||
# @POST: Returns a list of Role objects mapped to the provided AD groups.
|
||||
# @RELATION: DEPENDS_ON -> [Role]
|
||||
# @RELATION: DEPENDS_ON -> [ADGroupMapping]
|
||||
def get_roles_by_ad_groups(self, groups: List[str]) -> List[Role]:
|
||||
def get_roles_by_ad_groups(self, groups: list[str]) -> list[Role]:
|
||||
with belief_scope("AuthRepository.get_roles_by_ad_groups"):
|
||||
logger.reason(f"Fetching roles for AD groups: {groups}")
|
||||
if not groups:
|
||||
@@ -128,4 +131,4 @@ class AuthRepository:
|
||||
)
|
||||
# #endregion get_roles_by_ad_groups
|
||||
# #endregion AuthRepository
|
||||
# #endregion AuthRepositoryModule
|
||||
# #endregion AuthRepositoryModule
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import bcrypt
|
||||
|
||||
|
||||
# #region verify_password [TYPE Function]
|
||||
# @BRIEF Verifies a plain password against a hashed password.
|
||||
# @PRE: plain_password is a string, hashed_password is a bcrypt hash.
|
||||
|
||||
@@ -16,13 +16,17 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, List
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from .config_models import AppConfig, Environment, GlobalSettings
|
||||
from .database import SessionLocal
|
||||
|
||||
from ..models.config import AppConfigRecord
|
||||
from ..models.mapping import Environment as EnvironmentRecord
|
||||
from .logger import logger, configure_logger, belief_scope
|
||||
from .config_models import AppConfig, Environment, GlobalSettings
|
||||
from .database import SessionLocal
|
||||
from .logger import belief_scope, configure_logger, logger
|
||||
|
||||
|
||||
# #region ConfigManager [C:5] [TYPE Class]
|
||||
# @BRIEF Handles application configuration load, validation, mutation, and persistence lifecycle.
|
||||
# @PRE: Database is accessible and AppConfigRecord schema is loaded.
|
||||
@@ -106,7 +110,7 @@ class ConfigManager:
|
||||
"has_settings": "settings" in merged_payload,
|
||||
"extra_sections": sorted(
|
||||
key
|
||||
for key in merged_payload.keys()
|
||||
for key in merged_payload
|
||||
if key not in {"environments", "settings"}
|
||||
),
|
||||
},
|
||||
@@ -145,7 +149,7 @@ class ConfigManager:
|
||||
# #endregion _load_from_legacy_file
|
||||
# #region _get_record [TYPE Function]
|
||||
# @PURPOSE: Resolve global configuration record from DB.
|
||||
def _get_record(self, session: Session) -> Optional[AppConfigRecord]:
|
||||
def _get_record(self, session: Session) -> AppConfigRecord | None:
|
||||
with belief_scope("ConfigManager._get_record"):
|
||||
record = (
|
||||
session.query(AppConfigRecord)
|
||||
@@ -303,7 +307,7 @@ class ConfigManager:
|
||||
# #region _save_config_to_db [TYPE Function]
|
||||
# @PURPOSE: Persist provided AppConfig into the global DB configuration record.
|
||||
def _save_config_to_db(
|
||||
self, config: AppConfig, session: Optional[Session] = None
|
||||
self, config: AppConfig, session: Session | None = None
|
||||
) -> None:
|
||||
with belief_scope("ConfigManager._save_config_to_db"):
|
||||
owns_session = session is None
|
||||
@@ -426,7 +430,7 @@ class ConfigManager:
|
||||
# #endregion validate_path
|
||||
# #region get_environments [TYPE Function]
|
||||
# @PURPOSE: Return all configured environments.
|
||||
def get_environments(self) -> List[Environment]:
|
||||
def get_environments(self) -> list[Environment]:
|
||||
with belief_scope("ConfigManager.get_environments"):
|
||||
return list(self.config.environments)
|
||||
# #endregion get_environments
|
||||
@@ -438,7 +442,7 @@ class ConfigManager:
|
||||
# #endregion has_environments
|
||||
# #region get_environment [TYPE Function]
|
||||
# @PURPOSE: Resolve a configured environment by identifier.
|
||||
def get_environment(self, env_id: str) -> Optional[Environment]:
|
||||
def get_environment(self, env_id: str) -> Environment | None:
|
||||
with belief_scope("ConfigManager.get_environment", f"env_id={env_id}"):
|
||||
normalized = str(env_id or "").strip()
|
||||
if not normalized:
|
||||
@@ -534,4 +538,4 @@ class ConfigManager:
|
||||
return True
|
||||
# #endregion delete_environment
|
||||
# #endregion ConfigManager
|
||||
# #endregion ConfigManager
|
||||
# #endregion ConfigManager
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
# @RELATION IMPLEMENTS -> [CoreContracts]
|
||||
# @RELATION IMPLEMENTS -> [ConnectionContracts]
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
from ..models.storage import StorageConfig
|
||||
from ..services.llm_prompt_templates import (
|
||||
DEFAULT_LLM_ASSISTANT_SETTINGS,
|
||||
@@ -50,7 +51,7 @@ class LoggingConfig(BaseModel):
|
||||
task_log_level: str = (
|
||||
"INFO" # Minimum level for task-specific logs (DEBUG, INFO, WARNING, ERROR)
|
||||
)
|
||||
file_path: Optional[str] = None
|
||||
file_path: str | None = None
|
||||
max_bytes: int = 10 * 1024 * 1024
|
||||
backup_count: int = 5
|
||||
enable_belief_state: bool = True
|
||||
@@ -62,8 +63,8 @@ class LoggingConfig(BaseModel):
|
||||
# #region CleanReleaseConfig [TYPE DataClass]
|
||||
# @BRIEF Configuration for clean release compliance subsystem.
|
||||
class CleanReleaseConfig(BaseModel):
|
||||
active_policy_id: Optional[str] = None
|
||||
active_registry_id: Optional[str] = None
|
||||
active_policy_id: str | None = None
|
||||
active_registry_id: str | None = None
|
||||
|
||||
|
||||
# #endregion CleanReleaseConfig
|
||||
@@ -86,10 +87,10 @@ class FeaturesConfig(BaseModel):
|
||||
class GlobalSettings(BaseModel):
|
||||
storage: StorageConfig = Field(default_factory=StorageConfig)
|
||||
clean_release: CleanReleaseConfig = Field(default_factory=CleanReleaseConfig)
|
||||
default_environment_id: Optional[str] = None
|
||||
default_environment_id: str | None = None
|
||||
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
||||
features: FeaturesConfig = Field(default_factory=FeaturesConfig)
|
||||
connections: List[dict] = []
|
||||
connections: list[dict] = []
|
||||
llm: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"providers": [],
|
||||
@@ -120,7 +121,7 @@ class GlobalSettings(BaseModel):
|
||||
# #region AppConfig [TYPE DataClass]
|
||||
# @BRIEF The root configuration model containing all application settings.
|
||||
class AppConfig(BaseModel):
|
||||
environments: List[Environment] = []
|
||||
environments: list[Environment] = []
|
||||
settings: GlobalSettings
|
||||
|
||||
|
||||
|
||||
@@ -10,12 +10,10 @@
|
||||
# @SIDE_EFFECT: Writes structured JSON to the 'cot' Python logger.
|
||||
# @DATA_CONTRACT: Log call -> Single-line JSON to logging.StreamHandler/file.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from contextvars import ContextVar
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any
|
||||
|
||||
# #region cot_trace_context [C:1] [TYPE Data] [SEMANTICS contextvar, trace_id, span_id, propagation]
|
||||
# @BRIEF ContextVars for trace ID and span ID propagation across async boundaries.
|
||||
@@ -29,13 +27,13 @@ cot_logger = logging.getLogger("cot")
|
||||
# #endregion cot_logger_instance
|
||||
|
||||
__all__ = [
|
||||
"MarkerLogger",
|
||||
"get_trace_id",
|
||||
"log",
|
||||
"pop_span",
|
||||
"push_span",
|
||||
"seed_trace_id",
|
||||
"set_trace_id",
|
||||
"get_trace_id",
|
||||
"push_span",
|
||||
"pop_span",
|
||||
"log",
|
||||
"MarkerLogger",
|
||||
]
|
||||
|
||||
# #region seed_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, uuid, contextvar, set]
|
||||
@@ -119,9 +117,9 @@ def log(
|
||||
src: str,
|
||||
marker: str,
|
||||
intent: str,
|
||||
payload: Optional[Dict[str, Any]] = None,
|
||||
error: Optional[str] = None,
|
||||
level: Optional[str] = None,
|
||||
payload: dict[str, Any] | None = None,
|
||||
error: str | None = None,
|
||||
level: str | None = None,
|
||||
) -> None:
|
||||
"""Emit a single-line structured JSON log record through the 'cot' logger.
|
||||
|
||||
@@ -143,7 +141,7 @@ def log(
|
||||
# Build structured extra data for the LogRecord.
|
||||
# CotJsonFormatter will read these attributes to produce the final JSON output,
|
||||
# consistent with the main superset_tools_app logger.
|
||||
extra: Dict[str, Any] = {
|
||||
extra: dict[str, Any] = {
|
||||
"marker": marker,
|
||||
"intent": intent,
|
||||
"src": src,
|
||||
@@ -196,7 +194,7 @@ class MarkerLogger:
|
||||
# #region MarkerLogger.reason [TYPE Function]
|
||||
# @BRIEF Log a REASON marker — strict deduction, core logic.
|
||||
def reason(
|
||||
self, intent: str, *, payload: Optional[Dict[str, Any]] = None
|
||||
self, intent: str, *, payload: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""Log a REASON marker (INFO level)."""
|
||||
log(self._module_name, "REASON", intent, payload=payload)
|
||||
@@ -206,7 +204,7 @@ class MarkerLogger:
|
||||
# #region MarkerLogger.reflect [TYPE Function]
|
||||
# @BRIEF Log a REFLECT marker — self-check, structural validation.
|
||||
def reflect(
|
||||
self, intent: str, *, payload: Optional[Dict[str, Any]] = None
|
||||
self, intent: str, *, payload: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""Log a REFLECT marker (INFO level)."""
|
||||
log(self._module_name, "REFLECT", intent, payload=payload)
|
||||
@@ -219,8 +217,8 @@ class MarkerLogger:
|
||||
self,
|
||||
intent: str,
|
||||
*,
|
||||
payload: Optional[Dict[str, Any]] = None,
|
||||
error: Optional[str] = None,
|
||||
payload: dict[str, Any] | None = None,
|
||||
error: str | None = None,
|
||||
) -> None:
|
||||
"""Log an EXPLORE marker (WARNING level).
|
||||
|
||||
|
||||
@@ -8,25 +8,27 @@
|
||||
#
|
||||
# @INVARIANT: A single engine instance is used for the entire application.
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import create_engine, inspect, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from ..models.mapping import Base
|
||||
from ..models.connection import ConnectionConfig
|
||||
|
||||
from ..models import assistant as _assistant_models # noqa: F401
|
||||
from ..models import auth as _auth_models # noqa: F401
|
||||
from ..models import clean_release as _clean_release_models # noqa: F401
|
||||
from ..models import config as _config_models # noqa: F401
|
||||
from ..models import connection as _connection_models # noqa: F401
|
||||
from ..models import dataset_review as _dataset_review_models # noqa: F401
|
||||
from ..models import llm as _llm_models # noqa: F401
|
||||
from ..models import profile as _profile_models # noqa: F401
|
||||
|
||||
# Import models to ensure they're registered with Base
|
||||
from ..models import task as _task_models # noqa: F401
|
||||
from ..models import auth as _auth_models # noqa: F401
|
||||
from ..models import config as _config_models # noqa: F401
|
||||
from ..models import llm as _llm_models # noqa: F401
|
||||
from ..models import assistant as _assistant_models # noqa: F401
|
||||
from ..models import profile as _profile_models # noqa: F401
|
||||
from ..models import clean_release as _clean_release_models # noqa: F401
|
||||
from ..models import connection as _connection_models # noqa: F401
|
||||
from ..models import dataset_review as _dataset_review_models # noqa: F401
|
||||
from .logger import belief_scope, logger
|
||||
from ..models.connection import ConnectionConfig
|
||||
from ..models.mapping import Base
|
||||
from .auth.config import auth_config
|
||||
import os
|
||||
from pathlib import Path
|
||||
from .logger import belief_scope, logger
|
||||
|
||||
# #region BASE_DIR [C:1] [TYPE Variable]
|
||||
# @BRIEF Base directory for the backend.
|
||||
|
||||
@@ -15,7 +15,7 @@ from pathlib import Path
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from .logger import logger, belief_scope
|
||||
from .logger import belief_scope, logger
|
||||
|
||||
DEFAULT_ENV_FILE_PATH = Path(__file__).resolve().parents[2] / ".env"
|
||||
|
||||
|
||||
@@ -15,16 +15,16 @@ import json
|
||||
import logging
|
||||
import threading
|
||||
import types
|
||||
from datetime import datetime, timezone
|
||||
from contextlib import contextmanager
|
||||
from datetime import UTC, datetime
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Any, List, Optional
|
||||
from collections import deque
|
||||
|
||||
from .cot_logger import _trace_id, _span_id, log as cot_log
|
||||
from .ws_log_handler import WebSocketLogHandler # noqa: F401
|
||||
from .cot_logger import _span_id, _trace_id
|
||||
from .cot_logger import log as cot_log
|
||||
from .ws_log_handler import WebSocketLogHandler
|
||||
|
||||
# Thread-local storage for belief state
|
||||
_belief_state = threading.local()
|
||||
@@ -79,7 +79,7 @@ class CotJsonFormatter(logging.Formatter):
|
||||
intent = record.getMessage()
|
||||
|
||||
log_obj = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
|
||||
"ts": datetime.now(UTC).isoformat(timespec="milliseconds"),
|
||||
"level": record.levelname,
|
||||
"trace_id": _trace_id.get() or "no-trace",
|
||||
"src": src,
|
||||
@@ -128,7 +128,7 @@ class LogEntry(BaseModel):
|
||||
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
||||
level: str
|
||||
message: str
|
||||
context: Optional[Dict[str, Any]] = None
|
||||
context: dict[str, Any] | None = None
|
||||
# #endregion LogEntry
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ def belief_scope(anchor_id: str, message: str = ""):
|
||||
cot_log(anchor_id, "REFLECT", "Coherence OK", level="DEBUG")
|
||||
except Exception as e:
|
||||
# Log exception as EXPLORE (always logged)
|
||||
cot_log(anchor_id, "EXPLORE", f"Assumption violated", error=str(e), level="WARNING")
|
||||
cot_log(anchor_id, "EXPLORE", "Assumption violated", error=str(e), level="WARNING")
|
||||
raise
|
||||
finally:
|
||||
_belief_state.anchor_id = old_anchor
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user