Skip to content

Commit 630adca

Browse files
authored
Merge pull request #80 from labthings/better-events
Better events
2 parents 66e6731 + 8c9b9ef commit 630adca

File tree

11 files changed

+136
-70
lines changed

11 files changed

+136
-70
lines changed

.coveragerc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[run]
22
branch = True
3-
source = ./labthings
4-
omit = .venv/*, labthings/server/wsgi/*, , labthings/server/monkey.py
3+
source = ./src/labthings
4+
omit = .venv/*, ./src/labthings/server/wsgi/*, ./src/labthings/server/monkey.py
55
concurrency = greenlet
66

77
[report]

src/labthings/server/decorators.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .view import View
1515
from .find import current_labthing
1616
from .utilities import unpack
17+
from .event import PropertyStatusEvent, ActionStatusEvent
1718

1819
from labthings.core.tasks.pool import TaskThread
1920
from labthings.core.utilities import merge
@@ -122,6 +123,7 @@ def ThingAction(viewcls: View):
122123
Returns:
123124
View: View class with Action spec tags
124125
"""
126+
# TODO: Handle actionStatus messages
125127
# Update Views API spec
126128
tag_spec(viewcls, "actions")
127129
return viewcls
@@ -180,10 +182,20 @@ def wrapped(*args, **kwargs):
180182
# Call the update function first to update property value
181183
original_response = func(*args, **kwargs)
182184

183-
# Once updated, then notify all subscribers
184-
subscribers = getattr(current_labthing(), "subscribers", [])
185-
for sub in subscribers:
186-
sub.property_notify(viewcls)
185+
if hasattr(viewcls, "get_value") and callable(viewcls.get_value):
186+
property_value = viewcls().get_value()
187+
else:
188+
property_value = None
189+
190+
property_name = getattr(viewcls, "endpoint", None) or getattr(
191+
viewcls, "__name__", "unknown"
192+
)
193+
194+
if current_labthing():
195+
current_labthing().message(
196+
PropertyStatusEvent(property_name), property_value,
197+
)
198+
187199
return original_response
188200

189201
return wrapped

src/labthings/server/event.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import datetime
2+
3+
4+
class Event:
5+
def __init__(self, name, schema=None):
6+
self.name = name
7+
self.schema = schema
8+
9+
self.events = [] # TODO: Make rotating
10+
11+
def emit(self, data):
12+
response = {
13+
"messageType": "event",
14+
"timestamp": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
15+
"data": {self.name: data},
16+
} # TODO: Format data with schema
17+
self.events.append(response)
18+
return response
19+
20+
21+
class PropertyStatusEvent:
22+
def __init__(self, property_name, schema=None):
23+
self.name = property_name
24+
25+
def emit(self, data):
26+
response = {
27+
"messageType": "propertyStatus",
28+
"timestamp": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
29+
"data": {self.name: data},
30+
}
31+
return response
32+
33+
34+
class ActionStatusEvent:
35+
def __init__(self, action_name, schema=None):
36+
self.name = action_name
37+
38+
def emit(self, data):
39+
response = {
40+
"messageType": "actionStatus",
41+
"timestamp": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
42+
"data": {self.name: data},
43+
}
44+
return response

src/labthings/server/labthing.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .spec.td import ThingDescription
2121
from .decorators import tag
2222
from .sockets import Sockets
23+
from .event import Event
2324

2425
from .view.builder import property_of, action_from
2526

@@ -55,6 +56,8 @@ def __init__(
5556

5657
self.extensions = {}
5758

59+
self.events = {}
60+
5861
self.views = []
5962
self._property_views = {}
6063
self._action_views = {}
@@ -163,6 +166,9 @@ def init_app(self, app):
163166
self.sockets = Sockets(app)
164167
self._create_base_sockets()
165168

169+
# Create base events
170+
self.add_event("logging")
171+
166172
def _create_base_routes(self):
167173
# Add root representation
168174
self.add_view(RootView, "/", endpoint="root")
@@ -319,6 +325,28 @@ def _register_view(self, app, view, *urls, endpoint=None, **kwargs):
319325
self.thing_description.property(flask_rules, view)
320326
self._property_views[view.endpoint] = view
321327

328+
# Event stuff
329+
def add_event(self, name, schema=None):
330+
# TODO: Handle schema
331+
# TODO: Add view for event, returning list of Event.events
332+
self.events[name] = Event(name, schema=schema)
333+
self.thing_description.event(self.events[name])
334+
335+
def emit(self, event_type: str, data: dict):
336+
"""
337+
Find a matching event type if one exists, and emit some data to it
338+
"""
339+
event_object = self.events[event_type]
340+
self.message(event_object, data)
341+
342+
def message(self, event: Event, data: dict):
343+
"""
344+
Emit an event object to all subscribers
345+
"""
346+
event_response = event.emit(data)
347+
for sub in self.subscribers:
348+
sub.emit(event_response)
349+
322350
# Utilities
323351

324352
def url_for(self, view, **values):

src/labthings/server/logging.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,8 @@ def emit(self, record):
1212
log_event = self.rest_format_record(record)
1313

1414
# Broadcast to subscribers
15-
subscribers = getattr(current_labthing(), "subscribers", [])
16-
for sub in subscribers:
17-
sub.event_notify(log_event)
15+
if current_labthing():
16+
current_labthing().emit("logging", log_event)
1817

1918
def rest_format_record(self, record):
20-
data = {
21-
"data": str(record.msg),
22-
"timestamp": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
23-
}
24-
level_string = record.levelname.lower()
25-
26-
return {level_string: data}
19+
return {"message": str(record.msg), "level": record.levelname.lower()}

src/labthings/server/sockets/base.py

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,9 @@ class SocketSubscriber:
1010
def __init__(self, ws):
1111
self.ws = ws
1212

13-
def property_notify(self, viewcls):
14-
if hasattr(viewcls, "get_value") and callable(viewcls.get_value):
15-
property_value = viewcls().get_value()
16-
else:
17-
property_value = None
18-
19-
property_name = getattr(viewcls, "endpoint", None) or getattr(
20-
viewcls, "__name__", "unknown"
21-
)
22-
23-
response = encode_json(
24-
{"messageType": "propertyStatus", "data": {property_name: property_value}}
25-
)
26-
27-
self.ws.send(response)
28-
29-
def event_notify(self, event_dict: dict):
30-
response = encode_json({"messageType": "event", "data": event_dict})
31-
13+
def emit(self, event: dict):
14+
response = encode_json(event)
15+
# TODO: Logic surrounding if this subscriber is subscribed to the requested event type
3216
self.ws.send(response)
3317

3418

src/labthings/server/spec/td.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import weakref
44

55
from ..view import View
6+
from ..event import Event
67

78
from .utilities import get_spec, convert_schema, schema_to_json, get_topmost_spec_attr
89
from .paths import rule_to_params, rule_to_path
@@ -92,12 +93,17 @@ def to_dict(self):
9293
"description": current_labthing().description,
9394
"properties": self.properties,
9495
"actions": self.actions,
96+
# "events": self.events, # TODO: Enable once properly populated
9597
"links": self.links,
9698
# TODO: Add proper security schemes
9799
"securityDefinitions": {"nosec_sc": {"scheme": "nosec"}},
98100
"security": ["nosec_sc"],
99101
}
100102

103+
def event_to_thing_event(self, event: Event):
104+
# TODO: Include event schema
105+
return {}
106+
101107
def view_to_thing_property(self, rules: list, view: View):
102108
prop_urls = [rule_to_path(rule) for rule in rules]
103109

@@ -237,3 +243,6 @@ def build_forms_for_view(self, rules: list, view: View, op: list):
237243
forms.append({"op": op, "href": url, "contentType": content_type})
238244

239245
return forms
246+
247+
def event(self, event: Event):
248+
self.events[event.name] = self.event_to_thing_event(event)

tests/test_server_default_views_socket_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ def test_socket_handler(thing_ctx, fake_websocket):
77
socket_handler(ws)
88
# Only responses should be announcing new subscribers
99
for response in ws.responses:
10-
assert '"data": "Added subscriber' in response
10+
assert '"message": "Added subscriber' in response

tests/test_server_event.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from labthings.server import event
2+
3+
4+
def test_event():
5+
e = event.Event("eventName")
6+
event_data = {"key": "value"}
7+
8+
response = e.emit(event_data)
9+
assert response.get("messageType") == "event"
10+
assert response.get("data") == {"eventName": event_data}
11+
assert response in e.events
12+
13+
14+
def test_property_status_event():
15+
e = event.PropertyStatusEvent("propertyName")
16+
event_data = {"key": "value"}
17+
18+
response = e.emit(event_data)
19+
assert response.get("messageType") == "propertyStatus"
20+
assert response.get("data") == {"propertyName": event_data}
21+
22+
23+
def test_action_status_event():
24+
e = event.ActionStatusEvent("actionName")
25+
event_data = {"key": "value"}
26+
27+
response = e.emit(event_data)
28+
assert response.get("messageType") == "actionStatus"
29+
assert response.get("data") == {"actionName": event_data}

tests/test_server_sockets.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,6 @@
44
from flask import Blueprint
55

66

7-
def test_socket_subscriber_property_notify(view_cls, fake_websocket):
8-
setattr(view_cls, "endpoint", "index")
9-
ws = fake_websocket("", recieve_once=True)
10-
sub = base.SocketSubscriber(ws)
11-
12-
sub.property_notify(view_cls)
13-
assert json.loads(ws.response) == {
14-
"messageType": "propertyStatus",
15-
"data": {"index": "GET"},
16-
}
17-
18-
19-
def test_socket_subscriber_property_notify_empty_view(flask_view_cls, fake_websocket):
20-
ws = fake_websocket("", recieve_once=True)
21-
sub = base.SocketSubscriber(ws)
22-
23-
sub.property_notify(flask_view_cls)
24-
assert json.loads(ws.response) == {
25-
"messageType": "propertyStatus",
26-
"data": {flask_view_cls.__name__: None},
27-
}
28-
29-
30-
def test_socket_subscriber_event_notify(fake_websocket):
31-
ws = fake_websocket("", recieve_once=True)
32-
sub = base.SocketSubscriber(ws)
33-
34-
data = {"key": "value"}
35-
36-
sub.event_notify(data)
37-
assert json.loads(ws.response) == {"messageType": "event", "data": data}
38-
39-
407
def test_sockets_flask_init(app):
418
original_wsgi_app = app.wsgi_app
429
socket = gsocket.Sockets(app)

0 commit comments

Comments
 (0)