Skip to content

Commit 0ef460d

Browse files
author
Joel Collins
committed
Added support for Thing-wide external links
1 parent 0f1e15b commit 0ef460d

File tree

4 files changed

+76
-18
lines changed

4 files changed

+76
-18
lines changed

src/labthings/labthing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ class LabThing:
6666
:type types: list of str
6767
:param format_flask_exceptions: JSON format all exception responses
6868
:type format_flask_exceptions: bool
69+
:param external_links: Use external links in Thing Description where possible
70+
:type external_links: bool
6971
:param json_encoder: JSON encoder class for the app
7072
"""
7173

@@ -78,6 +80,7 @@ def __init__(
7880
version: str = "0.0.0",
7981
types: list = None,
8082
format_flask_exceptions: bool = True,
83+
external_links: bool = True,
8184
json_encoder=LabThingsJSONEncoder,
8285
):
8386
if types is None:
@@ -136,7 +139,7 @@ def __init__(
136139
)
137140

138141
# Thing description
139-
self.thing_description = ThingDescription()
142+
self.thing_description = ThingDescription(external_links=external_links)
140143

141144
# JSON encoder class
142145
self.json_encoder = json_encoder

src/labthings/quick.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def create_app(
1212
description: str = "",
1313
types: list = None,
1414
version: str = "0.0.0",
15+
external_links: bool = True,
1516
handle_errors: bool = True,
1617
handle_cors: bool = True,
1718
flask_kwargs: dict = None,
@@ -40,6 +41,7 @@ def create_app(
4041
:param description: str: (Default value = "")
4142
:param types: list: (Default value = None)
4243
:param version: str: (Default value = "0.0.0")
44+
:param external_links: bool: Use external links in Thing Description where possible
4345
:param handle_errors: bool: (Default value = True)
4446
:param handle_cors: bool: (Default value = True)
4547
:param flask_kwargs: dict: (Default value = None)
@@ -69,6 +71,7 @@ def create_app(
6971
types=types,
7072
version=str(version),
7173
format_flask_exceptions=handle_errors,
74+
external_links=external_links,
7275
)
7376

7477
return app, labthing

src/labthings/td.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
from flask import url_for, request
1+
from flask import url_for, request, has_request_context
22

33
from .views import View
44
from .event import Event
55
from .json.schemas import schema_to_json, rule_to_params, rule_to_path
66
from .find import current_labthing
7-
from .utilities import get_docstring, snake_to_camel
7+
from .utilities import ResourceURL, get_docstring, snake_to_camel
88

99

10-
def view_to_thing_action_forms(rules: list, view: View):
10+
def view_to_thing_action_forms(rules: list, view: View, external: bool = True):
1111
"""Build a W3C form description for an ActionView
1212
1313
:param rules: List of Flask rules
@@ -16,6 +16,7 @@ def view_to_thing_action_forms(rules: list, view: View):
1616
:type view: View
1717
:param rules: list:
1818
:param view: View:
19+
:param external: bool: Use external links where possible
1920
:returns: Form description
2021
:rtype: [dict]
2122
@@ -36,7 +37,7 @@ def view_to_thing_action_forms(rules: list, view: View):
3637
form = {
3738
"op": "invokeaction",
3839
"htv:methodName": "POST",
39-
"href": url,
40+
"href": ResourceURL(url, external=external),
4041
"contentType": content_type,
4142
}
4243
if response_content_type != content_type:
@@ -47,7 +48,7 @@ def view_to_thing_action_forms(rules: list, view: View):
4748
return forms
4849

4950

50-
def view_to_thing_property_forms(rules: list, view: View):
51+
def view_to_thing_property_forms(rules: list, view: View, external: bool = True):
5152
"""Build a W3C form description for a PropertyView
5253
5354
:param rules: List of Flask rules
@@ -56,6 +57,7 @@ def view_to_thing_property_forms(rules: list, view: View):
5657
:type view: View
5758
:param rules: list:
5859
:param view: View:
60+
:param external: bool: Use external links where possible
5961
:returns: Form description
6062
:rtype: [dict]
6163
@@ -74,7 +76,7 @@ def view_to_thing_property_forms(rules: list, view: View):
7476
form = {
7577
"op": "readproperty",
7678
"htv:methodName": "GET",
77-
"href": url,
79+
"href": ResourceURL(url, external=external),
7880
"contentType": content_type,
7981
}
8082
forms.append(form)
@@ -85,7 +87,7 @@ def view_to_thing_property_forms(rules: list, view: View):
8587
form = {
8688
"op": "writeproperty",
8789
"htv:methodName": "PUT",
88-
"href": url,
90+
"href": ResourceURL(url, external=external),
8991
"contentType": content_type,
9092
}
9193
forms.append(form)
@@ -96,7 +98,7 @@ def view_to_thing_property_forms(rules: list, view: View):
9698
form = {
9799
"op": "writeproperty",
98100
"htv:methodName": "POST",
99-
"href": url,
101+
"href": ResourceURL(url, external=external),
100102
"contentType": content_type,
101103
}
102104
forms.append(form)
@@ -106,11 +108,20 @@ def view_to_thing_property_forms(rules: list, view: View):
106108

107109
class ThingDescription:
108110
""" """
109-
def __init__(self):
111+
112+
def __init__(self, external_links: bool = True):
113+
# Public attributes
110114
self.properties = {}
111115
self.actions = {}
112116
self.events = {}
117+
118+
# Private attributes
113119
self._links = []
120+
121+
# Settings
122+
self.external_links = external_links
123+
124+
# Init
114125
super().__init__()
115126

116127
@property
@@ -124,7 +135,7 @@ def links(self):
124135
"href": current_labthing().url_for(
125136
link_description.get("view"),
126137
**link_description.get("params"),
127-
_external=True,
138+
_external=self.external_links,
128139
),
129140
**link_description.get("kwargs"),
130141
}
@@ -150,14 +161,13 @@ def add_link(self, view, rel, kwargs=None, params=None):
150161

151162
def to_dict(self):
152163
""" """
153-
return {
164+
td = {
154165
"@context": [
155166
"https://www.w3.org/2019/wot/td/v1",
156167
"https://iot.mozilla.org/schemas/",
157168
],
158169
"@type": current_labthing().types,
159170
"id": url_for("root", _external=True),
160-
"base": request.host_url,
161171
"title": current_labthing().title,
162172
"description": current_labthing().description,
163173
"properties": self.properties,
@@ -168,6 +178,11 @@ def to_dict(self):
168178
"security": "nosec_sc",
169179
}
170180

181+
if not self.external_links and has_request_context():
182+
td["base"] = request.host_url
183+
184+
return td
185+
171186
def event_to_thing_event(self, event: Event):
172187
"""
173188
@@ -194,8 +209,13 @@ def view_to_thing_property(self, rules: list, view: View):
194209
hasattr(view, "post") or hasattr(view, "put") or hasattr(view, "delete")
195210
),
196211
"writeOnly": not hasattr(view, "get"),
197-
"links": [{"href": f"{url}"} for url in prop_urls],
198-
"forms": view_to_thing_property_forms(rules, view),
212+
"links": [
213+
{"href": ResourceURL(url, external=self.external_links)}
214+
for url in prop_urls
215+
],
216+
"forms": view_to_thing_property_forms(
217+
rules, view, external=self.external_links
218+
),
199219
"uriVariables": {},
200220
}
201221

@@ -243,10 +263,15 @@ def view_to_thing_action(self, rules: list, view: View):
243263
action_description = {
244264
"title": getattr(view, "title", None) or view.__name__,
245265
"description": getattr(view, "description", None) or get_docstring(view),
246-
"links": [{"href": f"{url}"} for url in action_urls],
266+
"links": [
267+
{"href": ResourceURL(url, external=self.external_links)}
268+
for url in action_urls
269+
],
247270
"safe": getattr(view, "safe", False),
248271
"idempotent": getattr(view, "idempotent", False),
249-
"forms": view_to_thing_action_forms(rules, view),
272+
"forms": view_to_thing_action_forms(
273+
rules, view, external=self.external_links
274+
),
250275
}
251276

252277
# Look for a _params in the Action classes API Spec

src/labthings/utilities.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from werkzeug.http import HTTP_STATUS_CODES
2-
from flask import current_app
2+
from flask import current_app, has_request_context, request
33

44
import collections.abc
5+
from collections import UserString
56
import re
67
import operator
78
import sys
@@ -39,6 +40,32 @@ def stopped(self):
3940
return time.time() >= self.timeout_time
4041

4142

43+
class ResourceURL(UserString):
44+
"""
45+
Takes a URL path relative to the host_url (e.g. /api/myresource),
46+
and optionally prepends the host URL to generate a complete URL
47+
(e.g. http://localhost:7485/api/myresource).
48+
Behaves as a Python string.
49+
"""
50+
51+
def __init__(self, path: str, external: bool = True):
52+
self.path = path
53+
self.external = external
54+
UserString.__init__(self, path)
55+
56+
@property
57+
def data(self):
58+
if self.external and has_request_context():
59+
prefix = request.host_url.rstrip("/")
60+
else:
61+
prefix = ""
62+
return prefix + self.path
63+
64+
@data.setter
65+
def data(self, path: str):
66+
self.path = path
67+
68+
4269
def http_status_message(code):
4370
"""Maps an HTTP status code to the textual status
4471

0 commit comments

Comments
 (0)