Skip to content

Commit c618241

Browse files
author
ci bot
committed
Merge branch 'refactor/TG-1122-td-export-import-domain-errors' into 'enterprise'
refactor(api): TD export/import domain exceptions and version marker See merge request dkinternal/testgen/dataops-testgen!558
2 parents e091289 + c181259 commit c618241

4 files changed

Lines changed: 376 additions & 267 deletions

File tree

testgen/api/schemas.py

Lines changed: 3 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"""Pydantic request/response models for API v1 endpoints."""
22

33
from datetime import datetime
4-
from enum import StrEnum
54
from uuid import UUID
65

7-
from pydantic import BaseModel, field_validator
6+
from pydantic import BaseModel
87

98
from testgen.common.enums import JobSource, JobStatus, PublicJobKey
9+
from testgen.common.test_definition_export_import_service import ImportConfig, ImportPayload, ImportResponse
1010

1111
# --- Jobs ---
1212

@@ -127,183 +127,14 @@ class ErrorResponse(BaseModel):
127127
errors: list[ErrorDetail]
128128

129129

130-
# --- Test Definition Export/Import ---
131-
132-
133-
class Origin(StrEnum):
134-
manual = "manual"
135-
auto = "auto"
136-
both = "both"
137-
138-
139-
class ImportMode(StrEnum):
140-
preview = "preview"
141-
apply = "apply"
142-
apply_strict = "apply_strict"
143-
144-
145-
class OnMatch(StrEnum):
146-
overwrite_all = "overwrite_all"
147-
overwrite_unlocked = "overwrite_unlocked"
148-
skip = "skip"
149-
150-
151-
class OnNew(StrEnum):
152-
skip = "skip"
153-
create = "create"
154-
create_and_lock = "create_and_lock"
155-
156-
157-
class OnAbsence(StrEnum):
158-
do_nothing = "do_nothing"
159-
delete_all = "delete_all"
160-
delete_unlocked = "delete_unlocked"
161-
162-
163-
class ImportAction(StrEnum):
164-
create = "create"
165-
update = "update"
166-
skip = "skip"
167-
delete = "delete"
168-
169-
170-
class ImportReason(StrEnum):
171-
matched = "matched"
172-
no_match = "no_match"
173-
policy = "policy"
174-
locked = "locked"
175-
invalid_test_type = "invalid_test_type"
176-
invalid_table = "invalid_table"
177-
missing_external_id = "missing_external_id"
178-
absent = "absent"
179-
180-
181-
# Non-None defaults must match the ORM column defaults in TestDefinition:
182-
# test_active=True (YNString default="Y"), lock_refresh=False (YNString default="N"),
183-
# skip_errors=0 (ZeroIfEmptyInteger), window_days=0 (ZeroIfEmptyInteger),
184-
# history_lookback=0 (Column default=0).
185-
# On export, the model_serializer omits fields matching these defaults to keep the file compact.
186-
# On import, model_fields_set distinguishes explicit from defaulted.
187-
class TestDefinitionExport(BaseModel):
188-
"""Test definition fields included in the export/import file."""
189-
190-
model_config = {"from_attributes": True}
191-
192-
# Matching / identity
193-
test_type: str
194-
external_id: UUID | None = None
195-
last_auto_gen_date: datetime | None = None
196-
197-
# Definition fields
198-
table_name: str | None = None
199-
column_name: str | None = None
200-
test_description: str | None = None
201-
test_active: bool = True
202-
severity: str | None = None
203-
lock_refresh: bool = False
204-
export_to_observability: bool | None = None
205-
skip_errors: int = 0
206-
207-
# Calibration fields
208-
baseline_ct: str | None = None
209-
baseline_unique_ct: str | None = None
210-
baseline_value: str | None = None
211-
baseline_value_ct: str | None = None
212-
threshold_value: str | None = None
213-
baseline_sum: str | None = None
214-
baseline_avg: str | None = None
215-
baseline_sd: str | None = None
216-
lower_tolerance: str | None = None
217-
upper_tolerance: str | None = None
218-
219-
# Subset / grouping
220-
subset_condition: str | None = None
221-
groupby_names: str | None = None
222-
having_condition: str | None = None
223-
window_date_column: str | None = None
224-
window_days: int = 0
225-
226-
# Referential
227-
match_schema_name: str | None = None
228-
match_table_name: str | None = None
229-
match_column_names: str | None = None
230-
match_subset_condition: str | None = None
231-
match_groupby_names: str | None = None
232-
match_having_condition: str | None = None
233-
234-
# Query / history
235-
custom_query: str | None = None
236-
history_calculation: str | None = None
237-
history_calculation_upper: str | None = None
238-
history_lookback: int = 0
239-
240-
@field_validator("skip_errors", "window_days", "history_lookback", mode="before")
241-
@classmethod
242-
def _coerce_none_to_zero(cls, v: int | None) -> int:
243-
return v if v is not None else 0
244-
245-
246-
class ExportSource(BaseModel):
247-
project_code: str
248-
test_suite: str
249-
table_group: str
250-
table_group_schema: str
251-
exported_at: datetime
252-
testgen_version: str | None = None
253-
254-
255-
class ExportDocument(BaseModel):
256-
version: int = 1
257-
source: ExportSource
258-
definitions: list[TestDefinitionExport]
259-
260-
261-
# --- Import ---
262-
263-
264-
class ImportConfig(BaseModel):
265-
mode: ImportMode
266-
on_match: OnMatch
267-
on_new: OnNew
268-
on_absence: OnAbsence
269-
270-
271-
class ImportPayload(BaseModel):
272-
"""Import payload — same structure as an export document, but definitions are typed."""
273-
274-
version: int = 1
275-
source: ExportSource | None = None
276-
definitions: list[TestDefinitionExport]
130+
# --- Test Definition Export/Import (wire types) ---
277131

278132

279133
class ImportRequest(BaseModel):
280134
config: ImportConfig
281135
payload: ImportPayload
282136

283137

284-
class ImportItemTD(BaseModel):
285-
idx: int | None = None
286-
target_id: UUID | None = None
287-
288-
289-
class ImportItem(BaseModel):
290-
action: ImportAction
291-
reason: ImportReason
292-
tds: list[ImportItemTD]
293-
294-
295-
class ImportSummary(BaseModel):
296-
created: int = 0
297-
updated: int = 0
298-
skipped: int = 0
299-
deleted: int = 0
300-
301-
302-
class ImportResponse(BaseModel):
303-
summary: ImportSummary
304-
items: list[ImportItem]
305-
306-
307138
class ImportStrictError(ErrorResponse):
308139
"""400 response for apply_strict when entries would be skipped."""
309140

testgen/api/test_definitions.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@
22

33
from fastapi import APIRouter, Depends, HTTPException, Query
44

5-
from testgen.api import test_definition_service
6-
from testgen.api.deps import db_session, resolve_test_suite
5+
from testgen.api.deps import api_error, db_session, resolve_test_suite
76
from testgen.api.schemas import (
87
ErrorDetail,
98
ErrorResponse,
10-
ExportDocument,
11-
ImportMode,
129
ImportRequest,
13-
ImportResponse,
1410
ImportStrictError,
15-
Origin,
1611
)
1712
from testgen.common.models.test_suite import TestSuite
13+
from testgen.common.test_definition_export_import_service import (
14+
ExportDocument,
15+
ImportResponse,
16+
ImportStrictViolation,
17+
InvalidImportPayload,
18+
Origin,
19+
export_definitions,
20+
import_definitions,
21+
)
1822

1923
_error_responses = {
2024
404: {"model": ErrorResponse, "description": "Not found"},
@@ -39,33 +43,33 @@ def export_test_definitions(
3943
test_type: str | None = Query(default=None),
4044
) -> ExportDocument:
4145
"""Export test definitions from a test suite as a portable JSON document."""
42-
return test_definition_service.export_definitions(test_suite, origin, table_name, test_type)
46+
return export_definitions(test_suite, origin, table_name, test_type)
4347

4448

4549
@router.post(
4650
"/test-suites/{test_suite_id}/test-definition-import",
4751
response_model=ImportResponse,
4852
responses={
49-
400: {"model": ImportStrictError, "description": "Invalid request or strict validation failed"},
53+
400: {
54+
"model": ImportStrictError | ErrorResponse,
55+
"description": "Strict validation failed (includes the projected import result) or invalid payload",
56+
},
5057
},
5158
)
5259
def import_test_definitions(
5360
body: ImportRequest,
5461
test_suite: TestSuite = resolve_test_suite("edit"), # noqa: B008
5562
) -> ImportResponse:
5663
"""Import test definitions into a test suite from a portable JSON document."""
57-
result = test_definition_service.import_definitions(test_suite, body.config, body.payload)
58-
59-
if body.config.mode == ImportMode.apply_strict and result.summary.skipped > 0:
64+
try:
65+
return import_definitions(test_suite, body.config, body.payload)
66+
except InvalidImportPayload as err:
67+
raise api_error(400, err.code, str(err)) from err
68+
except ImportStrictViolation as err:
6069
raise HTTPException(
6170
status_code=400,
6271
detail=ImportStrictError(
63-
errors=[ErrorDetail(
64-
code="strict_validation_failed",
65-
detail=f"{result.summary.skipped} test definition(s) would be skipped",
66-
)],
67-
import_result=result,
72+
errors=[ErrorDetail(code="strict_validation_failed", detail=str(err))],
73+
import_result=err.result,
6874
).model_dump(mode="json"),
69-
)
70-
71-
return result
75+
) from err

0 commit comments

Comments
 (0)