Add basic device endpoints

master
esensar 2018-05-08 16:45:09 +02:00
parent 004bdbd0f3
commit b85e90cfb4
15 changed files with 311 additions and 164 deletions

View File

@ -15,9 +15,9 @@ def create_account(username, email, password):
:type username: string :type username: string
:type email: string :type email: string
:type password: string :type password: string
:returns True if account is successfully created :returns: True if account is successfully created
:rtype Boolean :rtype: Boolean
:raises ValueError if account already exists :raises: ValueError if account already exists
""" """
if not Account.exists_with_any_of(username=username, email=email): if not Account.exists_with_any_of(username=username, email=email):
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8') pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
@ -37,9 +37,9 @@ def create_token(username, password):
:param password: password of Account :param password: password of Account
:type username: string :type username: string
:type password: string :type password: string
:returns created token :returns: created token
:rtype string :rtype: string
:raises ValueError if credentials are invalid or account does not exist :raises: ValueError if credentials are invalid or account does not exist
""" """
if not Account.exists(username=username): if not Account.exists(username=username):
raise ValueError("Invalid credentials") raise ValueError("Invalid credentials")
@ -57,7 +57,7 @@ def validate_token(token):
:param token: auth token to validate :param token: auth token to validate
:type token: string :type token: string
:returns created token :returns: created token
:rtype Account :rtype: Account
""" """
return Account.validate_token(token) return Account.validate_token(token)

View File

@ -1,64 +0,0 @@
from app import bcrypt, status
from flask import request
from .models import Account
def initialize_routes(accounts):
@accounts.route("", methods=['POST'])
def create_account():
print(request.data)
user = request.data.get('user')
if not Account.exists_with_any_of(
username=user.get('username'), email=user.get('email')):
password_hash = bcrypt.generate_password_hash(
user.get('password')
).decode('utf-8')
acct = Account(user.get('username'),
password_hash,
user.get('email'))
acct.save()
response = {
'status': 'success',
'message': 'Success!'
}
return response, status.HTTP_200_OK
else:
response = {
'status': 'error',
'message': 'User already exists!'
}
return response, status.HTTP_422_UNPROCESSABLE_ENTITY
@accounts.route("/token", methods=['POST'])
def create_token():
print(request.data)
user = request.data.get('user')
if not user:
response = {
'status': 'error',
'message': 'Invalid request'
}
return response, status.HTTP_400_BAD_REQUEST
if not Account.exists(username=user.get('username')):
response = {
'status': 'error',
'message': 'Invalid credentials'
}
return response, status.HTTP_401_UNAUTHORIZED
account = Account.get(username=user.get('username'))
if not bcrypt.check_password_hash(
account.password, user.get('password')):
response = {
'status': 'error',
'message': 'Invalid credentials'
}
return response, status.HTTP_401_UNAUTHORIZED
response = {
'status': 'success',
'message': 'Successfully logged in',
'token': account.create_auth_token()
}
return response, status.HTTP_200_OK

View File

@ -1,6 +1,7 @@
import jwt import jwt
import datetime import datetime
from app import db, app from app import db, app
from calendar import timegm
class Account(db.Model): class Account(db.Model):
@ -108,10 +109,13 @@ class Account(db.Model):
app.config.get('SECRET_KEY'), app.config.get('SECRET_KEY'),
algorithms=['HS256'] algorithms=['HS256']
) )
current_time = timegm(datetime.datetime.utcnow().utctimetuple())
if current_time > payload['exp']:
raise ValueError("Expired token")
return Account.get(id=payload['sub']) return Account.get(id=payload['sub'])
def __repr__(self): def __repr__(self):
return '<Account (name=%s, role=%s)>' % self.username, self.role return '<Account (name=%s, role=%s)>' % (self.username, self.role)
class Role(db.Model): class Role(db.Model):
@ -146,4 +150,4 @@ class Role(db.Model):
return Role.query.filter_by(id=roleId) return Role.query.filter_by(id=roleId)
def __repr__(self): def __repr__(self):
return '<Role %s>' % self.name return '<Role %s>' % self.display_name

View File

@ -1,3 +1,4 @@
import sys
from flask import Blueprint, request, g from flask import Blueprint, request, g
from flask_restful import Api, Resource, abort from flask_restful import Api, Resource, abort
from functools import wraps from functools import wraps
@ -22,8 +23,13 @@ def protected(func):
if not g.current_account: if not g.current_account:
abort(401, message='Unauthorized', status='error') abort(401, message='Unauthorized', status='error')
except Exception: except Exception:
error_type, error_instance, traceback = sys.exc_info()
print(str(error_type))
print(str(error_instance))
abort(401, message='Unauthorized', status='error') abort(401, message='Unauthorized', status='error')
return func(*args, **kwargs)
return protected_function return protected_function
@ -34,10 +40,17 @@ class ProtectedResource(Resource):
def add_resources(): def add_resources():
from .resources.account import AccountResource, AccountListResource from .resources.account import AccountResource, AccountListResource
from .resources.token import TokenResource from .resources.token import TokenResource
from .resources.device import (DeviceResource,
DeviceRecordingResource,
DeviceListResource)
api.add_resource(AccountResource, '/v1/accounts/<int:account_id>') api.add_resource(AccountResource, '/v1/accounts/<int:account_id>')
api.add_resource(AccountListResource, '/v1/accounts') api.add_resource(AccountListResource, '/v1/accounts')
api.add_resource(TokenResource, '/v1/token') api.add_resource(TokenResource, '/v1/token')
api.add_resource(DeviceResource, '/v1/devices/<int:device_id>')
api.add_resource(DeviceRecordingResource,
'/v1/devices/<int:device_id>/recordings')
api.add_resource(DeviceListResource, '/v1/devices')
add_resources() add_resources()

View File

@ -1,31 +1,32 @@
from flask_restful import Resource, abort from flask_restful import Resource, abort
from flask import g from flask import g
from webargs import fields from marshmallow import Schema, fields
from webargs.flaskparser import use_args from webargs.flaskparser import use_args
from flasgger import swag_from from flasgger import swag_from
import app.accounts as accounts import app.accounts as accounts
from app.api import ProtectedResource, protected from app.api import ProtectedResource
user_args = {
'user': fields.Nested({ class UserSchema(Schema):
'username': fields.Str(required=True), username = fields.Str(required=True)
'email': fields.Email(required=True), email = fields.Email(required=True)
'password': fields.Str(required=True) password = fields.Str(required=True, load_only=True)
}, required=True, location='json')
}
class UserWrapperSchema(Schema):
user = fields.Nested(UserSchema, required=True, location='json')
class AccountResource(ProtectedResource): class AccountResource(ProtectedResource):
@swag_from('swagger/get_account_spec.yaml') @swag_from('swagger/get_account_spec.yaml')
def get(self, account_id): def get(self, account_id):
if g.current_account.id == account_id: if g.current_account.id == account_id:
return g.current_account, 200 return UserWrapperSchema().dump({'user': g.current_account}), 200
abort(403, message='You can only get your own account', status='error') abort(403, message='You can only get your own account', status='error')
class AccountListResource(Resource): class AccountListResource(Resource):
@use_args(user_args) @use_args(UserWrapperSchema())
@swag_from('swagger/create_account_spec.yaml') @swag_from('swagger/create_account_spec.yaml')
def post(self, args): def post(self, args):
try: try:
@ -38,7 +39,3 @@ class AccountListResource(Resource):
return '', 201 return '', 201
except ValueError: except ValueError:
abort(422, message='Account already exists', status='error') abort(422, message='Account already exists', status='error')
@protected
def get(self):
return '', 200

View File

@ -0,0 +1,50 @@
from marshmallow import Schema, fields
from webargs.flaskparser import use_args
from flasgger import swag_from
import app.devices as devices
from app.api import ProtectedResource
class DeviceSchema(Schema):
id = fields.Integer(dump_only=True)
name = fields.Str(required=True)
class DeviceWrapperSchema(Schema):
device = fields.Nested(DeviceSchema, required=True, location='json')
class RecordingsSchema(Schema):
recorded_at = fields.DateTime()
record_type = fields.Integer()
record_value = fields.String()
class RecordingsWrapperSchema(Schema):
recordings = fields.Nested(RecordingsSchema, required=True,
location='json', many=True)
class DeviceResource(ProtectedResource):
@swag_from('swagger/get_device_spec.yaml')
def get(self, device_id):
return DeviceWrapperSchema().dump(
{'device': devices.get_device(device_id)}), 200
class DeviceRecordingResource(ProtectedResource):
@swag_from('swagger/get_device_recordings_spec.yaml')
def get(self, device_id):
return RecordingsWrapperSchema().dump(
{'recordings': devices.get_device_recordings(device_id)}), 200
class DeviceListResource(ProtectedResource):
@use_args(DeviceWrapperSchema())
@swag_from('swagger/create_device_spec.yaml')
def post(self, args):
args = args['device']
success = devices.create_device(
args['name'])
if success:
return '', 201

View File

@ -14,10 +14,6 @@ parameters:
- user - user
properties: properties:
user: user:
required:
- username
- password
- email
$ref: '#/definitions/User' $ref: '#/definitions/User'
security: [] security: []
responses: responses:

View File

@ -0,0 +1,19 @@
Creates new device
Requires Device object and creates device
---
tags:
- Device
parameters:
- in: body
name: body
required: true
schema:
type: object
required:
- device
properties:
device:
$ref: '#/definitions/Device'
responses:
201:
description: Successful creation

View File

@ -1,11 +1,17 @@
Gets a user account Gets a user account
User may only get own account. Accessing other accounts will return 403.
--- ---
tags: tags:
- Account - Account
parameters: parameters:
- in: body - in: path
name: body name: user_id
required: true required: true
type: integer
description: Id of the user
responses:
200:
description: Success
schema: schema:
type: object type: object
required: required:
@ -13,10 +19,7 @@ parameters:
properties: properties:
user: user:
$ref: '#/definitions/User' $ref: '#/definitions/User'
responses: 403:
201: description: Accessed a different account
description: Successful creation
422:
description: Account already exists
schema: schema:
$ref: '#/definitions/Error' $ref: '#/definitions/Error'

View File

@ -0,0 +1,23 @@
Gets all recordings for given device
---
tags:
- Device
- Recording
parameters:
- in: path
name: device_id
required: true
type: integer
description: Id of the device
responses:
200:
description: Success
schema:
type: object
required:
- recordings
properties:
recordings:
type: array
items:
$ref: '#/definitions/Recording'

View File

@ -0,0 +1,21 @@
Gets a device
---
tags:
- Device
parameters:
- in: path
name: device_id
required: true
type: integer
description: Id of the device
responses:
200:
description: Success
schema:
type: object
required:
-device
properties:
device:
$ref: '#/definitions/Device'

View File

@ -1,30 +1,71 @@
import atexit import atexit
from flask import Blueprint from flask import Blueprint
from .mqtt_client import MqttClient from .mqtt_client import MqttClient
from .models import Device, Recording
devices_bp = Blueprint('devices', __name__) devices_bp = Blueprint('devices', __name__)
mqtt_client = None
# When app dies, stop mqtt connection # When app dies, stop mqtt connection
def on_stop(): def on_stop():
if mqtt_client: MqttClient.tear_down()
mqtt_client.tear_down()
atexit.register(on_stop) atexit.register(on_stop)
# Routes # Setup
@devices_bp.route("/")
def hello():
return "Hello from devices!"
@devices_bp.record @devices_bp.record
def on_blueprint_setup(setup_state): def __on_blueprint_setup(setup_state):
print('Blueprint setup') print('Blueprint setup')
mqtt_client = MqttClient() MqttClient.setup(setup_state.app)
if mqtt_client:
mqtt_client.setup(setup_state.app) # Public interface
def create_device(name, device_type=1):
"""
Tries to create device with given parameters
:param name: Desired device name
:param device_type: Id of desired device type.
By default it is 1 (STANDARD)
:type name: string
:type device_type: int
:returns: True if device is successfully created
:rtype: Boolean
"""
device = Device(name, None, device_type)
device.save()
def get_device_recordings(device_id):
"""
Tries to get device recording for device with given parameters. Raises
error on failure
:param device_id: Id of device
:type device_id: int
:returns: List of Recordings for given device
:rtype: List of Recording
:raises: ValueError if device does not exist
"""
if not Device.exists(id=device_id):
raise ValueError("Device with id %s does not exist" % device_id)
return Recording.get_many(device_id=device_id)
def get_device(device_id):
"""
Tries to get device with given parameters. Raises error on failure
:param device_id: Id of device
:type device_id: int
:returns: Requested device
:rtype: Device
:raises: ValueError if device does not exist
"""
if not Device.exists(id=device_id):
raise ValueError("Device with id %s does not exist" % device_id)
return Device.get(id=device_id)

View File

@ -137,6 +137,15 @@ class Device(db.Model):
""" """
return Device.query.filter_by(**kwargs).first() return Device.query.filter_by(**kwargs).first()
@staticmethod
def exists(**kwargs):
"""
Checks if device with all of the given arguments exists
"""
if Device.query.filter_by(**kwargs).first():
return True
return False
def __repr__(self): def __repr__(self):
return '<Device (name=%s, type=%s)>' % ( return '<Device (name=%s, type=%s)>' % (
self.name, self.device_type_id) self.name, self.device_type_id)

View File

@ -2,66 +2,55 @@ import sys
import json import json
from flask_mqtt import Mqtt from flask_mqtt import Mqtt
from .models import Recording from .models import Recording
from app import db, app from app import app
class MqttClient: class MqttClient:
class __MqttClient: __initialized = False
def __init__(self): mqtt = Mqtt()
self.mqtt = Mqtt()
self.initialized = False
def __str__(self):
return repr(self)
instance = None
def __init__(self):
if not MqttClient.instance:
MqttClient.instance = MqttClient.__MqttClient()
def __getattr__(self, name):
return getattr(self.instance, name)
# Mqtt setup # Mqtt setup
def setup(self, app): @staticmethod
if not self.initialized: def setup(app):
self.mqtt.init_app(app) if not MqttClient.__initialized:
self.mqtt.client.on_message = self.handle_mqtt_message MqttClient.mqtt.init_app(app)
self.mqtt.client.on_subscribe = self.handle_subscribe MqttClient.mqtt.client.on_message = MqttClient.handle_mqtt_message
initialized = True MqttClient.mqtt.client.on_subscribe = MqttClient.handle_subscribe
MqttClient.__initialized = True
@self.mqtt.on_connect() @MqttClient.mqtt.on_connect()
def handle_connect(client, userdata, flags, rc): def handle_connect(client, userdata, flags, rc):
print('MQTT client connected') print('MQTT client connected')
self.mqtt.subscribe('device/+') MqttClient.mqtt.subscribe('device/+')
@self.mqtt.on_disconnect() @MqttClient.mqtt.on_disconnect()
def handle_disconnect(): def handle_disconnect():
print('MQTT client disconnected') print('MQTT client disconnected')
print('MQTT client initialized') print('MQTT client initialized')
@staticmethod
def tear_down(self): def tear_down():
self.mqtt.unsubscribe_all() if MqttClient.__initialized:
if hasattr(self.mqtt, 'client') and self.mqtt.client is not None: MqttClient.mqtt.unsubscribe_all()
self.mqtt.client.disconnect() if (hasattr(MqttClient.mqtt, 'client') and
MqttClient.mqtt.client is not None):
MqttClient.mqtt.client.disconnect()
print('MQTT client destroyed') print('MQTT client destroyed')
@staticmethod
def handle_subscribe(self, client, userdata, mid, granted_qos): def handle_subscribe(client, userdata, mid, granted_qos):
print('MQTT client subscribed') print('MQTT client subscribed')
@staticmethod
def handle_mqtt_message(self, client, userdata, message): def handle_mqtt_message(client, userdata, message):
print("Received message!") print("Received message!")
print("Topic: " + message.topic) print("Topic: " + message.topic)
print("Payload: " + message.payload.decode()) print("Payload: " + message.payload.decode())
try: try:
# If type is JSON # If type is JSON
recording = self.parse_json_message(message.topic, message.payload.decode()) recording = MqttClient.parse_json_message(
message.topic, message.payload.decode())
with app.app_context(): with app.app_context():
recording.save() recording.save()
except ValueError: except ValueError:
@ -71,11 +60,11 @@ class MqttClient:
print("Instance: " + str(error_instance)) print("Instance: " + str(error_instance))
return return
@staticmethod
def parse_json_message(self, topic, payload) -> Recording: def parse_json_message(topic, payload) -> Recording:
try: try:
json_msg = json.loads(payload) json_msg = json.loads(payload)
device_id = self.get_device_id(topic) device_id = MqttClient.get_device_id(topic)
return Recording(device_id=device_id, return Recording(device_id=device_id,
record_type=json_msg["record_type"], record_type=json_msg["record_type"],
record_value=json_msg["record_value"], record_value=json_msg["record_value"],
@ -86,8 +75,8 @@ class MqttClient:
raise ValueError("JSON parsing failed! Key error: " raise ValueError("JSON parsing failed! Key error: "
+ str(error_instance)) + str(error_instance))
@staticmethod
def get_device_id(self, topic) -> int: def get_device_id(topic) -> int:
device_token, device_id = topic.split("/") device_token, device_id = topic.split("/")
if device_token == "device": if device_token == "device":
return int(device_id) return int(device_id)

View File

@ -12,6 +12,25 @@ definitions:
description: User's name in the system description: User's name in the system
default: testusername default: testusername
id:
type: integer
description: ID
default: 1
datetime:
type: string
description: Time
devicename:
type: string
description: Name of device
default: My device
devicetype:
type: int
description: Type of device
default: 1
email: email:
type: string type: string
format: email format: email
@ -57,6 +76,33 @@ definitions:
email: email:
$ref: '#/definitions/email' $ref: '#/definitions/email'
Recording:
type: object
required:
- recorded_at
- record_type
- record_value
properties:
recorded_at:
$ref: '#/definitions/datetime'
record_type:
$ref: '#/definitions/id'
record_value:
type: string
description: Value of the recording
default: '25 degrees'
Device:
type: object
required:
- id
- name
properties:
id:
$ref: '#/definitions/id'
name:
$ref: '#/definitions/devicename'
UnauthorizedError: UnauthorizedError:
type: object type: object
required: required: