Skip to content

Commit 3bdfeab

Browse files
author
testgen-ci-bot
committed
Merge remote-tracking branch 'origin/enterprise' into feat/TG-1121-normalize-run-summary-count-shapes
2 parents 82d7074 + c618241 commit 3bdfeab

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

@@ -141,183 +141,14 @@ class ErrorResponse(BaseModel):
141141
errors: list[ErrorDetail]
142142

143143

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

292146

293147
class ImportRequest(BaseModel):
294148
config: ImportConfig
295149
payload: ImportPayload
296150

297151

298-
class ImportItemTD(BaseModel):
299-
idx: int | None = None
300-
target_id: UUID | None = None
301-
302-
303-
class ImportItem(BaseModel):
304-
action: ImportAction
305-
reason: ImportReason
306-
tds: list[ImportItemTD]
307-
308-
309-
class ImportSummary(BaseModel):
310-
created: int = 0
311-
updated: int = 0
312-
skipped: int = 0
313-
deleted: int = 0
314-
315-
316-
class ImportResponse(BaseModel):
317-
summary: ImportSummary
318-
items: list[ImportItem]
319-
320-
321152
class ImportStrictError(ErrorResponse):
322153
"""400 response for apply_strict when entries would be skipped."""
323154

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)