Skip to content

Commit bf41b50

Browse files
committed
Added Events support
1 parent ce604ff commit bf41b50

File tree

14 files changed

+327
-167
lines changed

14 files changed

+327
-167
lines changed

examples/simple_thing.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from labthings import ActionView, PropertyView, create_app, fields, find_component, op
44
from labthings.example_components import PretendSpectrometer
55

6+
import logging
7+
68
"""
79
Class for our lab component functionality. This could include serial communication,
810
equipment API calls, network requests, or a "virtual" device as seen here.
@@ -56,6 +58,7 @@ def get(self):
5658
my_component = find_component("org.labthings.example.mycomponent")
5759
return my_component.data
5860

61+
5962
"""
6063
Create a view to start an averaged measurement, and register is as a Thing action
6164
"""
@@ -66,7 +69,9 @@ class MeasurementAction(ActionView):
6669
# Pass to post function as dictionary argument.
6770
args = {
6871
"averages": fields.Integer(
69-
missing=20, example=20, description="Number of data sets to average over",
72+
missing=20,
73+
example=20,
74+
description="Number of data sets to average over",
7075
)
7176
}
7277
# Marshal the response as a list of numbers
@@ -76,13 +81,16 @@ class MeasurementAction(ActionView):
7681
@op.invokeaction
7782
def post(self, args):
7883
"""Start an averaged measurement"""
84+
logging.warning("Starting a measurement")
7985

8086
# Find our attached component
8187
my_component = find_component("org.labthings.example.mycomponent")
8288

8389
# Get arguments and start a background task
8490
n_averages = args.get("averages")
8591

92+
logging.warning("Finished a measurement")
93+
8694
# Return the task information
8795
return my_component.average_data(n_averages)
8896

src/labthings/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from .sync import ClientEvent, CompositeLock, StrictLock
3131

3232
# Views
33-
from .views import ActionView, PropertyView, op
33+
from .views import ActionView, PropertyView, EventView, op
3434

3535
# Suggested WSGI server class
3636
from .wsgi import Server
@@ -58,5 +58,5 @@
5858
"json",
5959
"PropertyView",
6060
"ActionView",
61-
"op"
61+
"op",
6262
]

src/labthings/actions/thread.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from flask import copy_current_request_context, has_request_context
99

1010
from ..utilities import TimeoutTracker
11+
from ..schema import LogRecordSchema
12+
from ..deque import Deque
1113

1214
_LOG = logging.getLogger(__name__)
1315

@@ -29,6 +31,7 @@ def __init__(
2931
kwargs=None,
3032
daemon=True,
3133
default_stop_timeout: int = 5,
34+
log_len: int = 100,
3235
):
3336
threading.Thread.__init__(
3437
self,
@@ -76,10 +79,14 @@ def __init__(
7679
self._end_time = None # Task end time
7780

7881
# Public state properties
79-
self.input: dict = {} # Input arguments. TODO: Automate this. Currently manual via Action.dispatch_request
82+
self.input: dict = (
83+
{}
84+
) # Input arguments. TODO: Automate this. Currently manual via Action.dispatch_request
8085
self.progress: int = None # Percent progress of the task
8186
self.data = {} # Dictionary of custom data added during the task
82-
self.log = [] # The log will hold dictionary objects with log information
87+
self.log = Deque(
88+
None, log_len
89+
) # The log will hold dictionary objects with log information
8390

8491
# Stuff for handling termination
8592
self._running_lock = (
@@ -106,7 +113,7 @@ def status(self):
106113
Current running status of the thread.
107114
108115
============== =============================================
109-
Status Meaning
116+
Status Meaning
110117
============== =============================================
111118
``pending`` Not yet started
112119
``running`` Currently in-progress
@@ -143,7 +150,7 @@ def update_progress(self, progress: int):
143150
def update_data(self, data: dict):
144151
"""
145152
146-
:param data: dict:
153+
:param data: dict:
147154
148155
"""
149156
# Store data to be used before task finishes (eg for real-time plotting)
@@ -165,15 +172,15 @@ def _thread_proc(self, f):
165172
"""Wraps the target function to handle recording `status` and `return` to `state`.
166173
Happens inside the task thread.
167174
168-
:param f:
175+
:param f:
169176
170177
"""
171178

172179
def wrapped(*args, **kwargs):
173180
"""
174181
175-
:param *args:
176-
:param **kwargs:
182+
:param *args:
183+
:param **kwargs:
177184
178185
"""
179186
nonlocal self
@@ -222,7 +229,7 @@ def get(self, block=True, timeout=None):
222229
def _async_raise(self, exc_type):
223230
"""
224231
225-
:param exc_type:
232+
:param exc_type:
226233
227234
"""
228235
# Should only be called on a started thread, so raise otherwise.
@@ -319,18 +326,20 @@ def stop(self, timeout=None, exception=ActionKilledException):
319326

320327

321328
class ThreadLogHandler(logging.Handler):
322-
def __init__(self, thread=None, dest=None, level=logging.INFO):
329+
def __init__(
330+
self, thread=None, dest=None, level=logging.INFO, default_log_len: int = 100
331+
):
323332
"""Set up a log handler that appends messages to a list.
324-
333+
325334
This log handler will first filter by ``thread``, if one is
326335
supplied. This should be a ``threading.Thread`` object.
327336
Only log entries from the specified thread will be
328337
saved.
329-
338+
330339
``dest`` should specify a list, to which we will append
331340
each log entry as it comes in. If none is specified, a
332341
new list will be created.
333-
342+
334343
NB this log handler does not currently rotate or truncate
335344
the list - so if you use it on a thread that produces a
336345
lot of log messages, you may run into memory problems.
@@ -340,34 +349,28 @@ def __init__(self, thread=None, dest=None, level=logging.INFO):
340349
logging.Handler.__init__(self)
341350
self.setLevel(level)
342351
self.thread = thread
343-
self.dest = dest if dest is not None else []
352+
self.dest = dest if dest is not None else Deque(None, default_log_len)
344353
self.addFilter(self.check_thread)
345354

346355
def check_thread(self, record):
347356
"""Determine if a thread matches the desired record
348357
349-
:param record:
358+
:param record:
350359
351360
"""
352361
if self.thread is None:
353362
return 1
354-
355363
if threading.get_ident() == self.thread.ident:
356364
return 1
357365
return 0
358366

359367
def emit(self, record):
360368
"""Do something with a logged message
361369
362-
:param record:
370+
:param record:
363371
364372
"""
365-
record_dict = {"message": record.getMessage()}
366-
for k in ["created", "levelname", "levelno", "lineno", "filename"]:
367-
record_dict[k] = getattr(record, k)
368-
self.dest.append(record_dict)
369-
# FIXME: make sure this doesn't become a memory disaster!
370-
# We probably need to check the size of the list...
373+
self.dest.append(LogRecordSchema().dump(record))
371374
# TODO: think about whether any of the keys are security flaws
372375
# (this is why I don't dump the whole logrecord)
373376

src/labthings/apispec/plugins.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
from .. import fields
99
from ..json.schemas import schema_to_json
10-
from ..schema import build_action_schema
10+
from ..schema import build_action_schema, EventSchema
1111
from ..utilities import get_docstring, get_summary, merge
12-
from ..views import ActionView, PropertyView
12+
from ..views import ActionView, PropertyView, EventView
1313

1414

1515
class ExtendedOpenAPIConverter(OpenAPIConverter):
@@ -19,16 +19,16 @@ class ExtendedOpenAPIConverter(OpenAPIConverter):
1919

2020
def init_attribute_functions(self, *args, **kwargs):
2121
"""
22-
:param *args:
23-
:param **kwargs:
22+
:param *args:
23+
:param **kwargs:
2424
"""
2525
OpenAPIConverter.init_attribute_functions(self, *args, **kwargs)
2626
self.attribute_functions.append(self.jsonschema_type_mapping)
2727

2828
def jsonschema_type_mapping(self, field, **kwargs):
2929
"""
30-
:param field:
31-
:param **kwargs:
30+
:param field:
31+
:param **kwargs:
3232
"""
3333
ret = {}
3434
if hasattr(field, "_jsonschema_type_mapping"):
@@ -215,6 +215,37 @@ def spec_for_action(cls, action):
215215
d["post"]["responses"].update(action.responses)
216216
return d
217217

218+
@classmethod
219+
def spec_for_event(cls, event):
220+
class_json_schema = schema_to_json(event.schema) if event.schema else None
221+
queue_json_schema = schema_to_json(EventSchema(many=True))
222+
if class_json_schema:
223+
queue_json_schema["properties"]["data"] = class_json_schema
224+
225+
d = cls.spec_for_interaction(event)
226+
227+
# Add in Action spec
228+
d = merge(
229+
d,
230+
{
231+
"get": {
232+
"responses": {
233+
200: {
234+
"description": "Event queue",
235+
"content": {
236+
"application/json": (
237+
{"schema": queue_json_schema}
238+
if queue_json_schema
239+
else {}
240+
)
241+
},
242+
}
243+
},
244+
},
245+
},
246+
)
247+
return d
248+
218249
def operation_helper(self, path, operations, **kwargs):
219250
"""Path helper that allows passing a Flask view function."""
220251
# rule = self._rule_for_view(interaction.dispatch_request, app=app)
@@ -224,4 +255,6 @@ def operation_helper(self, path, operations, **kwargs):
224255
ops = self.spec_for_property(interaction)
225256
elif issubclass(interaction, ActionView):
226257
ops = self.spec_for_action(interaction)
258+
elif issubclass(interaction, EventView):
259+
ops = self.spec_for_event(interaction)
227260
operations.update(ops)

src/labthings/default_views/actions.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,38 @@ class ActionQueueView(View):
1111
"""List of all actions from the session"""
1212

1313
def get(self):
14-
""" """
14+
"""Action queue
15+
---
16+
description: Queue of most recent actions in the session
17+
summary: Queue of most recent actions in the session
18+
responses:
19+
200:
20+
content:
21+
application/json:
22+
schema: ActionSchema
23+
"""
1524
return ActionSchema(many=True).dump(current_labthing().actions.threads)
1625

1726

1827
class ActionObjectView(View):
1928
"""Manage a particular action.
20-
29+
2130
GET will safely return the current action progress.
2231
DELETE will cancel the action, if pending or running.
2332
2433
2534
"""
2635

2736
def get(self, task_id):
28-
"""Show status of a session task
29-
30-
Includes progress and intermediate data.
31-
32-
:param task_id:
33-
37+
"""Show the status of an Action
38+
---
39+
description: Status of an Action
40+
summary: Status of an Action
41+
responses:
42+
200:
43+
content:
44+
application/json:
45+
schema: ActionSchema
3446
"""
3547
task_dict = current_labthing().actions.to_dict()
3648

@@ -43,13 +55,15 @@ def get(self, task_id):
4355

4456
@use_args({"timeout": fields.Int()})
4557
def delete(self, args, task_id):
46-
"""Terminate a running task.
47-
48-
If the task is finished, deletes its entry.
49-
50-
:param args:
51-
:param task_id:
52-
58+
"""Cancel a running Action
59+
---
60+
description: Cancel an Action
61+
summary: Cancel an Action
62+
responses:
63+
200:
64+
content:
65+
application/json:
66+
schema: ActionSchema
5367
"""
5468
timeout = args.get("timeout", None)
5569
task_dict = current_labthing().actions.to_dict()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .. import fields
2+
from ..schema import LogRecordSchema
3+
from ..views import EventView
4+
5+
6+
class LoggingEventView(EventView):
7+
"""List of latest logging events from the session"""
8+
9+
schema = LogRecordSchema()

src/labthings/default_views/extensions.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ class ExtensionList(View):
1111

1212
def get(self):
1313
"""List enabled extensions.
14-
14+
1515
Returns a list of Extension representations, including basic documentation.
1616
Describes server methods, web views, and other relevant Lab Things metadata.
17-
18-
17+
---
18+
description: Extensions list
19+
summary: Extensions list
20+
responses:
21+
200:
22+
content:
23+
application/json:
24+
schema: ExtensionSchema
1925
"""
2026
return ExtensionSchema(many=True).dump(registered_extensions().values() or [])

src/labthings/default_views/root.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,9 @@ class RootView(View):
66
"""W3C Thing Description"""
77

88
def get(self):
9-
""" """
9+
"""Thing Description
10+
---
11+
description: Thing Description
12+
summary: Thing Description
13+
"""
1014
return current_labthing().thing_description.to_dict()

0 commit comments

Comments
 (0)