Merge remote-tracking branch 'origin/dev' into dev
# Conflicts: # api/account/account_api/endpoints/device.pypull/192/head^2
commit
4b1ec6eb9d
|
@ -4,7 +4,7 @@ verify_ssl = true
|
|||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
flask = "*"
|
||||
flask = "<1.1"
|
||||
uwsgi = "*"
|
||||
schematics = "*"
|
||||
stripe = "*"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "051b0cfc68c8cfbc1a1a34aa80760a4bedaafa5bf366716a1d18918ce20f62a8"
|
||||
"sha256": "0de16be64b31a76c653f5cec069779500b459052736318cbc0925a5e7dc44c1c"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -18,10 +18,10 @@
|
|||
"default": {
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
|
||||
"sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
|
||||
"sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
|
||||
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
|
||||
],
|
||||
"version": "==2019.3.9"
|
||||
"version": "==2019.6.16"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
|
@ -39,10 +39,10 @@
|
|||
},
|
||||
"deprecated": {
|
||||
"hashes": [
|
||||
"sha256:2f293eb0eee34b1fcf3da530fe8fc4b0d71d43ddc2dc78e2ffb444b6c0868557",
|
||||
"sha256:749f6cdcfbdc3f79258f8154bad43fced95adc632c337675d0385959895894bc"
|
||||
"sha256:a515c4cf75061552e0284d123c3066fbbe398952c87333a92b8fc3dd8e4f9cc1",
|
||||
"sha256:b07b414c8aac88f60c1d837d21def7e83ba711052e03b3cbaff27972567a8f8d"
|
||||
],
|
||||
"version": "==1.2.5"
|
||||
"version": "==1.2.6"
|
||||
},
|
||||
"facebook-sdk": {
|
||||
"hashes": [
|
||||
|
@ -53,11 +53,11 @@
|
|||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
|
||||
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
|
||||
"sha256:1a21ccca71cee5e55b6a367cc48c6eb47e3c447f76e64d41f3f3f931c17e7c96",
|
||||
"sha256:ed1330220a321138de53ec7c534c3d90cf2f7af938c7880fc3da13aa46bf870f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.2"
|
||||
"version": "==1.0.4"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
|
@ -122,42 +122,42 @@
|
|||
},
|
||||
"psycopg2-binary": {
|
||||
"hashes": [
|
||||
"sha256:163d3ee445a0b4c0109877da9e46271aacf4e5e3d60ae7368669555c30f13e7c",
|
||||
"sha256:1af0bfe7b0c13a0e613a27311fd4f9c5d024e8fc0f4b3d284e7df02a58a11fc0",
|
||||
"sha256:2169c3a1bf52d5b30cc98625b5919a964c571a32e8646be20be6c7e3e82079de",
|
||||
"sha256:218f079fa48e2ef812dc3d3ce6ec2f67ac56427ba4b038d5d6331f2cceb489c2",
|
||||
"sha256:26a958930687e94c4c6c73c171e4d4783b82ae4e16aa3424e6bcd4529bceedf0",
|
||||
"sha256:2c7c195aef3acdbc853942bc674844031a732890d2fee88a324298ed376b6c2b",
|
||||
"sha256:2ecdbfed7004669472bfa27c8d51012c717c241c7154ae17e4c8f93024043525",
|
||||
"sha256:345fc31b71a90ada1b51826537917b19a1af685a91c0f066787069c184d7d00f",
|
||||
"sha256:378a06649503f548be5f1e9eec2e94cc1d6138250b82a08dcc6151bca8cec107",
|
||||
"sha256:3f300bf2930e501dde09605de85cb2b84c2638e2c954be02a3c86f28176d3525",
|
||||
"sha256:6c2f66c653ce8bbd7e789d0f7f92c3f9fea881b55226f0ae5ee550cce9e3cf0e",
|
||||
"sha256:6fccbac2633831b877a8fbf865f7082d34895e82a015795a9f80f99a2efe2576",
|
||||
"sha256:7a166f8ccb6888358d3e67795b057540ea7caa71ab9e089b0cb0097f01088965",
|
||||
"sha256:8f6b84f887ec6fef6c1796779f8ec2603dc7e9ef52bc9269de719d4bcbdaebbb",
|
||||
"sha256:92cf3ceb7bb90cf35b8bd993c640b15d4832ba0e142a3b9da5006ef217da595d",
|
||||
"sha256:a20dfdf73f56da674926a3811929cff9fd23b9af90be9a6c36ac246a3486eef3",
|
||||
"sha256:a84415df4689251556c961e4fe3b25d30e32f00faa8064ce0909458dbe0d67b2",
|
||||
"sha256:ab1aa1cd50df3860f624c9713ee9e690eefd4e049d3a4d86577bab6e741e9616",
|
||||
"sha256:abc9dcf85e75a8687f2a6d560c0c1a2593e8e34ba6f9ad6721f8212c5de179a2",
|
||||
"sha256:c10454710a81a2f4b1ff4d1c83ac2cec63e0e55845a56324991514af5b1299d0",
|
||||
"sha256:c38f80719e4dfae7a6311a4f091f07f4fb2fb5d602352015d5639f63f8fabb68",
|
||||
"sha256:d75cf00605630b2cfefa5c62373c605dcda1cc0d607902847dbb8e8e9b67c1ce",
|
||||
"sha256:dce15cb6ef604c9e38fdaa848f58f83153ade9f4aa5e4cf5812aa27163561750",
|
||||
"sha256:e7e0db4311bb76bf3f6e0380f71912cfa6d0be7cc635e3772476050b0dabdabd",
|
||||
"sha256:eac59cae78dfe3fbf7ece25c170d7a152f88df7643381aa5e7344c2028a8d8d4",
|
||||
"sha256:ead7b3e1567bd14cacd44279c5e42cd19f54b9feed39180220253f4fbe3abd56",
|
||||
"sha256:ed772a5e8e7e5dd6bede960a86940c17cf653c7f158dafa5d52e919b676f10ba",
|
||||
"sha256:f2d73131acb94afa45de8b6b8a4bfb21bbe3736633d6478e53247f19dd8c299c"
|
||||
"sha256:080c72714784989474f97be9ab0ddf7b2ad2984527e77f2909fcd04d4df53809",
|
||||
"sha256:110457be80b63ff4915febb06faa7be002b93a76e5ba19bf3f27636a2ef58598",
|
||||
"sha256:171352a03b22fc099f15103959b52ee77d9a27e028895d7e5fde127aa8e3bac5",
|
||||
"sha256:19d013e7b0817087517a4b3cab39c084d78898369e5c46258aab7be4f233d6a1",
|
||||
"sha256:249b6b21ae4eb0f7b8423b330aa80fab5f821b9ffc3f7561a5e2fd6bb142cf5d",
|
||||
"sha256:2ac0731d2d84b05c7bb39e85b7e123c3a0acd4cda631d8d542802c88deb9e87e",
|
||||
"sha256:2b6d561193f0dc3f50acfb22dd52ea8c8dfbc64bcafe3938b5f209cc17cb6f00",
|
||||
"sha256:2bd23e242e954214944481124755cbefe7c2cf563b1a54cd8d196d502f2578bf",
|
||||
"sha256:3e1239242ca60b3725e65ab2f13765fc199b03af9eaf1b5572f0e97bdcee5b43",
|
||||
"sha256:3eb70bb697abbe86b1d2b1316370c02ba320bfd1e9e35cf3b9566a855ea8e4e5",
|
||||
"sha256:51a2fc7e94b98bd1bb5d4570936f24fc2b0541b63eccadf8fdea266db8ad2f70",
|
||||
"sha256:52f1bdafdc764b7447e393ed39bb263eccb12bfda25a4ac06d82e3a9056251f6",
|
||||
"sha256:5b3581319a3951f1e866f4f6c5e42023db0fae0284273b82e97dfd32c51985cd",
|
||||
"sha256:63c1b66e3b2a3a336288e4bcec499e0dc310cd1dceaed1c46fa7419764c68877",
|
||||
"sha256:8123a99f24ecee469e5c1339427bcdb2a33920a18bb5c0d58b7c13f3b0298ba3",
|
||||
"sha256:85e699fcabe7f817c0f0a412d4e7c6627e00c412b418da7666ff353f38e30f67",
|
||||
"sha256:8dbff4557bbef963697583366400822387cccf794ccb001f1f2307ed21854c68",
|
||||
"sha256:908d21d08d6b81f1b7e056bbf40b2f77f8c499ab29e64ec5113052819ef1c89b",
|
||||
"sha256:af39d0237b17d0a5a5f638e9dffb34013ce2b1d41441fd30283e42b22d16858a",
|
||||
"sha256:af51bb9f055a3f4af0187149a8f60c9d516cf7d5565b3dac53358796a8fb2a5b",
|
||||
"sha256:b2ecac57eb49e461e86c092761e6b8e1fd9654dbaaddf71a076dcc869f7014e2",
|
||||
"sha256:cd37cc170678a4609becb26b53a2bc1edea65177be70c48dd7b39a1149cabd6e",
|
||||
"sha256:d17e3054b17e1a6cb8c1140f76310f6ede811e75b7a9d461922d2c72973f583e",
|
||||
"sha256:d305313c5a9695f40c46294d4315ed3a07c7d2b55e48a9010dad7db7a66c8b7f",
|
||||
"sha256:dd0ef0eb1f7dd18a3f4187226e226a7284bda6af5671937a221766e6ef1ee88f",
|
||||
"sha256:e1adff53b56db9905db48a972fb89370ad5736e0450b96f91bcf99cadd96cfd7",
|
||||
"sha256:f0d43828003c82dbc9269de87aa449e9896077a71954fbbb10a614c017e65737",
|
||||
"sha256:f78e8b487de4d92640105c1389e5b90be3496b1d75c90a666edd8737cc2dbab7"
|
||||
],
|
||||
"version": "==2.8.1"
|
||||
"version": "==2.8.3"
|
||||
},
|
||||
"pygithub": {
|
||||
"hashes": [
|
||||
"sha256:74cbc6a0518bc7f4a8a1c047c954592abe622b735f1352ea30201668607fbe24"
|
||||
"sha256:db415a5aeb5ab1e4a3263b1a091b4f9ffbd85a12a06a0303d5bf083ce7c1b2c8"
|
||||
],
|
||||
"version": "==1.43.6"
|
||||
"version": "==1.43.8"
|
||||
},
|
||||
"pyhamcrest": {
|
||||
"hashes": [
|
||||
|
@ -181,18 +181,25 @@
|
|||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
"sha256:6946b5dca72e86103edc8033019cc3814c031232d339d5f4533b02ea85685175",
|
||||
"sha256:8ca418d2ddca1b1a850afa1680a7d2fd1f3322739271de4b704e0d4668449273"
|
||||
"sha256:0607faf60d44768e17f65e506fe390679b54be6fd6d5f0c2d28f3ebf4f0535e7",
|
||||
"sha256:9c96c5bf11a8c47eb33cefdefd41c47cf1ff68db41c51b56b3ec7938b7c627f7"
|
||||
],
|
||||
"version": "==3.2.1"
|
||||
"version": "==3.3.7"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
|
||||
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||
],
|
||||
"markers": "python_version >= '3.0'",
|
||||
"version": "==2.21.0"
|
||||
"version": "==2.22.0"
|
||||
},
|
||||
"schedule": {
|
||||
"hashes": [
|
||||
"sha256:3f895a1036799a25ab9c335de917073e63cf8256920917e932777382f101f08f",
|
||||
"sha256:f9fb5181283de4db6e701d476dd01b6a3dd81c38462a54991ddbb9d26db857c9"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"schematics": {
|
||||
"hashes": [
|
||||
|
@ -208,10 +215,10 @@
|
|||
},
|
||||
"sendgrid": {
|
||||
"hashes": [
|
||||
"sha256:351a7fc501d2b9d5afdcbc70a02490917057d6ce5cc22c558cadfc16229f157b",
|
||||
"sha256:e1f93c72b3db3bd00d86f79ee926a093ee7e65533936a140855916569b08e0b0"
|
||||
"sha256:297d33363a70df9b39419210e1273b165d487730e85c495695e0015bc626db71",
|
||||
"sha256:8b82c8c801dde8180a567913a9f80d8a63f38e39f209edde302b6df899b4bca1"
|
||||
],
|
||||
"version": "==6.0.4"
|
||||
"version": "==6.0.5"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
|
@ -222,18 +229,18 @@
|
|||
},
|
||||
"stripe": {
|
||||
"hashes": [
|
||||
"sha256:337149778e544c03d7a69da387b1ada1297c5878891dfa569e482c5433997e80",
|
||||
"sha256:3f7eac3b55035071cc0142315aea976cc36dae61525223f0d3030450e9587f76"
|
||||
"sha256:344cd691a542f08c508b9d12ac201da46b7f0f21a0a7f72f56199b3baee795eb",
|
||||
"sha256:e07efa567ae0831fe351ddb49de074aa1681569fd234d4f1dc0a9f7f4c017820"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.24.0"
|
||||
"version": "==2.35.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||
],
|
||||
"version": "==1.24.1"
|
||||
"version": "==1.25.3"
|
||||
},
|
||||
"uwsgi": {
|
||||
"hashes": [
|
||||
|
@ -244,16 +251,16 @@
|
|||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:0a73e8bb2ff2feecfc5d56e6f458f5b99290ef34f565ffb2665801ff7de6af7a",
|
||||
"sha256:7fad9770a8778f9576693f0cc29c7dcc36964df916b83734f4431c0e612a7fbc"
|
||||
"sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4",
|
||||
"sha256:a13b74dd3c45f758d4ebdb224be8f1ab8ef58b3c0ffc1783a8c7d9f4f50227e6"
|
||||
],
|
||||
"version": "==0.15.2"
|
||||
"version": "==0.15.5"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
"sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533"
|
||||
"sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"
|
||||
],
|
||||
"version": "==1.11.1"
|
||||
"version": "==1.11.2"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
|
@ -273,10 +280,10 @@
|
|||
},
|
||||
"parse-type": {
|
||||
"hashes": [
|
||||
"sha256:6e906a66f340252e4c324914a60d417d33a4bea01292ea9bbf68b4fc123be8c9",
|
||||
"sha256:f596bdc75d3dd93036fbfe3d04127da9f6df0c26c36e01e76da85adef4336b3c"
|
||||
"sha256:089a471b06327103865dfec2dd844230c3c658a4a1b5b4c8b6c16c8f77577f9e",
|
||||
"sha256:7f690b18d35048c15438d6d0571f9045cffbec5907e0b1ccf006f889e3a38c0b"
|
||||
],
|
||||
"version": "==0.4.2"
|
||||
"version": "==0.5.2"
|
||||
},
|
||||
"pyhamcrest": {
|
||||
"hashes": [
|
||||
|
|
|
@ -119,11 +119,6 @@ class DeviceEndpoint(SeleneEndpoint):
|
|||
else:
|
||||
last_contact_age = datetime.utcnow() - device.last_contact_ts
|
||||
else:
|
||||
last_contact_ts = last_contact_ts.decode()
|
||||
last_contact_ts = datetime.strptime(
|
||||
last_contact_ts,
|
||||
'%Y-%m-%d %H:%M:%S.%f'
|
||||
)
|
||||
last_contact_age = datetime.utcnow() - last_contact_ts
|
||||
|
||||
return last_contact_age
|
||||
|
|
|
@ -55,8 +55,8 @@ class SkillSettingsEndpoint(SeleneEndpoint):
|
|||
the value is the value.
|
||||
"""
|
||||
for skill_settings in self.family_settings:
|
||||
if skill_settings.settings_display is not None:
|
||||
for section in skill_settings.settings_display['sections']:
|
||||
if skill_settings.settings_definition is not None:
|
||||
for section in skill_settings.settings_definition['sections']:
|
||||
for field in section['fields']:
|
||||
if field['type'] == 'select':
|
||||
parsed_options = []
|
||||
|
@ -76,10 +76,10 @@ class SkillSettingsEndpoint(SeleneEndpoint):
|
|||
for skill_settings in self.family_settings:
|
||||
# The UI will throw an error if settings display is null due to how
|
||||
# the skill settings data structures are defined.
|
||||
if skill_settings.settings_display is None:
|
||||
skill_settings.settings_display = dict(sections=[])
|
||||
if skill_settings.settings_definition is None:
|
||||
skill_settings.settings_definition = dict(sections=[])
|
||||
response_skill = dict(
|
||||
settingsDisplay=skill_settings.settings_display,
|
||||
settingsDisplay=skill_settings.settings_definition,
|
||||
settingsValues=skill_settings.settings_values,
|
||||
deviceNames=skill_settings.device_names
|
||||
)
|
||||
|
@ -98,7 +98,7 @@ class SkillSettingsEndpoint(SeleneEndpoint):
|
|||
"""Update the value of the settings column on the device_skill table,"""
|
||||
for new_skill_settings in self.request.json['skillSettings']:
|
||||
account_skill_settings = AccountSkillSetting(
|
||||
settings_display=new_skill_settings['settingsDisplay'],
|
||||
settings_definition=new_skill_settings['settingsDisplay'],
|
||||
settings_values=new_skill_settings['settingsValues'],
|
||||
device_names=new_skill_settings['deviceNames']
|
||||
)
|
||||
|
|
|
@ -2,7 +2,6 @@ import os
|
|||
|
||||
from flask import Flask
|
||||
|
||||
from public_api.endpoints.stripe_webhook import StripeWebHookEndpoint
|
||||
from selene.api import SeleneResponse, selene_api
|
||||
from selene.api.base_config import get_base_config
|
||||
from selene.api.public_endpoint import check_oauth_token
|
||||
|
@ -17,14 +16,16 @@ from .endpoints.device_metrics import DeviceMetricsEndpoint
|
|||
from .endpoints.device_oauth import OauthServiceEndpoint
|
||||
from .endpoints.device_refresh_token import DeviceRefreshTokenEndpoint
|
||||
from .endpoints.device_setting import DeviceSettingEndpoint
|
||||
from .endpoints.device_skill import DeviceSkillEndpoint
|
||||
from .endpoints.device_skill import SkillSettingsMetaEndpoint
|
||||
from .endpoints.device_skill_manifest import DeviceSkillManifestEndpoint
|
||||
from .endpoints.device_skill_settings import DeviceSkillSettingsEndpoint
|
||||
from .endpoints.device_skill_settings import DeviceSkillSettingsEndpointV2
|
||||
from .endpoints.device_subscription import DeviceSubscriptionEndpoint
|
||||
from .endpoints.google_stt import GoogleSTTEndpoint
|
||||
from .endpoints.oauth_callback import OauthCallbackEndpoint
|
||||
from .endpoints.open_weather_map import OpenWeatherMapEndpoint
|
||||
from .endpoints.premium_voice import PremiumVoiceEndpoint
|
||||
from .endpoints.stripe_webhook import StripeWebHookEndpoint
|
||||
from .endpoints.wolfram_alpha import WolframAlphaEndpoint
|
||||
from .endpoints.wolfram_alpha_spoken import WolframAlphaSpokenEndpoint
|
||||
|
||||
|
@ -51,11 +52,17 @@ public.add_url_rule(
|
|||
)
|
||||
|
||||
public.add_url_rule(
|
||||
'/v1/device/<string:device_id>/userSkill',
|
||||
view_func=DeviceSkillEndpoint.as_view('device_user_skill_api'),
|
||||
'/v1/device/<string:device_id>/skill/settings',
|
||||
view_func=DeviceSkillSettingsEndpointV2.as_view('skill_settings_api'),
|
||||
methods=['GET']
|
||||
)
|
||||
|
||||
public.add_url_rule(
|
||||
'/v1/device/<string:device_id>/settingsMeta',
|
||||
view_func=SkillSettingsMetaEndpoint.as_view('device_user_skill_api'),
|
||||
methods=['PUT']
|
||||
)
|
||||
|
||||
public.add_url_rule(
|
||||
'/v1/device/<string:device_id>',
|
||||
view_func=DeviceEndpoint.as_view('device_api'),
|
||||
|
@ -153,9 +160,9 @@ public.add_url_rule(
|
|||
methods=['POST']
|
||||
)
|
||||
|
||||
"""
|
||||
This is a workaround to allow the API return 401 when we call a non existent path. Use case:
|
||||
GET /device/{uuid} with empty uuid. Core today uses the 401 to validate if it needs to perform a pairing process
|
||||
Whe should fix that in a future version because we have to return 404 when we call a non existent path
|
||||
"""
|
||||
|
||||
# This is a workaround to allow the API return 401 when we call a non existent
|
||||
# path. Use case: GET /device/{uuid} with empty uuid. Core today uses the 401
|
||||
# to validate if it needs to perform a pairing process. We should fix that in a
|
||||
# future version because we have to return 404 when we call a non existent path
|
||||
public.before_request(check_oauth_token)
|
||||
|
|
|
@ -1,13 +1,273 @@
|
|||
"""Applies a skill settings definition to the database.
|
||||
|
||||
Whenever a change is made to a skill's settings definition on a device, this
|
||||
endpoint is called to update the same on the database. If a skill has
|
||||
settings, the device's settings are also updated.
|
||||
|
||||
This endpoint assumes that the skill manifest is sent when the a device is
|
||||
paired or a skill is installed. A skill that is not installed on a device
|
||||
cannot send it's settings... right? The skill and its relationship to the
|
||||
device should already be known when this endpoint is called.
|
||||
"""
|
||||
from http import HTTPStatus
|
||||
from logging import getLogger
|
||||
|
||||
from schematics import Model
|
||||
from schematics.types import (
|
||||
BooleanType,
|
||||
ListType,
|
||||
ModelType,
|
||||
StringType
|
||||
)
|
||||
from schematics.exceptions import DataError
|
||||
|
||||
from selene.api import PublicEndpoint
|
||||
from selene.data.account import AccountRepository
|
||||
from selene.data.device import DeviceSkillRepository
|
||||
from selene.data.skill import (
|
||||
extract_family_from_global_id,
|
||||
SettingsDisplay,
|
||||
SettingsDisplayRepository,
|
||||
SkillRepository
|
||||
)
|
||||
from selene.data.skill import SkillSettingRepository
|
||||
|
||||
_log = getLogger(__package__)
|
||||
|
||||
|
||||
class DeviceSkillEndpoint(PublicEndpoint):
|
||||
"""Return a skill setting using the API v1 format for a given device and version_hash"""
|
||||
def _normalize_field_value(field):
|
||||
"""The field values in skillMetadata are all strings, convert to native."""
|
||||
normalized_value = field.get('value')
|
||||
if field['type'].lower() == 'checkbox':
|
||||
if field['value'] in ('false', 'False', '0'):
|
||||
normalized_value = False
|
||||
elif field['value'] in ('true', 'True', '1'):
|
||||
normalized_value = True
|
||||
elif field['type'].lower() == 'number' and isinstance(field['value'], str):
|
||||
if field['value']:
|
||||
normalized_value = float(field['value'])
|
||||
if not normalized_value % 1:
|
||||
normalized_value = int(field['value'])
|
||||
else:
|
||||
normalized_value = 0
|
||||
elif field['value'] == "[]":
|
||||
normalized_value = []
|
||||
|
||||
return normalized_value
|
||||
|
||||
|
||||
class RequestSkillField(Model):
|
||||
name = StringType()
|
||||
type = StringType()
|
||||
label = StringType()
|
||||
hint = StringType()
|
||||
placeholder = StringType()
|
||||
hide = BooleanType()
|
||||
value = StringType()
|
||||
options = StringType()
|
||||
|
||||
|
||||
class RequestSkillSection(Model):
|
||||
name = StringType(required=True)
|
||||
fields = ListType(ModelType(RequestSkillField))
|
||||
|
||||
|
||||
class RequestSkillMetadata(Model):
|
||||
sections = ListType(ModelType(RequestSkillSection))
|
||||
|
||||
|
||||
class RequestSkillIcon(Model):
|
||||
color = StringType()
|
||||
icon = StringType()
|
||||
|
||||
|
||||
class RequestDeviceSkill(Model):
|
||||
display_name = StringType(required=True)
|
||||
icon = ModelType(RequestSkillIcon)
|
||||
icon_img = StringType()
|
||||
skill_gid = StringType(required=True)
|
||||
skillMetadata = ModelType(RequestSkillMetadata)
|
||||
|
||||
|
||||
class SkillSettingsMetaEndpoint(PublicEndpoint):
|
||||
def __init__(self):
|
||||
super(DeviceSkillEndpoint, self).__init__()
|
||||
super().__init__()
|
||||
self.skill = None
|
||||
self.default_settings = None
|
||||
self.skill_has_settings = False
|
||||
self.settings_definition_id = None
|
||||
self._device_skill_repo = None
|
||||
|
||||
def get(self, device_id):
|
||||
@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
|
||||
|
||||
def put(self, device_id):
|
||||
self._authenticate(device_id)
|
||||
self._validate_request()
|
||||
self._get_skill()
|
||||
self._parse_skill_metadata()
|
||||
self._ensure_settings_definition_exists()
|
||||
self._update_device_skill(device_id)
|
||||
|
||||
return '', HTTPStatus.NO_CONTENT
|
||||
|
||||
def _validate_request(self):
|
||||
"""Ensure the request is well-formed."""
|
||||
request_model = RequestDeviceSkill(self.request.json)
|
||||
request_model.validate()
|
||||
|
||||
def _get_skill(self):
|
||||
"""Retrieve the skill associated with the request."""
|
||||
skill_repo = SkillRepository(self.db)
|
||||
self.skill = skill_repo.get_skill_by_global_id(
|
||||
self.request.json['skill_gid']
|
||||
)
|
||||
if self.skill is None:
|
||||
err_msg = (
|
||||
'No skill on database for skill ' +
|
||||
self.request.json['skill_gid']
|
||||
)
|
||||
_log.error(err_msg)
|
||||
raise DataError(dict(skill_gid=[err_msg]))
|
||||
|
||||
def _parse_skill_metadata(self):
|
||||
"""Inspect the contents of the skill settings definition.
|
||||
|
||||
Skill authors often write settings definition files with strings in
|
||||
fields that should be boolean or numeric. Ensure all fields are cast
|
||||
to the correct type before interacting with the database.
|
||||
"""
|
||||
self.skill_has_settings = 'skillMetadata' in self.request.json
|
||||
if self.skill_has_settings:
|
||||
skill_metadata = self.request.json['skillMetadata']
|
||||
self.default_settings = {}
|
||||
normalized_sections = []
|
||||
for section in skill_metadata['sections']:
|
||||
for field in section['fields']:
|
||||
if field['type'] != 'label':
|
||||
field['value'] = _normalize_field_value(field)
|
||||
self.default_settings[field['name']] = field['value']
|
||||
normalized_sections.append(section)
|
||||
self.request.json['skillMetadata'].update(
|
||||
sections=normalized_sections
|
||||
)
|
||||
|
||||
def _ensure_settings_definition_exists(self):
|
||||
"""Add a row to skill.settings_display if it doesn't already exist."""
|
||||
self.settings_definition_id = None
|
||||
self._check_for_existing_settings_definition()
|
||||
if self.settings_definition_id is None:
|
||||
self._add_settings_definition()
|
||||
|
||||
def _check_for_existing_settings_definition(self):
|
||||
"""Look for an existing database row matching the request."""
|
||||
settings_def_repo = SettingsDisplayRepository(self.db)
|
||||
settings_defs = settings_def_repo.get_settings_definitions_by_gid(
|
||||
self.skill.skill_gid
|
||||
)
|
||||
for settings_def in settings_defs:
|
||||
if settings_def.display_data == self.request.json:
|
||||
self.settings_definition_id = settings_def.id
|
||||
break
|
||||
|
||||
def _add_settings_definition(self):
|
||||
"""The settings definition does not exist on database so add it."""
|
||||
settings_def_repo = SettingsDisplayRepository(self.db)
|
||||
settings_definition = SettingsDisplay(
|
||||
skill_id=self.skill.id,
|
||||
display_data=self.request.json
|
||||
)
|
||||
self.settings_definition_id = settings_def_repo.add(
|
||||
settings_definition
|
||||
)
|
||||
|
||||
def _update_device_skill(self, device_id):
|
||||
"""Update device.device_skill to match the new settings definition.
|
||||
|
||||
If the skill has settings and the device_skill table does not, either
|
||||
use the default values in the settings definition or copy the settings
|
||||
from another device under the same account.
|
||||
"""
|
||||
device_skill = self._get_device_skill(device_id)
|
||||
device_skill.settings_display_id = self.settings_definition_id
|
||||
if self.skill_has_settings:
|
||||
if device_skill.settings_values is None:
|
||||
new_settings_values = self._initialize_skill_settings(
|
||||
device_id
|
||||
)
|
||||
else:
|
||||
new_settings_values = self._reconcile_skill_settings(
|
||||
device_skill.settings_values
|
||||
)
|
||||
device_skill.settings_values = new_settings_values
|
||||
self.device_skill_repo.update_device_skill_settings(
|
||||
device_id,
|
||||
device_skill
|
||||
)
|
||||
|
||||
def _get_device_skill(self, device_id):
|
||||
"""Retrieve the device's skill entry from the database."""
|
||||
device_skill = self.device_skill_repo.get_skill_settings_for_device(
|
||||
device_id,
|
||||
self.skill.id
|
||||
)
|
||||
if device_skill is None:
|
||||
error_msg = (
|
||||
'Received skill setting definition before manifest for '
|
||||
'skill ' + self.skill.skill_gid
|
||||
)
|
||||
_log.error(error_msg)
|
||||
raise DataError(dict(skill_gid=[error_msg]))
|
||||
|
||||
return device_skill
|
||||
|
||||
def _reconcile_skill_settings(self, settings_values):
|
||||
"""Fix any new or removed settings."""
|
||||
new_settings_values = {}
|
||||
for name, value in self.default_settings.items():
|
||||
if name in settings_values:
|
||||
new_settings_values[name] = settings_values[name]
|
||||
else:
|
||||
new_settings_values[name] = self.default_settings[name]
|
||||
for name, value in settings_values.items():
|
||||
if name in self.default_settings:
|
||||
new_settings_values[name] = settings_values[name]
|
||||
|
||||
return new_settings_values
|
||||
|
||||
def _initialize_skill_settings(self, device_id):
|
||||
"""Use default settings or copy from another device in same account."""
|
||||
_log.info('Initializing settings for skill ' + self.skill.skill_gid)
|
||||
account_repo = AccountRepository(self.db)
|
||||
account = account_repo.get_account_by_device_id(device_id)
|
||||
skill_settings_repo = SkillSettingRepository(self.db)
|
||||
skill_family = extract_family_from_global_id(self.skill.skill_gid)
|
||||
family_settings = skill_settings_repo.get_family_settings(
|
||||
account.id,
|
||||
skill_family
|
||||
)
|
||||
new_settings_values = self.default_settings
|
||||
if family_settings is not None:
|
||||
for settings in family_settings:
|
||||
if settings.settings_values is None:
|
||||
continue
|
||||
if settings.settings_values != self.default_settings:
|
||||
field_names = settings.settings_values.keys()
|
||||
if field_names == self.default_settings.keys():
|
||||
_log.info(
|
||||
'Copying settings from another device for skill' +
|
||||
self.skill.skill_gid
|
||||
)
|
||||
new_settings_values = settings.settings_values
|
||||
break
|
||||
else:
|
||||
_log.info(
|
||||
'Using default skill settings for skill ' +
|
||||
self.skill.skill_gid
|
||||
)
|
||||
|
||||
return new_settings_values
|
||||
|
|
|
@ -133,7 +133,7 @@ class SkillSettingUpdater(object):
|
|||
account_repo = AccountRepository(self.db)
|
||||
account = account_repo.get_account_by_device_id(self.device_id)
|
||||
skill_settings = (
|
||||
self.device_skill_repo.get_device_skill_settings_for_account(
|
||||
self.device_skill_repo.get_skill_settings_for_account(
|
||||
account.id,
|
||||
self.skill.id
|
||||
)
|
||||
|
@ -296,7 +296,9 @@ class DeviceSkillSettingsEndpoint(PublicEndpoint):
|
|||
if skill_gid is not None:
|
||||
response_skill.update(skill_gid=skill_gid)
|
||||
identifier = skill.settings_display.get('identifier')
|
||||
if identifier is not None:
|
||||
if identifier is None:
|
||||
response_skill.update(identifier=skill_gid)
|
||||
else:
|
||||
response_skill.update(identifier=identifier)
|
||||
response_data.append(response_skill)
|
||||
|
||||
|
@ -311,7 +313,7 @@ class DeviceSkillSettingsEndpoint(PublicEndpoint):
|
|||
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])
|
||||
field.update(value=str(settings_values[field_name]))
|
||||
sections_with_values.append(section_with_values)
|
||||
|
||||
return sections_with_values
|
||||
|
@ -343,30 +345,59 @@ class DeviceSkillSettingsEndpoint(PublicEndpoint):
|
|||
|
||||
return skill_setting_updater.skill.id
|
||||
|
||||
def delete(self, device_id, skill_gid):
|
||||
self._authenticate(device_id)
|
||||
skill = self.skill_repo.get_skill_by_global_id(skill_gid)
|
||||
settings_display_id = self._delete_skill_from_device(device_id, skill)
|
||||
self._delete_orphaned_settings_display(settings_display_id)
|
||||
return '', HTTPStatus.OK
|
||||
|
||||
def _delete_skill_from_device(self, device_id, skill):
|
||||
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)
|
||||
|
||||
|
||||
class DeviceSkillSettingsEndpointV2(PublicEndpoint):
|
||||
"""Replacement that decouples settings definition from values.
|
||||
|
||||
The older version of this class needs to be kept around for compatibility
|
||||
with pre 19.08 versions of mycroft-core. Once those versions are no
|
||||
longer supported, the older class can be deprecated.
|
||||
"""
|
||||
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))
|
||||
response_data = self._build_response_data(device_id)
|
||||
response = self._build_response(device_id, response_data)
|
||||
|
||||
return response
|
||||
|
||||
def _build_response_data(self, device_id):
|
||||
device_skill_repo = DeviceSkillRepository(self.db)
|
||||
device_skills = device_skill_repo.get_skill_settings_for_device(
|
||||
device_id
|
||||
)
|
||||
if device_skills is not None:
|
||||
response_data = {}
|
||||
for skill in device_skills:
|
||||
response_data[skill.skill_gid] = skill.settings_values
|
||||
|
||||
return response_data
|
||||
|
||||
def _build_response(self, device_id, response_data):
|
||||
if response_data is None:
|
||||
response = Response(
|
||||
'',
|
||||
status=HTTPStatus.NO_CONTENT,
|
||||
content_type='application/json'
|
||||
)
|
||||
else:
|
||||
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))
|
||||
|
||||
return response
|
||||
|
|
|
@ -55,10 +55,3 @@ Feature: Upload and fetch skills and their settings
|
|||
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
|
||||
|
|
|
@ -192,12 +192,3 @@ def validate_skill_setting_field_removed(context):
|
|||
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))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from .entity.device_skill import ManifestSkill, DeviceSkillSettings
|
||||
from .entity.device_skill import ManifestSkill, AccountSkillSettings
|
||||
from .entity.geography import Geography
|
||||
from .entity.preference import AccountPreferences
|
||||
from .entity.text_to_speech import TextToSpeech
|
||||
|
|
|
@ -23,3 +23,4 @@ class Device(object):
|
|||
wake_word: WakeWord
|
||||
last_contact_ts: datetime = None
|
||||
placement: str = None
|
||||
add_ts: datetime = None
|
||||
|
|
|
@ -17,9 +17,17 @@ class ManifestSkill(object):
|
|||
|
||||
|
||||
@dataclass
|
||||
class DeviceSkillSettings(object):
|
||||
device_ids: List[str]
|
||||
class AccountSkillSettings(object):
|
||||
install_method: str
|
||||
skill_id: str
|
||||
device_ids: List[str] = None
|
||||
settings_values: dict = None
|
||||
settings_display_id: str = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceSkillSettings(object):
|
||||
skill_id: str
|
||||
skill_gid: str
|
||||
settings_values: dict = None
|
||||
settings_display_id: str = None
|
||||
|
|
|
@ -4,7 +4,11 @@ from dataclasses import asdict
|
|||
from typing import List
|
||||
|
||||
from selene.data.skill import SettingsDisplay
|
||||
from ..entity.device_skill import ManifestSkill, DeviceSkillSettings
|
||||
from ..entity.device_skill import (
|
||||
AccountSkillSettings,
|
||||
DeviceSkillSettings,
|
||||
ManifestSkill
|
||||
)
|
||||
from ...repository_base import RepositoryBase
|
||||
|
||||
|
||||
|
@ -12,23 +16,31 @@ class DeviceSkillRepository(RepositoryBase):
|
|||
def __init__(self, db):
|
||||
super(DeviceSkillRepository, self).__init__(db, __file__)
|
||||
|
||||
def get_device_skill_settings_for_account(
|
||||
def get_skill_settings_for_account(
|
||||
self, account_id: str, skill_id: str
|
||||
) -> List[DeviceSkillSettings]:
|
||||
) -> List[AccountSkillSettings]:
|
||||
return self._select_all_into_dataclass(
|
||||
DeviceSkillSettings,
|
||||
sql_file_name='get_device_skill_settings_for_account.sql',
|
||||
AccountSkillSettings,
|
||||
sql_file_name='get_skill_settings_for_account.sql',
|
||||
args=dict(account_id=account_id, skill_id=skill_id)
|
||||
)
|
||||
|
||||
def get_device_skill_settings_for_device(
|
||||
self, device_id: str
|
||||
) -> List[DeviceSkillSettings]:
|
||||
return self._select_all_into_dataclass(
|
||||
def get_skill_settings_for_device(self, device_id, skill_id=None):
|
||||
device_skills = self._select_all_into_dataclass(
|
||||
DeviceSkillSettings,
|
||||
sql_file_name='get_device_skill_settings_for_device.sql',
|
||||
sql_file_name='get_skill_settings_for_device.sql',
|
||||
args=dict(device_id=device_id)
|
||||
)
|
||||
if skill_id is None:
|
||||
skill_settings = device_skills
|
||||
else:
|
||||
skill_settings = None
|
||||
for skill in device_skills:
|
||||
if skill.skill_id == skill_id:
|
||||
skill_settings = skill
|
||||
break
|
||||
|
||||
return skill_settings
|
||||
|
||||
def update_skill_settings(
|
||||
self, account_id: str, device_names: tuple, skill_name: str
|
||||
|
@ -65,6 +77,23 @@ class DeviceSkillRepository(RepositoryBase):
|
|||
)
|
||||
self.cursor.insert(db_request)
|
||||
|
||||
def update_device_skill_settings(self, device_id, device_skill):
|
||||
"""Update the skill settings columns on the device_skill table."""
|
||||
if device_skill.settings_values is None:
|
||||
db_settings_values = None
|
||||
else:
|
||||
db_settings_values = json.dumps(device_skill.settings_values)
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='update_device_skill_settings.sql',
|
||||
args=dict(
|
||||
device_id=device_id,
|
||||
skill_id=device_skill.skill_id,
|
||||
settings_display_id=device_skill.settings_display_id,
|
||||
settings_values=db_settings_values
|
||||
)
|
||||
)
|
||||
self.cursor.update(db_request)
|
||||
|
||||
def get_skill_manifest_for_device(
|
||||
self, device_id: str
|
||||
) -> List[ManifestSkill]:
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
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
|
|
@ -7,6 +7,7 @@ SELECT
|
|||
d.core_version,
|
||||
d.placement,
|
||||
d.last_contact_ts,
|
||||
d.insert_ts AS add_ts,
|
||||
json_build_object(
|
||||
'setting_name', ww.setting_name,
|
||||
'display_name', ww.display_name,
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
SELECT
|
||||
ds.skill_settings_display_id AS settings_display_id,
|
||||
ds.settings::jsonb AS settings_values,
|
||||
ds.skill_id,
|
||||
s.skill_gid
|
||||
FROM
|
||||
device.device_skill ds
|
||||
INNER JOIN skill.skill s ON ds.skill_id = s.id
|
||||
WHERE
|
||||
device_id = %(device_id)s
|
|
@ -0,0 +1,8 @@
|
|||
UPDATE
|
||||
device.device_skill
|
||||
SET
|
||||
skill_settings_display_id = %(settings_display_id)s,
|
||||
settings = %(settings_values)s
|
||||
WHERE
|
||||
device_id = %(device_id)s
|
||||
AND skill_id = %(skill_id)s
|
|
@ -8,4 +8,4 @@ from .entity.skill_setting import (
|
|||
from .repository.display import SkillDisplayRepository
|
||||
from .repository.setting import SkillSettingRepository
|
||||
from .repository.settings_display import SettingsDisplayRepository
|
||||
from .repository.skill import SkillRepository
|
||||
from .repository.skill import extract_family_from_global_id, SkillRepository
|
||||
|
|
|
@ -4,7 +4,7 @@ from typing import List
|
|||
|
||||
@dataclass
|
||||
class AccountSkillSetting(object):
|
||||
settings_display: dict
|
||||
settings_definition: dict
|
||||
settings_values: dict
|
||||
device_names: List[str]
|
||||
|
||||
|
|
|
@ -17,26 +17,11 @@ class SkillSettingRepository(RepositoryBase):
|
|||
account_id: str,
|
||||
family_name: str
|
||||
) -> List[AccountSkillSetting]:
|
||||
db_request = self._build_db_request(
|
||||
'get_settings_for_skill_family.sql',
|
||||
return self._select_all_into_dataclass(
|
||||
AccountSkillSetting,
|
||||
sql_file_name='get_settings_for_skill_family.sql',
|
||||
args=dict(family_name=family_name, account_id=account_id)
|
||||
)
|
||||
db_result = self.cursor.select_all(db_request)
|
||||
|
||||
skill_settings = []
|
||||
for row in db_result:
|
||||
settings_display = row['settings_display']
|
||||
if settings_display is not None:
|
||||
settings_display = settings_display.get('skillMetadata')
|
||||
skill_settings.append(
|
||||
AccountSkillSetting(
|
||||
settings_display=settings_display,
|
||||
settings_values=row['settings_values'],
|
||||
device_names=row['device_names'],
|
||||
)
|
||||
)
|
||||
|
||||
return skill_settings
|
||||
|
||||
def get_installer_settings(self, account_id) -> List[AccountSkillSetting]:
|
||||
skill_repo = SkillRepository(self.db)
|
||||
|
|
|
@ -10,6 +10,7 @@ class SettingsDisplayRepository(RepositoryBase):
|
|||
super(SettingsDisplayRepository, self).__init__(db, __file__)
|
||||
|
||||
def add(self, settings_display: SettingsDisplay) -> str:
|
||||
"""Add a new row to the skill.settings_display table."""
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='add_settings_display.sql',
|
||||
args=dict(
|
||||
|
@ -22,6 +23,7 @@ class SettingsDisplayRepository(RepositoryBase):
|
|||
return result['id']
|
||||
|
||||
def get_settings_display_id(self, settings_display: SettingsDisplay):
|
||||
"""Get the ID of a skill's settings definition."""
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='get_settings_display_id.sql',
|
||||
args=dict(
|
||||
|
@ -33,7 +35,21 @@ class SettingsDisplayRepository(RepositoryBase):
|
|||
|
||||
return None if result is None else result['id']
|
||||
|
||||
def get_settings_definitions_by_gid(self, global_id):
|
||||
"""Get all matching settings definitions for a global skill ID.
|
||||
|
||||
There can be more than one settings definition for a global skill ID.
|
||||
An example of when this could happen is if a skill author changed the
|
||||
settings definition and not all devices have updated to the latest.
|
||||
"""
|
||||
return self._select_all_into_dataclass(
|
||||
SettingsDisplay,
|
||||
sql_file_name='get_settings_definition_by_gid.sql',
|
||||
args=dict(global_id=global_id)
|
||||
)
|
||||
|
||||
def remove(self, settings_display_id: str):
|
||||
"""Delete a settings definition that is no longer used by any device"""
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='delete_settings_display.sql',
|
||||
args=dict(settings_display_id=settings_display_id)
|
||||
|
|
|
@ -4,7 +4,7 @@ from ..entity.skill import Skill, SkillFamily
|
|||
from ...repository_base import RepositoryBase
|
||||
|
||||
|
||||
def _parse_skill_gid(skill_gid):
|
||||
def extract_family_from_global_id(skill_gid):
|
||||
id_parts = skill_gid.split('|')
|
||||
if id_parts[0].startswith('@'):
|
||||
family_name = id_parts[1]
|
||||
|
@ -57,7 +57,7 @@ class SkillRepository(RepositoryBase):
|
|||
def ensure_skill_exists(self, skill_global_id: str) -> str:
|
||||
skill = self.get_skill_by_global_id(skill_global_id)
|
||||
if skill is None:
|
||||
family_name = _parse_skill_gid(skill_global_id)
|
||||
family_name = extract_family_from_global_id(skill_global_id)
|
||||
skill_id = self._add_skill(skill_global_id, family_name)
|
||||
else:
|
||||
skill_id = skill.id
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
SELECT
|
||||
sd.id,
|
||||
sd.skill_id,
|
||||
sd.settings_display as display_data
|
||||
FROM
|
||||
skill.settings_display sd
|
||||
INNER JOIN skill.skill s ON sd.skill_id = s.id
|
||||
WHERE
|
||||
s.skill_gid = %(global_id)s
|
|
@ -1,15 +1,15 @@
|
|||
SELECT
|
||||
sd.settings_display::jsonb AS settings_display,
|
||||
sd.settings_display::jsonb -> 'skillMetadata' AS settings_definition,
|
||||
ds.settings::jsonb AS settings_values,
|
||||
array_agg(d.name) AS device_names
|
||||
FROM
|
||||
skill.skill s
|
||||
LEFT JOIN skill.settings_display sd ON sd.skill_id = s.id
|
||||
INNER JOIN device.device_skill ds ON sd.id = ds.skill_settings_display_id
|
||||
device.device_skill ds
|
||||
INNER JOIN device.device d ON ds.device_id = d.id
|
||||
INNER JOIN skill.skill s ON ds.skill_id = s.id
|
||||
LEFT JOIN skill.settings_display sd ON ds.skill_settings_display_id = sd.id
|
||||
WHERE
|
||||
s.family_name = %(family_name)s
|
||||
AND d.account_id = %(account_id)s
|
||||
GROUP BY
|
||||
sd.settings_display::jsonb,
|
||||
ds.settings::jsonb
|
||||
settings_definition,
|
||||
settings_values
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
SELECT
|
||||
ss.id AS skill_id,
|
||||
dds.skill_id,
|
||||
dds.settings AS settings_values,
|
||||
ssd.settings_display
|
||||
FROM
|
||||
|
|
Loading…
Reference in New Issue