Skip to content

Commit

Permalink
Got basic simpler spec working
Browse files Browse the repository at this point in the history
  • Loading branch information
Joel Collins committed Jun 22, 2020
1 parent f130d66 commit caa983b
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 69 deletions.
26 changes: 12 additions & 14 deletions examples/simple_thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"""
Expand All @@ -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"""
Expand All @@ -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"""
Expand Down
6 changes: 3 additions & 3 deletions src/labthings/server/default_views/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 6 additions & 6 deletions src/labthings/server/default_views/tasks.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand All @@ -40,7 +41,6 @@ def get(self, task_id):

return task

@marshal_with(TaskSchema())
def delete(self, task_id):
"""
Terminate a running task.
Expand Down
6 changes: 3 additions & 3 deletions src/labthings/server/semantics/moz.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from .. import decorators
from .. import fields


# BASIC PROPERTIES
class Property:
def __init__(self, schema):
self.schema = 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


Expand Down
6 changes: 5 additions & 1 deletion src/labthings/server/spec/apispec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
}
},
}
Expand Down
18 changes: 9 additions & 9 deletions src/labthings/server/spec/td.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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)

Expand All @@ -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)

Expand Down
8 changes: 4 additions & 4 deletions src/labthings/server/spec/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
)
Expand Down
21 changes: 15 additions & 6 deletions src/labthings/server/view/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class View(MethodView):
docs: dict = {}
title: None
semtype: str = None
content_type: str = "application/json"

responses: dict = {}

Expand All @@ -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):
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
35 changes: 12 additions & 23 deletions src/labthings/server/view/builder.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
from labthings.server.decorators import (
PropertySchema,
use_args,
Doc,
Safe,
Idempotent,
Semtype,
)
from . import View, ActionView, PropertyView
from .. import fields

Expand Down Expand Up @@ -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

Expand All @@ -90,6 +80,7 @@ def action_from(
safe=False,
idempotent=False,
args=None,
schema=None,
semtype=None,
):

Expand All @@ -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

Expand Down

0 comments on commit caa983b

Please sign in to comment.