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",
+ )