diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 59acac4..8935e93 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.27.0" + ".": "0.28.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 2814bb7..c9a9bfa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1 +1 @@ -configured_endpoints: 62 +configured_endpoints: 63 diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f75e0..0ed65ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.28.0](https://github.com/kernel/hypeman-python/compare/v0.27.0...v0.28.0) (2026-09-01) + + +### Features + +* Add local image tagging on shared content storage ([a466b24](https://github.com/kernel/hypeman-python/commit/a466b241a26272ca1e1e1c5ba0d787c3e26ed582)) + ## [0.27.0](https://github.com/kernel/hypeman-python/compare/v0.2.0...v0.27.0) (2026-08-31) diff --git a/api.md b/api.md index 501bfe7..9f28555 100644 --- a/api.md +++ b/api.md @@ -41,7 +41,7 @@ Methods: Types: ```python -from hypeman.types import Image, ImageListResponse +from hypeman.types import Image, TagImageRequest, ImageListResponse ``` Methods: @@ -50,6 +50,7 @@ Methods: - client.images.list(\*\*params) -> ImageListResponse - client.images.delete(name) -> None - client.images.get(name) -> Image +- client.images.tag(name, \*\*params) -> Image # Instances diff --git a/pyproject.toml b/pyproject.toml index f9656f1..510aa2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hypeman" -version = "0.27.0" +version = "0.28.0" description = "The official Python library for the hypeman API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/hypeman/_version.py b/src/hypeman/_version.py index fdd589f..8b63c5a 100644 --- a/src/hypeman/_version.py +++ b/src/hypeman/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "hypeman" -__version__ = "0.27.0" # x-release-please-version +__version__ = "0.28.0" # x-release-please-version diff --git a/src/hypeman/resources/images.py b/src/hypeman/resources/images.py index c45e965..50bf8c6 100644 --- a/src/hypeman/resources/images.py +++ b/src/hypeman/resources/images.py @@ -6,7 +6,7 @@ import httpx -from ..types import image_list_params, image_create_params +from ..types import image_tag_params, image_list_params, image_create_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property @@ -206,6 +206,44 @@ def get( cast_to=Image, ) + def tag( + self, + name: str, + *, + target: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Image: + """ + Create or update a local image tag + + Args: + target: Target OCI image reference with a tag. The local tag points to the source image + without pulling it again. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not name: + raise ValueError(f"Expected a non-empty value for `name` but received {name!r}") + return self._post( + path_template("/images/{name}/tag", name=name), + body=maybe_transform({"target": target}, image_tag_params.ImageTagParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Image, + ) + class AsyncImagesResource(AsyncAPIResource): @cached_property @@ -388,6 +426,44 @@ async def get( cast_to=Image, ) + async def tag( + self, + name: str, + *, + target: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Image: + """ + Create or update a local image tag + + Args: + target: Target OCI image reference with a tag. The local tag points to the source image + without pulling it again. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not name: + raise ValueError(f"Expected a non-empty value for `name` but received {name!r}") + return await self._post( + path_template("/images/{name}/tag", name=name), + body=await async_maybe_transform({"target": target}, image_tag_params.ImageTagParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Image, + ) + class ImagesResourceWithRawResponse: def __init__(self, images: ImagesResource) -> None: @@ -405,6 +481,9 @@ def __init__(self, images: ImagesResource) -> None: self.get = to_raw_response_wrapper( images.get, ) + self.tag = to_raw_response_wrapper( + images.tag, + ) class AsyncImagesResourceWithRawResponse: @@ -423,6 +502,9 @@ def __init__(self, images: AsyncImagesResource) -> None: self.get = async_to_raw_response_wrapper( images.get, ) + self.tag = async_to_raw_response_wrapper( + images.tag, + ) class ImagesResourceWithStreamingResponse: @@ -441,6 +523,9 @@ def __init__(self, images: ImagesResource) -> None: self.get = to_streamed_response_wrapper( images.get, ) + self.tag = to_streamed_response_wrapper( + images.tag, + ) class AsyncImagesResourceWithStreamingResponse: @@ -459,3 +544,6 @@ def __init__(self, images: AsyncImagesResource) -> None: self.get = async_to_streamed_response_wrapper( images.get, ) + self.tag = async_to_streamed_response_wrapper( + images.tag, + ) diff --git a/src/hypeman/types/__init__.py b/src/hypeman/types/__init__.py index ca69a64..e786532 100644 --- a/src/hypeman/types/__init__.py +++ b/src/hypeman/types/__init__.py @@ -36,6 +36,7 @@ from .available_device import AvailableDevice as AvailableDevice from .build_provenance import BuildProvenance as BuildProvenance from .health_check_tcp import HealthCheckTcp as HealthCheckTcp +from .image_tag_params import ImageTagParams as ImageTagParams from .build_list_params import BuildListParams as BuildListParams from .capabilities_host import CapabilitiesHost as CapabilitiesHost from .health_check_exec import HealthCheckExec as HealthCheckExec diff --git a/src/hypeman/types/image_tag_params.py b/src/hypeman/types/image_tag_params.py new file mode 100644 index 0000000..256785f --- /dev/null +++ b/src/hypeman/types/image_tag_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ImageTagParams"] + + +class ImageTagParams(TypedDict, total=False): + target: Required[str] + """Target OCI image reference with a tag. + + The local tag points to the source image without pulling it again. + """ diff --git a/tests/api_resources/test_images.py b/tests/api_resources/test_images.py index 3d49984..1ef7c42 100644 --- a/tests/api_resources/test_images.py +++ b/tests/api_resources/test_images.py @@ -9,7 +9,10 @@ from hypeman import Hypeman, AsyncHypeman from tests.utils import assert_matches_type -from hypeman.types import Image, ImageListResponse +from hypeman.types import ( + Image, + ImageListResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -192,6 +195,52 @@ def test_path_params_get(self, client: Hypeman) -> None: "", ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_tag(self, client: Hypeman) -> None: + image = client.images.tag( + name="name", + target="docker.io/library/nginx:stable", + ) + assert_matches_type(Image, image, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_tag(self, client: Hypeman) -> None: + response = client.images.with_raw_response.tag( + name="name", + target="docker.io/library/nginx:stable", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + image = response.parse() + assert_matches_type(Image, image, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_tag(self, client: Hypeman) -> None: + with client.images.with_streaming_response.tag( + name="name", + target="docker.io/library/nginx:stable", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + image = response.parse() + assert_matches_type(Image, image, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_tag(self, client: Hypeman) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): + client.images.with_raw_response.tag( + name="", + target="docker.io/library/nginx:stable", + ) + class TestAsyncImages: parametrize = pytest.mark.parametrize( @@ -372,3 +421,49 @@ async def test_path_params_get(self, async_client: AsyncHypeman) -> None: await async_client.images.with_raw_response.get( "", ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_tag(self, async_client: AsyncHypeman) -> None: + image = await async_client.images.tag( + name="name", + target="docker.io/library/nginx:stable", + ) + assert_matches_type(Image, image, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_tag(self, async_client: AsyncHypeman) -> None: + response = await async_client.images.with_raw_response.tag( + name="name", + target="docker.io/library/nginx:stable", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + image = await response.parse() + assert_matches_type(Image, image, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_tag(self, async_client: AsyncHypeman) -> None: + async with async_client.images.with_streaming_response.tag( + name="name", + target="docker.io/library/nginx:stable", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + image = await response.parse() + assert_matches_type(Image, image, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_tag(self, async_client: AsyncHypeman) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): + await async_client.images.with_raw_response.tag( + name="", + target="docker.io/library/nginx:stable", + )