Skip to content

Commit 4bd66a8

Browse files
authored
python(feat): add internal units client (#604)
1 parent 960fe17 commit 4bd66a8

3 files changed

Lines changed: 129 additions & 0 deletions

File tree

python/lib/sift_client/_internal/low_level_wrappers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from sift_client._internal.low_level_wrappers.runs import RunsLowLevelClient
1313
from sift_client._internal.low_level_wrappers.tags import TagsLowLevelClient
1414
from sift_client._internal.low_level_wrappers.test_results import TestResultsLowLevelClient
15+
from sift_client._internal.low_level_wrappers.units import UnitsLowLevelClient
1516
from sift_client._internal.low_level_wrappers.upload import UploadLowLevelClient
1617

1718
__all__ = [
@@ -27,5 +28,6 @@
2728
"RunsLowLevelClient",
2829
"TagsLowLevelClient",
2930
"TestResultsLowLevelClient",
31+
"UnitsLowLevelClient",
3032
"UploadLowLevelClient",
3133
]
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import TYPE_CHECKING, Any, cast
5+
6+
from sift.unit.v2.unit_pb2 import (
7+
CreateUnitRequest,
8+
CreateUnitResponse,
9+
ListUnitsRequest,
10+
ListUnitsResponse,
11+
)
12+
from sift.unit.v2.unit_pb2 import Unit as UnitProto
13+
from sift.unit.v2.unit_pb2_grpc import UnitServiceStub
14+
15+
from sift_client._internal.low_level_wrappers.base import DEFAULT_PAGE_SIZE, LowLevelClientBase
16+
from sift_client.transport import WithGrpcClient
17+
18+
if TYPE_CHECKING:
19+
from sift_client.transport.grpc_transport import GrpcClient
20+
21+
# Configure logging
22+
logger = logging.getLogger(__name__)
23+
24+
25+
class UnitsLowLevelClient(LowLevelClientBase, WithGrpcClient):
26+
"""Low-level client for the Units service.
27+
28+
This class provides a thin wrapper around the autogenerated bindings for the Units service.
29+
"""
30+
31+
def __init__(self, grpc_client: GrpcClient):
32+
"""Initialize the UnitsLowLevelClient.
33+
34+
Args:
35+
grpc_client: The gRPC client to use for making API calls.
36+
"""
37+
super().__init__(grpc_client)
38+
39+
async def create_unit(self, name: str) -> UnitProto:
40+
"""Create a new unit.
41+
42+
If a unit with the same name already exists, it is returned instead of creating a duplicate.
43+
44+
Args:
45+
name: The name of the unit.
46+
47+
Returns:
48+
The created unit proto, whose unit_id is used to reference the unit.
49+
50+
Raises:
51+
ValueError: If name is not provided.
52+
"""
53+
if not name:
54+
raise ValueError("name must be provided")
55+
56+
request = CreateUnitRequest(name=name)
57+
response = await self._grpc_client.get_stub(UnitServiceStub).CreateUnit(request)
58+
return cast("CreateUnitResponse", response).unit
59+
60+
async def list_units(
61+
self,
62+
*,
63+
page_size: int | None = DEFAULT_PAGE_SIZE,
64+
page_token: str | None = None,
65+
query_filter: str | None = None,
66+
order_by: str | None = None,
67+
) -> tuple[list[UnitProto], str]:
68+
"""List units with optional filtering and pagination.
69+
70+
Args:
71+
page_size: The maximum number of units to return.
72+
page_token: A page token for pagination.
73+
query_filter: A CEL filter string (e.g. filtering on unit_id or abbreviated_name).
74+
order_by: How to order the retrieved units.
75+
76+
Returns:
77+
A tuple of (unit protos, next_page_token).
78+
"""
79+
request_kwargs: dict[str, Any] = {}
80+
if page_size is not None:
81+
request_kwargs["page_size"] = page_size
82+
if page_token is not None:
83+
request_kwargs["page_token"] = page_token
84+
if query_filter is not None:
85+
request_kwargs["filter"] = query_filter
86+
if order_by is not None:
87+
request_kwargs["order_by"] = order_by
88+
89+
request = ListUnitsRequest(**request_kwargs)
90+
response = await self._grpc_client.get_stub(UnitServiceStub).ListUnits(request)
91+
response = cast("ListUnitsResponse", response)
92+
93+
return list(response.units), response.next_page_token
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Tests for the Units low-level wrapper."""
2+
3+
from unittest.mock import AsyncMock, MagicMock
4+
5+
import pytest
6+
from sift.unit.v2.unit_pb2 import CreateUnitResponse
7+
from sift.unit.v2.unit_pb2 import Unit as UnitProto
8+
9+
from sift_client._internal.low_level_wrappers.units import UnitsLowLevelClient
10+
11+
12+
@pytest.mark.asyncio
13+
async def test_create_unit_rejects_empty_name():
14+
"""create_unit raises before making a request when name is empty."""
15+
client = UnitsLowLevelClient(grpc_client=MagicMock())
16+
17+
with pytest.raises(ValueError, match="name must be provided"):
18+
await client.create_unit("")
19+
20+
21+
@pytest.mark.asyncio
22+
async def test_create_unit_returns_created_unit_proto():
23+
"""create_unit unwraps the response and returns the unit proto (unit_id + abbreviated_name)."""
24+
stub = MagicMock()
25+
stub.CreateUnit = AsyncMock(
26+
return_value=CreateUnitResponse(unit=UnitProto(unit_id="u1", abbreviated_name="volts"))
27+
)
28+
grpc_client = MagicMock()
29+
grpc_client.get_stub.return_value = stub
30+
31+
unit = await UnitsLowLevelClient(grpc_client).create_unit("volts")
32+
33+
assert unit.unit_id == "u1"
34+
assert unit.abbreviated_name == "volts"

0 commit comments

Comments
 (0)