Skip to content

Commit a5d30cf

Browse files
authored
Merge pull request #117 from labthings/overridable-schemas
More opinionated schema documentation
2 parents 80410f1 + 2f1b117 commit a5d30cf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+599
-4605
lines changed

examples/simple_extensions.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
from labthings.monkey import patch_all
2+
3+
patch_all()
4+
15
import random
26
import math
7+
import time
38
import logging
49

510
from labthings.server.quick import create_app
611
from labthings.server.view import ActionView, PropertyView
712
from labthings.server.find import find_component
813
from labthings.server import fields
914
from labthings.core.utilities import path_relative_to
10-
from labthings.core.tasks import taskify
1115

1216
from labthings.server.extensions import BaseExtension
1317

@@ -35,10 +39,7 @@ def post(self, args):
3539

3640
# Get arguments and start a background task
3741
n_averages = args.get("averages")
38-
task = taskify(my_component.average_data)(n_averages)
39-
40-
# Return the task information
41-
return task
42+
return my_component.average_data(n_averages)
4243

4344

4445
def ext_on_register():
@@ -91,6 +92,18 @@ def data(self):
9192
"""
9293
return [self.noisy_pdf(x) for x in self.x_range]
9394

95+
def average_data(self, n: int):
96+
"""Average n-sets of data. Emulates a measurement that may take a while."""
97+
summed_data = self.data
98+
99+
for _ in range(n):
100+
summed_data = [summed_data[i] + el for i, el in enumerate(self.data)]
101+
time.sleep(0.25)
102+
103+
summed_data = [i / n for i in summed_data]
104+
105+
return summed_data
106+
94107

95108
"""
96109
Create a view to view and change our magic_denoise value, and register is as a Thing property

src/labthings/apispec/apispec.py

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from ..utilities import get_docstring, get_summary
66

7+
from flask.views import http_method_funcs
78
from werkzeug.routing import Rule
89
from http import HTTPStatus
910

@@ -22,7 +23,7 @@ def rule_to_apispec_path(rule: Rule, view, apispec: APISpec):
2223

2324
params = {
2425
"path": rule_to_path(rule),
25-
"operations": view_to_apispec_operations(view, apispec),
26+
"operations": view.get_apispec(),
2627
}
2728

2829
# Add URL arguments to operations
@@ -32,53 +33,3 @@ def rule_to_apispec_path(rule: Rule, view, apispec: APISpec):
3233
params["operations"][op].update({"parameters": rule_to_params(rule)})
3334

3435
return params
35-
36-
37-
def view_to_apispec_operations(view, apispec: APISpec):
38-
"""Generate APISpec `operations` argument from a flask View"""
39-
40-
# Build dictionary of operations (HTTP methods)
41-
ops = {}
42-
for op in ("get", "post", "put", "delete"):
43-
if hasattr(view, op):
44-
45-
ops[op] = {
46-
"description": getattr(view, "description", None)
47-
or get_docstring(view),
48-
"summary": getattr(view, "summary", None) or get_summary(view),
49-
"tags": list(view.get_tags()),
50-
}
51-
52-
# Add arguments schema
53-
if (op in (("post", "put", "delete"))) and hasattr(view, "get_args"):
54-
request_schema = convert_to_schema_or_json(view.get_args(), apispec)
55-
if request_schema:
56-
ops[op]["requestBody"] = {
57-
"content": {"application/json": {"schema": request_schema}}
58-
}
59-
60-
# Add response schema
61-
if hasattr(view, "get_responses"):
62-
ops[op]["responses"] = {}
63-
64-
for code, response in view.get_responses().items():
65-
ops[op]["responses"][code] = {
66-
"description": response.get("description")
67-
or HTTPStatus(code).phrase,
68-
"content": {
69-
# See if response description specifies a content_type
70-
# If not, assume application/json
71-
response.get("content_type", "application/json"): {
72-
"schema": convert_to_schema_or_json(
73-
response.get("schema"), apispec
74-
)
75-
}
76-
if response.get("schema")
77-
else {} # If no schema is defined, don't include one in the APISpec
78-
},
79-
}
80-
else:
81-
# If no explicit responses are known, populate with defaults
82-
ops[op]["responses"] = {200: {HTTPStatus(200).phrase}}
83-
84-
return ops

src/labthings/apispec/converter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ class ExtendedOpenAPIConverter(OpenAPIConverter):
66

77
def init_attribute_functions(self, *args, **kwargs):
88
OpenAPIConverter.init_attribute_functions(self, *args, **kwargs)
9-
self.attribute_functions.append(self.bytes2json)
9+
self.attribute_functions.append(self.jsonschema_type_mapping)
1010

11-
def bytes2json(self, field, **kwargs):
11+
def jsonschema_type_mapping(self, field, **kwargs):
1212
ret = {}
1313
if hasattr(field, "_jsonschema_type_mapping"):
1414
schema = field._jsonschema_type_mapping()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from flask import abort
2+
3+
from ..view import View
4+
from ..view.marshalling import marshal_with
5+
from ..schema import ActionSchema
6+
from ..find import current_thing
7+
8+
9+
class ActionQueue(View):
10+
"""
11+
List of all actions from the session
12+
"""
13+
14+
def get(self):
15+
return ActionSchema(many=True).dump(current_thing.actions.greenlets)
16+
17+
18+
class ActionView(View):
19+
"""
20+
Manage a particular action.
21+
22+
GET will safely return the current action progress.
23+
DELETE will cancel the action, if pending or running.
24+
"""
25+
26+
def get(self, task_id):
27+
"""
28+
Show status of a session task
29+
30+
Includes progress and intermediate data.
31+
"""
32+
task_dict = current_thing.actions.to_dict()
33+
34+
if task_id not in task_dict:
35+
return abort(404) # 404 Not Found
36+
37+
task = task_dict.get(task_id)
38+
39+
return ActionSchema().dump(task)
40+
41+
def delete(self, task_id):
42+
"""
43+
Terminate a running task.
44+
45+
If the task is finished, deletes its entry.
46+
"""
47+
task_dict = current_thing.actions.to_dict()
48+
49+
if task_id not in task_dict:
50+
return abort(404) # 404 Not Found
51+
52+
task = task_dict.get(task_id)
53+
54+
task.kill(block=True, timeout=3)
55+
56+
return ActionSchema().dump(task)

src/labthings/default_views/docs/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from flask import render_template, Blueprint, make_response
22

33
from ...view import View
4-
from ...find import current_labthing
4+
from ...find import current_thing
55

66

77
class APISpecView(View):
88
"""OpenAPI v3 documentation"""
99

1010
def get(self):
1111
"""OpenAPI v3 documentation"""
12-
return current_labthing().spec.to_dict()
12+
return current_thing.spec.to_dict()
1313

1414

1515
class SwaggerUIView(View):

src/labthings/default_views/extensions.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
class ExtensionList(View):
88
"""List and basic documentation for all enabled Extensions"""
99

10-
schema = ExtensionSchema(many=True)
1110
tags = ["extensions"]
1211

1312
def get(self):
@@ -17,4 +16,4 @@ def get(self):
1716
Returns a list of Extension representations, including basic documentation.
1817
Describes server methods, web views, and other relevant Lab Things metadata.
1918
"""
20-
return registered_extensions().values() or []
19+
return ExtensionSchema(many=True).dump(registered_extensions().values() or [])
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from ..find import current_labthing
1+
from ..find import current_thing
22
from ..view import View
33

44

55
class RootView(View):
66
"""W3C Thing Description"""
77

88
def get(self):
9-
return current_labthing().thing_description.to_dict()
9+
return current_thing.thing_description.to_dict()

src/labthings/default_views/sockets.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from ..sockets import SocketSubscriber
2-
from ..find import current_labthing
2+
from ..find import current_thing
33

44
import gevent
55
import logging
@@ -11,7 +11,7 @@
1111
def socket_handler(ws):
1212
# Create a socket subscriber
1313
wssub = SocketSubscriber(ws)
14-
current_labthing().subscribers.add(wssub)
14+
current_thing.subscribers.add(wssub)
1515
logging.info(f"Added subscriber {wssub}")
1616
# Start the socket connection handler loop
1717
while not ws.closed:
@@ -23,7 +23,7 @@ def socket_handler(ws):
2323
ws.send(response)
2424
gevent.sleep(0.1)
2525
# Remove the subscriber once the loop returns
26-
current_labthing().subscribers.remove(wssub)
26+
current_thing.subscribers.remove(wssub)
2727
logging.info(f"Removed subscriber {wssub}")
2828

2929

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
from flask import abort
2+
import logging
23

34
from ..view import View
5+
from ..view.marshalling import marshal_with
46
from ..schema import TaskSchema
5-
6-
from .. import tasks
7+
from ..find import current_thing
78

89

910
class TaskList(View):
11+
"""
12+
List of all background tasks from the session
13+
"""
14+
1015
tags = ["tasks"]
11-
schema = TaskSchema(many=True)
1216

1317
def get(self):
14-
"""List of all session tasks"""
15-
return tasks.tasks()
18+
logging.warning(
19+
"TaskList is deprecated and will be removed in a future version. Use the Actions list instead."
20+
)
21+
return TaskSchema(many=True).dump(current_thing.actions.greenlets)
1622

1723

1824
class TaskView(View):
@@ -24,32 +30,35 @@ class TaskView(View):
2430
"""
2531

2632
tags = ["tasks"]
27-
schema = TaskSchema()
28-
29-
marshal_methods = ("GET", "DELETE")
3033

3134
def get(self, task_id):
3235
"""
3336
Show status of a session task
3437
3538
Includes progress and intermediate data.
3639
"""
37-
task_dict = tasks.to_dict()
40+
logging.warning(
41+
"TaskView is deprecated and will be removed in a future version. Use the Action view instead."
42+
)
43+
task_dict = current_thing.actions.to_dict()
3844

3945
if task_id not in task_dict:
4046
return abort(404) # 404 Not Found
4147

4248
task = task_dict.get(task_id)
4349

44-
return task
50+
return TaskSchema().dump(task)
4551

4652
def delete(self, task_id):
4753
"""
4854
Terminate a running task.
4955
5056
If the task is finished, deletes its entry.
5157
"""
52-
task_dict = tasks.to_dict()
58+
logging.warning(
59+
"TaskView is deprecated and will be removed in a future version. Use the Action view instead."
60+
)
61+
task_dict = current_thing.actions.to_dict()
5362

5463
if task_id not in task_dict:
5564
return abort(404) # 404 Not Found
@@ -58,4 +67,4 @@ def delete(self, task_id):
5867

5968
task.kill(block=True, timeout=3)
6069

61-
return task
70+
return TaskSchema().dump(task)

src/labthings/deque.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from collections import deque as _deque
2+
3+
4+
class Deque(_deque):
5+
def __init__(self, iterable=None, maxlen=100):
6+
_deque.__init__(self, iterable or [], maxlen)
7+
8+
9+
def resize_deque(iterable: _deque, newsize: int):
10+
return deque(iterable, newsize)

0 commit comments

Comments
 (0)