Created endpoint to upload skills manifest

pull/85/head
Matheus Lima 2019-03-26 22:09:41 -03:00
parent 0deb202125
commit e23d9ab840
10 changed files with 196 additions and 3 deletions

View File

@ -2,7 +2,6 @@ import os
from flask import Flask from flask import Flask
from public_api.endpoints.device_location import DeviceLocationEndpoint
from selene.api import SeleneResponse, selene_api from selene.api import SeleneResponse, selene_api
from selene.api.base_config import get_base_config from selene.api.base_config import get_base_config
from selene.api.public_endpoint import check_oauth_token from selene.api.public_endpoint import check_oauth_token
@ -21,6 +20,8 @@ from .endpoints.google_stt import GoogleSTTEndpoint
from .endpoints.open_weather_map import OpenWeatherMapEndpoint from .endpoints.open_weather_map import OpenWeatherMapEndpoint
from .endpoints.wolfram_alpha import WolframAlphaEndpoint from .endpoints.wolfram_alpha import WolframAlphaEndpoint
from .endpoints.wolfram_alpha_spoken import WolframAlphaSpokenEndpoint from .endpoints.wolfram_alpha_spoken import WolframAlphaSpokenEndpoint
from .endpoints.device_location import DeviceLocationEndpoint
from .endpoints.device_skill_manifest import DeviceSkillManifest
public = Flask(__name__) public = Flask(__name__)
public.config.from_object(get_base_config()) public.config.from_object(get_base_config())
@ -121,6 +122,12 @@ public.add_url_rule(
view_func=DeviceLocationEndpoint.as_view('device_location_api'), view_func=DeviceLocationEndpoint.as_view('device_location_api'),
methods=['GET'] methods=['GET']
) )
public.add_url_rule(
'/v1/device/<string:device_id>/skillJson',
view_func=DeviceSkillManifest.as_view('skill_manifest_api'),
methods=['PUT']
)
""" """
This is a workaround to allow the API return 401 when we call a non existent path. Use case: This is a workaround to allow the API return 401 when we call a non existent path. Use case:

View File

@ -0,0 +1,39 @@
import json
from http import HTTPStatus
from schematics import Model
from schematics.types import StringType, ModelType, ListType, DateTimeType
from selene.api import PublicEndpoint
from selene.data.skill import SkillRepository
from selene.util.db import get_db_connection
class SkillManifest(Model):
name = StringType(default='')
origin = StringType(default='')
installation = StringType(default='')
failure_message = StringType(default='')
status = StringType(default='')
beta = StringType(default='')
installed = DateTimeType()
updated = DateTimeType()
class SkillJson(Model):
blacklist = ListType(StringType)
skills = ListType(ModelType(SkillManifest, required=True))
class DeviceSkillManifest(PublicEndpoint):
def __init__(self):
super(DeviceSkillManifest, self).__init__()
def put(self, device_id):
payload = json.loads(self.request.data)
skill_json = SkillJson(payload)
skill_json.validate()
with get_db_connection(self.config['DB_CONNECTION_POOL']) as db:
SkillRepository(db).update_skills_manifest(device_id, payload['skills'])
return '', HTTPStatus.OK

View File

@ -29,11 +29,19 @@ class SkillMetadata(Model):
sections = ListType(ModelType(SkillSection)) sections = ListType(ModelType(SkillSection))
class SkillIcon(Model):
color = StringType()
icon = StringType()
class Skill(Model): class Skill(Model):
name = StringType(required=True) name = StringType(required=True)
identifier = StringType(required=True) identifier = StringType(required=True)
skillMetadata = ModelType(SkillMetadata) skillMetadata = ModelType(SkillMetadata)
color = StringType() color = StringType()
icon_img = StringType()
icon = ModelType(SkillIcon)
class DeviceSkillsEndpoint(PublicEndpoint): class DeviceSkillsEndpoint(PublicEndpoint):
"""Fetch all skills associated with a given device using the API v1 format""" """Fetch all skills associated with a given device using the API v1 format"""

View File

@ -0,0 +1,6 @@
Feature: Upload and fetch skills manifest
Scenario: A skill manifest is successfully uploaded
Given a device with a skill
When a skill manifest is uploaded
Then the skill manifest endpoint should return 200 status code

View File

@ -0,0 +1,70 @@
import json
from http import HTTPStatus
from behave import given, when, then
from hamcrest import assert_that, equal_to
skill_manifest = {
'skills': [
{
'name': 'skill-name-1',
'origin': 'voice',
'installation': 'installed',
'failure_message': '',
'status': 'active',
'installed': 1553610007,
'updated': 1553610007
}
]
}
skill = {
'name': 'skill-name-1',
'identifier': 'skill-name-13-123',
'skillMetadata': {
'sections': [
{
'name': 'section1',
'fields': [
{
'label': 'test'
}
]
}
]
}
}
@given('a device with a skill')
def device_skill(context):
login = context.device_login
device_id = login['uuid']
access_token = login['accessToken']
headers = dict(Authorization='Bearer {token}'.format(token=access_token))
context.client.put(
'/v1/device/{uuid}/skill'.format(uuid=device_id),
data=json.dumps(skill),
content_type='application_json',
headers=headers
)
@when('a skill manifest is uploaded')
def device_skill_manifest(context):
login = context.device_login
device_id = login['uuid']
access_token = login['accessToken']
headers = dict(Authorization='Bearer {token}'.format(token=access_token))
context.upload_skill_manifest_response = context.client.put(
'/v1/device/{uuid}/skillJson'.format(uuid=device_id),
data=json.dumps(skill_manifest),
content_type='application_json',
headers=headers
)
@then('the skill manifest endpoint should return 200 status code')
def validate_skill_manifest_upload(context):
response = context.upload_skill_manifest_response
assert_that(response.status_code, equal_to(HTTPStatus.OK))

View File

@ -1,5 +1,7 @@
from os import path from os import path
from selene.util.db import Cursor, DatabaseRequest, get_sql_from_file from typing import List
from selene.util.db import Cursor, DatabaseRequest, get_sql_from_file, DatabaseBatchRequest
class RepositoryBase(object): class RepositoryBase(object):
@ -12,3 +14,9 @@ class RepositoryBase(object):
sql=get_sql_from_file(path.join(self.sql_dir, sql_file_name)), sql=get_sql_from_file(path.join(self.sql_dir, sql_file_name)),
args=args args=args
) )
def _build_db_batch_request(self, sql_file_name: str, args: List[dict]):
return DatabaseBatchRequest(
sql=get_sql_from_file(path.join(self.sql_dir, sql_file_name)),
args=args
)

View File

@ -1,4 +1,5 @@
import json import json
from datetime import datetime
from typing import List from typing import List
from selene.util.db import use_transaction from selene.util.db import use_transaction
@ -114,3 +115,20 @@ class SkillRepository(RepositoryBase):
settings[field['name']] = field['value'] settings[field['name']] = field['value']
field.pop('value', None) field.pop('value', None)
return settings, skill return settings, skill
def update_skills_manifest(self, device_id: str, skill_manifest: List[dict]):
for skill in skill_manifest:
skill['device_id'] = device_id
installed = skill.get('installed')
if installed:
installed = datetime.fromtimestamp(installed)
skill['installed'] = installed
updated = skill.get('updated')
if updated:
updated = datetime.fromtimestamp(updated)
skill['updated'] = updated
db_batch_request = self._build_db_batch_request(
'update_skill_manifest.sql',
args=skill_manifest
)
self.cursor.batch_update(db_batch_request)

View File

@ -0,0 +1,21 @@
UPDATE
device.device_skill dev_skill
SET
install_method = %(origin)s,
install_status = %(status)s,
install_failure_reason = %(failure_message)s,
install_ts = %(installed)s,
update_ts = %(updated)s
WHERE
id = (
SELECT
dev_skill.id
FROM
device.device dev
INNER JOIN
device.device_skill dev_skill ON dev.id = dev_skill.device_id
INNER JOIN
skill.skill skill ON dev_skill.skill_id = skill.id
WHERE
dev.id = %(device_id)s AND skill.name = %(name)s
)

View File

@ -1,4 +1,4 @@
from .connection import connect_to_db, DatabaseConnectionConfig from .connection import connect_to_db, DatabaseConnectionConfig
from .connection_pool import allocate_db_connection_pool, get_db_connection from .connection_pool import allocate_db_connection_pool, get_db_connection
from .cursor import DatabaseRequest, Cursor, get_sql_from_file from .cursor import DatabaseRequest, DatabaseBatchRequest, Cursor, get_sql_from_file
from .transaction import use_transaction from .transaction import use_transaction

View File

@ -9,6 +9,9 @@ Example Usage:
from dataclasses import dataclass, field from dataclasses import dataclass, field
from logging import getLogger from logging import getLogger
from os import path from os import path
from typing import List
from psycopg2.extras import execute_batch
_log = getLogger(__package__) _log = getLogger(__package__)
@ -38,6 +41,12 @@ class DatabaseRequest(object):
args: dict = field(default=None) args: dict = field(default=None)
@dataclass
class DatabaseBatchRequest(object):
sql: str
args: List[dict]
class Cursor(object): class Cursor(object):
def __init__(self, db): def __init__(self, db):
self.db = db self.db = db
@ -92,6 +101,10 @@ class Cursor(object):
_log.debug(str(cursor.rowcount) + 'rows affected') _log.debug(str(cursor.rowcount) + 'rows affected')
return cursor.rowcount return cursor.rowcount
def _execute_batch(self, db_request: DatabaseBatchRequest):
with self.db.cursor() as cursor:
execute_batch(cursor, db_request.sql, db_request.args)
def delete(self, db_request: DatabaseRequest): def delete(self, db_request: DatabaseRequest):
"""Helper function for SQL delete statements""" """Helper function for SQL delete statements"""
deleted_rows = self._execute(db_request) deleted_rows = self._execute(db_request)
@ -109,3 +122,6 @@ class Cursor(object):
"""Helper function for SQL update statements.""" """Helper function for SQL update statements."""
updated_rows = self._execute(db_request) updated_rows = self._execute(db_request)
return updated_rows return updated_rows
def batch_update(self, db_request: DatabaseBatchRequest):
self._execute_batch(db_request)