Skip to content

Commit 797388d

Browse files
committed
Started adding Mozilla semantic classes
1 parent b2ff9d6 commit 797388d

File tree

7 files changed

+190
-49
lines changed

7 files changed

+190
-49
lines changed

examples/simple_thing.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,8 @@
1313
import atexit
1414

1515
from labthings.server.quick import create_app
16-
from labthings.server.decorators import (
17-
PropertySchema,
18-
use_args,
19-
marshal_with,
20-
)
16+
from labthings.server.decorators import PropertySchema, use_args, marshal_with, Doc
17+
from labthings.server import semantics
2118
from labthings.server.view import ActionView, PropertyView
2219
from labthings.server.find import find_component
2320
from labthings.server import fields
@@ -77,15 +74,8 @@ def average_data(self, n: int):
7774

7875

7976
# Define the data we're going to output (get), and what to expect in (post)
80-
@PropertySchema(
81-
fields.Integer(
82-
required=True,
83-
example=200,
84-
minimum=100,
85-
maximum=500,
86-
description="Value of magic_denoise",
87-
)
88-
)
77+
@semantics.moz.LevelProperty(100, 500, example=200)
78+
@Doc(description="Value of magic_denoise",)
8979
class DenoiseProperty(PropertyView):
9080

9181
# Main function to handle GET requests (read)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import moz
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from .. import decorators
2+
from .. import fields
3+
4+
# BASIC PROPERTIES
5+
class Property:
6+
def __init__(self, schema):
7+
self.schema = schema
8+
9+
def __call__(self, viewcls):
10+
# Use the class name as the semantic type
11+
viewcls = decorators.Semtype(self.__class__.__name__)(viewcls)
12+
viewcls = decorators.PropertySchema(self.schema)(viewcls)
13+
return viewcls
14+
15+
16+
class BooleanProperty(Property):
17+
"""https://iot.mozilla.org/schemas/#BooleanProperty"""
18+
19+
def __init__(self, **kwargs):
20+
schema = fields.Bool(required=True, **kwargs)
21+
Property.__init__(self, schema)
22+
23+
24+
class LevelProperty(Property):
25+
"""https://iot.mozilla.org/schemas/#LevelProperty"""
26+
27+
def __init__(self, minimum, maximum, **kwargs):
28+
schema = fields.Int(required=True, minimum=minimum, maximum=maximum, **kwargs,)
29+
30+
Property.__init__(self, schema)
31+
32+
33+
# INHERITED PROPERTIES
34+
35+
36+
class BrightnessProperty(LevelProperty):
37+
"""https://iot.mozilla.org/schemas/#BrightnessProperty"""
38+
39+
def __init__(self, **kwargs):
40+
LevelProperty.__init__(self, 0, 100, unit="percent", **kwargs)
41+
42+
43+
class OnOffProperty(BooleanProperty):
44+
"""https://iot.mozilla.org/schemas/#OnOffProperty"""
45+
46+
def __init__(self, **kwargs):
47+
BooleanProperty.__init__(self, **kwargs)

tests/conftest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import pytest
22
import os
3+
import json
4+
import jsonschema
35
from flask import Flask
46
from flask.views import MethodView
57
from apispec import APISpec
@@ -11,6 +13,24 @@
1113
from flask.testing import FlaskClient
1214

1315

16+
class Helpers:
17+
@staticmethod
18+
def validate_thing_description(thing_description, app_ctx, schemas_path):
19+
schema = json.load(open(os.path.join(schemas_path, "td_schema.json"), "r"))
20+
jsonschema.Draft7Validator.check_schema(schema)
21+
22+
with app_ctx.test_request_context():
23+
td_json = thing_description.to_dict()
24+
assert td_json
25+
26+
jsonschema.validate(instance=td_json, schema=schema)
27+
28+
29+
@pytest.fixture
30+
def helpers():
31+
return Helpers
32+
33+
1434
class FakeWebsocket:
1535
def __init__(self, message: str, recieve_once=True):
1636
self.message = message

tests/schemas/td_schema.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@
170170
}
171171
},
172172
"required": {
173-
"type": "array",
173+
"type": ["array", "boolean"],
174174
"items": {
175175
"type": "string"
176176
}
@@ -486,7 +486,7 @@
486486
}
487487
},
488488
"required": {
489-
"type": "array",
489+
"type": ["array", "boolean"],
490490
"items": {
491491
"type": "string"
492492
}

tests/test_server_semantics.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import pytest
2+
3+
from labthings.server.view import View
4+
from labthings.server import semantics
5+
6+
7+
@pytest.fixture
8+
def thing_description(thing):
9+
return thing.thing_description
10+
11+
12+
def test_moz_BooleanProperty(helpers, app, thing_description, app_ctx, schemas_path):
13+
@semantics.moz.BooleanProperty()
14+
class Index(View):
15+
def get(self):
16+
return "GET"
17+
18+
app.add_url_rule("/", view_func=Index.as_view("index"))
19+
rules = app.url_map._rules_by_endpoint["index"]
20+
21+
thing_description.property(rules, Index)
22+
23+
with app_ctx.test_request_context():
24+
assert (
25+
thing_description.to_dict()["properties"]["index"]["@type"]
26+
== "BooleanProperty"
27+
)
28+
assert thing_description.to_dict()["properties"]["index"]["type"] == "boolean"
29+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)
30+
31+
32+
def test_moz_LevelProperty(helpers, app, thing_description, app_ctx, schemas_path):
33+
@semantics.moz.LevelProperty(0, 100)
34+
class Index(View):
35+
def get(self):
36+
return "GET"
37+
38+
app.add_url_rule("/", view_func=Index.as_view("index"))
39+
rules = app.url_map._rules_by_endpoint["index"]
40+
41+
thing_description.property(rules, Index)
42+
43+
with app_ctx.test_request_context():
44+
assert (
45+
thing_description.to_dict()["properties"]["index"]["@type"]
46+
== "LevelProperty"
47+
)
48+
assert thing_description.to_dict()["properties"]["index"]["type"] == "integer"
49+
assert thing_description.to_dict()["properties"]["index"]["minimum"] == 0
50+
assert thing_description.to_dict()["properties"]["index"]["maximum"] == 100
51+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)
52+
53+
54+
def test_moz_BrightnessProperty(helpers, app, thing_description, app_ctx, schemas_path):
55+
@semantics.moz.BrightnessProperty()
56+
class Index(View):
57+
def get(self):
58+
return "GET"
59+
60+
app.add_url_rule("/", view_func=Index.as_view("index"))
61+
rules = app.url_map._rules_by_endpoint["index"]
62+
63+
thing_description.property(rules, Index)
64+
65+
with app_ctx.test_request_context():
66+
assert (
67+
thing_description.to_dict()["properties"]["index"]["@type"]
68+
== "BrightnessProperty"
69+
)
70+
assert thing_description.to_dict()["properties"]["index"]["type"] == "integer"
71+
assert thing_description.to_dict()["properties"]["index"]["minimum"] == 0
72+
assert thing_description.to_dict()["properties"]["index"]["maximum"] == 100
73+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)
74+
75+
76+
def test_moz_OnOffProperty(helpers, app, thing_description, app_ctx, schemas_path):
77+
@semantics.moz.OnOffProperty()
78+
class Index(View):
79+
def get(self):
80+
return "GET"
81+
82+
app.add_url_rule("/", view_func=Index.as_view("index"))
83+
rules = app.url_map._rules_by_endpoint["index"]
84+
85+
thing_description.property(rules, Index)
86+
87+
with app_ctx.test_request_context():
88+
assert (
89+
thing_description.to_dict()["properties"]["index"]["@type"]
90+
== "OnOffProperty"
91+
)
92+
assert thing_description.to_dict()["properties"]["index"]["type"] == "boolean"
93+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)

tests/test_server_spec_td.py

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import pytest
22

3-
import os
4-
import json
5-
import jsonschema
63
from labthings.server import fields
74
from labthings.server.view import View
85
from labthings.server.spec import td
@@ -13,17 +10,6 @@ def thing_description(thing):
1310
return thing.thing_description
1411

1512

16-
def validate_thing_description(thing_description, app_ctx, schemas_path):
17-
schema = json.load(open(os.path.join(schemas_path, "td_schema.json"), "r"))
18-
jsonschema.Draft7Validator.check_schema(schema)
19-
20-
with app_ctx.test_request_context():
21-
td_json = thing_description.to_dict()
22-
assert td_json
23-
24-
jsonschema.validate(instance=td_json, schema=schema)
25-
26-
2713
def test_find_schema_for_view_readonly():
2814
class ViewClass:
2915
def get(self):
@@ -58,13 +44,13 @@ class ViewClass:
5844
assert td.find_schema_for_view(ViewClass) == {}
5945

6046

61-
def test_td_init(thing_description, app_ctx, schemas_path):
47+
def test_td_init(helpers, thing_description, app_ctx, schemas_path):
6248
assert thing_description
6349

64-
validate_thing_description(thing_description, app_ctx, schemas_path)
50+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)
6551

6652

67-
def test_td_add_link(thing_description, view_cls, app_ctx, schemas_path):
53+
def test_td_add_link(helpers, thing_description, view_cls, app_ctx, schemas_path):
6854
thing_description.add_link(view_cls, "rel")
6955
assert {
7056
"rel": "rel",
@@ -73,7 +59,7 @@ def test_td_add_link(thing_description, view_cls, app_ctx, schemas_path):
7359
"kwargs": {},
7460
} in thing_description._links
7561

76-
validate_thing_description(thing_description, app_ctx, schemas_path)
62+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)
7763

7864

7965
def test_td_add_link_options(thing_description, view_cls):
@@ -99,18 +85,20 @@ def test_td_links(thing_description, app_ctx, view_cls):
9985
)
10086

10187

102-
def test_td_action(app, thing_description, view_cls, app_ctx, schemas_path):
88+
def test_td_action(helpers, app, thing_description, view_cls, app_ctx, schemas_path):
10389
app.add_url_rule("/", view_func=view_cls.as_view("index"))
10490
rules = app.url_map._rules_by_endpoint["index"]
10591

10692
thing_description.action(rules, view_cls)
10793

10894
with app_ctx.test_request_context():
10995
assert "index" in thing_description.to_dict().get("actions")
110-
validate_thing_description(thing_description, app_ctx, schemas_path)
96+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)
11197

11298

113-
def test_td_action_with_schema(app, thing_description, view_cls, app_ctx, schemas_path):
99+
def test_td_action_with_schema(
100+
helpers, app, thing_description, view_cls, app_ctx, schemas_path
101+
):
114102
view_cls.post.__apispec__ = {
115103
"_params": {"integer": fields.Int()},
116104
"@type": "ToggleAction",
@@ -128,10 +116,10 @@ def test_td_action_with_schema(app, thing_description, view_cls, app_ctx, schema
128116
"properties": {"integer": {"type": "integer", "format": "int32"}},
129117
"@type": "ToggleAction",
130118
}
131-
validate_thing_description(thing_description, app_ctx, schemas_path)
119+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)
132120

133121

134-
def test_td_property(app, thing_description, app_ctx, schemas_path):
122+
def test_td_property(helpers, app, thing_description, app_ctx, schemas_path):
135123
class Index(View):
136124
def get(self):
137125
return "GET"
@@ -143,17 +131,18 @@ def get(self):
143131

144132
with app_ctx.test_request_context():
145133
assert "index" in thing_description.to_dict().get("properties")
146-
validate_thing_description(thing_description, app_ctx, schemas_path)
134+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)
147135

148136

149-
def test_td_property_with_schema(app, thing_description, app_ctx, schemas_path):
137+
def test_td_property_with_schema(
138+
helpers, app, thing_description, app_ctx, schemas_path
139+
):
150140
class Index(View):
151141
def get(self):
152142
return "GET"
153143

154144
Index.__apispec__ = {
155-
"_propertySchema": {"integer": fields.Int()},
156-
"@type": "OnOffProperty",
145+
"_propertySchema": fields.Int(required=True),
157146
}
158147

159148
app.add_url_rule("/", view_func=Index.as_view("index"))
@@ -163,11 +152,12 @@ def get(self):
163152

164153
with app_ctx.test_request_context():
165154
assert "index" in thing_description.to_dict().get("properties")
166-
assert "@type" in thing_description.to_dict().get("properties").get("index", {})
167-
validate_thing_description(thing_description, app_ctx, schemas_path)
155+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)
168156

169157

170-
def test_td_property_with_url_param(app, thing_description, app_ctx, schemas_path):
158+
def test_td_property_with_url_param(
159+
helpers, app, thing_description, app_ctx, schemas_path
160+
):
171161
class Index(View):
172162
def get(self):
173163
return "GET"
@@ -179,10 +169,10 @@ def get(self):
179169

180170
with app_ctx.test_request_context():
181171
assert "index" in thing_description.to_dict().get("properties")
182-
validate_thing_description(thing_description, app_ctx, schemas_path)
172+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)
183173

184174

185-
def test_td_property_write_only(app, thing_description, app_ctx, schemas_path):
175+
def test_td_property_write_only(helpers, app, thing_description, app_ctx, schemas_path):
186176
class Index(View):
187177
def post(self):
188178
return "POST"
@@ -196,4 +186,4 @@ def post(self):
196186

197187
with app_ctx.test_request_context():
198188
assert "index" in thing_description.to_dict().get("properties")
199-
validate_thing_description(thing_description, app_ctx, schemas_path)
189+
helpers.validate_thing_description(thing_description, app_ctx, schemas_path)

0 commit comments

Comments
 (0)