Skip to content

Commit 7760402

Browse files
committed
python(feat): add update, archive, and unarchive to Channel
1 parent 4bd66a8 commit 7760402

7 files changed

Lines changed: 214 additions & 6 deletions

File tree

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
GetChannelResponse,
1111
ListChannelsRequest,
1212
ListChannelsResponse,
13+
UpdateChannelRequest,
14+
UpdateChannelResponse,
1315
)
1416
from sift.channels.v3.channels_pb2_grpc import ChannelServiceStub
1517

1618
from sift_client._internal.low_level_wrappers.base import LowLevelClientBase
17-
from sift_client.sift_types.channel import Channel
19+
from sift_client.sift_types.channel import Channel, ChannelUpdate
1820
from sift_client.transport import WithGrpcClient
1921

2022
if TYPE_CHECKING:
@@ -120,6 +122,21 @@ async def list_all_channels(
120122
max_results=max_results,
121123
)
122124

125+
async def update_channel(self, update: ChannelUpdate) -> Channel:
126+
"""Update a channel.
127+
128+
Args:
129+
update: The ChannelUpdate to apply.
130+
131+
Returns:
132+
The updated Channel.
133+
"""
134+
grpc_channel, update_mask = update.to_proto_with_mask()
135+
request = UpdateChannelRequest(channel=grpc_channel, update_mask=update_mask)
136+
response = await self._grpc_client.get_stub(ChannelServiceStub).UpdateChannel(request)
137+
updated_grpc_channel = cast("UpdateChannelResponse", response).channel
138+
return Channel._from_proto(updated_grpc_channel)
139+
123140
async def batch_archive_channels(self, channel_ids: list[str]) -> None:
124141
"""Batch archive channels by setting active to false.
125142

python/lib/sift_client/_tests/sift_types/test_channel.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77

88
from sift_client.sift_types import Channel
9-
from sift_client.sift_types.channel import ChannelDataType, ChannelReference
9+
from sift_client.sift_types.channel import ChannelDataType, ChannelReference, ChannelUpdate
1010

1111

1212
@pytest.fixture
@@ -22,6 +22,10 @@ def mock_channel(mock_client):
2222
bit_field_elements=[],
2323
enum_types={},
2424
asset_id="test_asset_id",
25+
display_description="display description",
26+
display_unit="m/s",
27+
metadata={},
28+
active=True,
2529
created_date=datetime.now(timezone.utc),
2630
modified_date=datetime.now(timezone.utc),
2731
created_by_user_id="user1",
@@ -215,3 +219,83 @@ def test_data_method_with_minimal_params(self, mock_channel, mock_client):
215219
limit=None,
216220
)
217221
assert result == mock_data
222+
223+
def test_update_calls_client_and_updates_self(self, mock_channel, mock_client):
224+
"""Test that update() calls client.channels.update and calls _update."""
225+
updated_channel = MagicMock()
226+
mock_client.channels.update.return_value = updated_channel
227+
228+
with MagicMock() as mock_update:
229+
mock_channel._update = mock_update
230+
231+
update = ChannelUpdate(display_description="new description")
232+
result = mock_channel.update(update)
233+
234+
mock_client.channels.update.assert_called_once_with(channel=mock_channel, update=update)
235+
mock_update.assert_called_once_with(updated_channel)
236+
assert result is mock_channel
237+
238+
def test_archive_sets_active_false_via_update(self, mock_channel, mock_client):
239+
"""archive() delegates to update with active=False and updates in place."""
240+
updated = MagicMock()
241+
mock_client.channels.update.return_value = updated
242+
243+
with MagicMock() as mock_update:
244+
mock_channel._update = mock_update
245+
result = mock_channel.archive()
246+
247+
mock_client.channels.update.assert_called_once_with(
248+
channel=mock_channel, update={"active": False}
249+
)
250+
mock_update.assert_called_once_with(updated)
251+
assert result is mock_channel
252+
253+
def test_unarchive_sets_active_true_via_update(self, mock_channel, mock_client):
254+
"""unarchive() delegates to update with active=True and updates in place."""
255+
updated = MagicMock()
256+
mock_client.channels.update.return_value = updated
257+
258+
with MagicMock() as mock_update:
259+
mock_channel._update = mock_update
260+
result = mock_channel.unarchive()
261+
262+
mock_client.channels.update.assert_called_once_with(
263+
channel=mock_channel, update={"active": True}
264+
)
265+
mock_update.assert_called_once_with(updated)
266+
assert result is mock_channel
267+
268+
269+
class TestChannelUpdate:
270+
"""Channel-specific field wiring for ChannelUpdate.
271+
272+
The generic ModelUpdate behavior (field-mask generation, unset/None exclusion,
273+
resource-id requirement, the metadata converter and MappingHelper path expansion)
274+
is already covered in test_base.py and test_asset.py. These tests only assert the
275+
parts unique to ChannelUpdate: which proto fields its fields map onto.
276+
"""
277+
278+
def test_fields_map_to_correct_proto_fields(self):
279+
"""Each ChannelUpdate field targets its matching Channel proto field and mask path."""
280+
update = ChannelUpdate(
281+
display_description="new description",
282+
display_unit="volts",
283+
metadata={"source": "pytest"},
284+
active=False,
285+
)
286+
update.resource_id = "test_channel_id"
287+
288+
proto, mask = update.to_proto_with_mask()
289+
290+
assert proto.channel_id == "test_channel_id"
291+
assert proto.display_description == "new description"
292+
# display_unit is renamed onto the proto's display_unit_id field.
293+
assert proto.display_unit_id == "volts"
294+
assert {md.key.name: md.string_value for md in proto.metadata} == {"source": "pytest"}
295+
assert proto.active is False
296+
assert set(mask.paths) == {
297+
"display_description",
298+
"display_unit_id",
299+
"metadata",
300+
"active",
301+
}

python/lib/sift_client/_tests/sift_types/test_results.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,9 @@ def _channel(name: str) -> Channel:
372372
bit_field_elements=[],
373373
enum_types={},
374374
asset_id="asset_1",
375+
display_description="",
376+
display_unit="",
377+
active=True,
375378
created_date=now,
376379
modified_date=now,
377380
created_by_user_id="user1",

python/lib/sift_client/resources/channels.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from sift_client._internal.low_level_wrappers.channels import ChannelsLowLevelClient
66
from sift_client.resources._base import ResourceBase
77
from sift_client.sift_types.asset import Asset
8+
from sift_client.sift_types.channel import Channel, ChannelUpdate
89
from sift_client.sift_types.run import Run
910
from sift_client.util import cel_utils as cel
1011

@@ -16,7 +17,6 @@
1617
import pyarrow as pa
1718

1819
from sift_client.client import SiftClient
19-
from sift_client.sift_types.channel import Channel
2020

2121

2222
def _channel_ids_from_list(items: list[str | Channel]) -> list[str]:
@@ -191,6 +191,28 @@ async def find(self, **kwargs) -> Channel | None:
191191
return channels[0]
192192
return None
193193

194+
async def update(
195+
self,
196+
channel: str | Channel,
197+
update: ChannelUpdate | dict,
198+
) -> Channel:
199+
"""Update a Channel.
200+
201+
Args:
202+
channel: The Channel or channel ID to update.
203+
update: Updates to apply to the Channel. See ChannelUpdate for the updatable fields
204+
(display description, display unit, metadata, and active status).
205+
206+
Returns:
207+
The updated Channel.
208+
"""
209+
channel_id = channel._id_or_error if isinstance(channel, Channel) else channel
210+
if isinstance(update, dict):
211+
update = ChannelUpdate.model_validate(update)
212+
update.resource_id = channel_id
213+
updated_channel = await self._low_level_client.update_channel(update=update)
214+
return self._apply_client_to_instance(updated_channel)
215+
194216
async def archive(self, channels: list[str | Channel]) -> None:
195217
"""Batch archive channels by setting active to false.
196218

python/lib/sift_client/resources/sync_stubs/__init__.pyi

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
2323
CalculatedChannelCreate,
2424
CalculatedChannelUpdate,
2525
)
26-
from sift_client.sift_types.channel import Channel
26+
from sift_client.sift_types.channel import Channel, ChannelUpdate
2727
from sift_client.sift_types.data_import import (
2828
DataTypeKey,
2929
ImportConfig,
@@ -558,6 +558,19 @@ class ChannelsAPI:
558558
"""
559559
...
560560

561+
def update(self, channel: str | Channel, update: ChannelUpdate | dict) -> Channel:
562+
"""Update a Channel.
563+
564+
Args:
565+
channel: The Channel or channel ID to update.
566+
update: Updates to apply to the Channel. See ChannelUpdate for the updatable fields
567+
(display description, display unit, metadata, and active status).
568+
569+
Returns:
570+
The updated Channel.
571+
"""
572+
...
573+
561574
class DataExportAPI:
562575
"""Sync counterpart to `DataExportAPIAsync`.
563576

python/lib/sift_client/sift_types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
ChannelBitFieldElement,
143143
ChannelDataType,
144144
ChannelReference,
145+
ChannelUpdate,
145146
)
146147
from sift_client.sift_types.ingestion import (
147148
ChannelConfig,
@@ -215,6 +216,7 @@
215216
"ChannelConfig",
216217
"ChannelDataType",
217218
"ChannelReference",
219+
"ChannelUpdate",
218220
"DataExportDetails",
219221
"DataExportStatusDetails",
220222
"DataImportDetails",

python/lib/sift_client/sift_types/channel.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from datetime import datetime, timezone
44
from enum import Enum
5-
from typing import TYPE_CHECKING
5+
from typing import TYPE_CHECKING, ClassVar
66

77
import sift.common.type.v1.channel_data_type_pb2 as channel_pb
88
from pydantic import BaseModel, Field, model_validator
@@ -25,7 +25,8 @@
2525
Uint64Values,
2626
)
2727

28-
from sift_client.sift_types._base import BaseType
28+
from sift_client.sift_types._base import BaseType, MappingHelper, ModelUpdate
29+
from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict
2930

3031
if TYPE_CHECKING:
3132
from sift_stream_bindings import ChannelBitFieldElementPy, ChannelDataTypePy
@@ -253,6 +254,10 @@ class Channel(BaseType[ChannelProto, "Channel"]):
253254
bit_field_elements: list[ChannelBitFieldElement] = Field(default_factory=list)
254255
enum_types: dict[str, int] = Field(default_factory=dict)
255256
asset_id: str
257+
display_description: str
258+
display_unit: str
259+
metadata: dict[str, str | float | bool] = Field(default_factory=dict)
260+
active: bool
256261
created_date: datetime
257262
modified_date: datetime
258263
created_by_user_id: str
@@ -286,6 +291,10 @@ def _from_proto(cls, proto: ChannelProto, sift_client: SiftClient | None = None)
286291
],
287292
enum_types=cls._enum_types_from_proto_list(proto.enum_types), # type: ignore
288293
asset_id=proto.asset_id,
294+
display_description=proto.display_description,
295+
display_unit=proto.display_unit_id,
296+
metadata=metadata_proto_to_dict(proto.metadata), # type: ignore
297+
active=proto.active,
289298
created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc),
290299
modified_date=proto.modified_date.ToDatetime(tzinfo=timezone.utc),
291300
created_by_user_id=proto.created_by_user_id,
@@ -332,6 +341,35 @@ def data(
332341
)
333342
return data
334343

344+
def update(self, update: ChannelUpdate | dict) -> Channel:
345+
"""Update the Channel.
346+
347+
Args:
348+
update: Either a ChannelUpdate instance or a dictionary of fields to update.
349+
350+
Returns:
351+
The updated Channel.
352+
"""
353+
updated_channel = self.client.channels.update(channel=self, update=update)
354+
self._update(updated_channel)
355+
return self
356+
357+
def archive(self) -> Channel:
358+
"""Archive the channel by setting it inactive.
359+
360+
Returns:
361+
The archived Channel.
362+
"""
363+
return self.update({"active": False})
364+
365+
def unarchive(self) -> Channel:
366+
"""Unarchive the channel by setting it active.
367+
368+
Returns:
369+
The unarchived Channel.
370+
"""
371+
return self.update({"active": True})
372+
335373
@property
336374
def asset(self) -> Asset:
337375
"""Get the asset that this channel belongs to."""
@@ -344,6 +382,35 @@ def runs(self) -> list[Run]:
344382
return self.asset.runs
345383

346384

385+
class ChannelUpdate(ModelUpdate[ChannelProto]):
386+
"""Model of the Channel fields that can be updated."""
387+
388+
display_description: str | None = None
389+
display_unit: str | None = None
390+
metadata: dict[str, str | float | bool] | None = None
391+
active: bool | None = None
392+
393+
_to_proto_helpers: ClassVar[dict[str, MappingHelper]] = {
394+
"display_unit": MappingHelper(
395+
proto_attr_path="display_unit_id",
396+
update_field="display_unit_id",
397+
),
398+
"metadata": MappingHelper(
399+
proto_attr_path="metadata",
400+
update_field="metadata",
401+
converter=metadata_dict_to_proto,
402+
),
403+
}
404+
405+
def _get_proto_class(self) -> type[ChannelProto]:
406+
return ChannelProto
407+
408+
def _add_resource_id_to_proto(self, proto_msg: ChannelProto):
409+
if self._resource_id is None:
410+
raise ValueError("Resource ID must be set before adding to proto")
411+
proto_msg.channel_id = self._resource_id
412+
413+
347414
class ChannelReference(BaseModel):
348415
"""Channel reference for a calculated channel or rule expression.
349416

0 commit comments

Comments
 (0)