Merged in develop (pull request #35)

Version 0.3.4 release
master
Ensar Sarajcic 2018-10-30 19:19:33 +00:00
commit 936d5c2cc1
10 changed files with 199 additions and 8 deletions

View File

@ -18,6 +18,7 @@ def add_resources():
from .resources.token import TokenResource, ValidateTokenResource from .resources.token import TokenResource, ValidateTokenResource
from .resources.device import (DeviceResource, from .resources.device import (DeviceResource,
DeviceRecordingResource, DeviceRecordingResource,
DeviceRecordingQueryResource,
DeviceListResource, DeviceListResource,
DeviceTypeResource, DeviceTypeResource,
DeviceTypeListResource, DeviceTypeListResource,
@ -42,6 +43,8 @@ def add_resources():
api.add_resource(DeviceResource, '/v1/devices/<int:device_id>') api.add_resource(DeviceResource, '/v1/devices/<int:device_id>')
api.add_resource(DeviceRecordingResource, api.add_resource(DeviceRecordingResource,
'/v1/devices/<int:device_id>/recordings') '/v1/devices/<int:device_id>/recordings')
api.add_resource(DeviceRecordingQueryResource,
'/v1/devices/<int:device_id>/recordings/jsonql')
api.add_resource(DeviceListResource, '/v1/devices') api.add_resource(DeviceListResource, '/v1/devices')
api.add_resource(DeviceTypeResource, api.add_resource(DeviceTypeResource,
'/v1/devices/types/<int:device_type_id>') '/v1/devices/types/<int:device_type_id>')

View File

@ -31,7 +31,13 @@ class DeviceWithConfigurationSchema(DeviceSchema):
class RecordingsSchema(BaseResourceSchema): class RecordingsSchema(BaseResourceSchema):
recorded_at = fields.DateTime() recorded_at = fields.DateTime()
record_type = fields.Integer() record_type = fields.Integer()
record_value = fields.String() record_value = fields.Float()
class RecordingsQuerySchema(Schema):
selections = fields.Raw()
filters = fields.Raw()
groups = fields.Raw()
class DeviceSecretSchema(BaseResourceSchema): class DeviceSecretSchema(BaseResourceSchema):
@ -103,6 +109,17 @@ class DeviceRecordingResource(ProtectedResource):
return RecordingsSchema().dump(created_recording), 201 return RecordingsSchema().dump(created_recording), 201
class DeviceRecordingQueryResource(ProtectedResource):
@use_args(RecordingsQuerySchema(), locations=('json',))
@swag_from('swagger/create_device_recording_query_spec.yaml')
def post(self, args, device_id):
validate_device_ownership(device_id)
try:
return devices.run_custom_query(device_id, args), 200
except ValueError as e:
abort(400, message=str(e), status='error')
class DeviceListResource(ProtectedResource): class DeviceListResource(ProtectedResource):
@use_args(DeviceSchema(), locations=('json',)) @use_args(DeviceSchema(), locations=('json',))
@swag_from('swagger/create_device_spec.yaml') @swag_from('swagger/create_device_spec.yaml')

View File

@ -3,6 +3,7 @@ import hmac
import urllib.parse import urllib.parse
from .models import Device, Recording, DeviceAssociation, DeviceType from .models import Device, Recording, DeviceAssociation, DeviceType
from app.core import app from app.core import app
from app.jsonql import api as jsonql
# Private helpers # Private helpers
@ -260,3 +261,30 @@ def create_recording(device_id, raw_json):
recording = parse_raw_json_recording(device_id, raw_json) recording = parse_raw_json_recording(device_id, raw_json)
with app.app_context(): with app.app_context():
recording.save() recording.save()
def run_custom_query(device_id, request):
"""
"""
if not Device.exists(id=device_id):
raise ValueError("Device does not exist!")
def recording_field_provider(name):
if name == 'record_value':
return Recording.record_value
if name == 'record_type':
return Recording.record_type
if name == 'device_id':
return Recording.device_id
if name == 'recorded_at':
return Recording.recorded_at
if name == 'received_at':
return Recording.received_at
resulting_query = jsonql.run_query_on(Recording.query.with_entities(),
recording_field_provider,
**request)
print("Resulting query: " + str(resulting_query))
result = resulting_query.filter(Recording.device_id == device_id).all()
print("RESULT: " + str(result))
return result

View File

@ -16,14 +16,14 @@ class Recording(db.Model):
default=db.func.current_timestamp()) default=db.func.current_timestamp())
device_id = db.Column(db.Integer, db.ForeignKey('devices.id')) device_id = db.Column(db.Integer, db.ForeignKey('devices.id'))
record_type = db.Column(db.Integer, nullable=False) record_type = db.Column(db.Integer, nullable=False)
record_value = db.Column(db.String, nullable=False) record_value = db.Column(db.Float, nullable=False)
raw_record = db.Column(JSON, nullable=True) raw_record = db.Column(JSON, nullable=True)
def __init__(self, device_id, record_type, def __init__(self, device_id, record_type,
record_value, recorded_at, raw_json): record_value, recorded_at, raw_json):
self.device_id = int(device_id) self.device_id = int(device_id)
self.record_type = int(record_type) self.record_type = int(record_type)
self.record_value = str(record_value) self.record_value = float(record_value)
self.recorded_at = datetime.fromtimestamp(int(recorded_at)) self.recorded_at = datetime.fromtimestamp(int(recorded_at))
self.received_at = datetime.utcnow() self.received_at = datetime.utcnow()
self.raw_record = raw_json self.raw_record = raw_json

View File

93
app/jsonql/api.py 100644
View File

@ -0,0 +1,93 @@
from app.core import db
GROUPS = ['sum', 'avg', 'count']
FILTERS = ['$gt', '$lt', '$eq']
PERIODS = ['year', 'month', 'week', 'day']
def run_query_on(query_object, field_provider, **kwargs):
selections, filters, groups = validate_selections(**kwargs)
entities = []
print('Starting with args: ' + str(kwargs))
if selections is not None:
for selection in selections.keys():
entities.append(get_column(selections[selection],
field_provider(selection)))
print('New entities: ' + str(entities))
query_object = query_object.with_entities(*entities)
if filters is not None:
for filtered_column in filters.keys():
for actual_filter in filters[filtered_column].keys():
query_object = query_object.filter(
get_filter(field_provider(filtered_column),
actual_filter,
filters[filtered_column][actual_filter]))
if groups is not None:
for group in groups.keys():
query_object = query_object.group_by(
get_group(field_provider(group), groups[group]))
return query_object
def get_column(selection_name, field):
if selection_name == 'value':
return field
if selection_name == 'sum':
return db.func.sum(field)
if selection_name == 'avg':
return db.func.avg(field)
if selection_name == 'count':
return db.func.count(field)
def get_filter(filter_column, filter_key, filter_value):
if filter_key == '$gt':
return filter_column > filter_value
if filter_key == '$lt':
return filter_column < filter_value
if filter_key == '$eq':
return filter_column == filter_value
def get_group(group_column, group_value):
if group_value == 'value':
return group_column
if group_value in PERIODS:
return db.func.date_part(group_value, group_column)
# We now expect a date format
return db.func.to_char(group_column, group_value)
def is_group(**kwargs):
if kwargs.get('group') is not None:
return True
def validate_selections(**kwargs):
selections = kwargs.get('selections')
filters = kwargs.get('filters')
groups = kwargs.get('groups')
if selections is None:
raise ValueError("Missing selections!")
if is_group(**kwargs):
for key in selections.keys():
if selections[key] not in GROUPS:
raise ValueError("Can only use sum, avg and count when\
grouping!")
if filters is not None:
for key in filters.keys():
for inner_key in filters[key].keys():
if inner_key not in FILTERS:
raise ValueError("Unknown filter: " + str(
inner_key))
return selections, filters, groups

View File

@ -0,0 +1,16 @@
{
"target": "Recording",
"selection": {
"record_value": "sum",
"record_type": "count",
"record_type": "avg"
},
"filter": {
"date": {
"$gt": "12-12-02"
}
},
"group": {
"date": "month"
}
}

View File

@ -157,9 +157,9 @@ definitions:
record_type: record_type:
$ref: '#/definitions/id' $ref: '#/definitions/id'
record_value: record_value:
type: string type: float
description: Value of the recording description: Value of the recording
example: '25 degrees' example: 25.0
RecordingCreation: RecordingCreation:
type: object type: object
@ -173,9 +173,9 @@ definitions:
record_type: record_type:
$ref: '#/definitions/id' $ref: '#/definitions/id'
record_value: record_value:
type: string type: float
description: Value of the recording description: Value of the recording
example: '25 degrees' example: 25.0
DeviceType: DeviceType:
type: object type: object

View File

@ -2,7 +2,7 @@ import os
# App configuration # App configuration
DEBUG = os.environ['DEBUG'] DEBUG = os.environ['DEBUG']
APP_VERSION = '0.3.3' APP_VERSION = '0.3.4'
# Define the application directory # Define the application directory
BASE_DIR = os.path.abspath(os.path.dirname(__file__)) BASE_DIR = os.path.abspath(os.path.dirname(__file__))

View File

@ -0,0 +1,34 @@
"""empty message
Revision ID: c09252c2c547
Revises: dad1f9b4eec2
Create Date: 2018-10-30 19:33:17.778301
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c09252c2c547'
down_revision = 'dad1f9b4eec2'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('recordings', 'record_value',
existing_type=sa.String(),
type_=sa.Float(),
postgresql_using='record_value::double precision')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('recordings', 'record_value',
existing_type=sa.Float(),
type_=sa.String(),
nullable=False)
# ### end Alembic commands ###