Skip to content

Commit 2e8c224

Browse files
committed
refactor: move ConcurrencyConflictError to platform, fix cast style, exit code 3, GUI handler
- Move ConcurrencyConflictError from system/_exceptions to platform/_exceptions and re-export from aignostics.platform — eliminates application→system bidirectional dependency - Revert cast() string-form regression in runs.py (restore TC006-compliant cast("dict[str, Any]", ...) in all four call sites) - CLI conflict handlers now exit 3 (distinct from exit 2 for not-found) - Add specific ConcurrencyConflictError handler in GUI metadata editor with "reload and retry" guidance instead of generic error message - Update all imports and test assertions accordingly
1 parent 22a26a6 commit 2e8c224

10 files changed

Lines changed: 42 additions & 29 deletions

File tree

src/aignostics/application/_cli.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,7 +1227,7 @@ def run_update_metadata(
12271227
"""Update custom metadata for a run."""
12281228
import json # noqa: PLC0415
12291229

1230-
from aignostics.system import ConcurrencyConflictError # noqa: PLC0415
1230+
from aignostics.platform import ConcurrencyConflictError # noqa: PLC0415
12311231

12321232
logger.trace("Updating custom metadata for run with ID '{}'", run_id)
12331233

@@ -1252,7 +1252,7 @@ def run_update_metadata(
12521252
except ConcurrencyConflictError as e:
12531253
logger.warning(f"Concurrency conflict updating metadata for run '{run_id}': {e}")
12541254
console.print(f"[warning]Warning:[/warning] Metadata was modified by another process. Re-read and retry: {e}")
1255-
sys.exit(2)
1255+
sys.exit(3)
12561256
except ValueError as e:
12571257
logger.warning(f"Run ID '{run_id}' invalid or metadata invalid: {e}")
12581258
console.print(f"[warning]Warning:[/warning] Run ID '{run_id}' invalid or metadata invalid: {e}")
@@ -1284,7 +1284,7 @@ def run_update_item_metadata(
12841284
"""Update custom metadata for an item in a run."""
12851285
import json # noqa: PLC0415
12861286

1287-
from aignostics.system import ConcurrencyConflictError # noqa: PLC0415
1287+
from aignostics.platform import ConcurrencyConflictError # noqa: PLC0415
12881288

12891289
logger.trace("Updating custom metadata for item '{}' in run with ID '{}'", external_id, run_id)
12901290

@@ -1311,7 +1311,7 @@ def run_update_item_metadata(
13111311
except ConcurrencyConflictError as e:
13121312
logger.warning("Concurrency conflict updating metadata for item '{}' in run '{}': {}", external_id, run_id, e)
13131313
console.print(f"[warning]Warning:[/warning] Metadata was modified by another process. Re-read and retry: {e}")
1314-
sys.exit(2)
1314+
sys.exit(3)
13151315
except ValueError as e:
13161316
logger.warning(
13171317
"Run ID '{}' or item external ID '{}' invalid or metadata invalid: {}",

src/aignostics/application/_gui/_page_application_run_describe.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@
1919
)
2020
from nicegui import run as nicegui_run
2121

22-
from aignostics.platform import ArtifactOutput, ItemOutput, ItemResult, ItemState, Run, RunState
22+
from aignostics.platform import (
23+
ArtifactOutput,
24+
ConcurrencyConflictError,
25+
ItemOutput,
26+
ItemResult,
27+
ItemState,
28+
Run,
29+
RunState,
30+
)
2331
from aignostics.third_party.showinfm.showinfm import show_in_file_manager
2432
from aignostics.utils import GUILocalFilePicker, get_user_data_directory
2533

@@ -747,6 +755,11 @@ async def handle_metadata_change(e: Any) -> None: # noqa: ANN401
747755
)
748756
ui.notify("Custom metadata updated successfully!", type="positive")
749757
ui.navigate.reload()
758+
except ConcurrencyConflictError:
759+
ui.notify(
760+
"Metadata was modified by another process — reload the page and retry.",
761+
type="warning",
762+
)
750763
except Exception as ex:
751764
ui.notify(f"Failed to update custom metadata: {ex!s}", type="negative")
752765

src/aignostics/application/_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ApplicationSummary,
2424
ApplicationVersion,
2525
Client,
26+
ConcurrencyConflictError,
2627
ForbiddenException,
2728
InputArtifact,
2829
InputItem,
@@ -33,7 +34,6 @@
3334
RunState,
3435
)
3536
from aignostics.platform import Service as PlatformService
36-
from aignostics.system import ConcurrencyConflictError
3737
from aignostics.utils import BaseService, Health, sanitize_path_component
3838
from aignostics.wsi import Service as WSIService
3939

src/aignostics/platform/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
TOKEN_URL_STAGING,
8282
TOKEN_URL_TEST,
8383
)
84+
from ._exceptions import ConcurrencyConflictError
8485
from ._messages import AUTHENTICATION_FAILED, NOT_YET_IMPLEMENTED, UNKNOWN_ENDPOINT_URL
8586
from ._sdk_metadata import (
8687
PipelineConfig,
@@ -155,6 +156,7 @@
155156
"Artifact",
156157
"ArtifactOutput",
157158
"Client",
159+
"ConcurrencyConflictError",
158160
"Documents",
159161
"ForbiddenException",
160162
"InputArtifact",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Exceptions of platform module."""
2+
3+
4+
class ConcurrencyConflictError(ValueError):
5+
"""Raised when an optimistic concurrency precondition (HTTP 412) fails.
6+
7+
Subclasses ValueError so existing ``except ValueError`` callers still catch it,
8+
while callers that need to distinguish a conflict from a bad-ID error can use
9+
``except ConcurrencyConflictError``.
10+
"""

src/aignostics/platform/resources/runs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ def update_custom_metadata(
621621
self._api.put_run_custom_metadata_v1_runs_run_id_custom_metadata_put(
622622
self.run_id,
623623
custom_metadata_update_request=CustomMetadataUpdateRequest(
624-
custom_metadata=cast(dict[str, Any], convert_to_json_serializable(custom_metadata)), # noqa: TC006
624+
custom_metadata=cast("dict[str, Any]", convert_to_json_serializable(custom_metadata)),
625625
custom_metadata_checksum=custom_metadata_checksum,
626626
),
627627
_request_timeout=settings().run_submit_timeout,
@@ -660,7 +660,7 @@ def update_item_custom_metadata(
660660
self.run_id,
661661
external_id,
662662
custom_metadata_update_request=CustomMetadataUpdateRequest(
663-
custom_metadata=cast(dict[str, Any], convert_to_json_serializable(custom_metadata)), # noqa: TC006
663+
custom_metadata=cast("dict[str, Any]", convert_to_json_serializable(custom_metadata)),
664664
custom_metadata_checksum=custom_metadata_checksum,
665665
),
666666
_request_timeout=settings().run_submit_timeout,
@@ -761,7 +761,7 @@ def submit(
761761
payload = RunCreationRequest(
762762
application_id=application_id,
763763
version_number=application_version,
764-
custom_metadata=cast(dict[str, Any], convert_to_json_serializable(custom_metadata)), # noqa: TC006
764+
custom_metadata=cast("dict[str, Any]", convert_to_json_serializable(custom_metadata)),
765765
items=items,
766766
scheduling=scheduling,
767767
)
@@ -940,7 +940,7 @@ def _amend_input_items_with_sdk_metadata(items: builtins.list[ItemCreationReques
940940
validate_item_sdk_metadata(base_sdk_metadata)
941941
item_custom_metadata["sdk"] = base_sdk_metadata
942942

943-
item.custom_metadata = cast(dict[str, Any], convert_to_json_serializable(item_custom_metadata)) # noqa: TC006
943+
item.custom_metadata = cast("dict[str, Any]", convert_to_json_serializable(item_custom_metadata))
944944

945945
def _validate_input_items(self, payload: RunCreationRequest) -> None:
946946
"""Validates the input items in a run creation request.

src/aignostics/system/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
"""System module."""
22

33
from ._cli import cli
4-
from ._exceptions import ConcurrencyConflictError
54
from ._service import Service
65
from ._settings import Settings
76

87
__all__ = [
9-
"ConcurrencyConflictError",
108
"Service",
119
"Settings",
1210
"cli",

src/aignostics/system/_exceptions.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,3 @@ class OpenAPISchemaError(ValueError):
77
def __init__(self, error: Exception) -> None:
88
"""Initialize exception with the underlying error."""
99
super().__init__(f"Failed to load OpenAPI schema: {error}")
10-
11-
12-
class ConcurrencyConflictError(ValueError):
13-
"""Raised when an optimistic concurrency precondition (HTTP 412) fails.
14-
15-
Subclasses ValueError so existing ``except ValueError`` callers still catch it,
16-
while callers that need to distinguish a conflict from a bad-ID error can use
17-
``except ConcurrencyConflictError``.
18-
"""

tests/aignostics/application/cli_test.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,8 +1238,8 @@ def test_cli_run_update_metadata_success_with_checksum(runner: CliRunner) -> Non
12381238

12391239
@pytest.mark.unit
12401240
def test_cli_run_update_metadata_concurrency_conflict(runner: CliRunner) -> None:
1241-
"""Check run update-metadata exits 2 with a clear message on ConcurrencyConflictError."""
1242-
from aignostics.system import ConcurrencyConflictError
1241+
"""Check run update-metadata exits 3 with a clear message on ConcurrencyConflictError."""
1242+
from aignostics.platform import ConcurrencyConflictError
12431243

12441244
with patch(APPLICATION_CLI_SERVICE_PATCH_TARGET) as mock_service_cls:
12451245
mock_service_cls.return_value.application_run_update_custom_metadata.side_effect = ConcurrencyConflictError(
@@ -1249,7 +1249,7 @@ def test_cli_run_update_metadata_concurrency_conflict(runner: CliRunner) -> None
12491249
cli,
12501250
["application", "run", "update-metadata", "run-123", _TEST_METADATA_JSON, "--checksum", "old"],
12511251
)
1252-
assert result.exit_code == 2
1252+
assert result.exit_code == 3
12531253
assert "modified by another process" in result.output
12541254

12551255

@@ -1279,8 +1279,8 @@ def test_cli_run_update_item_metadata_success_with_checksum(runner: CliRunner) -
12791279

12801280
@pytest.mark.unit
12811281
def test_cli_run_update_item_metadata_concurrency_conflict(runner: CliRunner) -> None:
1282-
"""Check run update-item-metadata exits 2 with a clear message on ConcurrencyConflictError."""
1283-
from aignostics.system import ConcurrencyConflictError
1282+
"""Check run update-item-metadata exits 3 with a clear message on ConcurrencyConflictError."""
1283+
from aignostics.platform import ConcurrencyConflictError
12841284

12851285
with patch(APPLICATION_CLI_SERVICE_PATCH_TARGET) as mock_service_cls:
12861286
mock_service_cls.return_value.application_run_update_item_custom_metadata.side_effect = (
@@ -1299,7 +1299,7 @@ def test_cli_run_update_item_metadata_concurrency_conflict(runner: CliRunner) ->
12991299
"old",
13001300
],
13011301
)
1302-
assert result.exit_code == 2
1302+
assert result.exit_code == 3
13031303
assert "modified by another process" in result.output
13041304

13051305

tests/aignostics/application/service_test.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
from typer.testing import CliRunner
99

1010
from aignostics.application import Service as ApplicationService
11-
from aignostics.platform import ApiException, NotFoundException, RunData, RunOutput
12-
from aignostics.system import ConcurrencyConflictError
11+
from aignostics.platform import ApiException, ConcurrencyConflictError, NotFoundException, RunData, RunOutput
1312
from tests.constants_test import (
1413
HETA_APPLICATION_ID,
1514
HETA_APPLICATION_VERSION,

0 commit comments

Comments
 (0)