Skip to content

Commit dc262d7

Browse files
author
Joel Collins
committed
Added full support for Mozilla Web Things
1 parent 7cb9126 commit dc262d7

File tree

3 files changed

+110
-30
lines changed

3 files changed

+110
-30
lines changed

labthings_client/affordances.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from .json_typing import json_to_typing_basic
55
from .tasks import ActionTask
66

7+
78
class Affordance:
8-
def __init__(self, affordance_description: dict, base_url: str = ""):
9+
def __init__(self, title, affordance_description: dict, base_url: str = ""):
10+
self.title = title
911
self.base_url = base_url.strip("/")
1012
self.affordance_description = affordance_description
1113

@@ -14,24 +16,23 @@ def __init__(self, affordance_description: dict, base_url: str = ""):
1416

1517
self.description = self.affordance_description.get("description")
1618

17-
1819
def find_verbs(self):
1920
"""Verify available HTTP methods
2021
2122
Returns:
2223
[list] -- List of HTTP verb strings
2324
"""
24-
return requests.options(self.self_url).headers['allow'].split(", ")
25+
return requests.options(self.self_url).headers["allow"].split(", ")
2526

2627

2728
class Property(Affordance):
28-
def __init__(self, affordance_description: dict, base_url: str = ""):
29-
Affordance.__init__(self, affordance_description, base_url=base_url)
29+
def __init__(self, title, affordance_description: dict, base_url: str = ""):
30+
Affordance.__init__(self, title, affordance_description, base_url=base_url)
3031

3132
self.read_only = self.affordance_description.get("readOnly")
3233
self.write_only = self.affordance_description.get("writeOnly")
3334

34-
def __call__(self, *args, **kwargs):
35+
def __call__(self, *args, **kwargs):
3536
return self.get(*args, **kwargs)
3637

3738
def get(self):
@@ -70,24 +71,49 @@ def delete(self):
7071
raise AttributeError("Can't delete attribute, is read-only")
7172

7273

74+
class MozillaProperty(Property):
75+
def _post_process(self, value):
76+
if isinstance(value, dict) and self.title in value:
77+
return value.get(self.title)
78+
79+
def _pre_process(self, value):
80+
return {self.title: value}
81+
82+
def get(self):
83+
return self._post_process(Property.get(self))
84+
85+
def put(self, value):
86+
return self._post_process(Property.put(self, self._pre_process(value)))
87+
88+
def post(self, value):
89+
return self._post_process(Property.post(self, self._pre_process(value)))
90+
91+
def delete(self):
92+
return self._post_process(Property.delete(self))
93+
94+
7395
class Action(Affordance):
74-
def __init__(self, affordance_description: dict, base_url: str = ""):
75-
Affordance.__init__(self, affordance_description, base_url=base_url)
96+
def __init__(self, title, affordance_description: dict, base_url: str = ""):
97+
Affordance.__init__(self, title, affordance_description, base_url=base_url)
7698

7799
self.args = json_to_typing_basic(self.affordance_description.get("input", {}))
78100

79-
def __call__(self, *args, **kwargs):
101+
def __call__(self, *args, **kwargs):
80102
return self.post(*args, **kwargs)
81103

82104
def post(self, *args, **kwargs):
83105

84106
# Only accept a single positional argument, at most
85107
if len(args) > 1:
86-
raise ValueError("If passing parameters as a positional argument, the only argument must be a single dictionary")
108+
raise ValueError(
109+
"If passing parameters as a positional argument, the only argument must be a single dictionary"
110+
)
87111

88112
# Single positional argument MUST be a dictionary
89113
if args and not isinstance(args[0], dict):
90-
raise TypeError("If passing parameters as a positional argument, the argument must be a dictionary")
114+
raise TypeError(
115+
"If passing parameters as a positional argument, the argument must be a dictionary"
116+
)
91117

92118
# Use positional dictionary as parameters base
93119
if args:
@@ -100,4 +126,4 @@ def post(self, *args, **kwargs):
100126
r = requests.post(self.self_url, json=params or {})
101127
r.raise_for_status()
102128

103-
return ActionTask(r.json())
129+
return ActionTask(r.json())

labthings_client/discovery.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55

66
from pprint import pprint
77

8-
from .thing import FoundThing
8+
from .thing import LabThing, WebThing
9+
910

1011
class Browser:
1112
def __init__(self, types=["labthing", "webthing"], protocol="tcp"):
12-
self.service_types = [f"_{service_type}._{protocol}.local." for service_type in types]
13+
self.service_types = [
14+
f"_{service_type}._{protocol}.local." for service_type in types
15+
]
1316

1417
self.services = {}
1518

@@ -23,7 +26,7 @@ def __enter__(self):
2326
self.open()
2427
return self
2528

26-
def __exit__(self ,type, value, traceback):
29+
def __exit__(self, service_type, value, traceback):
2730
return self.close()
2831

2932
def open(self):
@@ -35,16 +38,16 @@ def close(self, *args, **kwargs):
3538
logging.info(f"Closing browser {self}")
3639
return self._zeroconf.close(*args, **kwargs)
3740

38-
def remove_service(self, zeroconf, type, name):
39-
service = zeroconf.get_service_info(type, name)
41+
def remove_service(self, zeroconf, service_type, name):
42+
service = zeroconf.get_service_info(service_type, name)
4043
if name in self.services:
4144
for callback in self.remove_service_callbacks:
4245
callback(self.services[name])
4346
del self.services[name]
4447

45-
def add_service(self, zeroconf, type, name):
46-
service = zeroconf.get_service_info(type, name)
47-
self.services[name] = parse_service(service)
48+
def add_service(self, zeroconf, service_type, name):
49+
service = zeroconf.get_service_info(service_type, name)
50+
self.services[name] = parse_service(service, service_type)
4851
for callback in self.add_service_callbacks:
4952
callback(self.services[name])
5053

@@ -71,7 +74,7 @@ def __init__(self, *args, **kwargs):
7174
self._things = set()
7275
self.add_add_service_callback(self.add_service_to_things)
7376
self.add_remove_service_callback(self.remove_service_from_things)
74-
77+
7578
@property
7679
def things(self):
7780
return list(self._things)
@@ -92,12 +95,14 @@ def wait_for_first(self):
9295
time.sleep(0.1)
9396
return self.things[0]
9497

95-
def parse_service(service):
98+
99+
def parse_service(service, service_type):
96100
properties = {}
97101
for k, v in service.properties.items():
98102
properties[k.decode()] = v.decode()
99103

100104
return {
105+
"type": service_type.split(".")[0].strip("_"),
101106
"address": ipaddress.ip_address(service.address),
102107
"addresses": {ipaddress.ip_address(a) for a in service.addresses},
103108
"port": service.port,
@@ -108,9 +113,26 @@ def parse_service(service):
108113

109114

110115
def service_to_thing(service: dict):
111-
if not ("addresses" in service or "port" in service or "path" in service.get("properties", {})):
116+
if not (
117+
"addresses" in service
118+
or "port" in service
119+
or "path" in service.get("properties", {})
120+
):
112121
raise KeyError("Invalid service. Missing keys.")
113-
return FoundThing(service.get("name"), service.get("addresses"), service.get("port"), service.get("properties").get("path"))
122+
123+
if service.get("type") == "webthing":
124+
thing_class = WebThing
125+
elif service.get("type") == "labthing":
126+
thing_class = LabThing
127+
else:
128+
raise KeyError("Invalid service. Invalid service type.")
129+
130+
return thing_class(
131+
service.get("name"),
132+
service.get("addresses"),
133+
service.get("port"),
134+
service.get("properties").get("path"),
135+
)
114136

115137

116138
if __name__ == "__main__":
@@ -122,4 +144,4 @@ def service_to_thing(service: dict):
122144
browser = ThingBrowser().open()
123145
atexit.register(browser.close)
124146

125-
thing = browser.wait_for_first()
147+
thing = browser.wait_for_first()

labthings_client/thing.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from ipaddress import IPv4Address, IPv6Address
33

44
from .utilities import AttributeDict
5-
from .affordances import Property, Action
5+
from .affordances import Property, MozillaProperty, Action
66

7-
class FoundThing:
7+
8+
class Thing:
89
def __init__(self, name, addresses, port, path, protocol="http"):
910
self.name = name
1011
self.addresses = addresses
@@ -40,13 +41,44 @@ def description(self):
4041

4142
def update(self):
4243
self.thing_description = self.fetch_description()
43-
self.properties = AttributeDict({k: Property(v, base_url = self.base) for k, v in self.thing_description.get("properties", {}).items()})
44-
self.actions = AttributeDict({k: Action(v, base_url = self.base) for k, v in self.thing_description.get("actions", {}).items()})
44+
self.properties = AttributeDict(
45+
{
46+
k: Property(k, v, base_url=self.base)
47+
for k, v in self.thing_description.get("properties", {}).items()
48+
}
49+
)
50+
self.actions = AttributeDict(
51+
{
52+
k: Action(k, v, base_url=self.base)
53+
for k, v in self.thing_description.get("actions", {}).items()
54+
}
55+
)
4556

4657
def fetch_description(self):
4758
for url in self.urls:
4859
response = requests.get(url)
4960
if response.json():
5061
return response.json()
5162
# If we reach this line, no URL gave a valid JSON response
52-
raise RuntimeError("No valid Thing Description found")
63+
raise RuntimeError("No valid Thing Description found")
64+
65+
66+
class WebThing(Thing):
67+
def update(self):
68+
self.thing_description = self.fetch_description()
69+
self.properties = AttributeDict(
70+
{
71+
k: MozillaProperty(k, v, base_url=self.base,)
72+
for k, v in self.thing_description.get("properties", {}).items()
73+
}
74+
)
75+
self.actions = AttributeDict(
76+
{
77+
k: Action(k, v, base_url=self.base)
78+
for k, v in self.thing_description.get("actions", {}).items()
79+
}
80+
)
81+
82+
83+
class LabThing(Thing):
84+
pass

0 commit comments

Comments
 (0)