Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def cleanup():
title="My Lab Device API",
description="Test LabThing-based API",
version="0.1.0",
types=["org.labthings.examples.builder"],
)

# Attach an instance of our component
Expand Down
5 changes: 4 additions & 1 deletion labthings/server/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,10 @@ def __init__(self, code, description=None, mimetype=None, **kwargs):

if self.mimetype:
self.response_dict.update(
{"responses": {self.code: {"content": {self.mimetype: {}}}}}
{
"responses": {self.code: {"content": {self.mimetype: {}}}},
"_content_type": self.mimetype,
}
)

def __call__(self, f):
Expand Down
7 changes: 4 additions & 3 deletions labthings/server/find.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,23 @@
from . import EXTENSION_NAME


def current_labthing():
def current_labthing(app=None):
"""The LabThing instance handling current requests.

Searches for a valid LabThing extension attached to the current Flask context.
"""
# We use _get_current_object so that Task threads can still
# reach the Flask app object. Just using current_app returns
# a wrapper, which breaks it's use in Task threads
app = current_app._get_current_object() # skipcq: PYL-W0212
if not app:
app = current_app._get_current_object() # skipcq: PYL-W0212
if not app:
return None
logging.debug("Active app extensions:")
logging.debug(app.extensions)
logging.debug("Active labthing:")
logging.debug(app.extensions[EXTENSION_NAME])
return app.extensions[EXTENSION_NAME]
return app.extensions.get(EXTENSION_NAME, None)


def registered_extensions(labthing_instance=None):
Expand Down
9 changes: 9 additions & 0 deletions labthings/server/labthing.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def __init__(
prefix: str = "",
title: str = "",
description: str = "",
types: list = [],
version: str = "0.0.0",
):
self.app = app # Becomes a Flask app
Expand All @@ -46,6 +47,14 @@ def __init__(
self.endpoints = set()

self.url_prefix = prefix

for t in types:
if ";" in t:
raise ValueError(
f'Error in type value "{t}". Thing types cannot contain ; character.'
)
self.types = types

self._description = description
self._title = title
self._version = version
Expand Down
8 changes: 7 additions & 1 deletion labthings/server/quick.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def create_app(
prefix: str = "",
title: str = "",
description: str = "",
types: list = [],
version: str = "0.0.0",
handle_errors: bool = True,
handle_cors: bool = True,
Expand Down Expand Up @@ -52,7 +53,12 @@ def create_app(

# Create a LabThing
labthing = LabThing(
app, prefix=prefix, title=title, description=description, version=str(version)
app,
prefix=prefix,
title=title,
description=description,
types=types,
version=str(version),
)

# Store references to added-in handlers
Expand Down
61 changes: 0 additions & 61 deletions labthings/server/sockets/eventlet.py

This file was deleted.

39 changes: 38 additions & 1 deletion labthings/server/spec/td.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from ..view import View

from .utilities import get_spec, convert_schema, schema_to_json
from .utilities import get_spec, convert_schema, schema_to_json, get_topmost_spec_attr
from .paths import rule_to_params, rule_to_path

from ..find import current_labthing
Expand Down Expand Up @@ -79,12 +79,17 @@ def add_link(self, view, rel, kwargs=None, params=None):
def to_dict(self):
return {
"@context": "https://www.w3.org/2019/wot/td/v1",
"@type": current_labthing().types,
"id": url_for("root", _external=True),
"base": url_for("root", _external=True),
"title": current_labthing().title,
"description": current_labthing().description,
"properties": self.properties,
"actions": self.actions,
"links": self.links,
# TODO: Add proper security schemes
"securityDefinitions": {"nosec_sc": {"scheme": "nosec"}},
"security": ["nosec_sc"],
}

def view_to_thing_property(self, rules: list, view: View):
Expand All @@ -104,6 +109,7 @@ def view_to_thing_property(self, rules: list, view: View):
"writeOnly": not hasattr(view, "get"),
# TODO: Make URLs absolute
"links": [{"href": f"{url}"} for url in prop_urls],
"forms": self.view_to_thing_property_forms(rules, view),
"uriVariables": {},
}

Expand Down Expand Up @@ -134,6 +140,20 @@ def view_to_thing_property(self, rules: list, view: View):

return prop_description

def view_to_thing_property_forms(self, rules: list, view: View):
readable = (
hasattr(view, "post") or hasattr(view, "put") or hasattr(view, "delete")
)
writeable = hasattr(view, "get")

op = []
if readable:
op.append("readproperty")
if writeable:
op.append("writeproperty")

return self.build_forms_for_view(rules, view, op=op)

def view_to_thing_action(self, rules: list, view: View):
action_urls = [rule_to_path(rule) for rule in rules]

Expand All @@ -145,14 +165,31 @@ def view_to_thing_action(self, rules: list, view: View):
or (get_docstring(view.post) if hasattr(view, "post") else ""),
# TODO: Make URLs absolute
"links": [{"href": f"{url}"} for url in action_urls],
"forms": self.view_to_thing_action_forms(rules, view),
}

return action_description

def view_to_thing_action_forms(self, rules: list, view: View):
return self.build_forms_for_view(rules, view, op=["invokeaction"])

def property(self, rules: list, view: View):
key = snake_to_camel(view.endpoint)
self.properties[key] = self.view_to_thing_property(rules, view)

def action(self, rules: list, view: View):
key = snake_to_camel(view.endpoint)
self.actions[key] = self.view_to_thing_action(rules, view)

def build_forms_for_view(self, rules: list, view: View, op: list):
forms = []
prop_urls = [rule_to_path(rule) for rule in rules]

content_type = (
get_topmost_spec_attr(view, "_content_type") or "application/json"
)

for url in prop_urls:
forms.append({"op": op, "href": url, "contentType": content_type})

return forms
25 changes: 25 additions & 0 deletions labthings/server/spec/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,35 @@ def get_spec(obj):
Returns:
dict: API spec dictionary. Returns empty dictionary if no spec is found.
"""
if not obj:
return {}
obj.__apispec__ = obj.__dict__.get("__apispec__", {})
return obj.__apispec__ or {}


def get_topmost_spec_attr(view, spec_key: str):
"""
Get the __apispec__ value corresponding to spec_key, from first the root view,
falling back to GET, POST, and PUT in that descending order of priority

Args:
obj: Python object

Returns:
spec value corresponding to spec_key
"""
spec = get_spec(view)
value = spec.get(spec_key)

if not value:
for meth in ["get", "post", "put"]:
spec = get_spec(getattr(view, meth, None))
value = spec.get(spec_key)
if value:
break
return value


def convert_schema(schema, spec: APISpec):
"""
Ensure that a given schema is either a real Marshmallow schema,
Expand Down
38 changes: 0 additions & 38 deletions labthings/server/wsgi/eventlet.py

This file was deleted.

Loading