Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Support for querying OS images by instance type via `verda.images.get(instance_type=...)`

### Changed

- Refactored `Image` model to use `@dataclass` and `@dataclass_json` for consistency with `Instance` and `Volume`

## [1.24.0] - 2026-03-30

### Added
Expand Down
46 changes: 36 additions & 10 deletions tests/unit_tests/images/test_images.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import json

import responses # https://github.com/getsentry/responses
from responses import matchers

from verda.images import Image, ImagesService

IMAGE_RESPONSE = {
'id': '0888da25-bb0d-41cc-a191-dccae45d96fd',
'name': 'Ubuntu 20.04 + CUDA 11.0',
'details': ['Ubuntu 20.04', 'CUDA 11.0'],
'image_type': 'ubuntu-20.04-cuda-11.0',
}


def test_images(http_client):
# arrange - add response mock
# arrange
responses.add(
responses.GET,
http_client._base_url + '/images',
json=[
{
'id': '0888da25-bb0d-41cc-a191-dccae45d96fd',
'name': 'Ubuntu 20.04 + CUDA 11.0',
'details': ['Ubuntu 20.04', 'CUDA 11.0'],
'image_type': 'ubuntu-20.04-cuda-11.0',
}
],
json=[IMAGE_RESPONSE],
status=200,
)

Expand All @@ -34,4 +37,27 @@ def test_images(http_client):
assert isinstance(images[0].details, list)
assert images[0].details[0] == 'Ubuntu 20.04'
assert images[0].details[1] == 'CUDA 11.0'
assert isinstance(images[0].__str__(), str)
assert json.loads(str(images[0])) == IMAGE_RESPONSE


def test_images_filter_by_instance_type(http_client):
# arrange
responses.add(
responses.GET,
http_client._base_url + '/images',
match=[matchers.query_param_matcher({'instance_type': '1A100.22V'})],
json=[IMAGE_RESPONSE],
status=200,
)

image_service = ImagesService(http_client)

# act
images = image_service.get(instance_type='1A100.22V')

# assert
assert isinstance(images, list)
assert len(images) == 1
assert isinstance(images[0], Image)
assert images[0].id == '0888da25-bb0d-41cc-a191-dccae45d96fd'
assert images[0].image_type == 'ubuntu-20.04-cuda-11.0'
95 changes: 27 additions & 68 deletions verda/images/_images.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,29 @@
from verda.helpers import stringify_class_object_properties
from dataclasses import dataclass

from dataclasses_json import Undefined, dataclass_json

IMAGES_ENDPOINT = '/images'


@dataclass_json(undefined=Undefined.EXCLUDE)
@dataclass
class Image:
"""An image model class."""

def __init__(self, id: str, name: str, image_type: str, details: list[str]) -> None:
"""Initialize an image object.

:param id: image id
:type id: str
:param name: image name
:type name: str
:param image_type: image type, e.g. 'ubuntu-20.04-cuda-11.0'
:type image_type: str
:param details: image details
:type details: list[str]
"""
self._id = id
self._name = name
self._image_type = image_type
self._details = details

@property
def id(self) -> str:
"""Get the image id.
"""Represents an OS image available for instances.

:return: image id
:rtype: str
"""
return self._id

@property
def name(self) -> str:
"""Get the image name.

:return: image name
:rtype: str
"""
return self._name
Attributes:
id: Unique identifier for the image.
name: Human-readable name of the image.
image_type: Image type identifier, e.g. 'ubuntu-20.04-cuda-11.0'.
details: List of additional image details.
"""

@property
def image_type(self) -> str:
"""Get the image type.

:return: image type
:rtype: str
"""
return self._image_type

@property
def details(self) -> list[str]:
"""Get the image details.

:return: image details
:rtype: list[str]
"""
return self._details
id: str
name: str
image_type: str
details: list[str]

def __str__(self) -> str:
"""Returns a string of the json representation of the image.

:return: json representation of the image
:rtype: str
"""
return stringify_class_object_properties(self)
return self.to_json(indent=2)


class ImagesService:
Expand All @@ -74,15 +32,16 @@ class ImagesService:
def __init__(self, http_client) -> None:
self._http_client = http_client

def get(self) -> list[Image]:
def get(self, instance_type: str | None = None) -> list[Image]:
"""Get the available instance images.

:return: list of images objects
:rtype: list[Image]
Args:
instance_type: Filter OS images by instance type, e.g. '1A100.22V'.
Default is all instance images.

Returns:
List of Image objects.
"""
images = self._http_client.get(IMAGES_ENDPOINT).json()
image_objects = [
Image(image['id'], image['name'], image['image_type'], image['details'])
for image in images
]
return image_objects
params = {'instance_type': instance_type} if instance_type else None
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

params = {'instance_type': instance_type} if instance_type else None treats empty strings as “no filter”. If a caller passes instance_type='' (or any other falsy-but-valid string), the SDK will silently omit the query param. Prefer checking instance_type is not None (or always passing {'instance_type': instance_type} and letting requests drop None values) so only None means “no filter.”

Suggested change
params = {'instance_type': instance_type} if instance_type else None
params = {'instance_type': instance_type} if instance_type is not None else None

Copilot uses AI. Check for mistakes.
images = self._http_client.get(IMAGES_ENDPOINT, params=params).json()
return [Image.from_dict(image) for image in images]
Loading