diff --git a/src/dstack/_internal/cli/services/presets/registry.py b/src/dstack/_internal/cli/services/presets/registry.py index 82e6f7008b..76fa085455 100644 --- a/src/dstack/_internal/cli/services/presets/registry.py +++ b/src/dstack/_internal/cli/services/presets/registry.py @@ -116,7 +116,9 @@ def push_preset_to_registry(store: PresetStore, local_ref: str, registry_ref: st raise CLIError(f"Preset {local_ref!r} cannot be pushed: {e}") from e try: client.presets.push(project, name=name, spec=spec) - except (URLNotFoundError, MethodNotAllowedError): + except URLNotFoundError as e: + raise _registry_url_not_found_error(project, client) from e + except MethodNotAllowedError: raise _registry_not_supported_error(project, client) console.print("OK") @@ -126,7 +128,9 @@ def pull_preset_from_registry(store: PresetStore, registry_ref: str) -> None: client = resolve_registry_client(project) try: remote = client.presets.get(project, name_or_id) - except (URLNotFoundError, MethodNotAllowedError): + except URLNotFoundError as e: + raise _registry_url_not_found_error(project, client) from e + except MethodNotAllowedError: raise _registry_not_supported_error(project, client) except ResourceNotExistsError as e: # The server's detail names the bare ref; the qualified one reads better. @@ -278,6 +282,17 @@ def _registry_not_supported_error(project: str, client: APIClient) -> CLIError: ) +def _registry_url_not_found_error(project: str, client: APIClient) -> CLIError: + """Distinguishes a missing project from a server without registry routes.""" + try: + client.projects.get(project) + except (URLNotFoundError, ResourceNotExistsError): + return CLIError(f"Project {project!r} does not exist") + except MethodNotAllowedError: + pass + return _registry_not_supported_error(project, client) + + def _relative_pushed_path(local_path: str, preset_dir: Path) -> str: path = Path(local_path) for base in (preset_dir, preset_dir.resolve()): diff --git a/src/tests/_internal/cli/services/presets/test_registry.py b/src/tests/_internal/cli/services/presets/test_registry.py index c5bd5a0092..edd896883e 100644 --- a/src/tests/_internal/cli/services/presets/test_registry.py +++ b/src/tests/_internal/cli/services/presets/test_registry.py @@ -97,6 +97,16 @@ def _raise_if_failing(self): raise self.error +class FakeProjectsAPIClient: + def __init__(self): + self.error: Optional[Exception] = None + + def get(self, project_name): + if self.error is not None: + raise self.error + return SimpleNamespace(name=project_name) + + def _registry_preset_info(*, name: Optional[str] = "qwen38") -> PushPresetResponse: return PushPresetResponse( id=uuid4(), @@ -183,7 +193,13 @@ def _archive_member_texts(blob: bytes) -> dict[str, str]: @pytest.fixture def stub_client(monkeypatch: pytest.MonkeyPatch): fake = FakePresetsAPIClient() - client = SimpleNamespace(presets=fake, files=fake.files, base_url="http://test-server") + fake.projects = FakeProjectsAPIClient() + client = SimpleNamespace( + presets=fake, + files=fake.files, + projects=fake.projects, + base_url="http://test-server", + ) monkeypatch.setattr(registry_module, "resolve_registry_client", lambda project: client) return fake @@ -656,6 +672,13 @@ def test_maps_a_missing_preset_to_a_qualified_does_not_exist_error( with pytest.raises(CLIError, match="'main/qwen38' does not exist"): pull_preset_from_registry(PresetStore(tmp_path / "presets"), "main/qwen38") + def test_maps_a_missing_project_to_a_does_not_exist_error(self, tmp_path, stub_client): + stub_client.error = URLNotFoundError("Status code 404") + stub_client.projects.error = URLNotFoundError("Status code 404") + + with pytest.raises(CLIError, match="Project 'unknown-project' does not exist"): + pull_preset_from_registry(PresetStore(tmp_path / "presets"), "unknown-project/qwen38") + @pytest.mark.parametrize( "error", [