|
14 | 14 | from collections.abc import Iterator |
15 | 15 | from contextlib import suppress |
16 | 16 | from pathlib import Path |
| 17 | +from typing import cast |
17 | 18 |
|
18 | 19 | import data_designer.config as dd |
19 | 20 | import httpx |
|
28 | 29 | AnonymizerConfigValidationError, |
29 | 30 | AnonymizerPreviewError, |
30 | 31 | ) |
31 | | -from nemo_anonymizer_plugin.sdk.job_resources import TERMINAL_INCOMPLETE_STATUSES, AnonymizerJobResource |
| 32 | +from nemo_anonymizer_plugin.sdk.job_resources import ( |
| 33 | + MAX_CONSECUTIVE_POLL_ERRORS, |
| 34 | + TERMINAL_INCOMPLETE_STATUSES, |
| 35 | + AnonymizerJobResource, |
| 36 | +) |
32 | 37 | from nemo_anonymizer_plugin.sdk.resources import AnonymizerPreviewResult |
33 | 38 | from nemo_platform import NeMoPlatform |
34 | 39 | from nemo_platform_plugin.files.client import FilesClient |
@@ -113,13 +118,20 @@ def _workspace_client(sdk: NeMoPlatform, workspace: str) -> NeMoPlatform: |
113 | 118 | ) |
114 | 119 |
|
115 | 120 |
|
| 121 | +def _require_workspace(workspace: str | None) -> str: |
| 122 | + assert workspace is not None |
| 123 | + return workspace |
| 124 | + |
| 125 | + |
116 | 126 | def _anonymizer_url(sdk: NeMoPlatform, workspace: str, path: str) -> str: |
117 | 127 | return f"{str(sdk.base_url).rstrip('/')}/apis/anonymizer/v2/workspaces/{workspace}/{path.lstrip('/')}" |
118 | 128 |
|
119 | 129 |
|
120 | | -def _raw_anonymizer_post(sdk: NeMoPlatform, workspace: str, path: str, payload: dict[str, object]) -> httpx.Response: |
| 130 | +def _raw_anonymizer_post( |
| 131 | + sdk: NeMoPlatform, workspace: str | None, path: str, payload: dict[str, object] |
| 132 | +) -> httpx.Response: |
121 | 133 | return sdk._client.post( |
122 | | - _anonymizer_url(sdk, workspace, path), |
| 134 | + _anonymizer_url(sdk, _require_workspace(workspace), path), |
123 | 135 | json=payload, |
124 | 136 | headers=_string_headers(sdk), |
125 | 137 | timeout=sdk.timeout, |
@@ -166,12 +178,12 @@ def _rewrite_config() -> AnonymizerConfig: |
166 | 178 | ) |
167 | 179 |
|
168 | 180 |
|
169 | | -def _fileset_ref(workspace: str, fileset: str, path: str) -> str: |
170 | | - return f"{workspace}/{fileset}#{path}" |
| 181 | +def _fileset_ref(workspace: str | None, fileset: str, path: str) -> str: |
| 182 | + return f"{_require_workspace(workspace)}/{fileset}#{path}" |
171 | 183 |
|
172 | 184 |
|
173 | | -def _fileset_uri_ref(workspace: str, fileset: str, path: str) -> str: |
174 | | - return f"fileset://{workspace}/{fileset}#{path}" |
| 185 | +def _fileset_uri_ref(workspace: str | None, fileset: str, path: str) -> str: |
| 186 | + return f"fileset://{_require_workspace(workspace)}/{fileset}#{path}" |
175 | 187 |
|
176 | 188 |
|
177 | 189 | def _input_spec(source: str, *, text_column: str = TEXT_COLUMN) -> AnonymizerInputSpec: |
@@ -281,14 +293,32 @@ def _job_name(job: AnonymizerJobResource) -> str: |
281 | 293 |
|
282 | 294 | def _wait_for_anonymizer_job(job: AnonymizerJobResource, *, timeout_seconds: float) -> None: |
283 | 295 | deadline = time.monotonic() + timeout_seconds |
284 | | - status = job.get_job_status() |
| 296 | + status = None |
| 297 | + consecutive_poll_errors = 0 |
| 298 | + last_poll_error: Exception | None = None |
285 | 299 | while status not in {"completed", *TERMINAL_INCOMPLETE_STATUSES}: |
286 | 300 | if time.monotonic() >= deadline: |
287 | | - logs = job.get_logs() |
288 | | - tail = logs[-5:] if logs else [] |
289 | | - raise TimeoutError(f"Anonymizer job {_job_name(job)} timed out with status {status!r}; logs={tail!r}") |
290 | | - time.sleep(ANONYMIZER_POLL_INTERVAL_SECONDS) |
291 | | - status = job.get_job_status() |
| 301 | + try: |
| 302 | + logs = job.get_logs() |
| 303 | + tail = logs[-5:] if logs else [] |
| 304 | + except Exception as exc: |
| 305 | + tail = [f"<log retrieval failed: {exc!r}>"] |
| 306 | + raise TimeoutError( |
| 307 | + f"Anonymizer job {_job_name(job)} timed out with status {status!r}; " |
| 308 | + f"last_poll_error={last_poll_error!r}; logs={tail!r}" |
| 309 | + ) |
| 310 | + try: |
| 311 | + status = job.get_job_status() |
| 312 | + except Exception as exc: |
| 313 | + consecutive_poll_errors += 1 |
| 314 | + last_poll_error = exc |
| 315 | + if consecutive_poll_errors >= MAX_CONSECUTIVE_POLL_ERRORS: |
| 316 | + raise |
| 317 | + else: |
| 318 | + consecutive_poll_errors = 0 |
| 319 | + last_poll_error = None |
| 320 | + if status not in {"completed", *TERMINAL_INCOMPLETE_STATUSES}: |
| 321 | + time.sleep(ANONYMIZER_POLL_INTERVAL_SECONDS) |
292 | 322 | assert status == "completed" |
293 | 323 |
|
294 | 324 |
|
@@ -433,7 +463,11 @@ def test_mock_provider_chat_completion_works_through_minikube_ingress( |
433 | 463 | }, |
434 | 464 | ) |
435 | 465 |
|
436 | | - assert SUBSTITUTE_NAME in response["choices"][0]["message"]["content"] |
| 466 | + choices = cast(list[dict[str, object]], response["choices"]) |
| 467 | + message = cast(dict[str, object], choices[0]["message"]) |
| 468 | + content = message["content"] |
| 469 | + assert isinstance(content, str) |
| 470 | + assert SUBSTITUTE_NAME in content |
437 | 471 |
|
438 | 472 |
|
439 | 473 | def test_file_upload_round_trips_through_minikube_ingress( |
@@ -558,7 +592,7 @@ def test_preview_missing_text_column_is_rejected( |
558 | 592 |
|
559 | 593 |
|
560 | 594 | def test_preview_invalid_strategy_payload_is_rejected(anonymizer_sdk: NeMoPlatform, anonymizer_fileset: str) -> None: |
561 | | - payload = { |
| 595 | + payload: dict[str, object] = { |
562 | 596 | "config": {"replace": {"kind": "explode"}, "emit_telemetry": False}, |
563 | 597 | "data": { |
564 | 598 | "source": _fileset_ref(anonymizer_sdk.workspace, anonymizer_fileset, CSV_REMOTE_PATH), |
|
0 commit comments