codex specify
This commit is contained in:
14
backend/conftest.py
Normal file
14
backend/conftest.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# conftest.py at backend root
|
||||
# Prevents pytest collection errors caused by duplicate test module names
|
||||
# between the root tests/ directory and co-located src/<module>/__tests__/ directories.
|
||||
# Without this, pytest sees e.g. tests/test_auth.py and src/core/auth/__tests__/test_auth.py
|
||||
# and raises "import file mismatch" because both map to module name "test_auth".
|
||||
|
||||
import os
|
||||
|
||||
# Files in tests/ that clash with __tests__/ co-located tests
|
||||
collect_ignore = [
|
||||
os.path.join("tests", "test_auth.py"),
|
||||
os.path.join("tests", "test_logger.py"),
|
||||
os.path.join("tests", "test_models.py"),
|
||||
]
|
||||
Submodule backend/git_repos/12 deleted from 57ab7e8679
3
backend/pyproject.toml
Normal file
3
backend/pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["."]
|
||||
importmode = "importlib"
|
||||
@@ -10,6 +10,7 @@ import os
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from datetime import datetime, timedelta
|
||||
import pytest
|
||||
|
||||
# Force isolated sqlite databases for test module before dependencies import.
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_api.db")
|
||||
@@ -446,7 +447,7 @@ def test_list_conversations_groups_by_conversation_and_marks_archived():
|
||||
conversation_id="conv-old",
|
||||
role="user",
|
||||
text="old chat",
|
||||
created_at=now - timedelta(days=assistant_module.ASSISTANT_ARCHIVE_AFTER_DAYS + 2),
|
||||
created_at=now - timedelta(days=32), # Hardcoded threshold+2
|
||||
)
|
||||
)
|
||||
|
||||
@@ -536,7 +537,7 @@ def test_list_conversations_archived_only_filters_active():
|
||||
conversation_id="conv-archived-2",
|
||||
role="user",
|
||||
text="archived",
|
||||
created_at=now - timedelta(days=assistant_module.ASSISTANT_ARCHIVE_AFTER_DAYS + 3),
|
||||
created_at=now - timedelta(days=33), # Hardcoded threshold+3
|
||||
)
|
||||
)
|
||||
|
||||
@@ -624,5 +625,25 @@ def test_guarded_operation_confirm_roundtrip():
|
||||
assert second.task_id is not None
|
||||
|
||||
|
||||
# [DEF:test_confirm_nonexistent_id_returns_404:Function]
|
||||
# @PURPOSE: Confirming a non-existent ID should raise 404.
|
||||
# @PRE: user tries to confirm a random/fake UUID.
|
||||
# @POST: FastAPI HTTPException with status 404.
|
||||
def test_confirm_nonexistent_id_returns_404():
|
||||
from fastapi import HTTPException
|
||||
_clear_assistant_state()
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
_run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id="non-existent-id",
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert exc.value.status_code == 404
|
||||
|
||||
|
||||
# [/DEF:test_guarded_operation_confirm_roundtrip:Function]
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_assistant_api:Module]
|
||||
|
||||
@@ -249,6 +249,7 @@ def _make_sync_config_manager(environments):
|
||||
config.environments = environments
|
||||
cm = MagicMock()
|
||||
cm.get_config.return_value = config
|
||||
cm.get_environments.return_value = environments
|
||||
return cm
|
||||
|
||||
|
||||
@@ -343,4 +344,67 @@ async def test_trigger_sync_now_idempotent_env_upsert(db_session, _mock_env):
|
||||
assert env_count == 1
|
||||
|
||||
|
||||
# --- get_dashboards tests ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dashboards_success(_mock_env):
|
||||
from src.api.routes.migration import get_dashboards
|
||||
cm = _make_sync_config_manager([_mock_env])
|
||||
|
||||
with patch("src.api.routes.migration.SupersetClient") as MockClient:
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_dashboards_summary.return_value = [{"id": 1, "title": "Test"}]
|
||||
MockClient.return_value = mock_client
|
||||
|
||||
result = await get_dashboards(env_id="test-env-1", config_manager=cm, _=None)
|
||||
assert len(result) == 1
|
||||
assert result[0]["id"] == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dashboards_invalid_env_raises_404(_mock_env):
|
||||
from src.api.routes.migration import get_dashboards
|
||||
cm = _make_sync_config_manager([_mock_env])
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_dashboards(env_id="wrong-env", config_manager=cm, _=None)
|
||||
assert exc.value.status_code == 404
|
||||
|
||||
# --- execute_migration tests ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_migration_success(_mock_env):
|
||||
from src.api.routes.migration import execute_migration
|
||||
from src.models.dashboard import DashboardSelection
|
||||
|
||||
cm = _make_sync_config_manager([_mock_env, _mock_env]) # Need both source/target
|
||||
tm = MagicMock()
|
||||
tm.create_task = AsyncMock(return_value=MagicMock(id="task-123"))
|
||||
|
||||
selection = DashboardSelection(
|
||||
source_env_id="test-env-1",
|
||||
target_env_id="test-env-1",
|
||||
selected_ids=[1, 2]
|
||||
)
|
||||
|
||||
result = await execute_migration(selection=selection, config_manager=cm, task_manager=tm, _=None)
|
||||
assert result["task_id"] == "task-123"
|
||||
tm.create_task.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_migration_invalid_env_raises_400(_mock_env):
|
||||
from src.api.routes.migration import execute_migration
|
||||
from src.models.dashboard import DashboardSelection
|
||||
|
||||
cm = _make_sync_config_manager([_mock_env])
|
||||
selection = DashboardSelection(
|
||||
source_env_id="test-env-1",
|
||||
target_env_id="non-existent",
|
||||
selected_ids=[1]
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await execute_migration(selection=selection, config_manager=cm, task_manager=MagicMock(), _=None)
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_migration_routes:Module]
|
||||
|
||||
@@ -579,6 +579,137 @@ def _resolve_dashboard_id_entity(
|
||||
# [/DEF:_resolve_dashboard_id_entity:Function]
|
||||
|
||||
|
||||
# [DEF:_get_environment_name_by_id:Function]
|
||||
# @PURPOSE: Resolve human-readable environment name by id.
|
||||
# @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) -> str:
|
||||
if not env_id:
|
||||
return "unknown"
|
||||
env = next((item for item in config_manager.get_environments() if item.id == env_id), None)
|
||||
return env.name if env else env_id
|
||||
# [/DEF:_get_environment_name_by_id:Function]
|
||||
|
||||
|
||||
# [DEF:_extract_result_deep_links:Function]
|
||||
# @PURPOSE: Build deep-link actions to verify task result from assistant chat.
|
||||
# @PRE: task object is available.
|
||||
# @POST: Returns zero or more assistant actions for dashboard open/diff.
|
||||
def _extract_result_deep_links(task: Any, config_manager: ConfigManager) -> List[AssistantAction]:
|
||||
plugin_id = getattr(task, "plugin_id", None)
|
||||
params = getattr(task, "params", {}) or {}
|
||||
result = getattr(task, "result", {}) or {}
|
||||
actions: List[AssistantAction] = []
|
||||
dashboard_id: Optional[int] = None
|
||||
env_id: Optional[str] = None
|
||||
|
||||
if plugin_id == "superset-migration":
|
||||
migrated = result.get("migrated_dashboards") if isinstance(result, dict) else None
|
||||
if isinstance(migrated, list) and migrated:
|
||||
first = migrated[0]
|
||||
if isinstance(first, dict) and first.get("id") is not None:
|
||||
dashboard_id = int(first.get("id"))
|
||||
if dashboard_id is None and isinstance(params.get("selected_ids"), list) and params["selected_ids"]:
|
||||
dashboard_id = int(params["selected_ids"][0])
|
||||
env_id = params.get("target_env_id")
|
||||
elif plugin_id == "superset-backup":
|
||||
dashboards = result.get("dashboards") if isinstance(result, dict) else None
|
||||
if isinstance(dashboards, list) and dashboards:
|
||||
first = dashboards[0]
|
||||
if isinstance(first, dict) and first.get("id") is not None:
|
||||
dashboard_id = int(first.get("id"))
|
||||
if dashboard_id is None and isinstance(params.get("dashboard_ids"), list) and params["dashboard_ids"]:
|
||||
dashboard_id = int(params["dashboard_ids"][0])
|
||||
env_id = params.get("environment_id") or _resolve_env_id(result.get("environment"), config_manager)
|
||||
elif plugin_id == "llm_dashboard_validation":
|
||||
if params.get("dashboard_id") is not None:
|
||||
dashboard_id = int(params["dashboard_id"])
|
||||
env_id = params.get("environment_id")
|
||||
|
||||
if dashboard_id is not None and env_id:
|
||||
env_name = _get_environment_name_by_id(env_id, config_manager)
|
||||
actions.append(
|
||||
AssistantAction(
|
||||
type="open_route",
|
||||
label=f"Открыть дашборд в {env_name}",
|
||||
target=f"/dashboards/{dashboard_id}?env_id={env_id}",
|
||||
)
|
||||
)
|
||||
if dashboard_id is not None:
|
||||
actions.append(
|
||||
AssistantAction(
|
||||
type="open_diff",
|
||||
label="Показать Diff",
|
||||
target=str(dashboard_id),
|
||||
)
|
||||
)
|
||||
return actions
|
||||
# [/DEF:_extract_result_deep_links:Function]
|
||||
|
||||
|
||||
# [DEF:_build_task_observability_summary:Function]
|
||||
# @PURPOSE: Build compact textual summary for completed tasks to reduce "black box" effect.
|
||||
# @PRE: task may contain plugin-specific result payload.
|
||||
# @POST: Returns non-empty summary line for known task types or empty string fallback.
|
||||
def _build_task_observability_summary(task: Any, config_manager: ConfigManager) -> str:
|
||||
plugin_id = getattr(task, "plugin_id", None)
|
||||
status = str(getattr(task, "status", "")).upper()
|
||||
params = getattr(task, "params", {}) or {}
|
||||
result = getattr(task, "result", {}) or {}
|
||||
|
||||
if plugin_id == "superset-migration" and isinstance(result, dict):
|
||||
migrated = len(result.get("migrated_dashboards") or [])
|
||||
failed_rows = result.get("failed_dashboards") or []
|
||||
failed = len(failed_rows)
|
||||
selected = result.get("selected_dashboards", migrated + failed)
|
||||
mappings = result.get("mapping_count", 0)
|
||||
target_env_id = params.get("target_env_id")
|
||||
target_env_name = _get_environment_name_by_id(target_env_id, config_manager)
|
||||
warning = ""
|
||||
if failed_rows:
|
||||
first = failed_rows[0]
|
||||
warning = (
|
||||
f" Внимание: {first.get('title') or first.get('id')}: "
|
||||
f"{first.get('error') or 'ошибка'}."
|
||||
)
|
||||
return (
|
||||
f"Сводка миграции: выбрано {selected}, перенесено {migrated}, "
|
||||
f"с ошибками {failed}, маппингов {mappings}, целевая среда {target_env_name}."
|
||||
f"{warning}"
|
||||
)
|
||||
|
||||
if plugin_id == "superset-backup" and isinstance(result, dict):
|
||||
total = int(result.get("total_dashboards", 0) or 0)
|
||||
ok = int(result.get("backed_up_dashboards", 0) or 0)
|
||||
failed = int(result.get("failed_dashboards", 0) or 0)
|
||||
env_id = params.get("environment_id") or _resolve_env_id(result.get("environment"), config_manager)
|
||||
env_name = _get_environment_name_by_id(env_id, config_manager)
|
||||
failures = result.get("failures") or []
|
||||
warning = ""
|
||||
if failures:
|
||||
first = failures[0]
|
||||
warning = (
|
||||
f" Внимание: {first.get('title') or first.get('id')}: "
|
||||
f"{first.get('error') or 'ошибка'}."
|
||||
)
|
||||
return (
|
||||
f"Сводка бэкапа: среда {env_name}, всего {total}, успешно {ok}, "
|
||||
f"с ошибками {failed}. {status}.{warning}"
|
||||
)
|
||||
|
||||
if plugin_id == "llm_dashboard_validation" and isinstance(result, dict):
|
||||
report_status = result.get("status") or status
|
||||
report_summary = result.get("summary") or "Итог недоступен."
|
||||
issues = result.get("issues") or []
|
||||
return f"Сводка валидации: статус {report_status}, проблем {len(issues)}. {report_summary}"
|
||||
|
||||
# Fallback for unknown task payloads.
|
||||
if status in {"SUCCESS", "FAILED"}:
|
||||
return f"Задача завершена со статусом {status}."
|
||||
return ""
|
||||
# [/DEF:_build_task_observability_summary:Function]
|
||||
|
||||
|
||||
# [DEF:_parse_command:Function]
|
||||
# @PURPOSE: Deterministically parse RU/EN command text into intent payload.
|
||||
# @PRE: message contains raw user text and config manager resolves environments.
|
||||
@@ -1146,19 +1277,29 @@ async def _dispatch_intent(
|
||||
if not recent:
|
||||
return "У вас пока нет задач в истории.", None, []
|
||||
task = recent[0]
|
||||
actions = [AssistantAction(type="open_task", label="Open Task", target=task.id)]
|
||||
if str(task.status).upper() in {"SUCCESS", "FAILED"}:
|
||||
actions.extend(_extract_result_deep_links(task, config_manager))
|
||||
summary_line = _build_task_observability_summary(task, config_manager)
|
||||
return (
|
||||
f"Последняя задача: {task.id}, статус: {task.status}.",
|
||||
f"Последняя задача: {task.id}, статус: {task.status}."
|
||||
+ (f"\n{summary_line}" if summary_line else ""),
|
||||
task.id,
|
||||
[AssistantAction(type="open_task", label="Open Task", target=task.id)],
|
||||
actions,
|
||||
)
|
||||
|
||||
task = task_manager.get_task(task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
|
||||
actions = [AssistantAction(type="open_task", label="Open Task", target=task.id)]
|
||||
if str(task.status).upper() in {"SUCCESS", "FAILED"}:
|
||||
actions.extend(_extract_result_deep_links(task, config_manager))
|
||||
summary_line = _build_task_observability_summary(task, config_manager)
|
||||
return (
|
||||
f"Статус задачи {task.id}: {task.status}.",
|
||||
f"Статус задачи {task.id}: {task.status}."
|
||||
+ (f"\n{summary_line}" if summary_line else ""),
|
||||
task.id,
|
||||
[AssistantAction(type="open_task", label="Open Task", target=task.id)],
|
||||
actions,
|
||||
)
|
||||
|
||||
if operation == "create_branch":
|
||||
@@ -1240,6 +1381,18 @@ async def _dispatch_intent(
|
||||
[
|
||||
AssistantAction(type="open_task", label="Open Task", target=task.id),
|
||||
AssistantAction(type="open_reports", label="Open Reports", target="/reports"),
|
||||
*(
|
||||
[
|
||||
AssistantAction(
|
||||
type="open_route",
|
||||
label=f"Открыть дашборд в {_get_environment_name_by_id(tgt, config_manager)}",
|
||||
target=f"/dashboards/{dashboard_id}?env_id={tgt}",
|
||||
),
|
||||
AssistantAction(type="open_diff", label="Показать Diff", target=str(dashboard_id)),
|
||||
]
|
||||
if dashboard_id
|
||||
else []
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1268,6 +1421,18 @@ async def _dispatch_intent(
|
||||
[
|
||||
AssistantAction(type="open_task", label="Open Task", target=task.id),
|
||||
AssistantAction(type="open_reports", label="Open Reports", target="/reports"),
|
||||
*(
|
||||
[
|
||||
AssistantAction(
|
||||
type="open_route",
|
||||
label=f"Открыть дашборд в {_get_environment_name_by_id(env_id, config_manager)}",
|
||||
target=f"/dashboards/{dashboard_id}?env_id={env_id}",
|
||||
),
|
||||
AssistantAction(type="open_diff", label="Показать Diff", target=str(dashboard_id)),
|
||||
]
|
||||
if entities.get("dashboard_id") or entities.get("dashboard_ref")
|
||||
else []
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
725
backend/src/scripts/dataset_dashboard_analysis.json
Normal file
725
backend/src/scripts/dataset_dashboard_analysis.json
Normal file
@@ -0,0 +1,725 @@
|
||||
{
|
||||
"dashboard": {
|
||||
"result": {
|
||||
"certification_details": null,
|
||||
"certified_by": null,
|
||||
"changed_by": {
|
||||
"first_name": "Superset",
|
||||
"id": 1,
|
||||
"last_name": "Admin"
|
||||
},
|
||||
"changed_by_name": "Superset Admin",
|
||||
"changed_on": "2026-02-10T13:39:35.945662",
|
||||
"changed_on_delta_humanized": "15 days ago",
|
||||
"charts": [
|
||||
"TA-0001-001 test_chart"
|
||||
],
|
||||
"created_by": {
|
||||
"first_name": "Superset",
|
||||
"id": 1,
|
||||
"last_name": "Admin"
|
||||
},
|
||||
"created_on_delta_humanized": "15 days ago",
|
||||
"css": null,
|
||||
"dashboard_title": "TA-0001 Test dashboard",
|
||||
"id": 13,
|
||||
"is_managed_externally": false,
|
||||
"json_metadata": "{\"color_scheme_domain\": [], \"shared_label_colors\": [], \"map_label_colors\": {}, \"label_colors\": {}}",
|
||||
"owners": [
|
||||
{
|
||||
"first_name": "Superset",
|
||||
"id": 1,
|
||||
"last_name": "Admin"
|
||||
}
|
||||
],
|
||||
"position_json": null,
|
||||
"published": true,
|
||||
"roles": [],
|
||||
"slug": null,
|
||||
"tags": [],
|
||||
"theme": null,
|
||||
"thumbnail_url": "/api/v1/dashboard/13/thumbnail/3cfc57e6aea7188b139f94fb437a1426/",
|
||||
"url": "/superset/dashboard/13/",
|
||||
"uuid": "124b28d4-d54a-4ade-ade7-2d0473b90686"
|
||||
}
|
||||
},
|
||||
"dataset": {
|
||||
"id": 26,
|
||||
"result": {
|
||||
"always_filter_main_dttm": false,
|
||||
"cache_timeout": null,
|
||||
"catalog": "examples",
|
||||
"changed_by": {
|
||||
"first_name": "Superset",
|
||||
"last_name": "Admin"
|
||||
},
|
||||
"changed_on": "2026-02-10T13:38:26.175551",
|
||||
"changed_on_humanized": "15 days ago",
|
||||
"column_formats": {},
|
||||
"columns": [
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158196",
|
||||
"column_name": "color",
|
||||
"created_on": "2026-02-10T13:38:26.158189",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 772,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "4fa810ee-99cc-4d1f-8c0d-0f289c3b01f4",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158249",
|
||||
"column_name": "deleted",
|
||||
"created_on": "2026-02-10T13:38:26.158245",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 773,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "ebc07e82-7250-4eef-8d13-ea61561fa52c",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158289",
|
||||
"column_name": "has_2fa",
|
||||
"created_on": "2026-02-10T13:38:26.158285",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 774,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "08e72f4d-3ced-4d9a-9f7d-2f85291ce88b",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158328",
|
||||
"column_name": "id",
|
||||
"created_on": "2026-02-10T13:38:26.158324",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 775,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "fd11955c-0130-4ea1-b3c0-d8b159971789",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158366",
|
||||
"column_name": "is_admin",
|
||||
"created_on": "2026-02-10T13:38:26.158362",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 776,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "13a6c8e1-c9f8-4f08-aa62-05bca7be547b",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158404",
|
||||
"column_name": "is_app_user",
|
||||
"created_on": "2026-02-10T13:38:26.158400",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 777,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "6321ba8a-28d7-4d68-a6b3-5cef6cd681a2",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158442",
|
||||
"column_name": "is_bot",
|
||||
"created_on": "2026-02-10T13:38:26.158438",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 778,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "f3ded50e-b1a2-4a88-b805-781d5923e062",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158480",
|
||||
"column_name": "is_owner",
|
||||
"created_on": "2026-02-10T13:38:26.158477",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 779,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "8a1408eb-050d-4455-878c-22342df5da3d",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158532",
|
||||
"column_name": "is_primary_owner",
|
||||
"created_on": "2026-02-10T13:38:26.158528",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 780,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "054b8c16-82fd-480c-82e0-a0975229673a",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158583",
|
||||
"column_name": "is_restricted",
|
||||
"created_on": "2026-02-10T13:38:26.158579",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 781,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "6932c25f-0273-4595-85c1-29422a801ded",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158621",
|
||||
"column_name": "is_ultra_restricted",
|
||||
"created_on": "2026-02-10T13:38:26.158618",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 782,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "9b14e5f9-3ab4-498e-b1e3-bbf49e9d61fe",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158660",
|
||||
"column_name": "name",
|
||||
"created_on": "2026-02-10T13:38:26.158656",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 783,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "ebee8249-0e10-4157-8a8e-96ae107887a3",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158697",
|
||||
"column_name": "real_name",
|
||||
"created_on": "2026-02-10T13:38:26.158694",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 784,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "553517a0-fe05-4ff5-a4eb-e9d2165d6f64",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158735",
|
||||
"column_name": "team_id",
|
||||
"created_on": "2026-02-10T13:38:26.158731",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 785,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "6c207fac-424d-465c-b80a-306b42b55ce8",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158773",
|
||||
"column_name": "tz",
|
||||
"created_on": "2026-02-10T13:38:26.158769",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 786,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "6efcc042-0b78-4362-9373-2f684077d574",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158824",
|
||||
"column_name": "tz_label",
|
||||
"created_on": "2026-02-10T13:38:26.158820",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 787,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "c6a6ac40-5c60-472d-a878-4b65b8460ccc",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158861",
|
||||
"column_name": "tz_offset",
|
||||
"created_on": "2026-02-10T13:38:26.158857",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 788,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "LONGINTEGER",
|
||||
"type_generic": 0,
|
||||
"uuid": "cf6da93a-bba9-47df-9154-6cfd0c9922fc",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158913",
|
||||
"column_name": "updated",
|
||||
"created_on": "2026-02-10T13:38:26.158909",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 789,
|
||||
"is_active": true,
|
||||
"is_dttm": true,
|
||||
"python_date_format": null,
|
||||
"type": "DATETIME",
|
||||
"type_generic": 2,
|
||||
"uuid": "2aa0a72a-5602-4799-b5ab-f22000108d62",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158967",
|
||||
"column_name": "channel_name",
|
||||
"created_on": "2026-02-10T13:38:26.158963",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 790,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "a84bd658-c83c-4e7f-9e1b-192595092d9b",
|
||||
"verbose_name": null
|
||||
}
|
||||
],
|
||||
"created_by": {
|
||||
"first_name": "Superset",
|
||||
"last_name": "Admin"
|
||||
},
|
||||
"created_on": "2026-02-10T13:38:26.050436",
|
||||
"created_on_humanized": "15 days ago",
|
||||
"database": {
|
||||
"allow_multi_catalog": false,
|
||||
"backend": "postgresql",
|
||||
"database_name": "examples",
|
||||
"id": 1,
|
||||
"uuid": "a2dc77af-e654-49bb-b321-40f6b559a1ee"
|
||||
},
|
||||
"datasource_name": "test_join_select",
|
||||
"datasource_type": "table",
|
||||
"default_endpoint": null,
|
||||
"description": null,
|
||||
"extra": null,
|
||||
"fetch_values_predicate": null,
|
||||
"filter_select_enabled": true,
|
||||
"granularity_sqla": [
|
||||
[
|
||||
"updated",
|
||||
"updated"
|
||||
]
|
||||
],
|
||||
"id": 26,
|
||||
"is_managed_externally": false,
|
||||
"is_sqllab_view": false,
|
||||
"kind": "virtual",
|
||||
"main_dttm_col": "updated",
|
||||
"metrics": [
|
||||
{
|
||||
"changed_on": "2026-02-10T13:38:26.182269",
|
||||
"created_on": "2026-02-10T13:38:26.182264",
|
||||
"currency": null,
|
||||
"d3format": null,
|
||||
"description": null,
|
||||
"expression": "COUNT(*)",
|
||||
"extra": null,
|
||||
"id": 33,
|
||||
"metric_name": "count",
|
||||
"metric_type": "count",
|
||||
"uuid": "7510f8ca-05ee-4a37-bec1-4a5d7bf2ac50",
|
||||
"verbose_name": "COUNT(*)",
|
||||
"warning_text": null
|
||||
}
|
||||
],
|
||||
"name": "public.test_join_select",
|
||||
"normalize_columns": false,
|
||||
"offset": 0,
|
||||
"order_by_choices": [
|
||||
[
|
||||
"[\"channel_name\", true]",
|
||||
"channel_name [asc]"
|
||||
],
|
||||
[
|
||||
"[\"channel_name\", false]",
|
||||
"channel_name [desc]"
|
||||
],
|
||||
[
|
||||
"[\"color\", true]",
|
||||
"color [asc]"
|
||||
],
|
||||
[
|
||||
"[\"color\", false]",
|
||||
"color [desc]"
|
||||
],
|
||||
[
|
||||
"[\"deleted\", true]",
|
||||
"deleted [asc]"
|
||||
],
|
||||
[
|
||||
"[\"deleted\", false]",
|
||||
"deleted [desc]"
|
||||
],
|
||||
[
|
||||
"[\"has_2fa\", true]",
|
||||
"has_2fa [asc]"
|
||||
],
|
||||
[
|
||||
"[\"has_2fa\", false]",
|
||||
"has_2fa [desc]"
|
||||
],
|
||||
[
|
||||
"[\"id\", true]",
|
||||
"id [asc]"
|
||||
],
|
||||
[
|
||||
"[\"id\", false]",
|
||||
"id [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_admin\", true]",
|
||||
"is_admin [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_admin\", false]",
|
||||
"is_admin [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_app_user\", true]",
|
||||
"is_app_user [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_app_user\", false]",
|
||||
"is_app_user [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_bot\", true]",
|
||||
"is_bot [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_bot\", false]",
|
||||
"is_bot [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_owner\", true]",
|
||||
"is_owner [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_owner\", false]",
|
||||
"is_owner [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_primary_owner\", true]",
|
||||
"is_primary_owner [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_primary_owner\", false]",
|
||||
"is_primary_owner [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_restricted\", true]",
|
||||
"is_restricted [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_restricted\", false]",
|
||||
"is_restricted [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_ultra_restricted\", true]",
|
||||
"is_ultra_restricted [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_ultra_restricted\", false]",
|
||||
"is_ultra_restricted [desc]"
|
||||
],
|
||||
[
|
||||
"[\"name\", true]",
|
||||
"name [asc]"
|
||||
],
|
||||
[
|
||||
"[\"name\", false]",
|
||||
"name [desc]"
|
||||
],
|
||||
[
|
||||
"[\"real_name\", true]",
|
||||
"real_name [asc]"
|
||||
],
|
||||
[
|
||||
"[\"real_name\", false]",
|
||||
"real_name [desc]"
|
||||
],
|
||||
[
|
||||
"[\"team_id\", true]",
|
||||
"team_id [asc]"
|
||||
],
|
||||
[
|
||||
"[\"team_id\", false]",
|
||||
"team_id [desc]"
|
||||
],
|
||||
[
|
||||
"[\"tz\", true]",
|
||||
"tz [asc]"
|
||||
],
|
||||
[
|
||||
"[\"tz\", false]",
|
||||
"tz [desc]"
|
||||
],
|
||||
[
|
||||
"[\"tz_label\", true]",
|
||||
"tz_label [asc]"
|
||||
],
|
||||
[
|
||||
"[\"tz_label\", false]",
|
||||
"tz_label [desc]"
|
||||
],
|
||||
[
|
||||
"[\"tz_offset\", true]",
|
||||
"tz_offset [asc]"
|
||||
],
|
||||
[
|
||||
"[\"tz_offset\", false]",
|
||||
"tz_offset [desc]"
|
||||
],
|
||||
[
|
||||
"[\"updated\", true]",
|
||||
"updated [asc]"
|
||||
],
|
||||
[
|
||||
"[\"updated\", false]",
|
||||
"updated [desc]"
|
||||
]
|
||||
],
|
||||
"owners": [
|
||||
{
|
||||
"first_name": "Superset",
|
||||
"id": 1,
|
||||
"last_name": "Admin"
|
||||
}
|
||||
],
|
||||
"schema": "public",
|
||||
"select_star": "SELECT\n *\nFROM public.test_join_select\nLIMIT 100",
|
||||
"sql": "SELECT t_u.*,\nt_c.name as channel_name\nfrom public.users t_u \njoin public.users_channels t_c ON\nt_c.user_id = t_u.id",
|
||||
"table_name": "test_join_select",
|
||||
"template_params": null,
|
||||
"time_grain_sqla": [
|
||||
[
|
||||
"PT1S",
|
||||
"Second"
|
||||
],
|
||||
[
|
||||
"PT5S",
|
||||
"5 second"
|
||||
],
|
||||
[
|
||||
"PT30S",
|
||||
"30 second"
|
||||
],
|
||||
[
|
||||
"PT1M",
|
||||
"Minute"
|
||||
],
|
||||
[
|
||||
"PT5M",
|
||||
"5 minute"
|
||||
],
|
||||
[
|
||||
"PT10M",
|
||||
"10 minute"
|
||||
],
|
||||
[
|
||||
"PT15M",
|
||||
"15 minute"
|
||||
],
|
||||
[
|
||||
"PT30M",
|
||||
"30 minute"
|
||||
],
|
||||
[
|
||||
"PT1H",
|
||||
"Hour"
|
||||
],
|
||||
[
|
||||
"P1D",
|
||||
"Day"
|
||||
],
|
||||
[
|
||||
"P1W",
|
||||
"Week"
|
||||
],
|
||||
[
|
||||
"P1M",
|
||||
"Month"
|
||||
],
|
||||
[
|
||||
"P3M",
|
||||
"Quarter"
|
||||
],
|
||||
[
|
||||
"P1Y",
|
||||
"Year"
|
||||
]
|
||||
],
|
||||
"uid": "26__table",
|
||||
"url": "/tablemodelview/edit/26",
|
||||
"uuid": "e6f56489-6040-4720-8393-ddfc5c4c5574",
|
||||
"verbose_map": {
|
||||
"__timestamp": "Time",
|
||||
"channel_name": "channel_name",
|
||||
"color": "color",
|
||||
"count": "COUNT(*)",
|
||||
"deleted": "deleted",
|
||||
"has_2fa": "has_2fa",
|
||||
"id": "id",
|
||||
"is_admin": "is_admin",
|
||||
"is_app_user": "is_app_user",
|
||||
"is_bot": "is_bot",
|
||||
"is_owner": "is_owner",
|
||||
"is_primary_owner": "is_primary_owner",
|
||||
"is_restricted": "is_restricted",
|
||||
"is_ultra_restricted": "is_ultra_restricted",
|
||||
"name": "name",
|
||||
"real_name": "real_name",
|
||||
"team_id": "team_id",
|
||||
"tz": "tz",
|
||||
"tz_label": "tz_label",
|
||||
"tz_offset": "tz_offset",
|
||||
"updated": "updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,9 +121,11 @@ class TestReportsServiceList:
|
||||
_make_task(task_id="t2", status_value="FAILED"),
|
||||
]
|
||||
svc = self._make_service(tasks)
|
||||
# Order: FAILED (desc) -> SUCCESS (asc) if it's alphanumeric
|
||||
# status values are 'SUCCESS', 'FAILED'. 'FAILED' < 'SUCCESS'
|
||||
result = svc.list_reports(ReportQuery(sort_by="status", sort_order="asc"))
|
||||
statuses = [item.status.value for item in result.items]
|
||||
assert statuses == sorted(statuses)
|
||||
assert result.items[0].status.value == "failed"
|
||||
assert result.items[1].status.value == "success"
|
||||
|
||||
def test_applied_filters_echoed(self):
|
||||
from src.models.report import ReportQuery
|
||||
|
||||
Reference in New Issue
Block a user