updated the skill settings endpoint in the public API handle more use cases
parent
e411332667
commit
12dc2175d2
|
@ -4,7 +4,7 @@ from http import HTTPStatus
|
|||
from typing import List
|
||||
|
||||
from selene.api import SeleneEndpoint
|
||||
from selene.data.device import DeviceSkill, DeviceSkillRepository
|
||||
from selene.data.device import DeviceSkillRepository, ManifestSkill
|
||||
from selene.util.auth import AuthenticationError
|
||||
|
||||
VALID_STATUS_VALUES = (
|
||||
|
@ -36,7 +36,7 @@ class SkillInstallStatusEndpoint(SeleneEndpoint):
|
|||
|
||||
def _get_installed_skills(self):
|
||||
skill_repo = DeviceSkillRepository(self.db)
|
||||
installed_skills = skill_repo.get_device_skills_for_account(
|
||||
installed_skills = skill_repo.get_skill_manifest_for_account(
|
||||
self.account.id
|
||||
)
|
||||
for skill in installed_skills:
|
||||
|
@ -65,9 +65,9 @@ class SkillInstallStatusEndpoint(SeleneEndpoint):
|
|||
class SkillManifestAggregator(object):
|
||||
"""Base class containing functionality shared by summary and detail"""
|
||||
|
||||
def __init__(self, installed_skills: List[DeviceSkill]):
|
||||
def __init__(self, installed_skills: List[ManifestSkill]):
|
||||
self.installed_skills = installed_skills
|
||||
self.aggregate_skill = DeviceSkill(**asdict(installed_skills[0]))
|
||||
self.aggregate_skill = ManifestSkill(**asdict(installed_skills[0]))
|
||||
|
||||
def aggregate_skill_status(self):
|
||||
"""Aggregate skill data on all devices into a single skill.
|
||||
|
|
|
@ -18,7 +18,7 @@ from .endpoints.device_refresh_token import DeviceRefreshTokenEndpoint
|
|||
from .endpoints.device_setting import DeviceSettingEndpoint
|
||||
from .endpoints.device_skill import DeviceSkillEndpoint
|
||||
from .endpoints.device_skill_manifest import DeviceSkillManifestEndpoint
|
||||
from .endpoints.device_skills import DeviceSkillsEndpoint
|
||||
from .endpoints.device_skill_settings import DeviceSkillsEndpoint
|
||||
from .endpoints.device_subscription import DeviceSubscriptionEndpoint
|
||||
from .endpoints.google_stt import GoogleSTTEndpoint
|
||||
from .endpoints.oauth_callback import OauthCallbackEndpoint
|
||||
|
|
|
@ -0,0 +1,342 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
from flask import Response
|
||||
from schematics import Model
|
||||
from schematics.exceptions import ValidationError
|
||||
from schematics.types import StringType, BooleanType, ListType, ModelType
|
||||
|
||||
from selene.api import PublicEndpoint
|
||||
from selene.data.account import AccountRepository
|
||||
from selene.data.device import DeviceSkillRepository
|
||||
from selene.data.skill import (
|
||||
SettingsDisplay,
|
||||
SettingsDisplayRepository,
|
||||
SkillRepository,
|
||||
SkillSettingRepository
|
||||
)
|
||||
from selene.util.cache import DEVICE_SKILL_ETAG_KEY
|
||||
|
||||
# matches <submodule_name>|<branch>
|
||||
GLOBAL_ID_PATTERN = '^([^\|@]+)\|([^\|]+$)'
|
||||
# matches @<device_id>|<submodule_name>|<branch>
|
||||
GLOBAL_ID_DIRTY_PATTERN = '^@(.*)\|(.*)\|(.*)$'
|
||||
# matches @<device_id>|<folder_name>
|
||||
GLOBAL_ID_NON_MSM_PATTERN = '^@([^\|]+)\|([^\|]+$)'
|
||||
GLOBAL_ID_ANY_PATTERN = '(?:{})|(?:{})|(?:{})'.format(
|
||||
GLOBAL_ID_PATTERN,
|
||||
GLOBAL_ID_DIRTY_PATTERN,
|
||||
GLOBAL_ID_NON_MSM_PATTERN
|
||||
)
|
||||
|
||||
|
||||
class SkillSettingUpdater(object):
|
||||
"""Update the settings data for all devices with a skill
|
||||
|
||||
Skills and their settings are global across devices. While the PUT
|
||||
request specifies a single device to update, all devices with
|
||||
the same skill must be updated as well.
|
||||
"""
|
||||
_device_skill_repo = None
|
||||
_settings_display_repo = None
|
||||
|
||||
def __init__(self, db, device_id, display_data: dict):
|
||||
self.db = db
|
||||
self.device_id = device_id
|
||||
self.display_data = display_data
|
||||
self.settings_values = None
|
||||
self.skill = None
|
||||
|
||||
@property
|
||||
def device_skill_repo(self):
|
||||
if self._device_skill_repo is None:
|
||||
self._device_skill_repo = DeviceSkillRepository(self.db)
|
||||
|
||||
return self._device_skill_repo
|
||||
|
||||
@property
|
||||
def settings_display_repo(self):
|
||||
if self._settings_display_repo is None:
|
||||
self._settings_display_repo = SettingsDisplayRepository(self.db)
|
||||
|
||||
return self._settings_display_repo
|
||||
|
||||
def update(self):
|
||||
self._extract_settings_values()
|
||||
self._get_skill_id()
|
||||
self._ensure_settings_display_exists()
|
||||
self._update_device_skill()
|
||||
|
||||
def _extract_settings_values(self):
|
||||
"""Extract the settings values from the skillMetadata
|
||||
|
||||
The device applies the settings values in settings.json to the
|
||||
settings_meta.json file before sending the result to this API. The
|
||||
settings values are stored separately from the metadata in the database.
|
||||
"""
|
||||
settings_definition = self.display_data.get('skillMetadata')
|
||||
if settings_definition is not None:
|
||||
self.settings_values = dict()
|
||||
sections_without_values = []
|
||||
for section in settings_definition['sections']:
|
||||
section_without_values = dict(**section)
|
||||
for field in section_without_values['fields']:
|
||||
field_name = field.get('name')
|
||||
field_value = field.get('value')
|
||||
if field_name is not None and field_value is not None:
|
||||
self.settings_values[field_name] = field_value
|
||||
del(field['value'])
|
||||
sections_without_values.append(section_without_values)
|
||||
settings_definition['sections'] = sections_without_values
|
||||
|
||||
def _get_skill_id(self):
|
||||
"""Get the id of the skill in the request"""
|
||||
skill_global_id = (
|
||||
self.display_data['skill_gid'] or
|
||||
self.display_data['identifier']
|
||||
)
|
||||
skill_repo = SkillRepository(self.db)
|
||||
self.skill = skill_repo.get_skill_by_global_id(skill_global_id)
|
||||
|
||||
def _ensure_settings_display_exists(self) -> bool:
|
||||
"""If the settings display changed, a new row needs to be added."""
|
||||
new_settings_display = False
|
||||
self.settings_display = SettingsDisplay(
|
||||
self.skill.id,
|
||||
self.display_data
|
||||
)
|
||||
self.settings_display.id = (
|
||||
self.settings_display_repo.get_settings_display_id(
|
||||
self.settings_display
|
||||
)
|
||||
)
|
||||
if self.settings_display.id is None:
|
||||
self.settings_display.id = self.settings_display_repo.add(
|
||||
self.settings_display
|
||||
)
|
||||
new_settings_display = True
|
||||
|
||||
return new_settings_display
|
||||
|
||||
def _update_device_skill(self):
|
||||
"""Update the account's devices with the skill to have new settings"""
|
||||
account_repo = AccountRepository(self.db)
|
||||
account = account_repo.get_account_by_device_id(self.device_id)
|
||||
device_skill_settings = (
|
||||
self.device_skill_repo.get_device_skill_settings_for_account(
|
||||
account.id
|
||||
)
|
||||
)
|
||||
|
||||
for device_skill in device_skill_settings:
|
||||
if device_skill.skill_id == self.skill.id:
|
||||
if device_skill.install_method in ('voice', 'cli'):
|
||||
device_ids = [self.device_id]
|
||||
else:
|
||||
device_ids = device_skill.device_ids
|
||||
self.device_skill_repo.update_device_skill_settings(
|
||||
device_ids,
|
||||
self.settings_display,
|
||||
self.settings_values if self.settings_values else None
|
||||
)
|
||||
|
||||
|
||||
class SkillField(Model):
|
||||
name = StringType()
|
||||
type = StringType()
|
||||
label = StringType()
|
||||
hint = StringType()
|
||||
placeholder = StringType()
|
||||
hide = BooleanType()
|
||||
value = StringType()
|
||||
options = StringType()
|
||||
|
||||
|
||||
class SkillSection(Model):
|
||||
name = StringType(required=True)
|
||||
fields = ListType(ModelType(SkillField))
|
||||
|
||||
|
||||
class SkillMetadata(Model):
|
||||
sections = ListType(ModelType(SkillSection))
|
||||
|
||||
|
||||
class SkillIcon(Model):
|
||||
color = StringType()
|
||||
icon = StringType()
|
||||
|
||||
|
||||
class Skill(Model):
|
||||
name = StringType()
|
||||
skill_gid = StringType(regex=GLOBAL_ID_ANY_PATTERN)
|
||||
skillMetadata = ModelType(SkillMetadata)
|
||||
icon_img = StringType()
|
||||
icon = ModelType(SkillIcon)
|
||||
display_name = StringType()
|
||||
color = StringType()
|
||||
identifier = StringType()
|
||||
|
||||
def validate_skill_gid(self, data, value):
|
||||
if data['skill_gid'] is None and data['identifier'] is None:
|
||||
raise ValidationError(
|
||||
'skill should have either skill_gid or identifier defined'
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class DeviceSkillsEndpoint(PublicEndpoint):
|
||||
"""Fetch all skills associated with a device using the API v1 format"""
|
||||
_device_skill_repo = None
|
||||
_skill_repo = None
|
||||
_skill_setting_repo = None
|
||||
_settings_display_repo = None
|
||||
|
||||
@property
|
||||
def device_skill_repo(self):
|
||||
if self._device_skill_repo is None:
|
||||
self._device_skill_repo = DeviceSkillRepository(self.db)
|
||||
|
||||
return self._device_skill_repo
|
||||
|
||||
@property
|
||||
def settings_display_repo(self):
|
||||
if self._settings_display_repo is None:
|
||||
self._settings_display_repo = SettingsDisplayRepository(self.db)
|
||||
|
||||
return self._settings_display_repo
|
||||
|
||||
@property
|
||||
def skill_repo(self):
|
||||
if self._skill_repo is None:
|
||||
self._skill_repo = SkillRepository(self.db)
|
||||
|
||||
return self._skill_repo
|
||||
|
||||
@property
|
||||
def skill_setting_repo(self):
|
||||
if self._skill_setting_repo is None:
|
||||
self._skill_setting_repo = SkillSettingRepository(self.db)
|
||||
|
||||
return self._skill_setting_repo
|
||||
|
||||
def get(self, device_id):
|
||||
"""
|
||||
Retrieve skills installed on device from the database.
|
||||
|
||||
:raises NotModifiedException: when etag in request matches cache
|
||||
"""
|
||||
self._authenticate(device_id)
|
||||
self._validate_etag(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
|
||||
device_skills = self.skill_setting_repo.get_skill_settings_for_device(
|
||||
device_id
|
||||
)
|
||||
|
||||
if device_skills:
|
||||
response_data = self._build_response_data(device_skills)
|
||||
response = Response(
|
||||
json.dumps(response_data),
|
||||
status=HTTPStatus.OK,
|
||||
content_type='application/json'
|
||||
)
|
||||
self._add_etag(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
|
||||
else:
|
||||
response = Response(
|
||||
'',
|
||||
status=HTTPStatus.NO_CONTENT,
|
||||
content_type='application/json'
|
||||
)
|
||||
return response
|
||||
|
||||
def _build_response_data(self, device_skills):
|
||||
response_data = []
|
||||
for skill in device_skills:
|
||||
response_skill = dict(uuid=skill.skill_id)
|
||||
settings_definition = skill.settings_display.get('skillMetadata')
|
||||
if settings_definition:
|
||||
settings_sections = self._apply_settings_values(
|
||||
settings_definition, skill.settings_values
|
||||
)
|
||||
if settings_sections:
|
||||
response_skill.update(
|
||||
skillMetadata=dict(sections=settings_sections)
|
||||
)
|
||||
skill_gid = skill.settings_display.get('skill_gid')
|
||||
if skill_gid is not None:
|
||||
response_skill.update(skill_gid=skill_gid)
|
||||
identifier = skill.settings_display.get('identifier')
|
||||
if identifier is not None:
|
||||
response_skill.update(identifier=identifier)
|
||||
response_data.append(response_skill)
|
||||
|
||||
return response_data
|
||||
|
||||
@staticmethod
|
||||
def _apply_settings_values(settings_definition, settings_values):
|
||||
"""Build a copy of the settings sections populated with values."""
|
||||
sections_with_values = []
|
||||
for section in settings_definition['sections']:
|
||||
section_with_values = dict(**section)
|
||||
for field in section_with_values['fields']:
|
||||
field_name = field.get('name')
|
||||
if field_name is not None and field_name in settings_values:
|
||||
field.update(value=settings_values[field_name])
|
||||
sections_with_values.append(section_with_values)
|
||||
|
||||
return sections_with_values
|
||||
|
||||
def put(self, device_id):
|
||||
self._authenticate(device_id)
|
||||
self._validate_put_request()
|
||||
skill_id = self._update_skill_settings(device_id)
|
||||
self.etag_manager.expire(
|
||||
DEVICE_SKILL_ETAG_KEY.format(device_id=device_id)
|
||||
)
|
||||
|
||||
return dict(uuid=skill_id), HTTPStatus.OK
|
||||
|
||||
def _validate_put_request(self):
|
||||
skill = Skill(self.request.json)
|
||||
skill.validate()
|
||||
|
||||
def _update_skill_settings(self, device_id):
|
||||
skill_setting_updater = SkillSettingUpdater(
|
||||
self.db,
|
||||
device_id,
|
||||
self.request.json
|
||||
)
|
||||
skill_setting_updater.update()
|
||||
self._delete_orphaned_settings_display(
|
||||
skill_setting_updater.settings_display.id
|
||||
)
|
||||
|
||||
return skill_setting_updater.skill.id
|
||||
|
||||
def delete(self, device_id, skill_id):
|
||||
self._authenticate(device_id)
|
||||
settings_display_id = self._delete_skill_from_device(
|
||||
device_id,
|
||||
skill_id
|
||||
)
|
||||
self._delete_orphaned_settings_display(settings_display_id)
|
||||
return '', HTTPStatus.OK
|
||||
|
||||
def _delete_skill_from_device(self, device_id, skill_id):
|
||||
settings_display_id = None
|
||||
device_skills = (
|
||||
self.device_skill_repo.get_device_skill_settings_for_device(
|
||||
device_id
|
||||
)
|
||||
)
|
||||
for device_skill in device_skills:
|
||||
if device_skill.skill_id == skill_id:
|
||||
self.device_skill_repo.remove(device_id, skill_id)
|
||||
settings_display_id = device_skill.settings_display_id
|
||||
|
||||
return settings_display_id
|
||||
|
||||
def _delete_orphaned_settings_display(self, settings_display_id):
|
||||
skill_count = self.device_skill_repo.get_settings_display_usage(
|
||||
settings_display_id
|
||||
)
|
||||
if not skill_count:
|
||||
self.settings_display_repo.remove(settings_display_id)
|
|
@ -1,109 +0,0 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
from flask import Response
|
||||
from schematics import Model
|
||||
from schematics.exceptions import ValidationError
|
||||
from schematics.types import StringType, BooleanType, ListType, ModelType
|
||||
|
||||
from selene.api import PublicEndpoint
|
||||
from selene.data.skill import SkillRepository
|
||||
from selene.data.skill.repository.device_skill import DeviceSkillRepository
|
||||
from selene.util.cache import DEVICE_SKILL_ETAG_KEY
|
||||
|
||||
global_id_pattern = '^([^\|@]+)\|([^\|]+$)' # matches <submodule_name>|<branch>
|
||||
global_id_dirt_pattern = '^@(.*)\|(.*)\|(.*)$' # matches @<device_id>|<submodule_name>|<branch>
|
||||
global_id_non_msm_pattern = '^@([^\|]+)\|([^\|]+$)' # matches @<device_id>|<folder_name>
|
||||
global_id_any_pattern = '(?:{})|(?:{})|(?:{})'.format(
|
||||
global_id_pattern,
|
||||
global_id_dirt_pattern,
|
||||
global_id_non_msm_pattern
|
||||
)
|
||||
|
||||
|
||||
class SkillField(Model):
|
||||
name = StringType()
|
||||
type = StringType()
|
||||
label = StringType()
|
||||
hint = StringType()
|
||||
placeholder = StringType()
|
||||
hide = BooleanType()
|
||||
value = StringType()
|
||||
options = StringType()
|
||||
|
||||
|
||||
class SkillSection(Model):
|
||||
name = StringType(required=True)
|
||||
fields = ListType(ModelType(SkillField))
|
||||
|
||||
|
||||
class SkillMetadata(Model):
|
||||
sections = ListType(ModelType(SkillSection))
|
||||
|
||||
|
||||
class SkillIcon(Model):
|
||||
color = StringType()
|
||||
icon = StringType()
|
||||
|
||||
|
||||
class Skill(Model):
|
||||
name = StringType()
|
||||
skill_gid = StringType(regex=global_id_any_pattern)
|
||||
skillMetadata = ModelType(SkillMetadata)
|
||||
icon_img = StringType()
|
||||
icon = ModelType(SkillIcon)
|
||||
display_name = StringType()
|
||||
color = StringType()
|
||||
identifier = StringType()
|
||||
|
||||
def validate_skill_gid(self, data, value):
|
||||
if data['skill_gid'] is None and data['identifier'] is None:
|
||||
raise ValidationError(
|
||||
'skill should have either skill_gid or identifier define'
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class DeviceSkillsEndpoint(PublicEndpoint):
|
||||
"""Fetch all skills associated with a device using the API v1 format"""
|
||||
_skill_repo = None
|
||||
|
||||
@property
|
||||
def skill_repo(self):
|
||||
if self._skill_repo is None:
|
||||
self._skill_repo = SkillRepository(self.db)
|
||||
|
||||
return self._skill_repo
|
||||
|
||||
def get(self, device_id):
|
||||
self._authenticate(device_id)
|
||||
self._validate_etag(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
|
||||
skills = self.skill_repo.get_skill_settings_by_device_id(device_id)
|
||||
|
||||
if skills is not None:
|
||||
response = Response(
|
||||
json.dumps(skills),
|
||||
status=HTTPStatus.OK,
|
||||
content_type='application_json'
|
||||
)
|
||||
self._add_etag(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
|
||||
else:
|
||||
response = Response(
|
||||
'',
|
||||
status=HTTPStatus.NO_CONTENT,
|
||||
content_type='application_json'
|
||||
)
|
||||
return response
|
||||
|
||||
def put(self, device_id):
|
||||
self._authenticate(device_id)
|
||||
payload = json.loads(self.request.data)
|
||||
skill = Skill(payload)
|
||||
skill.validate()
|
||||
skill_id = SkillRepository(self.db).add(device_id, payload)
|
||||
return {'uuid': skill_id}, HTTPStatus.OK
|
||||
|
||||
def delete(self, device_id, skill_id):
|
||||
self._authenticate(device_id)
|
||||
DeviceSkillRepository(self.db).delete(device_id, skill_id)
|
||||
return '', HTTPStatus.OK
|
|
@ -0,0 +1,54 @@
|
|||
Feature: Upload and fetch skills and their settings
|
||||
Test all endpoints related to upload and fetch skill settings
|
||||
|
||||
Scenario: A device requests the settings for its skills
|
||||
Given an authorized device
|
||||
When a device requests the settings for its skills
|
||||
Then the request will be successful
|
||||
And the settings are returned
|
||||
And an E-tag is generated for these settings
|
||||
And device last contact timestamp is updated
|
||||
|
||||
# This scenario uses the ETag generated by the first scenario
|
||||
Scenario: Device requests skill settings that have not changed since last they were requested
|
||||
Given an authorized device
|
||||
And a valid device skill E-tag
|
||||
When a device requests the settings for its skills
|
||||
Then the request will succeed with a "not modified" return code
|
||||
And device last contact timestamp is updated
|
||||
|
||||
# This scenario uses the ETag generated by the first scenario
|
||||
Scenario: Device requests skill settings that have changed since last they were requested
|
||||
Given an authorized device
|
||||
And an expired device skill E-tag
|
||||
When a device requests the settings for its skills
|
||||
Then the request will be successful
|
||||
And an E-tag is generated for these settings
|
||||
And device last contact timestamp is updated
|
||||
|
||||
Scenario: A device uploads a change to a single skill setting value
|
||||
Given an authorized device
|
||||
And a valid device skill E-tag
|
||||
And skill settings with a new value
|
||||
When the device sends a request to update the skill settings
|
||||
Then the request will be successful
|
||||
And the skill settings are updated with the new value
|
||||
And the device skill E-tag is expired
|
||||
And device last contact timestamp is updated
|
||||
|
||||
Scenario: A device uploads skill settings with a field deleted from the settings
|
||||
Given an authorized device
|
||||
And a valid device skill E-tag
|
||||
And skill settings with a deleted field
|
||||
When the device sends a request to update the skill settings
|
||||
Then the request will be successful
|
||||
And the field is no longer in the skill settings
|
||||
And the device skill E-tag is expired
|
||||
And device last contact timestamp is updated
|
||||
|
||||
Scenario: A device requests a skill to be deleted
|
||||
Given an authorized device
|
||||
When the device requests a skill to be deleted
|
||||
Then the request will be successful
|
||||
And the skill will be removed from the device skill list
|
||||
And device last contact timestamp is updated
|
|
@ -1,33 +0,0 @@
|
|||
Feature: Upload and fetch skills
|
||||
Test all endpoints related to upload and fetch skill settings
|
||||
|
||||
Scenario: A skill is successfully uploaded and retrieved
|
||||
Given a device with skill settings
|
||||
When the skill settings are updated
|
||||
And the skill settings is fetched
|
||||
Then the skill settings should be retrieved with the new values
|
||||
And device last contact timestamp is updated
|
||||
|
||||
Scenario: Get a 304 when try to get the device's skills using a valid etag
|
||||
Given a device with skill settings
|
||||
When the skill settings are fetched using a valid etag
|
||||
Then the skill setting endpoint should return 304
|
||||
And device last contact timestamp is updated
|
||||
|
||||
Scenario: Get a new etag when try to fetch the skill settings using an expired etag
|
||||
Given a device with skill settings
|
||||
When the skill settings are fetched using an expired etag
|
||||
Then the skill settings endpoint should return a new etag
|
||||
And device last contact timestamp is updated
|
||||
|
||||
Scenario: Upload a skill with empty settings
|
||||
When a skill with empty settings is uploaded
|
||||
Then the endpoint to retrieve the skill should return 200
|
||||
And device last contact timestamp is updated
|
||||
|
||||
Scenario: A skill setting is successfully deleted
|
||||
Given a device with skill settings
|
||||
When the skill settings is deleted
|
||||
And the skill settings is fetched
|
||||
Then the endpoint to delete the skills settings should return 200
|
||||
And device last contact timestamp is updated
|
|
@ -9,10 +9,16 @@ from selene.testing.account_geography import add_account_geography
|
|||
from selene.testing.account_preference import add_account_preference
|
||||
from selene.testing.device import add_device
|
||||
from selene.testing.device_skill import (
|
||||
add_skill_to_manifest,
|
||||
remove_manifest_skill
|
||||
add_device_skill,
|
||||
add_device_skill_settings,
|
||||
remove_device_skill
|
||||
)
|
||||
from selene.testing.skill import (
|
||||
add_skill,
|
||||
build_label_field,
|
||||
build_text_field,
|
||||
remove_skill
|
||||
)
|
||||
from selene.testing.skill import add_skill, remove_skill
|
||||
from selene.testing.text_to_speech import (
|
||||
add_text_to_speech,
|
||||
remove_text_to_speech
|
||||
|
@ -50,24 +56,10 @@ def after_all(context):
|
|||
def before_scenario(context, _):
|
||||
context.etag_manager = ETagManager(context.cache, context.client_config)
|
||||
try:
|
||||
context.account = add_account(context.db)
|
||||
add_account_preference(context.db, context.account.id)
|
||||
context.geography_id = add_account_geography(
|
||||
context.db,
|
||||
context.account
|
||||
)
|
||||
context.wake_word = add_wake_word(context.db)
|
||||
context.voice = add_text_to_speech(context.db)
|
||||
_add_account(context)
|
||||
_add_skills(context)
|
||||
_add_device(context)
|
||||
context.skill = add_skill(
|
||||
context.db,
|
||||
skill_global_id='selene-test-skill|19.02'
|
||||
)
|
||||
context.manifest_skill = add_skill_to_manifest(
|
||||
context.db,
|
||||
context.device_id,
|
||||
context.skill
|
||||
)
|
||||
_add_device_skills(context)
|
||||
except:
|
||||
import traceback
|
||||
print(traceback.print_exc())
|
||||
|
@ -77,10 +69,22 @@ def after_scenario(context, _):
|
|||
remove_account(context.db, context.account)
|
||||
remove_wake_word(context.db, context.wake_word)
|
||||
remove_text_to_speech(context.db, context.voice)
|
||||
remove_skill(context.db, skill_global_id=context.skill.skill_gid)
|
||||
for skill in context.skills.values():
|
||||
remove_skill(context.db, skill[0])
|
||||
|
||||
|
||||
def _add_account(context):
|
||||
context.account = add_account(context.db)
|
||||
add_account_preference(context.db, context.account.id)
|
||||
context.geography_id = add_account_geography(
|
||||
context.db,
|
||||
context.account
|
||||
)
|
||||
|
||||
|
||||
def _add_device(context):
|
||||
context.wake_word = add_wake_word(context.db)
|
||||
context.voice = add_text_to_speech(context.db)
|
||||
device_id = add_device(context.db, context.account.id, context.geography_id)
|
||||
context.device_id = device_id
|
||||
context.device_name = 'Selene Test Device'
|
||||
|
@ -88,26 +92,66 @@ def _add_device(context):
|
|||
context.access_token = context.device_login['accessToken']
|
||||
|
||||
|
||||
def _add_skills(context):
|
||||
foo_skill, foo_settings_display = add_skill(
|
||||
context.db,
|
||||
skill_global_id='foo-skill|19.02',
|
||||
)
|
||||
bar_skill, bar_settings_display = add_skill(
|
||||
context.db,
|
||||
skill_global_id='bar-skill|19.02',
|
||||
settings_fields=[build_label_field(), build_text_field()]
|
||||
)
|
||||
context.skills = dict(
|
||||
foo=(foo_skill, foo_settings_display),
|
||||
bar=(bar_skill, bar_settings_display)
|
||||
)
|
||||
|
||||
|
||||
def _add_device_skills(context):
|
||||
for value in context.skills.values():
|
||||
skill, settings_display = value
|
||||
context.manifest_skill = add_device_skill(
|
||||
context.db,
|
||||
context.device_id,
|
||||
skill
|
||||
)
|
||||
settings_values = None
|
||||
if skill.skill_gid.startswith('bar'):
|
||||
settings_values = dict(textfield='Device text value')
|
||||
add_device_skill_settings(
|
||||
context.db,
|
||||
context.device_id,
|
||||
settings_display,
|
||||
settings_values=settings_values
|
||||
)
|
||||
|
||||
|
||||
def before_tag(context, tag):
|
||||
if tag == 'device_specific_skill':
|
||||
_add_device_specific_skil(context)
|
||||
_add_device_specific_skill(context)
|
||||
|
||||
|
||||
def _add_device_specific_skil(context):
|
||||
context.device_specific_skill = add_skill(
|
||||
def _add_device_specific_skill(context):
|
||||
dirty_skill, dirty_skill_settings = add_skill(
|
||||
context.db,
|
||||
skill_global_id='@{device_id}|device-specific-skill|19.02'.format(
|
||||
device_id=context.device_id
|
||||
)
|
||||
)
|
||||
context.device_specific_manifest = add_skill_to_manifest(
|
||||
context.skills.update(dirty=(dirty_skill, dirty_skill_settings))
|
||||
context.device_specific_manifest = add_device_skill(
|
||||
context.db,
|
||||
context.device_id,
|
||||
context.skill
|
||||
dirty_skill
|
||||
)
|
||||
|
||||
|
||||
def after_tag(context, tag):
|
||||
if tag == 'new_skill':
|
||||
remove_manifest_skill(context.db, context.new_manifest_skill)
|
||||
remove_skill(context.db, context.new_skill.skill_gid)
|
||||
_delete_new_skill(context)
|
||||
|
||||
|
||||
def _delete_new_skill(context):
|
||||
remove_device_skill(context.db, context.new_manifest_skill)
|
||||
remove_skill(context.db, context.new_skill)
|
||||
|
|
|
@ -1,268 +0,0 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
from behave import when, then, given
|
||||
from hamcrest import assert_that, equal_to, not_none, is_not, has_key
|
||||
|
||||
from selene.api.etag import ETagManager
|
||||
from selene.data.skill import AccountSkillSetting, SkillSettingRepository
|
||||
from selene.util.cache import DEVICE_SKILL_ETAG_KEY
|
||||
from selene.util.db import connect_to_db
|
||||
|
||||
skill = {
|
||||
'skill_gid': 'wolfram-alpha|19.02',
|
||||
'identifier': 'wolfram-alpha-123456',
|
||||
"skillMetadata": {
|
||||
"sections": [
|
||||
{
|
||||
"name": "Test-456",
|
||||
"fields": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "label-test"
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"type": "text",
|
||||
"label": "Username",
|
||||
"value": "name test",
|
||||
"placeholder": "this is a test"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"type": "password",
|
||||
"label": "Password",
|
||||
"value": "123"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
skill_empty_settings = {
|
||||
'skill_gid': 'mycroft-alarm|19.02',
|
||||
'identifier': 'mycroft-alarm|19.02'
|
||||
}
|
||||
|
||||
new_settings = {
|
||||
'user': 'this name is a test',
|
||||
'password': 'this is a new password'
|
||||
}
|
||||
|
||||
skill_updated = {
|
||||
'skill_gid': 'wolfram-alpha|19.02',
|
||||
"skillMetadata": {
|
||||
"sections": [
|
||||
{
|
||||
"name": "Test-456",
|
||||
"fields": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "label-test"
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"type": "text",
|
||||
"label": "Username",
|
||||
"value": "this name is a test",
|
||||
"placeholder": "this is a test"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"type": "password",
|
||||
"label": "Password",
|
||||
"value": "this is a new password"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@given('a device with skill settings')
|
||||
def create_skill_settings(context):
|
||||
login = context.device_login
|
||||
device_id = login['uuid']
|
||||
access_token = login['accessToken']
|
||||
headers = dict(Authorization='Bearer {token}'.format(token=access_token))
|
||||
context.upload_device_response = context.client.put(
|
||||
'/v1/device/{uuid}/skill'.format(uuid=device_id),
|
||||
data=json.dumps(skill),
|
||||
content_type='application_json',
|
||||
headers=headers
|
||||
)
|
||||
context.get_skill_response = context.client.get(
|
||||
'/v1/device/{uuid}/skill'.format(uuid=device_id),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
|
||||
@when('the skill settings are updated')
|
||||
def update_skill(context):
|
||||
response = json.loads(context.upload_device_response.data)
|
||||
update_settings = AccountSkillSetting(
|
||||
settings_display={},
|
||||
settings_values=new_settings,
|
||||
device_names=[context.device_name]
|
||||
)
|
||||
skill_ids = [response['uuid']]
|
||||
db = connect_to_db(context.client_config['DB_CONNECTION_CONFIG'])
|
||||
skill_setting_repo = SkillSettingRepository(db, context.account.id)
|
||||
skill_setting_repo.update_skill_settings(update_settings, skill_ids)
|
||||
|
||||
|
||||
@when('the skill settings is fetched')
|
||||
def retrieve_skill_updated(context):
|
||||
login = context.device_login
|
||||
device_id = login['uuid']
|
||||
access_token = login['accessToken']
|
||||
headers=dict(Authorization='Bearer {token}'.format(token=access_token))
|
||||
context.get_skill_updated_response = context.client.get(
|
||||
'/v1/device/{uuid}/skill'.format(uuid=device_id),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
|
||||
@then('the skill settings should be retrieved with the new values')
|
||||
def validate_get_skill_updated_response(context):
|
||||
# First we validate the skill uploading
|
||||
response = context.upload_device_response
|
||||
assert_that(response.status_code, equal_to(HTTPStatus.OK))
|
||||
|
||||
# Then we validate if the skill we fetch is the same that was uploaded
|
||||
response = context.get_skill_response
|
||||
assert_that(response.status_code, equal_to(HTTPStatus.OK))
|
||||
skills_response = json.loads(response.data)
|
||||
assert_that(len(skills_response), equal_to(1))
|
||||
response = skills_response[0]
|
||||
assert_that(response, has_key('uuid'))
|
||||
assert_that(response['skill_gid'], equal_to(skill['skill_gid']))
|
||||
assert_that(response['identifier'], equal_to(skill['identifier']))
|
||||
assert_that(response['skillMetadata'], equal_to(skill['skillMetadata']))
|
||||
|
||||
# Then we validate if the skill was properly updated
|
||||
response = context.get_skill_updated_response
|
||||
assert_that(response.status_code, equal_to(HTTPStatus.OK))
|
||||
response_data = json.loads(response.data)
|
||||
assert_that(len(response_data), equal_to(1))
|
||||
response_data = response_data[0]
|
||||
assert_that(response_data['skill_gid'], equal_to(skill_updated['skill_gid']))
|
||||
assert_that(response_data['skillMetadata'], equal_to(skill_updated['skillMetadata']))
|
||||
|
||||
|
||||
@when('the skill settings are fetched using a valid etag')
|
||||
def get_skills_etag(context):
|
||||
etag_manager: ETagManager = context.etag_manager
|
||||
login = context.device_login
|
||||
device_id = login['uuid']
|
||||
skill_etag = etag_manager.get(
|
||||
DEVICE_SKILL_ETAG_KEY.format(device_id=device_id)
|
||||
)
|
||||
access_token = login['accessToken']
|
||||
headers = {
|
||||
'Authorization': 'Bearer {token}'.format(token=access_token),
|
||||
'If-None-Match': skill_etag
|
||||
}
|
||||
context.get_skill_response = context.client.get(
|
||||
'/v1/device/{device_id}/skill'.format(device_id=device_id),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
|
||||
@then('the skill setting endpoint should return 304')
|
||||
def validate_etag(context):
|
||||
response = context.get_skill_response
|
||||
assert_that(response.status_code, HTTPStatus.NOT_MODIFIED)
|
||||
|
||||
|
||||
@when('the skill settings are fetched using an expired etag')
|
||||
def get_skills_expired_etag(context):
|
||||
etag_manager: ETagManager = context.etag_manager
|
||||
login = context.device_login
|
||||
device_id = login['uuid']
|
||||
context.old_skill_etag = etag_manager.get(
|
||||
DEVICE_SKILL_ETAG_KEY.format(device_id=device_id)
|
||||
)
|
||||
etag_manager.expire_skill_etag_by_device_id(device_id)
|
||||
access_token = login['accessToken']
|
||||
headers = {
|
||||
'Authorization': 'Bearer {token}'.format(token=access_token),
|
||||
'If-None-Match': context.old_skill_etag
|
||||
}
|
||||
context.get_skill_response = context.client.get(
|
||||
'/v1/device/{device_id}/skill'.format(device_id=device_id),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
|
||||
@then('the skill settings endpoint should return a new etag')
|
||||
def validate_expired_etag(context):
|
||||
response = context.get_skill_response
|
||||
assert_that(response.status_code, equal_to(HTTPStatus.OK))
|
||||
new_etag = response.headers.get('ETag')
|
||||
assert_that(new_etag, not_none())
|
||||
assert_that(context.old_skill_etag, is_not(new_etag))
|
||||
|
||||
|
||||
@when('a skill with empty settings is uploaded')
|
||||
def upload_empty_skill(context):
|
||||
login = context.device_login
|
||||
device_id = login['uuid']
|
||||
access_token = login['accessToken']
|
||||
headers = dict(Authorization='Bearer {token}'.format(token=access_token))
|
||||
context.upload_device_response = context.client.put(
|
||||
'/v1/device/{uuid}/skill'.format(uuid=device_id),
|
||||
data=json.dumps(skill_empty_settings),
|
||||
content_type='application_json',
|
||||
headers=headers
|
||||
)
|
||||
context.get_skill_response = context.client.get(
|
||||
'/v1/device/{uuid}/skill'.format(uuid=device_id),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
|
||||
@then('the endpoint to retrieve the skill should return 200')
|
||||
def validate_empty_skill_uploading(context):
|
||||
response = context.upload_device_response
|
||||
assert_that(response.status_code, equal_to(HTTPStatus.OK))
|
||||
|
||||
response = context.get_skill_response
|
||||
assert_that(response.status_code, equal_to(HTTPStatus.OK))
|
||||
new_etag = response.headers.get('ETag')
|
||||
assert_that(new_etag, not_none())
|
||||
retrieved_skill = json.loads(context.get_skill_response.data)[0]
|
||||
assert_that(skill_empty_settings['skill_gid'], retrieved_skill['skill_gid'])
|
||||
assert_that(skill_empty_settings['identifier'], retrieved_skill['identifier'])
|
||||
|
||||
|
||||
@when('the skill settings is deleted')
|
||||
def delete_skill(context):
|
||||
skills = json.loads(context.get_skill_response.data)
|
||||
skill_fetched = skills[0]
|
||||
skill_uuid = skill_fetched['uuid']
|
||||
login = context.device_login
|
||||
device_id = login['uuid']
|
||||
access_token = login['accessToken']
|
||||
headers = dict(Authorization='Bearer {token}'.format(token=access_token))
|
||||
context.delete_skill_response = context.client.delete(
|
||||
'/v1/device/{device_uuid}/skill/{skill_uuid}'.format(device_uuid=device_id, skill_uuid=skill_uuid),
|
||||
headers=headers
|
||||
)
|
||||
context.get_skill_after_delete_response = context.client.get(
|
||||
'/v1/device/{uuid}/skill'.format(uuid=device_id),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
|
||||
@then('the endpoint to delete the skills settings should return 200')
|
||||
def validate_delete_skill(context):
|
||||
# Validating that the deletion happened successfully
|
||||
response = context.delete_skill_response
|
||||
assert_that(response.status_code, HTTPStatus.OK)
|
||||
|
||||
# Validating that the skill is not listed after we fetch the device's skills
|
||||
response = context.get_skill_after_delete_response
|
||||
assert_that(response.status_code, equal_to(HTTPStatus.NO_CONTENT))
|
|
@ -0,0 +1,172 @@
|
|||
import json
|
||||
|
||||
from behave import when, then, given
|
||||
from hamcrest import assert_that, equal_to, is_not, is_in
|
||||
|
||||
from selene.api.etag import ETAG_REQUEST_HEADER_KEY
|
||||
from selene.data.device import DeviceSkillRepository
|
||||
from selene.data.skill import SkillSettingRepository
|
||||
from selene.util.cache import DEVICE_SKILL_ETAG_KEY
|
||||
|
||||
|
||||
@given('skill settings with a new value')
|
||||
def change_skill_setting_value(context):
|
||||
_, bar_settings_display = context.skills['bar']
|
||||
section = bar_settings_display.display_data['skillMetadata']['sections'][0]
|
||||
field_with_value = section['fields'][1]
|
||||
field_with_value['value'] = 'New device text value'
|
||||
|
||||
|
||||
@given('skill settings with a deleted field')
|
||||
def delete_field_from_settings(context):
|
||||
_, bar_settings_display = context.skills['bar']
|
||||
section = bar_settings_display.display_data['skillMetadata']['sections'][0]
|
||||
context.removed_field = section['fields'].pop(1)
|
||||
|
||||
|
||||
@given('a valid device skill E-tag')
|
||||
def set_skill_setting_etag(context):
|
||||
context.device_skill_etag = context.etag_manager.get(
|
||||
DEVICE_SKILL_ETAG_KEY.format(device_id=context.device_id)
|
||||
)
|
||||
|
||||
|
||||
@given('an expired device skill E-tag')
|
||||
def expire_skill_setting_etag(context):
|
||||
valid_device_skill_etag = context.etag_manager.get(
|
||||
DEVICE_SKILL_ETAG_KEY.format(device_id=context.device_id)
|
||||
)
|
||||
context.device_skill_etag = context.etag_manager.expire(
|
||||
valid_device_skill_etag
|
||||
)
|
||||
|
||||
|
||||
@when('a device requests the settings for its skills')
|
||||
def get_device_skill_settings(context):
|
||||
if hasattr(context, 'device_skill_etag'):
|
||||
context.request_header[ETAG_REQUEST_HEADER_KEY] = (
|
||||
context.device_skill_etag
|
||||
)
|
||||
context.response = context.client.get(
|
||||
'/v1/device/{device_id}/skill'.format(device_id=context.device_id),
|
||||
content_type='application/json',
|
||||
headers=context.request_header
|
||||
)
|
||||
|
||||
|
||||
@when('the device sends a request to update the skill settings')
|
||||
def update_skill_settings(context):
|
||||
_, bar_settings_display = context.skills['bar']
|
||||
context.response = context.client.put(
|
||||
'/v1/device/{device_id}/skill'.format(device_id=context.device_id),
|
||||
data=json.dumps(bar_settings_display.display_data),
|
||||
content_type='application/json',
|
||||
headers=context.request_header
|
||||
)
|
||||
|
||||
|
||||
@when('the device requests a skill to be deleted')
|
||||
def delete_skill(context):
|
||||
foo_skill, _ = context.skills['foo']
|
||||
context.response = context.client.delete(
|
||||
'/v1/device/{device_id}/skill/{skill_id}'.format(
|
||||
device_id=context.device_id,
|
||||
skill_id=foo_skill.id
|
||||
),
|
||||
headers=context.request_header
|
||||
)
|
||||
|
||||
|
||||
@then('the settings are returned')
|
||||
def validate_response(context):
|
||||
response = context.response.json
|
||||
assert_that(len(response), equal_to(2))
|
||||
foo_skill, foo_settings_display = context.skills['foo']
|
||||
foo_skill_expected_result = dict(
|
||||
uuid=foo_skill.id,
|
||||
skill_gid=foo_skill.skill_gid,
|
||||
identifier=foo_settings_display.display_data['identifier']
|
||||
)
|
||||
assert_that(foo_skill_expected_result, is_in(response))
|
||||
|
||||
bar_skill, bar_settings_display = context.skills['bar']
|
||||
section = bar_settings_display.display_data['skillMetadata']['sections'][0]
|
||||
field_with_value = section['fields'][1]
|
||||
field_with_value['value'] = 'Device text value'
|
||||
bar_skill_expected_result = dict(
|
||||
uuid=bar_skill.id,
|
||||
skill_gid=bar_skill.skill_gid,
|
||||
identifier=bar_settings_display.display_data['identifier'],
|
||||
skillMetadata=bar_settings_display.display_data['skillMetadata']
|
||||
)
|
||||
assert_that(bar_skill_expected_result, is_in(response))
|
||||
|
||||
|
||||
@then('the device skill E-tag is expired')
|
||||
def check_for_expired_etag(context):
|
||||
"""An E-tag is expired by changing its value."""
|
||||
expired_device_skill_etag = context.etag_manager.get(
|
||||
DEVICE_SKILL_ETAG_KEY.format(device_id=context.device_id)
|
||||
)
|
||||
assert_that(
|
||||
expired_device_skill_etag.decode(),
|
||||
is_not(equal_to(context.device_skill_etag))
|
||||
)
|
||||
|
||||
|
||||
@then('the skill settings are updated with the new value')
|
||||
def validate_updated_skill_setting_value(context):
|
||||
settings_repo = SkillSettingRepository(context.db)
|
||||
device_skill_settings = settings_repo.get_skill_settings_for_device(
|
||||
context.device_id
|
||||
)
|
||||
device_settings_values = [
|
||||
dss.settings_values for dss in device_skill_settings
|
||||
]
|
||||
assert_that(len(device_skill_settings), equal_to(2))
|
||||
expected_settings_values = dict(textfield='New device text value')
|
||||
assert_that(
|
||||
expected_settings_values,
|
||||
is_in(device_settings_values)
|
||||
)
|
||||
|
||||
|
||||
@then('an E-tag is generated for these settings')
|
||||
def get_skills_etag(context):
|
||||
response_headers = context.response.headers
|
||||
response_etag = response_headers['ETag']
|
||||
skill_etag = context.etag_manager.get(
|
||||
DEVICE_SKILL_ETAG_KEY.format(device_id=context.device_id)
|
||||
)
|
||||
assert_that(skill_etag.decode(), equal_to(response_etag))
|
||||
|
||||
|
||||
@then('the field is no longer in the skill settings')
|
||||
def validate_skill_setting_field_removed(context):
|
||||
settings_repo = SkillSettingRepository(context.db)
|
||||
device_skill_settings = settings_repo.get_skill_settings_for_device(
|
||||
context.device_id
|
||||
)
|
||||
device_settings_values = [
|
||||
dss.settings_values for dss in device_skill_settings
|
||||
]
|
||||
assert_that(len(device_skill_settings), equal_to(2))
|
||||
assert_that([None, None], equal_to(device_settings_values))
|
||||
|
||||
new_section = dict(fields=None)
|
||||
for device_skill_setting in device_skill_settings:
|
||||
skill_gid = device_skill_setting.settings_display['skill_gid']
|
||||
if skill_gid.startswith('bar'):
|
||||
new_settings_display = device_skill_setting.settings_display
|
||||
new_skill_definition = new_settings_display['skillMetadata']
|
||||
new_section = new_skill_definition['sections'][0]
|
||||
assert_that(context.removed_field, not is_in(new_section['fields']))
|
||||
|
||||
|
||||
@then('the skill will be removed from the device skill list')
|
||||
def validate_delete_skill(context):
|
||||
device_skill_repo = DeviceSkillRepository(context.db)
|
||||
device_skills = device_skill_repo.get_device_skill_settings_for_device(
|
||||
context.device_id
|
||||
)
|
||||
assert_that(len(device_skills), equal_to(1))
|
|
@ -112,7 +112,7 @@ for db_setup_file in DB_CREATE_FILES:
|
|||
postgres_db.close_db()
|
||||
|
||||
|
||||
template_db = PostgresDB(db_name='mycroft_template')
|
||||
template_db = PostgresDB(db_name='mycroft_template', user='mycroft')
|
||||
|
||||
print('Creating the extensions')
|
||||
template_db.execute_sql(
|
||||
|
|
|
@ -5,6 +5,8 @@ from selene.data.device import DeviceRepository
|
|||
from selene.util.cache import SeleneCache, DEVICE_SKILL_ETAG_KEY
|
||||
from selene.util.db import connect_to_db
|
||||
|
||||
ETAG_REQUEST_HEADER_KEY = 'If-None-Match'
|
||||
|
||||
|
||||
def device_etag_key(device_id: str):
|
||||
return 'device.etag:{uuid}'.format(uuid=device_id)
|
||||
|
@ -37,7 +39,7 @@ class ETagManager(object):
|
|||
self.cache.set(key, etag)
|
||||
return etag
|
||||
|
||||
def _expire(self, key):
|
||||
def expire(self, key):
|
||||
"""Expires an existent etag
|
||||
:param key: key where the etag is stored"""
|
||||
etag = ''.join(random.choice(self.etag_chars) for _ in range(32))
|
||||
|
@ -46,12 +48,12 @@ class ETagManager(object):
|
|||
def expire_device_etag_by_device_id(self, device_id: str):
|
||||
"""Expire the etag associated with a device entity
|
||||
:param device_id: device uuid"""
|
||||
self._expire(device_etag_key(device_id))
|
||||
self.expire(device_etag_key(device_id))
|
||||
|
||||
def expire_device_setting_etag_by_device_id(self, device_id: str):
|
||||
"""Expire the etag associated with a device's settings entity
|
||||
:param device_id: device uuid"""
|
||||
self._expire(device_setting_etag_key(device_id))
|
||||
self.expire(device_setting_etag_key(device_id))
|
||||
|
||||
def expire_device_setting_etag_by_account_id(self, account_id: str):
|
||||
"""Expire the settings' etags for all devices from a given account. Used when the settings are updated
|
||||
|
@ -64,7 +66,7 @@ class ETagManager(object):
|
|||
def expire_device_location_etag_by_device_id(self, device_id: str):
|
||||
"""Expire the etag associate with the device's location entity
|
||||
:param device_id: device uuid"""
|
||||
self._expire(device_location_etag_key(device_id))
|
||||
self.expire(device_location_etag_key(device_id))
|
||||
|
||||
def expire_device_location_etag_by_account_id(self, account_id: str):
|
||||
"""Expire the locations' etag fpr açç device for a given acccount
|
||||
|
@ -79,7 +81,7 @@ class ETagManager(object):
|
|||
|
||||
:param device_id: device uuid
|
||||
"""
|
||||
self._expire(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
|
||||
self.expire(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
|
||||
|
||||
def expire_skill_etag_by_account_id(self, account_id):
|
||||
db = connect_to_db(self.db_connection_config)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from .entity.device_skill import DeviceSkill, ManifestSkill
|
||||
from .entity.device_skill import ManifestSkill, DeviceSkillSettings
|
||||
from .entity.geography import Geography
|
||||
from .entity.preference import AccountPreferences
|
||||
from .entity.text_to_speech import TextToSpeech
|
||||
|
|
|
@ -1,21 +1,6 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceSkill(object):
|
||||
id: str
|
||||
device_id: str
|
||||
install_method: str
|
||||
install_status: str
|
||||
skill_id: str
|
||||
skill_gid: str
|
||||
device_name: str = None
|
||||
install_failure_reason: str = None
|
||||
install_ts: datetime = None
|
||||
update_ts: datetime = None
|
||||
skill_settings: dict = None
|
||||
skill_settings_display_id: str = None
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -26,5 +11,15 @@ class ManifestSkill(object):
|
|||
skill_gid: str
|
||||
install_failure_reason: str = None
|
||||
install_ts: datetime = None
|
||||
skill_id: str = None
|
||||
update_ts: datetime = None
|
||||
id: str = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceSkillSettings(object):
|
||||
device_ids: List[str]
|
||||
install_method: str
|
||||
skill_id: str
|
||||
settings_values: dict = None
|
||||
settings_display_id: str = None
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
"""Data repository code for the skills on a device"""
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
from typing import List
|
||||
|
||||
from ..entity.device_skill import DeviceSkill, ManifestSkill
|
||||
from selene.data.skill import SettingsDisplay
|
||||
from ..entity.device_skill import ManifestSkill, DeviceSkillSettings
|
||||
from ...repository_base import RepositoryBase
|
||||
|
||||
|
||||
|
@ -10,15 +12,24 @@ class DeviceSkillRepository(RepositoryBase):
|
|||
def __init__(self, db):
|
||||
super(DeviceSkillRepository, self).__init__(db, __file__)
|
||||
|
||||
def get_installed_skills_for_account(
|
||||
def get_device_skill_settings_for_account(
|
||||
self, account_id: str
|
||||
) -> List[DeviceSkill]:
|
||||
) -> List[DeviceSkillSettings]:
|
||||
return self._select_all_into_dataclass(
|
||||
dataclass=DeviceSkill,
|
||||
sql_file_name='get_device_skills_for_account.sql',
|
||||
DeviceSkillSettings,
|
||||
sql_file_name='get_device_skill_settings_for_account.sql',
|
||||
args=dict(account_id=account_id)
|
||||
)
|
||||
|
||||
def get_device_skill_settings_for_device(
|
||||
self, device_id: str
|
||||
) -> List[DeviceSkillSettings]:
|
||||
return self._select_all_into_dataclass(
|
||||
DeviceSkillSettings,
|
||||
sql_file_name='get_device_skill_settings_for_device.sql',
|
||||
args=dict(device_id=device_id)
|
||||
)
|
||||
|
||||
def update_skill_settings(
|
||||
self, account_id: str, device_names: tuple, skill_name: str
|
||||
):
|
||||
|
@ -32,13 +43,41 @@ class DeviceSkillRepository(RepositoryBase):
|
|||
)
|
||||
self.cursor.update(db_request)
|
||||
|
||||
def get_skill_manifest_for_device(self, device_id: str):
|
||||
def update_device_skill_settings(
|
||||
self,
|
||||
device_ids: List[str],
|
||||
settings_display: SettingsDisplay,
|
||||
settings_values: str,
|
||||
):
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='update_device_skill_settings.sql',
|
||||
args=dict(
|
||||
device_ids=tuple(device_ids),
|
||||
skill_id=settings_display.skill_id,
|
||||
settings_values=json.dumps(settings_values),
|
||||
settings_display_id=settings_display.id
|
||||
)
|
||||
)
|
||||
self.cursor.update(db_request)
|
||||
|
||||
def get_skill_manifest_for_device(
|
||||
self, device_id: str
|
||||
) -> List[ManifestSkill]:
|
||||
return self._select_all_into_dataclass(
|
||||
dataclass=ManifestSkill,
|
||||
sql_file_name='get_device_skill_manifest.sql',
|
||||
args=dict(device_id=device_id)
|
||||
)
|
||||
|
||||
def get_skill_manifest_for_account(
|
||||
self, account_id: str
|
||||
) -> List[ManifestSkill]:
|
||||
return self._select_all_into_dataclass(
|
||||
dataclass=ManifestSkill,
|
||||
sql_file_name='get_skill_manifest_for_account.sql',
|
||||
args=dict(account_id=account_id)
|
||||
)
|
||||
|
||||
def update_manifest_skill(self, manifest_skill: ManifestSkill):
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='update_skill_manifest.sql',
|
||||
|
@ -47,12 +86,10 @@ class DeviceSkillRepository(RepositoryBase):
|
|||
|
||||
self.cursor.update(db_request)
|
||||
|
||||
def add_manifest_skill(self, skill_id: str, manifest_skill: ManifestSkill):
|
||||
db_request_args = dict(skill_id=skill_id)
|
||||
db_request_args.update(asdict(manifest_skill))
|
||||
def add_manifest_skill(self, manifest_skill: ManifestSkill):
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='add_manifest_skill.sql',
|
||||
args=db_request_args
|
||||
args=asdict(manifest_skill)
|
||||
)
|
||||
db_result = self.cursor.insert_returning(db_request)
|
||||
|
||||
|
@ -67,3 +104,22 @@ class DeviceSkillRepository(RepositoryBase):
|
|||
)
|
||||
)
|
||||
self.cursor.delete(db_request)
|
||||
|
||||
def get_settings_display_usage(self, settings_display_id: str) -> int:
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='get_settings_display_usage.sql',
|
||||
args=dict(settings_display_id=settings_display_id)
|
||||
)
|
||||
db_result = self.cursor.select_one(db_request)
|
||||
|
||||
return db_result['usage']
|
||||
|
||||
def remove(self, device_id, skill_id):
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='delete_device_skill.sql',
|
||||
args=dict(
|
||||
device_id=device_id,
|
||||
skill_id=skill_id
|
||||
)
|
||||
)
|
||||
self.cursor.delete(db_request)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
DELETE FROM
|
||||
device.device_skill
|
||||
WHERE
|
||||
device_id = %(device_id)s
|
||||
AND skill_id = %(skill_id)s
|
|
@ -5,6 +5,7 @@ SELECT
|
|||
ds.install_method,
|
||||
ds.install_status,
|
||||
ds.install_ts,
|
||||
ds.skill_id,
|
||||
ds.update_ts,
|
||||
s.skill_gid
|
||||
FROM
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
SELECT
|
||||
dds.skill_settings_display_id AS settings_display_id,
|
||||
dds.settings::jsonb AS settings_values,
|
||||
dds.install_method,
|
||||
dds.skill_id,
|
||||
array_agg(dds.device_id::text) AS device_ids
|
||||
FROM
|
||||
device.device dd
|
||||
INNER JOIN device.device_skill dds ON dd.id = dds.device_id
|
||||
WHERE
|
||||
dd.account_id = %(account_id)s
|
||||
GROUP BY
|
||||
dds.skill_settings_display_id,
|
||||
dds.settings::jsonb,
|
||||
dds.install_method,
|
||||
dds.skill_id
|
|
@ -0,0 +1,15 @@
|
|||
SELECT
|
||||
skill_settings_display_id AS settings_display_id,
|
||||
settings::jsonb AS settings_values,
|
||||
install_method,
|
||||
skill_id,
|
||||
array_agg(device_id::text) AS device_ids
|
||||
FROM
|
||||
device.device_skill
|
||||
WHERE
|
||||
device_id = %(device_id)s
|
||||
GROUP BY
|
||||
skill_settings_display_id,
|
||||
settings::jsonb,
|
||||
install_method,
|
||||
skill_id
|
|
@ -0,0 +1,6 @@
|
|||
SELECT
|
||||
count(*) AS usage
|
||||
FROM
|
||||
device.device_skill
|
||||
WHERE
|
||||
skill_settings_display_id = %(settings_display_id)s
|
|
@ -1,14 +1,10 @@
|
|||
SELECT
|
||||
d.name AS device_name,
|
||||
ds.id,
|
||||
ds.device_id,
|
||||
ds.install_failure_reason,
|
||||
ds.install_method,
|
||||
ds.install_status,
|
||||
ds.install_ts,
|
||||
ds.settings,
|
||||
ds.skill_id,
|
||||
ds.skill_settings_display_id,
|
||||
ds.update_ts,
|
||||
s.skill_gid
|
||||
FROM
|
|
@ -0,0 +1,8 @@
|
|||
UPDATE
|
||||
device.device_skill
|
||||
SET
|
||||
skill_settings_display_id = %(settings_display_id)s,
|
||||
settings = %(settings_values)s
|
||||
WHERE
|
||||
skill_id = %(skill_id)s
|
||||
AND device_id IN %(device_ids)s
|
|
@ -1,6 +1,11 @@
|
|||
from .entity.display import SkillDisplay
|
||||
from .entity.skill import Skill
|
||||
from .entity.skill_setting import AccountSkillSetting
|
||||
from .entity.skill_setting import (
|
||||
AccountSkillSetting,
|
||||
DeviceSkillSetting,
|
||||
SettingsDisplay
|
||||
)
|
||||
from .repository.display import SkillDisplayRepository
|
||||
from .repository.setting import SkillSettingRepository
|
||||
from .repository.settings_display import SettingsDisplayRepository
|
||||
from .repository.skill import SkillRepository
|
||||
|
|
|
@ -7,3 +7,17 @@ class AccountSkillSetting(object):
|
|||
settings_display: dict
|
||||
settings_values: dict
|
||||
device_names: List[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceSkillSetting(object):
|
||||
settings_display: dict
|
||||
settings_values: dict
|
||||
skill_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingsDisplay(object):
|
||||
skill_id: str
|
||||
display_data: dict
|
||||
id: str = None
|
||||
|
|
|
@ -77,6 +77,6 @@ class SkillSettingRepository(RepositoryBase):
|
|||
"""Return all skills and their settings for a given device id"""
|
||||
return self._select_all_into_dataclass(
|
||||
DeviceSkillSetting,
|
||||
sql_file_name='get_skill_setting_by_device_id.sql',
|
||||
sql_file_name='get_skill_setting_by_device.sql',
|
||||
args=dict(device_id=device_id)
|
||||
)
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
from selene.data.repository_base import RepositoryBase
|
||||
import json
|
||||
|
||||
from ...repository_base import RepositoryBase
|
||||
from ..entity.skill_setting import SettingsDisplay
|
||||
|
||||
|
||||
class SettingsDisplayRepository(RepositoryBase):
|
||||
|
@ -6,16 +9,33 @@ class SettingsDisplayRepository(RepositoryBase):
|
|||
def __init__(self, db):
|
||||
super(SettingsDisplayRepository, self).__init__(db, __file__)
|
||||
|
||||
def add(self, skill_id: str, settings_display: str) -> str:
|
||||
def add(self, settings_display: SettingsDisplay) -> str:
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='get_settings_display.sql',
|
||||
args=dict(skill_id=skill_id, settings_display=settings_display)
|
||||
sql_file_name='add_settings_display.sql',
|
||||
args=dict(
|
||||
skill_id=settings_display.skill_id,
|
||||
display_data=json.dumps(settings_display.display_data)
|
||||
)
|
||||
)
|
||||
result = self.cursor.insert_returning(db_request)
|
||||
|
||||
return result['id']
|
||||
|
||||
def get_settings_display_id(self, settings_display: SettingsDisplay):
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='get_settings_display_id.sql',
|
||||
args=dict(
|
||||
skill_id=settings_display.skill_id,
|
||||
display_data=json.dumps(settings_display.display_data)
|
||||
)
|
||||
)
|
||||
result = self.cursor.select_one(db_request)
|
||||
if result is None:
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='add_settings_display.sql',
|
||||
args=dict(skill_id=skill_id, settings_display=settings_display)
|
||||
)
|
||||
result = self.cursor.insert_returning(db_request)
|
||||
return result['id']
|
||||
|
||||
return None if result is None else result['id']
|
||||
|
||||
def remove(self, settings_display_id: str):
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='delete_settings_display.sql',
|
||||
args=dict(settings_display_id=settings_display_id)
|
||||
)
|
||||
self.cursor.delete(db_request)
|
||||
|
|
|
@ -23,69 +23,6 @@ class SkillRepository(RepositoryBase):
|
|||
self.db = db
|
||||
super(SkillRepository, self).__init__(db, __file__)
|
||||
|
||||
def get_skill_settings_by_device_id(self, device_id):
|
||||
"""Return all skill settings from a given device id
|
||||
|
||||
:param device_id: device uuid
|
||||
:return list of skills using the format from the API v1"""
|
||||
query = self._build_db_request(
|
||||
'get_skill_setting_by_device_id.sql',
|
||||
args=dict(device_id=device_id)
|
||||
)
|
||||
sql_results = self.cursor.select_all(query)
|
||||
if sql_results:
|
||||
skills = []
|
||||
for result in sql_results:
|
||||
sections = self._fill_setting_with_values(result['settings'], result['settings_display'])
|
||||
skill = {'uuid': result['id']}
|
||||
if sections:
|
||||
skill['skillMetadata'] = {'sections': sections}
|
||||
display = result['settings_display']
|
||||
skill_gid = display.get('skill_gid')
|
||||
if skill_gid:
|
||||
skill['skill_gid'] = skill_gid
|
||||
identifier = display.get('identifier')
|
||||
if identifier:
|
||||
skill['identifier'] = identifier
|
||||
skills.append(skill)
|
||||
return skills
|
||||
|
||||
def get_skill_settings_by_device_id_and_version_hash(self, device_id, version_hash):
|
||||
"""Return a skill setting for a given device id and skill version hash
|
||||
|
||||
:param device_id: device uuid
|
||||
:param version_hash: skill setting version hash
|
||||
:return skill setting using the format from the API v1
|
||||
"""
|
||||
query = self._build_db_request(
|
||||
'get_skill_setting_by_device_id_and_version_hash.sql',
|
||||
args=dict(device_id=device_id, version_hash=version_hash)
|
||||
)
|
||||
sql_results = self.cursor.select_one(query)
|
||||
if sql_results:
|
||||
sections = self._fill_setting_with_values(sql_results['settings'], sql_results['settings_display'])
|
||||
skill = {
|
||||
'skill_gid': sql_results['settings_display']['skill_gid'],
|
||||
'skillMetadata': {
|
||||
'sections': sections
|
||||
}
|
||||
}
|
||||
return skill
|
||||
|
||||
def _fill_setting_with_values(self, settings: dict, setting_meta: dict):
|
||||
skill_metadata = setting_meta.get('skillMetadata')
|
||||
if skill_metadata:
|
||||
sections = skill_metadata['sections']
|
||||
if settings:
|
||||
for section in sections:
|
||||
section_fields = section['fields']
|
||||
for field in section_fields:
|
||||
if 'name' in field:
|
||||
name = field['name']
|
||||
if name in settings:
|
||||
field['value'] = settings[field['name']]
|
||||
return sections
|
||||
|
||||
def get_skills_for_account(self, account_id) -> List[SkillFamily]:
|
||||
skills = []
|
||||
db_request = self._build_db_request(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
INSERT INTO
|
||||
skill.settings_display (skill_id, settings_display)
|
||||
VALUES
|
||||
(%(skill_id)s, %(settings_display)s)
|
||||
(%(skill_id)s, %(display_data)s)
|
||||
RETURNING
|
||||
id
|
||||
id
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
DELETE FROM
|
||||
skill.settings_display
|
||||
WHERE
|
||||
id = %(settings_display_id)s
|
|
@ -1,6 +0,0 @@
|
|||
SELECT
|
||||
id
|
||||
FROM
|
||||
skill.settings_display
|
||||
WHERE
|
||||
skill_id = %(skill_id)s AND settings_display = %(settings_display)s
|
|
@ -0,0 +1,7 @@
|
|||
SELECT
|
||||
id
|
||||
FROM
|
||||
skill.settings_display
|
||||
WHERE
|
||||
skill_id = %(skill_id)s
|
||||
AND settings_display = %(display_data)s
|
|
@ -0,0 +1,10 @@
|
|||
SELECT
|
||||
ss.id AS skill_id,
|
||||
dds.settings AS settings_values,
|
||||
ssd.settings_display
|
||||
FROM
|
||||
device.device_skill dds
|
||||
INNER JOIN skill.skill ss ON dds.skill_id = ss.id
|
||||
INNER JOIN skill.settings_display ssd ON dds.skill_settings_display_id = ssd.id
|
||||
WHERE
|
||||
dds.device_id = %(device_id)s
|
|
@ -1,14 +0,0 @@
|
|||
SELECT
|
||||
skill.id,
|
||||
dev_skill.settings,
|
||||
display.settings_display
|
||||
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
|
||||
INNER JOIN
|
||||
skill.settings_display display ON dev_skill.skill_settings_display_id = display.id
|
||||
WHERE
|
||||
dev.id = %(device_id)s
|
|
@ -1,14 +0,0 @@
|
|||
SELECT
|
||||
skill.id,
|
||||
dev_skill.settings,
|
||||
display.settings_display
|
||||
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
|
||||
INNER JOIN
|
||||
skill.settings_display display ON dev_skill.skill_settings_display_id = display.id
|
||||
WHERE
|
||||
dev.id = %(device_id)s AND display.settings_display->>'identifier' = %(version_hash)s
|
|
@ -1,25 +1,34 @@
|
|||
import json
|
||||
from datetime import datetime
|
||||
from selene.data.device import ManifestSkill, DeviceSkillRepository
|
||||
|
||||
from selene.data.device import DeviceSkillRepository, ManifestSkill
|
||||
|
||||
|
||||
def add_skill_to_manifest(db, device_id, skill):
|
||||
def add_device_skill(db, device_id, skill):
|
||||
manifest_skill = ManifestSkill(
|
||||
device_id=device_id,
|
||||
install_method='test_install_method',
|
||||
install_status='test_install_status',
|
||||
skill_id=skill.id,
|
||||
skill_gid=skill.skill_gid,
|
||||
install_ts=datetime.utcnow(),
|
||||
update_ts=datetime.utcnow()
|
||||
)
|
||||
device_skill_repo = DeviceSkillRepository(db)
|
||||
manifest_skill.id = device_skill_repo.add_manifest_skill(
|
||||
skill.id,
|
||||
manifest_skill
|
||||
)
|
||||
manifest_skill.id = device_skill_repo.add_manifest_skill(manifest_skill)
|
||||
|
||||
return manifest_skill
|
||||
|
||||
|
||||
def remove_manifest_skill(db, manifest_skill):
|
||||
def add_device_skill_settings(db, device_id, settings_display, settings_values):
|
||||
device_skill_repo = DeviceSkillRepository(db)
|
||||
device_skill_repo.update_device_skill_settings(
|
||||
[device_id],
|
||||
settings_display,
|
||||
settings_values
|
||||
)
|
||||
|
||||
|
||||
def remove_device_skill(db, manifest_skill):
|
||||
device_skill_repo = DeviceSkillRepository(db)
|
||||
device_skill_repo.remove_manifest_skill(manifest_skill)
|
||||
|
|
|
@ -1,13 +1,62 @@
|
|||
from selene.data.skill import Skill, SkillRepository
|
||||
from selene.data.skill import (
|
||||
SettingsDisplay,
|
||||
SettingsDisplayRepository,
|
||||
Skill,
|
||||
SkillRepository
|
||||
)
|
||||
|
||||
|
||||
def add_skill(db, skill_global_id):
|
||||
def build_text_field():
|
||||
return dict(
|
||||
name='textfield',
|
||||
type='text',
|
||||
label='Text Field',
|
||||
placeholder='Text Placeholder'
|
||||
)
|
||||
|
||||
|
||||
def build_label_field():
|
||||
return dict(
|
||||
type='label',
|
||||
label='This is a section label.'
|
||||
)
|
||||
|
||||
|
||||
def _build_display_data(skill_gid, fields):
|
||||
gid_parts = skill_gid.split('|')
|
||||
if len(gid_parts) == 3:
|
||||
skill_name = gid_parts[1]
|
||||
|
||||
else:
|
||||
skill_name = gid_parts[0]
|
||||
skill_identifier = skill_name + '-123456'
|
||||
settings_display = dict(
|
||||
skill_gid=skill_gid,
|
||||
identifier=skill_identifier,
|
||||
display_name=skill_name,
|
||||
)
|
||||
if fields is not None:
|
||||
settings_display.update(
|
||||
skillMetadata=dict(
|
||||
sections=[dict(name='Section Name', fields=fields)]
|
||||
)
|
||||
)
|
||||
|
||||
return settings_display
|
||||
|
||||
|
||||
def add_skill(db, skill_global_id, settings_fields=None):
|
||||
display_data = _build_display_data(skill_global_id, settings_fields)
|
||||
skill_repo = SkillRepository(db)
|
||||
skill_id = skill_repo.ensure_skill_exists(skill_global_id=skill_global_id)
|
||||
skill_id = skill_repo.ensure_skill_exists(skill_global_id)
|
||||
skill = Skill(skill_global_id, skill_id)
|
||||
settings_display = SettingsDisplay(skill_id, display_data)
|
||||
settings_display_repo = SettingsDisplayRepository(db)
|
||||
settings_display.id = settings_display_repo.add(settings_display)
|
||||
|
||||
return Skill(skill_global_id, skill_id)
|
||||
return skill, settings_display
|
||||
|
||||
|
||||
def remove_skill(db, skill_global_id):
|
||||
def remove_skill(db, skill):
|
||||
skill_repo = SkillRepository(db)
|
||||
skill_repo.remove_by_gid(skill_gid=skill_global_id)
|
||||
skill_repo.remove_by_gid(skill_gid=skill.skill_gid)
|
||||
|
|
Loading…
Reference in New Issue