Skip to content

Commit 684b8f8

Browse files
feat: Raise InvalidDiscoveryConfigError on SD/FDD start failures
The server returns a 400 keyed under `discovery_config` when the referenced config is missing, archived, or not in `valid` validation state. Surface that as a specific exception subclass so callers can distinguish discovery-config trouble from other start failures, mirroring the existing `InvalidRulesetError` / `InvalidLibraryError` pattern.
1 parent f007707 commit 684b8f8

4 files changed

Lines changed: 104 additions & 4 deletions

File tree

‎datamasque/client/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
DataMasqueUserError,
2020
FailedToStartError,
2121
IfmAuthError,
22+
InvalidDiscoveryConfigError,
2223
InvalidLibraryError,
2324
InvalidRulesetError,
2425
RunNotCancellableError,
@@ -158,6 +159,7 @@
158159
"IfmTokenInfo",
159160
"InDataDiscoveryConfig",
160161
"InDataDiscoveryRule",
162+
"InvalidDiscoveryConfigError",
161163
"InvalidLibraryError",
162164
"InvalidRulesetError",
163165
"JsonPath",

‎datamasque/client/discovery.py‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
from pathlib import Path
55
from typing import Iterator, Optional, Union
66

7+
from requests import Response
8+
79
from datamasque.client.base import BaseClient, UploadFile
810
from datamasque.client.exceptions import (
911
AsyncRulesetGenerationInProgressError,
1012
DataMasqueException,
1113
FailedToStartError,
14+
InvalidDiscoveryConfigError,
1215
)
1316
from datamasque.client.models.connection import ConnectionId
1417
from datamasque.client.models.data_selection import (
@@ -208,7 +211,9 @@ def start_schema_discovery_run(self, discovery_config: SchemaDiscoveryRequest) -
208211
RunId: The ID of the started discovery run
209212
210213
Raises:
211-
FailedToStartError: If run fails to start
214+
InvalidDiscoveryConfigError: the run failed to start because the referenced
215+
discovery config is missing, archived, or not in a `valid` validation state.
216+
FailedToStartError: the run failed to start for any other reason.
212217
"""
213218

214219
data = discovery_config.model_dump(exclude_none=True, mode="json")
@@ -218,13 +223,14 @@ def start_schema_discovery_run(self, discovery_config: SchemaDiscoveryRequest) -
218223
data=data,
219224
require_status_check=False,
220225
)
221-
run_data = response.json()
226+
run_data = response.json() if response.content else {}
222227

223228
if response.status_code == 201:
224229
logger.info("Schema discovery run %s started successfully", run_data["id"])
225230
return RunId(run_data["id"])
226231

227232
logger.error("Schema discovery run failed to start: %s", run_data)
233+
self._maybe_raise_discovery_config_error(run_data, response, "Schema discovery")
228234
raise FailedToStartError(
229235
f"Schema discovery run failed to start "
230236
f"(server responded with status {response.status_code}: {response.text}).",
@@ -242,7 +248,9 @@ def start_file_data_discovery_run(self, request: FileDataDiscoveryRequest) -> Ru
242248
RunId: The ID of the started discovery run
243249
244250
Raises:
245-
FailedToStartError: If run fails to start
251+
InvalidDiscoveryConfigError: the run failed to start because the referenced
252+
discovery config is missing, archived, or not in a `valid` validation state.
253+
FailedToStartError: the run failed to start for any other reason.
246254
"""
247255

248256
data = request.model_dump(exclude_none=True, mode="json")
@@ -252,19 +260,33 @@ def start_file_data_discovery_run(self, request: FileDataDiscoveryRequest) -> Ru
252260
data=data,
253261
require_status_check=False,
254262
)
255-
run_data = response.json()
263+
run_data = response.json() if response.content else {}
256264

257265
if response.status_code == 201:
258266
logger.info("File data discovery run %s started successfully", run_data["id"])
259267
return RunId(run_data["id"])
260268

261269
logger.error("File data discovery run failed to start: %s", run_data)
270+
self._maybe_raise_discovery_config_error(run_data, response, "File data discovery")
262271
raise FailedToStartError(
263272
f"File data discovery run failed to start "
264273
f"(server responded with status {response.status_code}: {response.text}).",
265274
response=response,
266275
)
267276

277+
@staticmethod
278+
def _maybe_raise_discovery_config_error(run_data: object, response: Response, run_kind: str) -> None:
279+
"""Raise `InvalidDiscoveryConfigError` if the server's 400 body cites `discovery_config`."""
280+
if not isinstance(run_data, dict) or "discovery_config" not in run_data:
281+
return
282+
283+
errors = run_data["discovery_config"]
284+
detail = errors[0] if isinstance(errors, list) and errors else str(errors)
285+
raise InvalidDiscoveryConfigError(
286+
f"{run_kind} run failed to start due to discovery config error: {detail}",
287+
response=response,
288+
)
289+
268290
def iter_schema_discovery_results(self, run_id: RunId) -> Iterator[SchemaDiscoveryResult]:
269291
"""Lazily iterate all schema discovery results for a run via the paginated v2 endpoint."""
270292

‎datamasque/client/exceptions.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ class InvalidLibraryError(FailedToStartError):
4141
"""Specific error for when runs fail to start due to having an invalid ruleset library."""
4242

4343

44+
class InvalidDiscoveryConfigError(FailedToStartError):
45+
"""
46+
Raised when a discovery run fails to start due to an unusable discovery config.
47+
48+
The referenced config is missing, archived, or not in a `valid` validation state.
49+
"""
50+
51+
4452
class DataMasqueTransportError(DataMasqueException):
4553
"""
4654
Raised when a request to the DataMasque server fails before any response is received.

‎tests/test_discovery.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
DataMasqueApiError,
2626
DataMasqueException,
2727
FailedToStartError,
28+
InvalidDiscoveryConfigError,
2829
)
2930
from datamasque.client.models.connection import ConnectionId, DatabaseConnectionConfig, DatabaseType
3031
from datamasque.client.models.data_selection import SelectedColumns, SelectedFileData, UserSelection
@@ -848,3 +849,70 @@ def test_start_file_data_discovery_run_raises_on_non_201(client):
848849
)
849850
with pytest.raises(FailedToStartError, match="File data discovery run failed to start"):
850851
client.start_file_data_discovery_run(FileDataDiscoveryRequest(connection="nope"))
852+
853+
854+
def test_start_schema_discovery_raises_invalid_discovery_config_when_not_valid(client):
855+
"""A 400 with a `discovery_config` validation message raises the specific subclass."""
856+
with requests_mock.Mocker() as m:
857+
m.post(
858+
"http://test-server/api/schema-discovery/",
859+
json={
860+
"discovery_config": ['Discovery config "my_cfg" cannot be used: validation status is `invalid`.'],
861+
},
862+
status_code=400,
863+
)
864+
with pytest.raises(
865+
InvalidDiscoveryConfigError,
866+
match=r'Schema discovery run failed to start due to discovery config error: .*"my_cfg".*invalid',
867+
):
868+
client.start_schema_discovery_run(
869+
SchemaDiscoveryRequest(connection="conn-1", discovery_config=DiscoveryConfigId(DISCOVERY_CONFIG_ID)),
870+
)
871+
872+
873+
def test_start_schema_discovery_raises_invalid_discovery_config_when_missing(client):
874+
"""A 400 from DRF's PrimaryKeyRelatedField (config not found / archived) is also classified."""
875+
with requests_mock.Mocker() as m:
876+
m.post(
877+
"http://test-server/api/schema-discovery/",
878+
json={"discovery_config": [f'Invalid pk "{DISCOVERY_CONFIG_ID}" - object does not exist.']},
879+
status_code=400,
880+
)
881+
with pytest.raises(InvalidDiscoveryConfigError, match="object does not exist"):
882+
client.start_schema_discovery_run(
883+
SchemaDiscoveryRequest(connection="conn-1", discovery_config=DiscoveryConfigId(DISCOVERY_CONFIG_ID)),
884+
)
885+
886+
887+
def test_start_file_data_discovery_raises_invalid_discovery_config(client):
888+
with requests_mock.Mocker() as m:
889+
m.post(
890+
"http://test-server/api/run-file-data-discovery/",
891+
json={
892+
"discovery_config": ['Discovery config "my_cfg" cannot be used: validation status is `in_progress`.'],
893+
},
894+
status_code=400,
895+
)
896+
with pytest.raises(
897+
InvalidDiscoveryConfigError,
898+
match=r"File data discovery run failed to start due to discovery config error: .*in_progress",
899+
):
900+
client.start_file_data_discovery_run(
901+
FileDataDiscoveryRequest(
902+
connection="conn-1",
903+
discovery_config=DiscoveryConfigId(DISCOVERY_CONFIG_ID),
904+
),
905+
)
906+
907+
908+
def test_start_schema_discovery_non_discovery_config_400_still_raises_generic_error(client):
909+
"""Other 400s (e.g. unknown connection) keep raising the base FailedToStartError."""
910+
with requests_mock.Mocker() as m:
911+
m.post(
912+
"http://test-server/api/schema-discovery/",
913+
json={"connection": ['Invalid pk "nope" - object does not exist.']},
914+
status_code=400,
915+
)
916+
with pytest.raises(FailedToStartError) as exc_info:
917+
client.start_schema_discovery_run(SchemaDiscoveryRequest(connection="nope"))
918+
assert not isinstance(exc_info.value, InvalidDiscoveryConfigError)

0 commit comments

Comments
 (0)