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 email: string
:type password: string
:returns True if account is successfully created
:rtype Boolean
:raises ValueError if account already exists
:returns: True if account is successfully created
:rtype: Boolean
:raises: ValueError if account already exists
"""
if not Account.exists_with_any_of(username=username, email=email):
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
@ -37,9 +37,9 @@ def create_token(username, password):
:param password: password of Account
:type username: string
:type password: string
:returns created token
:rtype string
:raises ValueError if credentials are invalid or account does not exist
:returns: created token
:rtype: string
:raises: ValueError if credentials are invalid or account does not exist
"""
if not Account.exists(username=username):
raise ValueError("Invalid credentials")
@ -57,7 +57,7 @@ def validate_token(token):
:param token: auth token to validate
:type token: string
:returns created token
:rtype Account
:returns: created token
:rtype: Account
"""
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 datetime
from app import db, app
from calendar import timegm
class Account(db.Model):
@ -108,10 +109,13 @@ class Account(db.Model):
app.config.get('SECRET_KEY'),
algorithms=['HS256']
)
current_time = timegm(datetime.datetime.utcnow().utctimetuple())
if current_time > payload['exp']:
raise ValueError("Expired token")
return Account.get(id=payload['sub'])
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):
@ -146,4 +150,4 @@ class Role(db.Model):
return Role.query.filter_by(id=roleId)
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_restful import Api, Resource, abort
from functools import wraps
@ -22,8 +23,13 @@ def protected(func):
if not g.current_account:
abort(401, message='Unauthorized', status='error')
except Exception:
error_type, error_instance, traceback = sys.exc_info()
print(str(error_type))
print(str(error_instance))
abort(401, message='Unauthorized', status='error')
return func(*args, **kwargs)
return protected_function
@ -34,10 +40,17 @@ class ProtectedResource(Resource):
def add_resources():
from .resources.account import AccountResource, AccountListResource
from .resources.token import TokenResource
from .resources.device import (DeviceResource,
DeviceRecordingResource,
DeviceListResource)
api.add_resource(AccountResource, '/v1/accounts/<int:account_id>')
api.add_resource(AccountListResource, '/v1/accounts')
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()

View File

@ -1,31 +1,32 @@
from flask_restful import Resource, abort
from flask import g
from webargs import fields
from marshmallow import Schema, fields
from webargs.flaskparser import use_args
from flasgger import swag_from
import app.accounts as accounts
from app.api import ProtectedResource, protected
from app.api import ProtectedResource
user_args = {
'user': fields.Nested({
'username': fields.Str(required=True),
'email': fields.Email(required=True),
'password': fields.Str(required=True)
}, required=True, location='json')
}
class UserSchema(Schema):
username = fields.Str(required=True)
email = fields.Email(required=True)
password = fields.Str(required=True, load_only=True)
class UserWrapperSchema(Schema):
user = fields.Nested(UserSchema, required=True, location='json')
class AccountResource(ProtectedResource):
@swag_from('swagger/get_account_spec.yaml')
def get(self, 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')
class AccountListResource(Resource):
@use_args(user_args)
@use_args(UserWrapperSchema())
@swag_from('swagger/create_account_spec.yaml')
def post(self, args):
try:
@ -38,7 +39,3 @@ class AccountListResource(Resource):
return '', 201
except ValueError:
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
properties:
user:
required:
- username
- password
- email
$ref: '#/definitions/User'
security: []
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
User may only get own account. Accessing other accounts will return 403.
---
tags:
- Account
parameters:
- in: body
name: body
- in: path
name: user_id
required: true
type: integer
description: Id of the user
responses:
200:
description: Success
schema:
type: object
required:
@ -13,10 +19,7 @@ parameters:
properties:
user:
$ref: '#/definitions/User'
responses:
201:
description: Successful creation
422:
description: Account already exists
403:
description: Accessed a different account
schema:
$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
from flask import Blueprint
from .mqtt_client import MqttClient
from .models import Device, Recording
devices_bp = Blueprint('devices', __name__)
mqtt_client = None
# When app dies, stop mqtt connection
def on_stop():
if mqtt_client:
mqtt_client.tear_down()
MqttClient.tear_down()
atexit.register(on_stop)
# Routes
@devices_bp.route("/")
def hello():
return "Hello from devices!"
# Setup
@devices_bp.record
def on_blueprint_setup(setup_state):
def __on_blueprint_setup(setup_state):
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()
@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):
return '<Device (name=%s, type=%s)>' % (
self.name, self.device_type_id)

View File

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

View File

@ -12,6 +12,25 @@ definitions:
description: User's name in the system
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:
type: string
format: email
@ -57,6 +76,33 @@ 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:
type: object
required: