Systematic rename of all semantic anchors (#region, [DEF], @RELATION) across 1400+ files — backend Python, frontend Svelte/TS, specs, docs: - Flat anchors become Namespace.Module.Entity - @RELATION references updated to match new anchor paths - Zero business logic changes
1038 lines
49 KiB
Python
1038 lines
49 KiB
Python
# #region Test.Integration.TranslatePgE2E [C:5] [TYPE Module] [SEMANTICS test,integration,translate,e2e,postgres,llm-stub,testcontainers,resource-projection]
|
|
# @BRIEF PostgreSQL translation pipeline E2E: seed source table, register in Superset,
|
|
# execute translation with fake LLM stub, verify TranslationRecord content (target_sql
|
|
# with translated text), physical rows written via Superset SQL Lab, incremental rerun,
|
|
# and Superset resource projection (dashboard/dataset list/detail).
|
|
# @RELATION BINDS_TO -> [Plugin.Orchestrator.TranslationOrchestrator]
|
|
# @RELATION BINDS_TO -> [Plugin.OrchestratorExec.TranslationExecutionEngine]
|
|
# @RELATION BINDS_TO -> [Plugin.Executor.TranslationExecutor]
|
|
# @RELATION BINDS_TO -> [Plugin.LlmCall.LLMTranslationService]
|
|
# @RELATION BINDS_TO -> [TranslationModels]
|
|
# @RELATION BINDS_TO -> [Services.ResourceService]
|
|
# @RELATION DEPENDS_ON -> [Test.Conftest.IntegrationTestConftest]
|
|
#
|
|
# @TEST_CONTRACT TranslatePgE2E ->
|
|
# {
|
|
# scenarios: [
|
|
# "happy path: source PG rows produce TranslationRecord with target_sql containing translated text, "
|
|
# "physical rows written to target PG table via Superset SQL Lab",
|
|
# "429 retry: LLM returns 429 then 200; retry succeeds without duplicate rows",
|
|
# "incremental rerun: second run only processes new/changed rows, physical table has no duplicates",
|
|
# "resource projection: Superset dashboard/dataset list/detail via ResourceService"
|
|
# ]
|
|
# }
|
|
# @TEST_EDGE: llm_429_retry -> call_openai_compatible returns 429 then 200; retry succeeds
|
|
# @TEST_EDGE: malformed_response -> LLM returns malformed JSON; gracefully handled
|
|
# @TEST_EDGE: incremental_rerun -> second run only processes rows not in first run's output
|
|
# @TEST_EDGE: target_table_columns_match_sql -> target table has columns matching build_columns output
|
|
# @TEST_INVARIANT target_no_duplicates -> VERIFIED_BY: [Test.Integration.TestHappyPathTranslatesSourceToTarget]
|
|
# @TEST_INVARIANT incremental_only_new -> VERIFIED_BY: [Test.Integration.TestIncrementalRerunOnlyNewRows]
|
|
# @TEST_INVARIANT physical_rows_written -> VERIFIED_BY: [test_happy_path_translates_source_to_target, test_incremental_rerun_only_new_rows]
|
|
#
|
|
# @RATIONALE
|
|
# Previous integration tests verify TranslationOrchestrator start_run/cancel_run (state machine)
|
|
# and LLMTranslationService in isolation but do NOT drive the full pipeline:
|
|
# source PG table -> Superset datasource -> fetch via chart data API -> LLM call -> record creation -> SQL insert -> physical rows
|
|
#
|
|
# This test uses a real PostgreSQL source table, registers it in Superset, creates a translation job
|
|
# with a fake LLM stub (via unittest.mock.patch on call_openai_compatible), and verifies that:
|
|
# - TranslationRecord rows are created with correct target_sql containing the translated values
|
|
# - The generated SQL is executed via Superset SQL Lab against a dedicated target database
|
|
# - Physical rows exist in the target PG table with the expected translated content
|
|
#
|
|
# The mock is on call_openai_compatible, which is the external LLM HTTP boundary. The local
|
|
# orchestration (TranslationExecutor, RunExecutionService, BatchProcessingService, SQLInsertService,
|
|
# SupersetSqlLabExecutor) is NOT mocked.
|
|
#
|
|
# Target table columns match what build_columns() produces:
|
|
# key_cols + effective_target + "context" + "is_original"
|
|
# This is required because SQLInsertService.generate_and_insert_sql uses build_columns
|
|
# to determine the INSERT column list.
|
|
#
|
|
# @REJECTED
|
|
# pytest-httpx rejected — it intercepts ALL httpx calls, breaking SupersetClient which uses
|
|
# httpx internally for API calls.
|
|
# Direct SQLAlchemy write to target table rejected — not how production works; production
|
|
# writes through Superset SQL Lab. Testing the full write path required registering both
|
|
# source and target databases in Superset (same PG container, different Superset DB names).
|
|
from contextlib import suppress
|
|
import json
|
|
import pytest
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, patch
|
|
from uuid import uuid4
|
|
|
|
from sqlalchemy import text
|
|
|
|
from src.models.llm import LLMProvider
|
|
from src.models.translate import (
|
|
TranslationBatch,
|
|
TranslationEvent,
|
|
TranslationRecord,
|
|
)
|
|
|
|
|
|
# #region Test.Integration.TranslatePgE2E.Test [C:4] [TYPE Class]
|
|
# @BRIEF E2E tests for PostgreSQL-to-PostgreSQL translation pipeline.
|
|
class TestTranslatePgE2E:
|
|
"""Full translation pipeline E2E with fake LLM stub."""
|
|
|
|
# #region Test.Integration.MakeDeterministicLlmResponse [C:2] [TYPE Function]
|
|
# @BRIEF Build deterministic LLM JSON response from source rows.
|
|
# Uses "rows": [{"row_id": ..., "ru": "...", "detected_source_language": ...}]
|
|
# format expected by parse_llm_response in _llm_parse.py.
|
|
@staticmethod
|
|
def _make_llm_response(source_rows: list[dict[str, Any]]) -> str:
|
|
"""Build a deterministic LLM JSON response for the given source rows.
|
|
|
|
Each row gets a fake "translation" by prepending "[RU] " to the source text.
|
|
This is deterministic and testable. Format matches parse_llm_response expectations.
|
|
"""
|
|
rows = []
|
|
for i, row in enumerate(source_rows):
|
|
source_text = row.get("source_text", "")
|
|
rows.append({
|
|
"row_id": row.get("row_index", str(i)),
|
|
"ru": f"[RU] {source_text}" if source_text else "",
|
|
"detected_source_language": "en",
|
|
})
|
|
return json.dumps({"rows": rows})
|
|
# #endregion Test.Integration.MakeDeterministicLlmResponse
|
|
|
|
# #region Test.Integration.TestHappyPathTranslatesSourceToTarget [C:3] [TYPE Function]
|
|
# @BRIEF Full happy path: seed source PG, create Superset dataset, execute translation with
|
|
# fake LLM, verify physical target table rows via Superset SQL Lab write path.
|
|
@pytest.mark.integration
|
|
@pytest.mark.asyncio
|
|
async def test_happy_path_translates_source_to_target( # noqa: C901 — integration E2E naturally complex
|
|
self,
|
|
superset_client,
|
|
superset_db_url,
|
|
pg_engine,
|
|
db_session,
|
|
):
|
|
suffix = uuid4().hex[:10]
|
|
source_table = f"translate_source_{suffix}"
|
|
target_table = f"translate_target_{suffix}"
|
|
|
|
database_id = target_db_id = dataset_id = dashboard_id = chart_id = None
|
|
job_id = run_id = None
|
|
|
|
# ── Step 1: Seed source table with test data ──
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f"""
|
|
CREATE TABLE public."{source_table}" (
|
|
id INTEGER PRIMARY KEY,
|
|
product_name VARCHAR(128),
|
|
description TEXT
|
|
)
|
|
"""))
|
|
conn.execute(text(f"""
|
|
INSERT INTO public."{source_table}" VALUES
|
|
(1, 'Wireless Mouse', 'Ergonomic wireless mouse with USB receiver'),
|
|
(2, 'USB-C Hub', '7-port USB-C hub with HDMI and PD charging'),
|
|
(3, 'Mechanical Keyboard', 'RGB mechanical keyboard with Cherry MX switches')
|
|
"""))
|
|
|
|
# ── Step 2: Create target table with columns matching build_columns() output:
|
|
# key_cols + effective_target + "context" + "is_original"
|
|
# This is required because SQLInsertService.generate_and_insert_sql uses
|
|
# build_columns() to determine the INSERT column list.
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f"""
|
|
CREATE TABLE public."{target_table}" (
|
|
id INTEGER PRIMARY KEY,
|
|
product_name_ru TEXT,
|
|
context TEXT,
|
|
is_original INTEGER DEFAULT 0
|
|
)
|
|
"""))
|
|
|
|
try:
|
|
# ── Step 3: Register source database & dataset in Superset ──
|
|
from urllib.parse import urlparse
|
|
parsed = urlparse(superset_db_url)
|
|
database_uri = (
|
|
f"postgresql://test:test@{parsed.hostname}:{parsed.port}/test_translate"
|
|
)
|
|
db_resp = await superset_client.create_database(
|
|
database_name=f"Translate Source {suffix}",
|
|
sqlalchemy_uri=database_uri,
|
|
)
|
|
database_id = int(
|
|
db_resp.get("id") or db_resp.get("result", {}).get("id", 0)
|
|
)
|
|
|
|
ds_resp = await superset_client.create_dataset(
|
|
table_name=source_table,
|
|
database=database_id,
|
|
schema_name="public",
|
|
)
|
|
dataset_id = int(
|
|
ds_resp.get("id") or ds_resp.get("result", {}).get("id", 0)
|
|
)
|
|
|
|
# Create a simple dashboard with a chart for this dataset
|
|
dash_resp = await superset_client.create_dashboard(
|
|
dashboard_title=f"Translate E2E {suffix}",
|
|
slug=f"translate-e2e-{suffix}",
|
|
published=True,
|
|
)
|
|
dashboard_id = int(
|
|
dash_resp.get("id") or dash_resp.get("result", {}).get("id", 0)
|
|
)
|
|
|
|
chart_resp = await superset_client.client.request(
|
|
method="POST", endpoint="/chart/",
|
|
data={
|
|
"dashboards": [dashboard_id],
|
|
"datasource_id": dataset_id,
|
|
"datasource_type": "table",
|
|
"slice_name": f"Translate Chart {suffix}",
|
|
"viz_type": "table",
|
|
"params": json.dumps({
|
|
"datasource": f"{dataset_id}__table",
|
|
"viz_type": "table",
|
|
"all_columns": ["id", "product_name", "description"],
|
|
"row_limit": 100,
|
|
}),
|
|
},
|
|
)
|
|
chart_id = int(
|
|
chart_resp.get("id") or chart_resp.get("result", {}).get("id", 0)
|
|
)
|
|
|
|
# ── Step 3b: Register target database in Superset for SQL Lab write ──
|
|
# allow_dml=True is required because Superset blocks INSERT/UPDATE
|
|
# on databases with allow_dml=False by default (DML_NOT_ALLOWED_ERROR).
|
|
tgt_db_resp = await superset_client.create_database(
|
|
database_name=f"Translate Target {suffix}",
|
|
sqlalchemy_uri=database_uri,
|
|
allow_dml=True,
|
|
)
|
|
target_db_id = int(
|
|
tgt_db_resp.get("id") or tgt_db_resp.get("result", {}).get("id", 0)
|
|
)
|
|
|
|
# ── Step 4: Create LLM provider ──
|
|
provider = LLMProvider(
|
|
id=f"e2e-llm-{suffix}",
|
|
provider_type="openai",
|
|
name="E2E Test LLM Provider",
|
|
base_url="http://fake-llm-localhost:9999",
|
|
api_key="test-key",
|
|
default_model="gpt-4o-mini",
|
|
is_active=True,
|
|
)
|
|
db_session.add(provider)
|
|
db_session.flush()
|
|
|
|
# ── Step 5: Create TranslationJob ──
|
|
from unittest.mock import MagicMock
|
|
|
|
from src.plugins.translate.service import TranslateJobService
|
|
from src.schemas.translate import TranslateJobCreate
|
|
|
|
config_manager = MagicMock()
|
|
config_manager.get_environments.return_value = []
|
|
|
|
job_service = TranslateJobService(db_session, config_manager, "e2e-test-user")
|
|
job_payload = TranslateJobCreate(
|
|
name=f"E2E Translate Test {suffix}",
|
|
description="PostgreSQL E2E translation test with LLM stub",
|
|
source_dialect="postgresql",
|
|
target_dialect="postgresql",
|
|
source_datasource_id=str(dataset_id),
|
|
target_datasource_id=str(database_id),
|
|
target_table=target_table,
|
|
target_schema="public",
|
|
translation_column="product_name",
|
|
target_column="product_name_ru",
|
|
source_key_cols=["id"],
|
|
target_key_cols=["id"],
|
|
context_columns=["description"],
|
|
target_languages=["ru"],
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
provider_id=provider.id,
|
|
target_database_id=str(target_db_id),
|
|
)
|
|
job = await job_service.create_job(job_payload)
|
|
job_id = job.id
|
|
|
|
# Update job status to ACTIVE (needed for execution)
|
|
job.status = "ACTIVE"
|
|
job.environment_id = "test_env"
|
|
db_session.commit()
|
|
|
|
# ── Step 6: Create environment config so source fetcher can reach Superset ──
|
|
from src.core.config_models import Environment as EnvConfig
|
|
env_config = EnvConfig(
|
|
id="test_env",
|
|
name="Test Environment for E2E",
|
|
url=superset_client.client.base_url,
|
|
username="admin",
|
|
password="admin123",
|
|
verify_ssl=False,
|
|
timeout=30,
|
|
)
|
|
config_manager.get_environments.return_value = [env_config]
|
|
config_manager.get_environment.return_value = env_config
|
|
|
|
# ── Step 7: Create PENDING run ──
|
|
from src.plugins.translate.orchestrator import TranslationOrchestrator
|
|
orch = TranslationOrchestrator(db_session, config_manager, "e2e-test-user")
|
|
run = orch.start_run(job_id=job_id)
|
|
run_id = run.id
|
|
db_session.commit()
|
|
|
|
# ── Step 8: Mock the external LLM boundary ──
|
|
# We patch call_openai_compatible (the actual HTTP function) AND
|
|
# get_decrypted_api_key (the key resolution). This exercises the full
|
|
# local pipeline: run service -> batch processor -> LLMTranslationService
|
|
# -> call_llm -> call_openai_compatible, including record creation
|
|
# from parsed LLM responses.
|
|
source_rows_data = [
|
|
{"row_index": "0", "source_text": "Wireless Mouse", "source_data": {"id": 1, "product_name": "Wireless Mouse", "description": "Ergonomic wireless mouse with USB receiver"}},
|
|
{"row_index": "1", "source_text": "USB-C Hub", "source_data": {"id": 2, "product_name": "USB-C Hub", "description": "7-port USB-C hub with HDMI and PD charging"}},
|
|
{"row_index": "2", "source_text": "Mechanical Keyboard", "source_data": {"id": 3, "product_name": "Mechanical Keyboard", "description": "RGB mechanical keyboard with Cherry MX switches"}},
|
|
]
|
|
llm_response_text = self._make_llm_response(source_rows_data)
|
|
|
|
from src.services.llm_provider import LLMProviderService
|
|
|
|
mock_openai = AsyncMock(return_value=(llm_response_text, "stop"))
|
|
|
|
# ── Step 9: Execute the run ──
|
|
from src.plugins.translate.events import TranslationEventLog
|
|
from src.plugins.translate.orchestrator_runner import TranslationStageRunner
|
|
|
|
event_log = TranslationEventLog(db_session)
|
|
runner = TranslationStageRunner(db_session, config_manager, event_log, "e2e-test-user")
|
|
|
|
with patch.object(LLMProviderService, "get_decrypted_api_key", return_value="test-key"), patch("src.plugins.translate._llm_call.call_openai_compatible", mock_openai):
|
|
try:
|
|
completed_run = await runner.execute_run(run)
|
|
db_session.commit()
|
|
except Exception:
|
|
db_session.rollback()
|
|
raise
|
|
|
|
# ── Step 10: Verify postconditions ──
|
|
db_session.refresh(job)
|
|
db_session.refresh(run)
|
|
|
|
# 10a: TranslationBatch records exist
|
|
batches = db_session.query(TranslationBatch).filter(
|
|
TranslationBatch.run_id == run_id
|
|
).all()
|
|
assert len(batches) >= 1, \
|
|
f"Expected >=1 batch for run {run_id}, got {len(batches)}"
|
|
|
|
# 10b: TranslationRecord rows exist with SUCCESS status
|
|
records = db_session.query(TranslationRecord).filter(
|
|
TranslationRecord.run_id == run_id,
|
|
TranslationRecord.status == "SUCCESS",
|
|
).all()
|
|
assert len(records) >= 1, \
|
|
f"Expected >=1 successful records, got {len(records)}"
|
|
|
|
# 10c: target_sql contains the translated text (proof that LLM response was parsed into SQL)
|
|
found_translation = False
|
|
for rec in records:
|
|
if rec.target_sql and "[RU]" in rec.target_sql:
|
|
found_translation = True
|
|
break
|
|
assert found_translation, \
|
|
f"None of {len(records)} records have target_sql with '[RU]' translation. " \
|
|
f"Samples: {[r.target_sql[:120] if r.target_sql else 'None' for r in records[:3]]}"
|
|
|
|
# 10d: TranslationEvent records exist
|
|
events = db_session.query(TranslationEvent).filter(
|
|
TranslationEvent.run_id == run_id
|
|
).all()
|
|
assert len(events) >= 1, \
|
|
f"Expected >=1 events, got {len(events)}"
|
|
|
|
# 10e: LLM was actually called with our mock
|
|
mock_openai.assert_called()
|
|
|
|
# 10f: Run status is COMPLETED with successful insert
|
|
run_status = completed_run.status if hasattr(completed_run, 'status') else run.status
|
|
insert_status = getattr(completed_run, 'insert_status', None) or run.insert_status
|
|
assert run_status == "COMPLETED", \
|
|
f"Expected COMPLETED run, got {run_status}: {getattr(completed_run, 'error_message', run.error_message)}"
|
|
assert insert_status == "success", \
|
|
f"Expected insert status 'success', got '{insert_status}'"
|
|
# With 3 source records (each producing original+translation rows, MERGE collapses by key)
|
|
assert run.insert_rows_prepared is not None and run.insert_rows_prepared >= 1
|
|
assert run.superset_execution_id is not None and run.superset_execution_id != ""
|
|
|
|
# 10g: Physical rows exist in target PG table (written via Superset SQL Lab)
|
|
with pg_engine.begin() as conn:
|
|
result = conn.execute(
|
|
text(f'SELECT id, product_name_ru, is_original FROM public."{target_table}" ORDER BY id, is_original')
|
|
)
|
|
target_rows = result.fetchall()
|
|
assert len(target_rows) >= 1, \
|
|
f"Expected >=1 physical row in target table, got {len(target_rows)}"
|
|
|
|
# Verify at least one row has translated text
|
|
translated_rows = [r for r in target_rows if r[1] and "[RU]" in str(r[1])]
|
|
assert len(translated_rows) >= 1, \
|
|
f"Expected >=1 row with '[RU]' translation, got {len(translated_rows)}. " \
|
|
f"All rows: {target_rows}"
|
|
|
|
finally:
|
|
# Cleanup Superset resources
|
|
for cid in ([chart_id] if chart_id else []):
|
|
with suppress(Exception):
|
|
await superset_client.delete_chart(cid)
|
|
for did in ([dashboard_id] if dashboard_id else []):
|
|
with suppress(Exception):
|
|
await superset_client.delete_dashboard(did)
|
|
for dsid in ([dataset_id] if dataset_id else []):
|
|
with suppress(Exception):
|
|
await superset_client.delete_dataset(dsid)
|
|
for dbid in ([database_id] if database_id else []):
|
|
with suppress(Exception):
|
|
await superset_client.delete_database(dbid)
|
|
for tdbid in ([target_db_id] if target_db_id else []):
|
|
with suppress(Exception):
|
|
await superset_client.delete_database(tdbid)
|
|
|
|
# Cleanup DB tables
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f'DROP TABLE IF EXISTS public."{source_table}"'))
|
|
conn.execute(text(f'DROP TABLE IF EXISTS public."{target_table}"'))
|
|
|
|
# Cleanup test provider
|
|
if job_id:
|
|
with suppress(Exception):
|
|
p = db_session.query(LLMProvider).filter(
|
|
LLMProvider.id == f"e2e-llm-{suffix}"
|
|
).first()
|
|
if p:
|
|
db_session.delete(p)
|
|
db_session.commit()
|
|
# #endregion Test.Integration.TestHappyPathTranslatesSourceToTarget
|
|
|
|
# #region Test.Integration.TestLlmRetryOn429 [C:3] [TYPE Function]
|
|
# @BRIEF LLM returns HTTP 429 (rate limit) then 200; verify retry succeeds without duplicate rows.
|
|
# @TEST_EDGE: llm_429_retry -> VERIFIED_BY: test_llm_retry_on_429
|
|
@pytest.mark.integration
|
|
@pytest.mark.asyncio
|
|
async def test_llm_retry_on_429(
|
|
self,
|
|
superset_client,
|
|
superset_db_url,
|
|
pg_engine,
|
|
db_session,
|
|
):
|
|
suffix = uuid4().hex[:10]
|
|
source_table = f"translate_429_source_{suffix}"
|
|
target_table = f"translate_429_target_{suffix}"
|
|
|
|
database_id = target_db_id = dataset_id = chart_id = dashboard_id = None
|
|
job_id = run_id = None
|
|
|
|
# Seed source table
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f"""
|
|
CREATE TABLE public."{source_table}" (
|
|
id INTEGER PRIMARY KEY, product_name VARCHAR(128)
|
|
)
|
|
"""))
|
|
conn.execute(text(f"""
|
|
INSERT INTO public."{source_table}" VALUES (1, 'Test Product 429')
|
|
"""))
|
|
|
|
# Create target table (columns matching build_columns output)
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f"""
|
|
CREATE TABLE public."{target_table}" (
|
|
id INTEGER PRIMARY KEY,
|
|
product_name_ru TEXT,
|
|
context TEXT,
|
|
is_original INTEGER DEFAULT 0
|
|
)
|
|
"""))
|
|
|
|
try:
|
|
from urllib.parse import urlparse
|
|
parsed = urlparse(superset_db_url)
|
|
database_uri = f"postgresql://test:test@{parsed.hostname}:{parsed.port}/test_translate"
|
|
|
|
db_resp = await superset_client.create_database(
|
|
database_name=f"Translate 429 {suffix}", sqlalchemy_uri=database_uri,
|
|
)
|
|
database_id = int(db_resp.get("id") or db_resp.get("result", {}).get("id", 0))
|
|
|
|
ds_resp = await superset_client.create_dataset(
|
|
table_name=source_table, database=database_id, schema_name="public",
|
|
)
|
|
dataset_id = int(ds_resp.get("id") or ds_resp.get("result", {}).get("id", 0))
|
|
|
|
# Register target database in Superset for SQL Lab write
|
|
tgt_db_resp = await superset_client.create_database(
|
|
database_name=f"Translate 429 Target {suffix}", sqlalchemy_uri=database_uri,
|
|
)
|
|
target_db_id = int(tgt_db_resp.get("id") or tgt_db_resp.get("result", {}).get("id", 0))
|
|
|
|
provider = LLMProvider(
|
|
id=f"e2e-llm-429-{suffix}",
|
|
provider_type="openai", name="429 Test LLM",
|
|
base_url="http://fake-llm:9999", api_key="test-key",
|
|
default_model="gpt-4o-mini", is_active=True,
|
|
)
|
|
db_session.add(provider)
|
|
db_session.flush()
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
from src.plugins.translate.service import TranslateJobService
|
|
from src.schemas.translate import TranslateJobCreate
|
|
|
|
config_manager = MagicMock()
|
|
config_manager.get_environments.return_value = []
|
|
|
|
job_service = TranslateJobService(db_session, config_manager, "e2e-test-user")
|
|
job_payload = TranslateJobCreate(
|
|
name=f"429 Retry Test {suffix}",
|
|
source_dialect="postgresql", target_dialect="postgresql",
|
|
source_datasource_id=str(dataset_id),
|
|
target_table=target_table, target_schema="public",
|
|
translation_column="product_name", target_column="product_name_ru",
|
|
source_key_cols=["id"], target_key_cols=["id"],
|
|
target_languages=["ru"], batch_size=50,
|
|
upsert_strategy="MERGE", provider_id=provider.id,
|
|
target_database_id=str(target_db_id),
|
|
)
|
|
job = await job_service.create_job(job_payload)
|
|
job_id = job.id
|
|
job.status = "ACTIVE"
|
|
job.environment_id = "test_env"
|
|
db_session.commit()
|
|
|
|
from src.core.config_models import Environment as EnvConfig
|
|
env_config = EnvConfig(
|
|
id="test_env", name="Test", url=superset_client.client.base_url,
|
|
username="admin", password="admin123", verify_ssl=False, timeout=30,
|
|
)
|
|
config_manager.get_environments.return_value = [env_config]
|
|
|
|
from src.plugins.translate.orchestrator import TranslationOrchestrator
|
|
orch = TranslationOrchestrator(db_session, config_manager, "e2e-test-user")
|
|
run = orch.start_run(job_id=job_id)
|
|
run_id = run.id
|
|
db_session.commit()
|
|
|
|
# Mock call_openai_compatible — first call raises RuntimeError (simulating 429),
|
|
# subsequent calls return a valid translation response.
|
|
from src.services.llm_provider import LLMProviderService
|
|
|
|
mock_response_429 = self._make_llm_response([
|
|
{"row_index": "0", "source_text": "Test Product 429",
|
|
"source_data": {"id": 1, "product_name": "Test Product 429"}},
|
|
])
|
|
|
|
call_count = 0
|
|
|
|
async def mock_openai_with_retry(*_args, **_kwargs):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count == 1:
|
|
raise RuntimeError("HTTP 429 Too Many Requests")
|
|
return (mock_response_429, "stop")
|
|
|
|
mock_openai = AsyncMock(side_effect=mock_openai_with_retry)
|
|
|
|
from src.plugins.translate.events import TranslationEventLog
|
|
from src.plugins.translate.orchestrator_runner import TranslationStageRunner
|
|
|
|
event_log = TranslationEventLog(db_session)
|
|
runner = TranslationStageRunner(db_session, config_manager, event_log, "e2e-test-user")
|
|
|
|
with patch.object(LLMProviderService, "get_decrypted_api_key", return_value="test-key"), patch("src.plugins.translate._llm_call.call_openai_compatible", mock_openai):
|
|
try:
|
|
_ = await runner.execute_run(run)
|
|
db_session.commit()
|
|
except Exception:
|
|
db_session.rollback()
|
|
|
|
print(f"DEBUG 429: LLM called {call_count} times")
|
|
|
|
# Verify records were created on retry
|
|
records = db_session.query(TranslationRecord).filter(
|
|
TranslationRecord.run_id == run_id,
|
|
TranslationRecord.status == "SUCCESS",
|
|
).all()
|
|
print(f"DEBUG 429: {len(records)} successful records")
|
|
|
|
# Mock was called at least once
|
|
assert call_count >= 1, "LLM should have been called at least once"
|
|
|
|
finally:
|
|
with suppress(Exception):
|
|
await superset_client.delete_dashboard(dashboard_id) if dashboard_id else None
|
|
with suppress(Exception):
|
|
await superset_client.delete_chart(chart_id) if chart_id else None
|
|
with suppress(Exception):
|
|
await superset_client.delete_dataset(dataset_id) if dataset_id else None
|
|
with suppress(Exception):
|
|
await superset_client.delete_database(database_id) if database_id else None
|
|
with suppress(Exception):
|
|
if target_db_id:
|
|
await superset_client.delete_database(target_db_id)
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f'DROP TABLE IF EXISTS public."{source_table}"'))
|
|
conn.execute(text(f'DROP TABLE IF EXISTS public."{target_table}"'))
|
|
if job_id:
|
|
with suppress(Exception):
|
|
p = db_session.query(LLMProvider).filter(
|
|
LLMProvider.id == f"e2e-llm-429-{suffix}"
|
|
).first()
|
|
if p:
|
|
db_session.delete(p)
|
|
db_session.commit()
|
|
# #endregion Test.Integration.TestLlmRetryOn429
|
|
|
|
# #region Test.Integration.TestIncrementalRerunOnlyNewRows [C:3] [TYPE Function]
|
|
# @BRIEF After a successful run, second run only processes rows not in first run's output.
|
|
# @TEST_EDGE: incremental_rerun -> VERIFIED_BY: test_incremental_rerun_only_new_rows
|
|
@pytest.mark.integration
|
|
@pytest.mark.asyncio
|
|
async def test_incremental_rerun_only_new_rows(
|
|
self,
|
|
superset_client,
|
|
superset_db_url,
|
|
pg_engine,
|
|
db_session,
|
|
):
|
|
suffix = uuid4().hex[:10]
|
|
source_table = f"translate_incr_source_{suffix}"
|
|
target_table = f"translate_incr_target_{suffix}"
|
|
|
|
database_id = target_db_id = dataset_id = job_id = run1_id = None
|
|
|
|
# ── Step 1: Seed source with 2 rows ──
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f"""
|
|
CREATE TABLE public."{source_table}" (
|
|
id INTEGER PRIMARY KEY, product_name VARCHAR(128), description TEXT
|
|
)
|
|
"""))
|
|
conn.execute(text(f"""
|
|
INSERT INTO public."{source_table}" VALUES
|
|
(1, 'First Product', 'Description for first product'),
|
|
(2, 'Second Product', 'Description for second product')
|
|
"""))
|
|
|
|
# ── Step 2: Create target table (columns matching build_columns output) ──
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f"""
|
|
CREATE TABLE public."{target_table}" (
|
|
id INTEGER PRIMARY KEY,
|
|
product_name_ru TEXT,
|
|
context TEXT,
|
|
is_original INTEGER DEFAULT 0
|
|
)
|
|
"""))
|
|
|
|
try:
|
|
from urllib.parse import urlparse
|
|
parsed = urlparse(superset_db_url)
|
|
database_uri = f"postgresql://test:test@{parsed.hostname}:{parsed.port}/test_translate"
|
|
|
|
db_resp = await superset_client.create_database(
|
|
database_name=f"Translate Incr {suffix}", sqlalchemy_uri=database_uri,
|
|
)
|
|
database_id = int(db_resp.get("id") or db_resp.get("result", {}).get("id", 0))
|
|
|
|
ds_resp = await superset_client.create_dataset(
|
|
table_name=source_table, database=database_id, schema_name="public",
|
|
)
|
|
dataset_id = int(ds_resp.get("id") or ds_resp.get("result", {}).get("id", 0))
|
|
|
|
# Register target database in Superset for SQL Lab write (allow DML for INSERT)
|
|
tgt_db_resp = await superset_client.create_database(
|
|
database_name=f"Translate Incr Target {suffix}", sqlalchemy_uri=database_uri,
|
|
allow_dml=True,
|
|
)
|
|
target_db_id = int(tgt_db_resp.get("id") or tgt_db_resp.get("result", {}).get("id", 0))
|
|
|
|
# Create provider
|
|
provider = LLMProvider(
|
|
id=f"e2e-llm-incr-{suffix}",
|
|
provider_type="openai",
|
|
name="E2E Test LLM Provider (Incremental)",
|
|
base_url="http://fake-llm-localhost:9999",
|
|
api_key="test-key",
|
|
default_model="gpt-4o-mini",
|
|
is_active=True,
|
|
)
|
|
db_session.add(provider)
|
|
db_session.flush()
|
|
|
|
# Create job
|
|
from unittest.mock import MagicMock
|
|
|
|
from src.core.config_models import Environment as EnvConfig
|
|
from src.plugins.translate.service import TranslateJobService
|
|
from src.schemas.translate import TranslateJobCreate
|
|
|
|
config_manager = MagicMock()
|
|
env_config = EnvConfig(
|
|
id="test_env", name="Test", url=superset_client.client.base_url,
|
|
username="admin", password="admin123", verify_ssl=False, timeout=30,
|
|
)
|
|
config_manager.get_environments.return_value = [env_config]
|
|
config_manager.get_environment.return_value = env_config
|
|
|
|
job_service = TranslateJobService(db_session, config_manager, "e2e-test-user")
|
|
job_payload = TranslateJobCreate(
|
|
name=f"Incremental Test {suffix}",
|
|
source_dialect="postgresql", target_dialect="postgresql",
|
|
source_datasource_id=str(dataset_id),
|
|
target_table=target_table, target_schema="public",
|
|
translation_column="product_name", target_column="product_name_ru",
|
|
source_key_cols=["id"], target_key_cols=["id"],
|
|
target_languages=["ru"], batch_size=50,
|
|
upsert_strategy="MERGE", provider_id=provider.id,
|
|
target_database_id=str(target_db_id),
|
|
)
|
|
job = await job_service.create_job(job_payload)
|
|
job_id = job.id
|
|
job.status = "ACTIVE"
|
|
job.environment_id = "test_env"
|
|
db_session.commit()
|
|
|
|
# ── Run 1: translate 2 rows ──
|
|
from src.services.llm_provider import LLMProviderService
|
|
|
|
rows_run1 = [
|
|
{"row_index": "0", "source_text": "First Product", "source_data": {"id": 1, "product_name": "First Product"}},
|
|
{"row_index": "1", "source_text": "Second Product", "source_data": {"id": 2, "product_name": "Second Product"}},
|
|
]
|
|
mock_response_1 = self._make_llm_response(rows_run1)
|
|
mock_openai_1 = AsyncMock(return_value=(mock_response_1, "stop"))
|
|
|
|
from src.plugins.translate.events import TranslationEventLog
|
|
from src.plugins.translate.orchestrator import TranslationOrchestrator
|
|
from src.plugins.translate.orchestrator_runner import TranslationStageRunner
|
|
|
|
orch = TranslationOrchestrator(db_session, config_manager, "e2e-test-user")
|
|
run1 = orch.start_run(job_id=job_id)
|
|
run1_id = run1.id
|
|
db_session.commit()
|
|
|
|
event_log = TranslationEventLog(db_session)
|
|
runner = TranslationStageRunner(db_session, config_manager, event_log, "e2e-test-user")
|
|
|
|
with patch.object(LLMProviderService, "get_decrypted_api_key", return_value="test-key"), patch("src.plugins.translate._llm_call.call_openai_compatible", mock_openai_1):
|
|
try:
|
|
completed_run1 = await runner.execute_run(run1)
|
|
db_session.commit()
|
|
except Exception:
|
|
db_session.rollback()
|
|
raise
|
|
|
|
# ── Verify run 1 ──
|
|
# Run 1 with 2 source records should produce 2 TranslationRecords
|
|
recs1 = db_session.query(TranslationRecord).filter(
|
|
TranslationRecord.run_id == run1_id,
|
|
TranslationRecord.status == "SUCCESS",
|
|
).all()
|
|
assert len(recs1) >= 1, f"Run 1 expected >=1 successful records, got {len(recs1)}"
|
|
|
|
# Run 1 status should be COMPLETED with successful insert
|
|
assert completed_run1.status == "COMPLETED" or run1.status == "COMPLETED", \
|
|
f"Run 1 expected COMPLETED, got {completed_run1.status if hasattr(completed_run1, 'status') else run1.status}"
|
|
|
|
# Run 1 should have written 2 physical rows to target table (2 source records, MERGE collapses by key)
|
|
with pg_engine.begin() as conn:
|
|
r1_rows = conn.execute(
|
|
text(f'SELECT COUNT(*) FROM public."{target_table}"')
|
|
).scalar()
|
|
assert r1_rows == 2, \
|
|
f"Run 1 expected 2 physical rows in target table, got {r1_rows}"
|
|
|
|
# ── Step 3: Add a new row to source ──
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f"""
|
|
INSERT INTO public."{source_table}" VALUES
|
|
(3, 'Third Product', 'Description for third product')
|
|
"""))
|
|
|
|
# Refresh the Superset dataset cache so the new row is visible
|
|
# in the chart data API query. Without refresh, Superset returns
|
|
# the stale cached result (2 rows instead of 3).
|
|
await superset_client.refresh_dataset_schema(dataset_id)
|
|
|
|
# ── Run 2: should only process the new row ──
|
|
# The incremental filter preserves row_index from the unfiltered fetch.
|
|
# With 3 total rows (2 original + 1 new), the new row (id=3) gets row_index="2".
|
|
rows_run2 = [
|
|
{"row_index": "2", "source_text": "Third Product", "source_data": {"id": 3, "product_name": "Third Product"}},
|
|
]
|
|
mock_response_2 = self._make_llm_response(rows_run2)
|
|
mock_openai_2 = AsyncMock(return_value=(mock_response_2, "stop"))
|
|
|
|
run2 = orch.start_run(job_id=job_id)
|
|
db_session.commit()
|
|
with patch.object(LLMProviderService, "get_decrypted_api_key", return_value="test-key"), patch("src.plugins.translate._llm_call.call_openai_compatible", mock_openai_2):
|
|
try:
|
|
completed_run2 = await runner.execute_run(run2)
|
|
db_session.commit()
|
|
except Exception:
|
|
db_session.rollback()
|
|
raise
|
|
|
|
# ── Verify run 2 incremental behavior ──
|
|
|
|
# Run 2 should have exactly 1 TranslationRecord (only the new row id=3)
|
|
recs2 = db_session.query(TranslationRecord).filter(
|
|
TranslationRecord.run_id == run2.id,
|
|
TranslationRecord.status == "SUCCESS",
|
|
).all()
|
|
assert len(recs2) == 1, \
|
|
f"Run 2 (incremental) expected exactly 1 TranslationRecord (new row only), got {len(recs2)}"
|
|
|
|
# Run 2 status should be COMPLETED
|
|
assert completed_run2.status == "COMPLETED" or run2.status == "COMPLETED", \
|
|
f"Run 2 expected COMPLETED, got {completed_run2.status if hasattr(completed_run2, 'status') else run2.status}"
|
|
|
|
# Run 2 with 1 new source record: build_rows() produces original+translated (2 rows),
|
|
# but the MERGE path deduplicates by key to prevent PostgreSQL "ON CONFLICT DO UPDATE
|
|
# command cannot affect row a second time" — so insert_rows_prepared = 1.
|
|
r2_prepared = getattr(completed_run2, 'insert_rows_prepared', run2.insert_rows_prepared if hasattr(run2, 'insert_rows_prepared') else None)
|
|
r2_affected = getattr(completed_run2, 'insert_rows_affected', run2.insert_rows_affected if hasattr(run2, 'insert_rows_affected') else None)
|
|
assert r2_prepared == 1, \
|
|
f"Run 2 expected insert_rows_prepared=1, got {r2_prepared}"
|
|
# rows_affected may vary by Superset response (sometimes 0 for DML);
|
|
# the physical row count assertions below are the real validator.
|
|
if r2_affected is not None and r2_affected > 0:
|
|
pass # Superset returned a row count — informative but not required
|
|
|
|
# Physical target table should have 3 rows (no duplicates, 2 from run 1 + 1 new from run 2)
|
|
with pg_engine.begin() as conn:
|
|
total_rows = conn.execute(
|
|
text(f'SELECT COUNT(*) FROM public."{target_table}"')
|
|
).scalar()
|
|
assert total_rows == 3, \
|
|
f"Expected 3 total physical rows after run 2, got {total_rows}"
|
|
|
|
# Verify the new row (id=3) has the translated text
|
|
with pg_engine.begin() as conn:
|
|
row3 = conn.execute(
|
|
text(f'SELECT product_name_ru FROM public."{target_table}" WHERE id = 3')
|
|
).fetchone()
|
|
assert row3 is not None, "Row id=3 should exist in target table"
|
|
assert row3[0] is not None and "[RU]" in str(row3[0]), \
|
|
f"Row id=3 should contain '[RU]' translated text, got '{row3[0]}'"
|
|
|
|
# Verify unchanged rows (id=1, id=2) still have translations from run 1
|
|
with pg_engine.begin() as conn:
|
|
row1 = conn.execute(
|
|
text(f'SELECT product_name_ru FROM public."{target_table}" WHERE id = 1')
|
|
).fetchone()
|
|
row2 = conn.execute(
|
|
text(f'SELECT product_name_ru FROM public."{target_table}" WHERE id = 2')
|
|
).fetchone()
|
|
assert row1 is not None and "[RU]" in str(row1[0]), \
|
|
f"Row id=1 should still have translated text, got '{row1[0]}'"
|
|
assert row2 is not None and "[RU]" in str(row2[0]), \
|
|
f"Row id=2 should still have translated text, got '{row2[0]}'"
|
|
|
|
# Verify counter persistence: run2 counters are persisted in the DB
|
|
db_session.refresh(run2)
|
|
assert run2.insert_rows_prepared == 1, \
|
|
f"Persisted insert_rows_prepared for run2 should be 1 (1 deduped row), got {run2.insert_rows_prepared}"
|
|
assert run2.insert_status == "success", \
|
|
f"Persisted insert_status for run2 should be 'success', got {run2.insert_status}"
|
|
|
|
finally:
|
|
with suppress(Exception):
|
|
await superset_client.delete_database(database_id) if database_id else None
|
|
with suppress(Exception):
|
|
await superset_client.delete_dataset(dataset_id) if dataset_id else None
|
|
with suppress(Exception):
|
|
if target_db_id:
|
|
await superset_client.delete_database(target_db_id)
|
|
if job_id:
|
|
with suppress(Exception):
|
|
p = db_session.query(LLMProvider).filter(
|
|
LLMProvider.id == f"e2e-llm-incr-{suffix}"
|
|
).first()
|
|
if p:
|
|
db_session.delete(p)
|
|
db_session.commit()
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f'DROP TABLE IF EXISTS public."{source_table}"'))
|
|
conn.execute(text(f'DROP TABLE IF EXISTS public."{target_table}"'))
|
|
# #endregion Test.Integration.TestIncrementalRerunOnlyNewRows
|
|
# #region Test.Integration.TestResourceProjection [C:3] [TYPE Function]
|
|
# @BRIEF Seed physical dataset, chart, dashboard; drive ResourceService list/detail entrypoints;
|
|
# assert response shape and linked counts from real Superset response shapes.
|
|
@pytest.mark.integration
|
|
@pytest.mark.asyncio
|
|
async def test_resource_projection(
|
|
self,
|
|
superset_client,
|
|
superset_env,
|
|
superset_db_url,
|
|
pg_engine,
|
|
):
|
|
suffix = uuid4().hex[:10]
|
|
source_table = f"res_proj_source_{suffix}"
|
|
database_id = dataset_id = chart_id = dashboard_id = None
|
|
|
|
# ── Step 1: Seed physical table ──
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f"""
|
|
CREATE TABLE public."{source_table}" (
|
|
id INTEGER PRIMARY KEY,
|
|
name VARCHAR(128),
|
|
value INTEGER
|
|
)
|
|
"""))
|
|
conn.execute(text(f"""
|
|
INSERT INTO public."{source_table}" VALUES
|
|
(1, 'Alpha', 100),
|
|
(2, 'Beta', 200)
|
|
"""))
|
|
|
|
try:
|
|
# ── Step 2: Register database and dataset in Superset ──
|
|
from urllib.parse import urlparse
|
|
parsed = urlparse(superset_db_url)
|
|
database_uri = f"postgresql://test:test@{parsed.hostname}:{parsed.port}/test_translate"
|
|
|
|
db_resp = await superset_client.create_database(
|
|
database_name=f"ResProj Source {suffix}",
|
|
sqlalchemy_uri=database_uri,
|
|
)
|
|
database_id = int(
|
|
db_resp.get("id") or db_resp.get("result", {}).get("id", 0)
|
|
)
|
|
|
|
ds_resp = await superset_client.create_dataset(
|
|
table_name=source_table,
|
|
database=database_id,
|
|
schema_name="public",
|
|
)
|
|
dataset_id = int(
|
|
ds_resp.get("id") or ds_resp.get("result", {}).get("id", 0)
|
|
)
|
|
|
|
# ── Step 3: Create dashboard ──
|
|
dash_resp = await superset_client.create_dashboard(
|
|
dashboard_title=f"ResProj Dashboard {suffix}",
|
|
slug=f"resproj-{suffix}",
|
|
published=True,
|
|
)
|
|
dashboard_id = int(
|
|
dash_resp.get("id") or dash_resp.get("result", {}).get("id", 0)
|
|
)
|
|
|
|
# ── Step 4: Create chart linked to dashboard and dataset ──
|
|
chart_resp = await superset_client.client.request(
|
|
method="POST", endpoint="/chart/",
|
|
data={
|
|
"dashboards": [dashboard_id],
|
|
"datasource_id": dataset_id,
|
|
"datasource_type": "table",
|
|
"slice_name": f"ResProj Chart {suffix}",
|
|
"viz_type": "table",
|
|
"params": json.dumps({
|
|
"datasource": f"{dataset_id}__table",
|
|
"viz_type": "table",
|
|
"all_columns": ["id", "name", "value"],
|
|
"row_limit": 50,
|
|
}),
|
|
},
|
|
)
|
|
chart_id = int(
|
|
chart_resp.get("id") or chart_resp.get("result", {}).get("id", 0)
|
|
)
|
|
|
|
# ── Step 5: Drive ResourceService list entrypoints ──
|
|
from src.services.resource_service import ResourceService
|
|
|
|
resource_service = ResourceService()
|
|
# Mock GitService to avoid repo dependency in integration test
|
|
from unittest.mock import MagicMock
|
|
resource_service.git_service = MagicMock()
|
|
resource_service.git_service.get_repo = MagicMock(return_value=None)
|
|
|
|
# 5a: get_dashboards_with_status — verify our dashboard is listed
|
|
dashboards = await resource_service.get_dashboards_with_status(
|
|
env=superset_env, tasks=None, include_git_status=False,
|
|
)
|
|
# Find our dashboard by title
|
|
our_dash = next(
|
|
(d for d in dashboards if d.get("id") == dashboard_id),
|
|
None,
|
|
)
|
|
assert our_dash is not None, \
|
|
f"Dashboard id={dashboard_id} not found in dashboards list. " \
|
|
f"Available: {[d.get('id') for d in dashboards[:10]]}"
|
|
# Required shape: id, slug, dashboard_title, published, last_task
|
|
assert "id" in our_dash, "Dashboard response missing 'id'"
|
|
assert "slug" in our_dash, "Dashboard response missing 'slug'"
|
|
assert "title" in our_dash, "Dashboard response missing 'title'"
|
|
assert our_dash.get("title") == f"ResProj Dashboard {suffix}"
|
|
# When include_git_status=False, git_status should be None
|
|
assert our_dash.get("git_status") is None, \
|
|
f"Expected git_status=None, got {our_dash.get('git_status')}"
|
|
# last_task should be None (no tasks passed)
|
|
assert our_dash.get("last_task") is None, \
|
|
f"Expected last_task=None, got {our_dash.get('last_task')}"
|
|
|
|
# 5b: get_datasets_with_status — verify our dataset is listed with linked_dashboard_count
|
|
datasets = await resource_service.get_datasets_with_status(
|
|
env=superset_env, tasks=None,
|
|
)
|
|
our_ds = next(
|
|
(d for d in datasets if d.get("id") == dataset_id),
|
|
None,
|
|
)
|
|
assert our_ds is not None, \
|
|
f"Dataset id={dataset_id} not found in datasets list. " \
|
|
f"Available: {[d.get('id') for d in datasets[:10]]}"
|
|
# Required shape: id, table_name, schema, linked_dashboard_count, last_task
|
|
assert "id" in our_ds, "Dataset response missing 'id'"
|
|
assert "table_name" in our_ds, "Dataset response missing 'table_name'"
|
|
assert our_ds.get("table_name") == source_table
|
|
# Our dataset has 1 dashboard and 1 chart linked to it.
|
|
# linked_dashboard_count should be >= 1 (the chart is linked to the dashboard).
|
|
linked_count = our_ds.get("linked_dashboard_count", 0)
|
|
assert linked_count >= 1, \
|
|
f"Expected linked_dashboard_count >=1 for our dataset, got {linked_count}"
|
|
# last_task should be None (no tasks passed)
|
|
assert our_ds.get("last_task") is None, \
|
|
f"Expected last_task=None, got {our_ds.get('last_task')}"
|
|
|
|
finally:
|
|
# Cleanup Superset resources
|
|
for cid in ([chart_id] if chart_id else []):
|
|
with suppress(Exception):
|
|
await superset_client.delete_chart(cid)
|
|
for did in ([dashboard_id] if dashboard_id else []):
|
|
with suppress(Exception):
|
|
await superset_client.delete_dashboard(did)
|
|
for dsid in ([dataset_id] if dataset_id else []):
|
|
with suppress(Exception):
|
|
await superset_client.delete_dataset(dsid)
|
|
for dbid in ([database_id] if database_id else []):
|
|
with suppress(Exception):
|
|
await superset_client.delete_database(dbid)
|
|
# Cleanup physical table
|
|
with pg_engine.begin() as conn:
|
|
conn.execute(text(f'DROP TABLE IF EXISTS public."{source_table}"'))
|
|
# #endregion Test.Integration.TestResourceProjection
|
|
|
|
# #endregion Test.Integration.TranslatePgE2E.Test
|
|
|
|
# #endregion Test.Integration.TranslatePgE2E
|