diff --git a/examples/simple_thing.py b/examples/simple_thing.py index 3857c4a4..a0d1a2c3 100644 --- a/examples/simple_thing.py +++ b/examples/simple_thing.py @@ -13,7 +13,6 @@ import atexit from labthings.server.quick import create_app -from labthings.server.decorators import PropertySchema, use_args, marshal_with, Doc from labthings.server import semantics from labthings.server.view import ActionView, PropertyView from labthings.server.find import find_component @@ -75,8 +74,9 @@ def average_data(self, n: int): # Define the data we're going to output (get), and what to expect in (post) @semantics.moz.LevelProperty(100, 500, example=200) -@Doc(description="Value of magic_denoise",) class DenoiseProperty(PropertyView): + """Value of magic_denoise""" + # Main function to handle GET requests (read) def get(self): """Show the current magic_denoise value""" @@ -103,8 +103,9 @@ def post(self, new_property_value): """ -@PropertySchema(fields.List(fields.Float())) class QuickDataProperty(PropertyView): + schema = fields.List(fields.Float()) + # Main function to handle GET requests def get(self): """Show the current data value""" @@ -122,17 +123,14 @@ def get(self): class MeasurementAction(ActionView): # Expect JSON parameters in the request body. # Pass to post function as dictionary argument. - @use_args( - { - "averages": fields.Integer( - missing=20, - example=20, - description="Number of data sets to average over", - ) - } - ) - # Output schema - @marshal_with(fields.List(fields.Number)) + args = { + "averages": fields.Integer( + missing=20, example=20, description="Number of data sets to average over", + ) + } + + schema = fields.List(fields.Number) + # Main function to handle POST requests def post(self, args): """Start an averaged measurement""" diff --git a/src/labthings/server/default_views/extensions.py b/src/labthings/server/default_views/extensions.py index 543091b4..0c666d0d 100644 --- a/src/labthings/server/default_views/extensions.py +++ b/src/labthings/server/default_views/extensions.py @@ -2,14 +2,14 @@ from ..view import View from ..find import registered_extensions from ..schema import ExtensionSchema -from ..decorators import marshal_with, Tag -@Tag("extensions") class ExtensionList(View): """List and basic documentation for all enabled Extensions""" - @marshal_with(ExtensionSchema(many=True)) + schema = ExtensionSchema(many=True) + tags = ["extensions"] + def get(self): """ List enabled extensions. diff --git a/src/labthings/server/default_views/tasks.py b/src/labthings/server/default_views/tasks.py index 25e6ab2f..81f09d9d 100644 --- a/src/labthings/server/default_views/tasks.py +++ b/src/labthings/server/default_views/tasks.py @@ -1,21 +1,20 @@ from flask import abort -from ..decorators import marshal_with, Tag from ..view import View from ..schema import TaskSchema from ...core import tasks -@Tag("tasks") class TaskList(View): - @marshal_with(TaskSchema(many=True)) + tags = ["tasks"] + schema = TaskSchema(many=True) + def get(self): """List of all session tasks""" return tasks.tasks() -@Tag(["tasks"]) class TaskView(View): """ Manage a particular background task. @@ -24,7 +23,9 @@ class TaskView(View): DELETE will terminate the background task, if running. """ - @marshal_with(TaskSchema()) + tags = ["tasks"] + schema = TaskSchema() + def get(self, task_id): """ Show status of a session task @@ -40,7 +41,6 @@ def get(self, task_id): return task - @marshal_with(TaskSchema()) def delete(self, task_id): """ Terminate a running task. diff --git a/src/labthings/server/semantics/moz.py b/src/labthings/server/semantics/moz.py index 317b6ab4..01630a92 100644 --- a/src/labthings/server/semantics/moz.py +++ b/src/labthings/server/semantics/moz.py @@ -1,6 +1,6 @@ -from .. import decorators from .. import fields + # BASIC PROPERTIES class Property: def __init__(self, schema): @@ -8,8 +8,8 @@ def __init__(self, schema): def __call__(self, viewcls): # Use the class name as the semantic type - viewcls = decorators.Semtype(self.__class__.__name__)(viewcls) - viewcls = decorators.PropertySchema(self.schema)(viewcls) + viewcls.semtype = self.__class__.__name__ + viewcls.schema = self.schema return viewcls diff --git a/src/labthings/server/spec/apispec.py b/src/labthings/server/spec/apispec.py index e7f44903..b6a91ffc 100644 --- a/src/labthings/server/spec/apispec.py +++ b/src/labthings/server/spec/apispec.py @@ -2,6 +2,8 @@ from .paths import rule_to_path, rule_to_params from .utilities import convert_to_schema_or_json +from ..schema import Schema + from werkzeug.routing import Rule from http import HTTPStatus @@ -55,12 +57,14 @@ def view_to_apispec_operations(view, apispec: APISpec): if hasattr(view, "get_responses"): ops[op]["responses"] = {} + print(view.get_responses()) for code, schema in view.get_responses().items(): ops[op]["responses"][code] = { "description": HTTPStatus(code).phrase, "content": { - "application/json": { + getattr(view, "content_type", "application/json"): { "schema": convert_to_schema_or_json(schema, apispec) + or Schema() } }, } diff --git a/src/labthings/server/spec/td.py b/src/labthings/server/spec/td.py index 3cbff9e5..4e17aa87 100644 --- a/src/labthings/server/spec/td.py +++ b/src/labthings/server/spec/td.py @@ -111,7 +111,7 @@ def view_to_thing_property(self, rules: list, view: View): # Basic description prop_description = { - "title": getattr(view, "title") or view.__name__, + "title": getattr(view, "title", None) or view.__name__, "description": get_docstring(view), "readOnly": not ( hasattr(view, "post") or hasattr(view, "put") or hasattr(view, "delete") @@ -123,7 +123,7 @@ def view_to_thing_property(self, rules: list, view: View): } # Look for a _propertySchema in the Property classes API SPec - prop_schema = getattr(view, "schema") + prop_schema = getattr(view, "schema", None) if prop_schema: # Ensure valid schema type @@ -157,15 +157,15 @@ def view_to_thing_action(self, rules: list, view: View): # Basic description action_description = { - "title": getattr(view, "title") or view.__name__, + "title": getattr(view, "title", None) or view.__name__, "description": get_docstring(view), "links": [{"href": f"{url}"} for url in action_urls], - "safe": getattr(view, "safe"), - "idempotent": getattr(view, "idempotent"), + "safe": getattr(view, "safe", False), + "idempotent": getattr(view, "idempotent", False), } # Look for a _params in the Action classes API Spec - action_input_schema = getattr(view, "args") + action_input_schema = getattr(view, "args", None) if action_input_schema: # Ensure valid schema type action_input_schema = convert_to_schema_or_json( @@ -178,7 +178,7 @@ def view_to_thing_action(self, rules: list, view: View): action_description["input"].update(get_semantic_type(view)) # Look for a _schema in the Action classes API Spec - action_output_schema = getattr(view, "schema") + action_output_schema = getattr(view, "schema", None) if action_output_schema: # Ensure valid schema type action_output_schema = convert_to_schema_or_json( @@ -192,7 +192,7 @@ def view_to_thing_action(self, rules: list, view: View): return action_description def property(self, rules: list, view: View): - endpoint = getattr(view, "endpoint") or getattr(rules[0], "endpoint") + endpoint = getattr(view, "endpoint", None) or getattr(rules[0], "endpoint") key = snake_to_camel(endpoint) self.properties[key] = self.view_to_thing_property(rules, view) @@ -206,7 +206,7 @@ def action(self, rules: list, view: View): raise AttributeError( f"The API View '{view}' was added as an Action, but it does not have a POST method." ) - endpoint = getattr(view, "endpoint") or getattr(rules[0], "endpoint") + endpoint = getattr(view, "endpoint", None) or getattr(rules[0], "endpoint") key = snake_to_camel(endpoint) self.actions[key] = self.view_to_thing_action(rules, view) diff --git a/src/labthings/server/spec/utilities.py b/src/labthings/server/spec/utilities.py index 9a5053e3..4e28ea1d 100644 --- a/src/labthings/server/spec/utilities.py +++ b/src/labthings/server/spec/utilities.py @@ -4,12 +4,12 @@ from ...core.utilities import merge, get_docstring, get_summary, snake_to_camel from ..fields import Field -from ..schema import build_action_schema +from ..schema import Schema, build_action_schema from ..view import ActionView -from marshmallow import Schema as BaseSchema from collections.abc import Mapping + def update_spec(obj, spec: dict): """Add API spec data to an object @@ -109,7 +109,7 @@ def convert_to_schema_or_json(schema, spec: APISpec): return schema # Expand/convert actual schema data - if isinstance(schema, BaseSchema): + if isinstance(schema, Schema): return schema elif isinstance(schema, Mapping): return map_to_properties(schema, spec) @@ -164,7 +164,7 @@ def schema_to_json(schema, spec: APISpec): This is used, for example, in the Thing Description. """ - if isinstance(schema, BaseSchema): + if isinstance(schema, Schema): marshmallow_plugin = next( plugin for plugin in spec.plugins if isinstance(plugin, MarshmallowPlugin) ) diff --git a/src/labthings/server/view/__init__.py b/src/labthings/server/view/__init__.py index 1b1aa524..a20533a8 100644 --- a/src/labthings/server/view/__init__.py +++ b/src/labthings/server/view/__init__.py @@ -45,6 +45,7 @@ class View(MethodView): docs: dict = {} title: None semtype: str = None + content_type: str = "application/json" responses: dict = {} @@ -57,7 +58,13 @@ def __init__(self, *args, **kwargs): @classmethod def get_responses(cls): - return {200: cls.schema}.update(cls.responses) + r = {200: cls.schema} + r.update(cls.responses) + return r + + @classmethod + def get_schema(cls): + return cls.schema @classmethod def get_args(cls): @@ -87,12 +94,12 @@ def dispatch_request(self, *args, **kwargs): assert meth is not None, f"Unimplemented method {request.method!r}" # Inject request arguments if an args schema is defined - if self.args: - meth = use_args(self.args)(meth) + if self.get_args(): + meth = use_args(self.get_args())(meth) # Marhal response if a response schema is defines - if self.schema: - meth = marshal_with(self.schema)(meth) + if self.get_schema(): + meth = marshal_with(self.get_schema())(meth) # Generate basic response return self.represent_response(meth(*args, **kwargs)) @@ -127,7 +134,9 @@ class ActionView(View): @classmethod def get_responses(cls): """Build an output schema that includes the Action wrapper object""" - return {201: build_action_schema(cls.schema, cls.args)}.update(cls.responses) + r = {201: build_action_schema(cls.schema, cls.args)()} + r.update(cls.responses) + return r def dispatch_request(self, *args, **kwargs): meth = getattr(self, request.method.lower(), None) diff --git a/src/labthings/server/view/builder.py b/src/labthings/server/view/builder.py index e5864f91..cdb77789 100644 --- a/src/labthings/server/view/builder.py +++ b/src/labthings/server/view/builder.py @@ -1,11 +1,3 @@ -from labthings.server.decorators import ( - PropertySchema, - use_args, - Doc, - Safe, - Idempotent, - Semtype, -) from . import View, ActionView, PropertyView from .. import fields @@ -70,15 +62,13 @@ def _update(self, args): # Add decorators for arguments etc if schema: - generated_class = PropertySchema(schema)(generated_class) + generated_class.schema = schema if description: - generated_class = Doc(description=description, summary=description)( - generated_class - ) + generated_class.docs = {"description": description, "summary": description} if semtype: - generated_class = Semtype(semtype)(generated_class) + generated_class.semtype = semtype return generated_class @@ -90,6 +80,7 @@ def action_from( safe=False, idempotent=False, args=None, + schema=None, semtype=None, ): @@ -107,23 +98,21 @@ def _post_with_args(self, args): # Add decorators for arguments etc if args is not None: generated_class = type(name, (ActionView, object), {"post": _post_with_args}) - generated_class.post = use_args(args)(generated_class.post) + generated_class.args = args else: generated_class = type(name, (ActionView, object), {"post": _post}) + if schema: + generated_class.schema = schema + if description: - generated_class = Doc(description=description, summary=description)( - generated_class - ) + generated_class.docs = {"description": description, "summary": description} if semtype: - generated_class = Semtype(semtype)(generated_class) - - if safe: - generated_class = Safe(generated_class) + generated_class.semtype = semtype - if idempotent: - generated_class = Idempotent(generated_class) + generated_class.safe = safe + generated_class.idempotent = idempotent return generated_class