Add basic device endpoints
parent
004bdbd0f3
commit
b85e90cfb4
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -14,10 +14,6 @@ parameters:
|
|||
- user
|
||||
properties:
|
||||
user:
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
- email
|
||||
$ref: '#/definitions/User'
|
||||
security: []
|
||||
responses:
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
|
@ -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'
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue