Skip to content

Commit

Permalink
Added full support for Mozilla Web Things
Browse files Browse the repository at this point in the history
  • Loading branch information
Joel Collins committed Jun 17, 2020
1 parent 7cb9126 commit dc262d7
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 30 deletions.
50 changes: 38 additions & 12 deletions labthings_client/affordances.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from .json_typing import json_to_typing_basic
from .tasks import ActionTask


class Affordance:
def __init__(self, affordance_description: dict, base_url: str = ""):
def __init__(self, title, affordance_description: dict, base_url: str = ""):
self.title = title
self.base_url = base_url.strip("/")
self.affordance_description = affordance_description

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

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


def find_verbs(self):
"""Verify available HTTP methods
Returns:
[list] -- List of HTTP verb strings
"""
return requests.options(self.self_url).headers['allow'].split(", ")
return requests.options(self.self_url).headers["allow"].split(", ")


class Property(Affordance):
def __init__(self, affordance_description: dict, base_url: str = ""):
Affordance.__init__(self, affordance_description, base_url=base_url)
def __init__(self, title, affordance_description: dict, base_url: str = ""):
Affordance.__init__(self, title, affordance_description, base_url=base_url)

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

def __call__(self, *args, **kwargs):
def __call__(self, *args, **kwargs):
return self.get(*args, **kwargs)

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


class MozillaProperty(Property):
def _post_process(self, value):
if isinstance(value, dict) and self.title in value:
return value.get(self.title)

def _pre_process(self, value):
return {self.title: value}

def get(self):
return self._post_process(Property.get(self))

def put(self, value):
return self._post_process(Property.put(self, self._pre_process(value)))

def post(self, value):
return self._post_process(Property.post(self, self._pre_process(value)))

def delete(self):
return self._post_process(Property.delete(self))


class Action(Affordance):
def __init__(self, affordance_description: dict, base_url: str = ""):
Affordance.__init__(self, affordance_description, base_url=base_url)
def __init__(self, title, affordance_description: dict, base_url: str = ""):
Affordance.__init__(self, title, affordance_description, base_url=base_url)

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

def __call__(self, *args, **kwargs):
def __call__(self, *args, **kwargs):
return self.post(*args, **kwargs)

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

# Only accept a single positional argument, at most
if len(args) > 1:
raise ValueError("If passing parameters as a positional argument, the only argument must be a single dictionary")
raise ValueError(
"If passing parameters as a positional argument, the only argument must be a single dictionary"
)

# Single positional argument MUST be a dictionary
if args and not isinstance(args[0], dict):
raise TypeError("If passing parameters as a positional argument, the argument must be a dictionary")
raise TypeError(
"If passing parameters as a positional argument, the argument must be a dictionary"
)

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

return ActionTask(r.json())
return ActionTask(r.json())
48 changes: 35 additions & 13 deletions labthings_client/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

from pprint import pprint

from .thing import FoundThing
from .thing import LabThing, WebThing


class Browser:
def __init__(self, types=["labthing", "webthing"], protocol="tcp"):
self.service_types = [f"_{service_type}._{protocol}.local." for service_type in types]
self.service_types = [
f"_{service_type}._{protocol}.local." for service_type in types
]

self.services = {}

Expand All @@ -23,7 +26,7 @@ def __enter__(self):
self.open()
return self

def __exit__(self ,type, value, traceback):
def __exit__(self, service_type, value, traceback):
return self.close()

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

def remove_service(self, zeroconf, type, name):
service = zeroconf.get_service_info(type, name)
def remove_service(self, zeroconf, service_type, name):
service = zeroconf.get_service_info(service_type, name)
if name in self.services:
for callback in self.remove_service_callbacks:
callback(self.services[name])
del self.services[name]

def add_service(self, zeroconf, type, name):
service = zeroconf.get_service_info(type, name)
self.services[name] = parse_service(service)
def add_service(self, zeroconf, service_type, name):
service = zeroconf.get_service_info(service_type, name)
self.services[name] = parse_service(service, service_type)
for callback in self.add_service_callbacks:
callback(self.services[name])

Expand All @@ -71,7 +74,7 @@ def __init__(self, *args, **kwargs):
self._things = set()
self.add_add_service_callback(self.add_service_to_things)
self.add_remove_service_callback(self.remove_service_from_things)

@property
def things(self):
return list(self._things)
Expand All @@ -92,12 +95,14 @@ def wait_for_first(self):
time.sleep(0.1)
return self.things[0]

def parse_service(service):

def parse_service(service, service_type):
properties = {}
for k, v in service.properties.items():
properties[k.decode()] = v.decode()

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


def service_to_thing(service: dict):
if not ("addresses" in service or "port" in service or "path" in service.get("properties", {})):
if not (
"addresses" in service
or "port" in service
or "path" in service.get("properties", {})
):
raise KeyError("Invalid service. Missing keys.")
return FoundThing(service.get("name"), service.get("addresses"), service.get("port"), service.get("properties").get("path"))

if service.get("type") == "webthing":
thing_class = WebThing
elif service.get("type") == "labthing":
thing_class = LabThing
else:
raise KeyError("Invalid service. Invalid service type.")

return thing_class(
service.get("name"),
service.get("addresses"),
service.get("port"),
service.get("properties").get("path"),
)


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

thing = browser.wait_for_first()
thing = browser.wait_for_first()
42 changes: 37 additions & 5 deletions labthings_client/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from ipaddress import IPv4Address, IPv6Address

from .utilities import AttributeDict
from .affordances import Property, Action
from .affordances import Property, MozillaProperty, Action

class FoundThing:

class Thing:
def __init__(self, name, addresses, port, path, protocol="http"):
self.name = name
self.addresses = addresses
Expand Down Expand Up @@ -40,13 +41,44 @@ def description(self):

def update(self):
self.thing_description = self.fetch_description()
self.properties = AttributeDict({k: Property(v, base_url = self.base) for k, v in self.thing_description.get("properties", {}).items()})
self.actions = AttributeDict({k: Action(v, base_url = self.base) for k, v in self.thing_description.get("actions", {}).items()})
self.properties = AttributeDict(
{
k: Property(k, v, base_url=self.base)
for k, v in self.thing_description.get("properties", {}).items()
}
)
self.actions = AttributeDict(
{
k: Action(k, v, base_url=self.base)
for k, v in self.thing_description.get("actions", {}).items()
}
)

def fetch_description(self):
for url in self.urls:
response = requests.get(url)
if response.json():
return response.json()
# If we reach this line, no URL gave a valid JSON response
raise RuntimeError("No valid Thing Description found")
raise RuntimeError("No valid Thing Description found")


class WebThing(Thing):
def update(self):
self.thing_description = self.fetch_description()
self.properties = AttributeDict(
{
k: MozillaProperty(k, v, base_url=self.base,)
for k, v in self.thing_description.get("properties", {}).items()
}
)
self.actions = AttributeDict(
{
k: Action(k, v, base_url=self.base)
for k, v in self.thing_description.get("actions", {}).items()
}
)


class LabThing(Thing):
pass

0 comments on commit dc262d7

Please sign in to comment.