diff --git a/CHANGELOG.md b/CHANGELOG.md index 64d9c68..824e7ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/tests/unit_tests/images/test_images.py b/tests/unit_tests/images/test_images.py index a5e9828..2d1ed33 100644 --- a/tests/unit_tests/images/test_images.py +++ b/tests/unit_tests/images/test_images.py @@ -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, ) @@ -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' diff --git a/verda/images/_images.py b/verda/images/_images.py index 1ed1e48..a7f2129 100644 --- a/verda/images/_images.py +++ b/verda/images/_images.py @@ -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: @@ -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 + images = self._http_client.get(IMAGES_ENDPOINT, params=params).json() + return [Image.from_dict(image) for image in images]