Skip to content

Commit c928bbe

Browse files
author
8ashar
authored
[RSDK-3639] Create Location Client (#372)
1 parent e6c39f6 commit c928bbe

File tree

11 files changed

+1697
-271
lines changed

11 files changed

+1697
-271
lines changed

docs/examples/example.ipynb

Lines changed: 195 additions & 195 deletions
Large diffs are not rendered by default.

src/viam/app/app_client.py

Lines changed: 754 additions & 0 deletions
Large diffs are not rendered by default.

src/viam/app/data/__init__.py

Whitespace-only changes.

src/viam/app/data/client.py renamed to src/viam/app/data_client.py

Lines changed: 37 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@
33
from typing import Any, List, Mapping, Optional, Tuple
44

55
from google.protobuf.struct_pb2 import Struct
6-
from google.protobuf.timestamp_pb2 import Timestamp
76
from grpclib.client import Channel
87

98
from viam import logging
109
from viam.proto.app.data import (
1110
AddTagsToBinaryDataByFilterRequest,
12-
AddTagsToBinaryDataByFilterResponse,
1311
AddTagsToBinaryDataByIDsRequest,
14-
AddTagsToBinaryDataByIDsResponse,
1512
BinaryDataByFilterRequest,
1613
BinaryDataByFilterResponse,
1714
BinaryDataByIDsRequest,
@@ -51,23 +48,23 @@
5148
SensorMetadata,
5249
UploadMetadata,
5350
)
54-
from viam.utils import struct_to_dict
51+
from viam.utils import datetime_to_timestamp, struct_to_dict
5552

5653
LOGGER = logging.getLogger(__name__)
5754

5855

5956
class DataClient:
6057
"""gRPC client for uploading and retrieving data from app.
6158
62-
Constructor is used by `AppClient` to instantiate relevant service stubs. Calls to `DataClient` methods should be made through
63-
`AppClient`.
59+
Constructor is used by `ViamClient` to instantiate relevant service stubs. Calls to `DataClient` methods should be made through
60+
`ViamClient`.
6461
"""
6562

6663
def __init__(self, channel: Channel, metadata: Mapping[str, str]):
6764
"""Create a `DataClient` that maintains a connection to app.
6865
6966
Args:
70-
channel (Channel): Connection to app.
67+
channel (grpclib.client.Channel): Connection to app.
7168
metadata (Mapping[str, str]): Required authorization token to send requests to app.
7269
"""
7370
self._metadata = metadata
@@ -232,7 +229,7 @@ async def add_tags_to_binary_data_by_ids(self, tags: List[str], binary_ids: List
232229
GRPCError: If no `BinaryID` objects or tags are provided.
233230
"""
234231
request = AddTagsToBinaryDataByIDsRequest(binary_ids=binary_ids, tags=tags)
235-
_: AddTagsToBinaryDataByIDsResponse = await self._data_client.AddTagsToBinaryDataByIDs(request, metadata=self._metadata)
232+
await self._data_client.AddTagsToBinaryDataByIDs(request, metadata=self._metadata)
236233

237234
async def add_tags_to_binary_data_by_filter(self, tags: List[str], filter: Optional[Filter] = None) -> None:
238235
"""Add tags to binary data.
@@ -247,7 +244,7 @@ async def add_tags_to_binary_data_by_filter(self, tags: List[str], filter: Optio
247244
"""
248245
filter = filter if filter else Filter()
249246
request = AddTagsToBinaryDataByFilterRequest(filter=filter, tags=tags)
250-
_: AddTagsToBinaryDataByFilterResponse = await self._data_client.AddTagsToBinaryDataByFilter(request, metadata=self._metadata)
247+
await self._data_client.AddTagsToBinaryDataByFilter(request, metadata=self._metadata)
251248

252249
async def remove_tags_from_binary_data_by_ids(self, tags: List[str], binary_ids: List[BinaryID]) -> int:
253250
"""Remove tags from binary.
@@ -302,7 +299,7 @@ async def tags_by_filter(self, filter: Optional[Filter] = None) -> List[str]:
302299
filter = filter if filter else Filter()
303300
request = TagsByFilterRequest(filter=filter)
304301
response: TagsByFilterResponse = await self._data_client.TagsByFilter(request, metadata=self._metadata)
305-
return response.tags
302+
return list(response.tags)
306303

307304
# TODO: implement
308305
async def add_bounding_box_to_image_by_id(self):
@@ -325,7 +322,7 @@ async def bounding_box_labels_by_filter(self, filter: Optional[Filter] = None) -
325322
filter = filter if filter else Filter()
326323
request = BoundingBoxLabelsByFilterRequest(filter=filter)
327324
response: BoundingBoxLabelsByFilterResponse = await self._data_client.BoundingBoxLabelsByFilter(request, metadata=self._metadata)
328-
return response.labels
325+
return list(response.labels)
329326

330327
async def binary_data_capture_upload(
331328
self,
@@ -360,8 +357,8 @@ async def binary_data_capture_upload(
360357
sensor_contents = SensorData(
361358
metadata=(
362359
SensorMetadata(
363-
time_requested=self.datetime_to_timestamp(data_request_times[0]) if data_request_times[0] else None,
364-
time_received=self.datetime_to_timestamp(data_request_times[1]) if data_request_times[1] else None,
360+
time_requested=datetime_to_timestamp(data_request_times[0]) if data_request_times[0] else None,
361+
time_received=datetime_to_timestamp(data_request_times[1]) if data_request_times[1] else None,
365362
)
366363
if data_request_times
367364
else None
@@ -375,12 +372,10 @@ async def binary_data_capture_upload(
375372
component_name=component_name,
376373
method_name=method_name,
377374
type=DataType.DATA_TYPE_BINARY_SENSOR,
378-
file_name=None, # Not used in app.
379375
method_parameters=method_parameters,
380-
file_extension=None, # Will be stored as empty string "".
381376
tags=tags,
382377
)
383-
_: DataCaptureUploadResponse = await self._data_capture_upload(metadata=metadata, sensor_contents=[sensor_contents])
378+
await self._data_capture_upload(metadata=metadata, sensor_contents=[sensor_contents])
384379

385380
async def tabular_data_capture_upload(
386381
self,
@@ -418,7 +413,7 @@ async def tabular_data_capture_upload(
418413
AssertionError: If a list of `Timestamp` objects is provided and its length does not match the length of the list of tabular
419414
data.
420415
"""
421-
sensor_contents = [None] * len(tabular_data)
416+
sensor_contents = [SensorData()] * len(tabular_data)
422417
if data_request_times:
423418
assert len(data_request_times) == len(tabular_data)
424419

@@ -428,8 +423,8 @@ async def tabular_data_capture_upload(
428423
sensor_contents[i] = SensorData(
429424
metadata=(
430425
SensorMetadata(
431-
time_requested=self.datetime_to_timestamp(data_request_times[i][0]) if data_request_times[i][0] else None,
432-
time_received=self.datetime_to_timestamp(data_request_times[i][1]) if data_request_times[i][1] else None,
426+
time_requested=datetime_to_timestamp(data_request_times[i][0]) if data_request_times[i][0] else None,
427+
time_received=datetime_to_timestamp(data_request_times[i][1]) if data_request_times[i][1] else None,
433428
)
434429
if data_request_times[i]
435430
else None
@@ -445,12 +440,10 @@ async def tabular_data_capture_upload(
445440
component_name=component_name,
446441
method_name=method_name,
447442
type=DataType.DATA_TYPE_TABULAR_SENSOR,
448-
file_name=None, # Not used in app.
449443
method_parameters=method_parameters,
450-
file_extension=None, # Will be stored as empty string "".
451444
tags=tags,
452445
)
453-
_: DataCaptureUploadResponse = await self._data_capture_upload(metadata=metadata, sensor_contents=sensor_contents)
446+
await self._data_capture_upload(metadata=metadata, sensor_contents=sensor_contents)
454447

455448
async def _data_capture_upload(self, metadata: UploadMetadata, sensor_contents: List[SensorData]) -> DataCaptureUploadResponse:
456449
request = DataCaptureUploadRequest(metadata=metadata, sensor_contents=sensor_contents)
@@ -491,16 +484,16 @@ async def file_upload(
491484
"""
492485
metadata = UploadMetadata(
493486
part_id=part_id,
494-
component_type=component_type,
495-
component_name=component_name,
496-
method_name=method_name,
487+
component_type=component_type if component_type else "",
488+
component_name=component_name if component_name else "",
489+
method_name=method_name if method_name else "",
497490
type=DataType.DATA_TYPE_FILE,
498-
file_name=file_name,
491+
file_name=file_name if file_name else "",
499492
method_parameters=method_parameters,
500-
file_extension=file_extension,
493+
file_extension=file_extension if file_extension else "",
501494
tags=tags,
502495
)
503-
_: FileUploadResponse = await self._file_upload(metadata=metadata, file_contents=FileData(data=data))
496+
await self._file_upload(metadata=metadata, file_contents=FileData(data=data if data else bytes()))
504497

505498
async def file_upload_from_path(
506499
self,
@@ -539,40 +532,27 @@ async def file_upload_from_path(
539532

540533
metadata = UploadMetadata(
541534
part_id=part_id,
542-
component_type=component_type,
543-
component_name=component_name,
544-
method_name=method_name,
535+
component_type=component_type if component_type else "",
536+
component_name=component_name if component_name else "",
537+
method_name=method_name if method_name else "",
545538
type=DataType.DATA_TYPE_FILE,
546539
file_name=file_name,
547540
method_parameters=method_parameters,
548-
file_extension=file_extension,
541+
file_extension=file_extension if file_extension else "",
549542
tags=tags,
550543
)
551-
_: FileUploadResponse = await self._file_upload(metadata=metadata, file_contents=FileData(data=data))
544+
await self._file_upload(metadata=metadata, file_contents=FileData(data=data))
552545

553546
async def _file_upload(self, metadata: UploadMetadata, file_contents: FileData) -> FileUploadResponse:
554547
request_metadata = FileUploadRequest(metadata=metadata)
555548
request_file_contents = FileUploadRequest(file_contents=file_contents)
556549
async with self._data_sync_client.FileUpload.open(metadata=self._metadata) as stream:
557550
await stream.send_message(request_metadata)
558551
await stream.send_message(request_file_contents, end=True)
559-
response: FileUploadResponse = await stream.recv_message()
552+
response = await stream.recv_message()
553+
assert response is not None
560554
return response
561555

562-
@staticmethod
563-
def datetime_to_timestamp(dt: datetime) -> Timestamp:
564-
"""Convert a Python native `datetime` into a `Timestamp`.
565-
566-
Args:
567-
dt (datetime.datetime): A `datetime` object. UTC is assumed in the conversion if the object is naive to timezone information.
568-
569-
Returns:
570-
google.protobuf.timestamp_pb2.Timestamp: The `Timestamp` object.
571-
"""
572-
timestamp = Timestamp()
573-
timestamp.FromDatetime(dt)
574-
return timestamp
575-
576556
@staticmethod
577557
def create_filter(
578558
component_name: Optional[str] = None,
@@ -613,20 +593,20 @@ def create_filter(
613593
viam.proto.app.data.Filter: The `Filter` object.
614594
"""
615595
return Filter(
616-
component_name=component_name,
617-
component_type=component_type,
618-
method=method,
619-
robot_name=robot_name,
620-
robot_id=robot_id,
621-
part_name=part_name,
622-
part_id=part_id,
596+
component_name=component_name if component_name else "",
597+
component_type=component_type if component_type else "",
598+
method=method if method else "",
599+
robot_name=robot_name if robot_name else "",
600+
robot_id=robot_id if robot_id else "",
601+
part_name=part_name if part_name else "",
602+
part_id=part_id if part_id else "",
623603
location_ids=location_ids,
624604
organization_ids=organization_ids,
625605
mime_type=mime_type,
626606
interval=(
627607
CaptureInterval(
628-
start=DataClient.datetime_to_timestamp(start_time) if start_time else None,
629-
end=DataClient.datetime_to_timestamp(end_time) if end_time else None,
608+
start=datetime_to_timestamp(start_time) if start_time else None,
609+
end=datetime_to_timestamp(end_time) if end_time else None,
630610
)
631611
)
632612
if start_time and end_time

src/viam/app/client.py renamed to src/viam/app/viam_client.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,24 @@
44
from typing_extensions import Self
55

66
from viam import logging
7-
from viam.app.data.client import DataClient
7+
from viam.app.app_client import AppClient
8+
from viam.app.data_client import DataClient
89
from viam.rpc.dial import DialOptions, _dial_app, _get_access_token
910

1011
LOGGER = logging.getLogger(__name__)
1112

1213

13-
class AppClient:
14+
class ViamClient:
1415
"""gRPC client for all communication and interaction with app.
1516
16-
Use create() to instantiate an AppClient::
17+
There is currently 1 way to instantiate a `ViamClient` object::
1718
18-
AppClient.create(...)
19+
ViamClient.create_from_dial_options(...)
1920
"""
2021

2122
@classmethod
22-
async def create(cls, dial_options: DialOptions) -> Self:
23-
"""Create an AppClient that establishes a connection to app.viam.com.
23+
async def create_from_dial_options(cls, dial_options: DialOptions) -> Self:
24+
"""Create `ViamClient` that establishes a connection to app.viam.com.
2425
2526
Args:
2627
@@ -31,10 +32,17 @@ async def create(cls, dial_options: DialOptions) -> Self:
3132
AssertionError: If the type provided in the credentials of the `DialOptions` object is 'robot-secret'.
3233
3334
Returns:
34-
Self: The `AppClient`.
35+
Self: The `ViamClient`.
3536
"""
36-
assert dial_options.credentials.type != "robot-secret"
37+
if dial_options.credentials is None:
38+
raise ValueError("dial_options.credentials cannot be None.")
39+
if dial_options.credentials.type == "robot-secret":
40+
raise ValueError("dial_options.credentials.type cannot be 'robot-secret'")
41+
if dial_options.auth_entity is None:
42+
raise ValueError("dial_options.auth_entity cannot be None.")
43+
3744
self = cls()
45+
self._location_id = dial_options.auth_entity.split(".")[1]
3846
self._channel = await _dial_app(dial_options)
3947
access_token = await _get_access_token(self._channel, dial_options.auth_entity, dial_options)
4048
self._metadata = {"authorization": f"Bearer {access_token}"}
@@ -43,12 +51,18 @@ async def create(cls, dial_options: DialOptions) -> Self:
4351
_channel: Channel
4452
_metadata: Mapping[str, str]
4553
_closed: bool = False
54+
_location_id: str
4655

4756
@property
4857
def data_client(self) -> DataClient:
49-
"""Insantiate and return a DataClient used to make `data` and `data_sync` method calls."""
58+
"""Insantiate and return a `DataClient` used to make `data` and `data_sync` method calls."""
5059
return DataClient(self._channel, self._metadata)
5160

61+
@property
62+
def app_client(self) -> AppClient:
63+
"""Insantiate and return an `AppClient` used to make `app` method calls."""
64+
return AppClient(self._channel, self._metadata, self._location_id)
65+
5266
def close(self):
5367
"""Close opened channels used for the various service stubs initialized."""
5468
if self._closed:

src/viam/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import functools
44
import sys
55
import threading
6+
from datetime import datetime
67
from typing import Any, Dict, List, Mapping, SupportsBytes, SupportsFloat, Type, TypeVar, Union
78

89
from google.protobuf.json_format import MessageToDict, ParseDict
910
from google.protobuf.message import Message
1011
from google.protobuf.struct_pb2 import ListValue, Struct, Value
12+
from google.protobuf.timestamp_pb2 import Timestamp
1113

1214
from viam.proto.common import GeoPoint, Orientation, ResourceName, Vector3
1315
from viam.resource.base import ResourceBase
@@ -151,6 +153,12 @@ def struct_to_dict(struct: Struct) -> Dict[str, ValueTypes]:
151153
return {key: value_to_primitive(value) for (key, value) in struct.fields.items()}
152154

153155

156+
def datetime_to_timestamp(dt: datetime) -> Timestamp:
157+
timestamp = Timestamp()
158+
timestamp.FromDatetime(dt)
159+
return timestamp
160+
161+
154162
def sensor_readings_native_to_value(readings: Mapping[str, Any]) -> Mapping[str, Any]:
155163
prim_readings = dict(readings)
156164
for key, reading in readings.items():

0 commit comments

Comments
 (0)