Skip to content

Commit

Permalink
Support custom device properties (#83)
Browse files Browse the repository at this point in the history
* Support creating properties on device
* Support editing a device
* Return properties with devices
* Version bump
  • Loading branch information
bryfox authored Jun 23, 2023
1 parent c5e49f6 commit 7cf3207
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 17 deletions.
69 changes: 59 additions & 10 deletions foxglove_data_platform/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from enum import Enum
from io import BytesIO
import json
import warnings
from typing import IO, Any, Dict, List, Optional, Union
from typing import IO, Any, Dict, List, Optional, TypeVar, Union
import base64

import arrow
Expand All @@ -31,6 +30,9 @@ def decoder(message_content: bytes):

DEFAULT_DECODER_FACTORIES: List[DecoderFactory] = [_JsonDecoderFactory()]

T = TypeVar("T")


try:
from mcap_ros1.decoder import DecoderFactory as Ros1DecoderFactory

Expand Down Expand Up @@ -70,6 +72,13 @@ def bool_query_param(val: bool) -> Optional[str]:
return str(val).lower() if val is not None else None


def without_nulls(params: Dict[str, Union[T, None]]) -> Dict[str, T]:
"""
Filter out `None` values from params
"""
return {key: val for key, val in params.items() if val is not None}


class FoxgloveException(Exception):
pass

Expand Down Expand Up @@ -446,6 +455,7 @@ def get_device(
return {
"id": device["id"],
"name": device["name"],
"properties": device["properties"] if "properties" in device else None,
}

def get_devices(self):
Expand All @@ -463,6 +473,7 @@ def get_devices(self):
{
"id": d["id"],
"name": d["name"],
"properties": d["properties"] if "properties" in d else None,
}
for d in json
]
Expand All @@ -471,27 +482,65 @@ def create_device(
self,
*,
name: str,
serial_number: Optional[str] = None,
properties: Optional[Dict[str, Union[str, bool, float, int]]] = None,
):
"""
Creates a new device.
name: The name of the devicee.
serial_number: DEPRECATED: a serial number for the device. This argument has no effect.
:param name: The name of the device.
:param properties: Optional custom properties for the device.
Each key must be defined as a custom property for your organization,
and each value must be of the appropriate type
"""
if serial_number is not None:
warnings.warn(
"serial number argument is deprecated and will be removed in the next release"
)
response = requests.post(
self.__url__("/v1/devices"), headers=self.__headers, json={"name": name}
self.__url__("/v1/devices"),
headers=self.__headers,
json=without_nulls({"name": name, "properties": properties}),
)

device = json_or_raise(response)

return {
"id": device["id"],
"name": device["name"],
"properties": device["properties"] if "properties" in device else None,
}

def update_device(
self,
*,
device_id: Optional[str] = None,
device_name: Optional[str] = None,
new_name: Optional[str] = None,
properties: Optional[Dict[str, Union[str, bool, float, int]]] = None,
):
"""
Updates a device.
:param device_id: The id of the device to retrieve.
:param device_name: The name of the device to retrieve.
:param new_name: Optional new name to assign to the device.
:param properties: Optional custom properties to add to or edit on the device.
Each key must be defined as a custom property for your organization
and each value must be of the appropriate type.
"""
if device_name and device_id:
raise RuntimeError("device_id and device_name are mutually exclusive")
if device_name is None and device_id is None:
raise RuntimeError("device_id or device_name must be provided")

response = requests.patch(
self.__url__(f"/v1/devices/{device_name or device_id}"),
headers=self.__headers,
json=without_nulls({"name": new_name, "properties": properties}),
)

device = json_or_raise(response)

return {
"id": device["id"],
"name": device["name"],
"properties": device["properties"] if "properties" in device else None,
}

def delete_device(
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = foxglove-data-platform
version = 0.9.0
version = 0.10.0
description = Client library for Foxglove Data Platform.
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down
79 changes: 73 additions & 6 deletions tests/test_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import responses
from faker import Faker
from foxglove_data_platform.client import Client
from responses.matchers import json_params_matcher

from .api_url import api_url

Expand All @@ -16,6 +17,7 @@ def test_create_device():
responses.add(
responses.POST,
api_url("/v1/devices"),
match=[json_params_matcher({"name": name}, strict_match=True)],
json={
"id": id,
"name": name,
Expand All @@ -26,6 +28,31 @@ def test_create_device():
assert device["name"] == name


@responses.activate
def test_create_device_with_properties():
id = fake.uuid4()
name = "name"
properties = {"sn": 1}
responses.add(
responses.POST,
api_url("/v1/devices"),
match=[
json_params_matcher(
{"name": name, "properties": properties}, strict_match=True
)
],
json={
"id": id,
"name": name,
"properties": properties,
},
)
client = Client("test")
device = client.create_device(name=name, properties=properties)
assert device["name"] == name
assert device["properties"] == properties


@responses.activate
def test_get_device():
id = fake.uuid4()
Expand All @@ -36,8 +63,6 @@ def test_get_device():
json={
"id": id,
"name": fake.sentence(2),
"createdAt": datetime.now().isoformat(),
"updatedAt": datetime.now().isoformat(),
},
)
client = Client("test")
Expand All @@ -50,12 +75,11 @@ def test_get_device():
json={
"id": id,
"name": fake.sentence(2),
"createdAt": datetime.now().isoformat(),
"updatedAt": datetime.now().isoformat(),
},
)
device = client.get_device(device_name=name)
assert device["id"] == id
assert device["properties"] is None


@responses.activate
Expand All @@ -68,15 +92,14 @@ def test_get_devices():
{
"id": id,
"name": fake.sentence(2),
"createdAt": datetime.now().isoformat(),
"updatedAt": datetime.now().isoformat(),
}
],
)
client = Client("test")
devices = client.get_devices()
assert len(devices) == 1
assert devices[0]["id"] == id
assert devices[0]["properties"] is None


@responses.activate
Expand All @@ -103,3 +126,47 @@ def test_delete_device():
client.delete_device(device_name=name)
except:
assert False


@responses.activate
def test_update_device():
old_name = "old_name"
new_name = "new_name"
properties = {"sn": 1}
# Patching name alone
responses.add(
responses.PATCH,
api_url(f"/v1/devices/{old_name}"),
match=[json_params_matcher({"name": new_name}, strict_match=True)],
json={
"id": "no-new-properties",
"name": new_name,
"properties": properties,
},
)
# Patching name and properties
responses.add(
responses.PATCH,
api_url(f"/v1/devices/{old_name}"),
match=[
json_params_matcher(
{"name": new_name, "properties": properties}, strict_match=True
)
],
json={
"id": "with-new-properties",
"name": new_name,
"properties": properties,
},
)
client = Client("test")
device = client.update_device(
device_name=old_name, new_name=new_name, properties=properties
)
assert device["id"] == "with-new-properties"
assert device["name"] == new_name
assert device["properties"] == properties

device = client.update_device(device_name=old_name, new_name=new_name)
assert device["id"] == "no-new-properties"
assert device["name"] == new_name

0 comments on commit 7cf3207

Please sign in to comment.