Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/dstack/_internal/cli/services/presets/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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.
Expand Down Expand Up @@ -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()):
Expand Down
25 changes: 24 additions & 1 deletion src/tests/_internal/cli/services/presets/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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",
[
Expand Down