changed how install status was communicated and how common functions were structured

pull/9/head
Chris Veilleux 2018-10-11 15:08:55 -05:00
parent 45bdcb8d96
commit 058b03069d
3 changed files with 166 additions and 91 deletions

View File

@ -1,10 +1,10 @@
from collections import defaultdict
from dataclasses import dataclass, field
from http import HTTPStatus
from dataclasses import asdict, dataclass
from typing import List
import requests as service_request
from selene_util.api import SeleneEndpoint
VALID_INSTALLATION_VALUES = ('failed', 'installed', 'installing', 'uninstalled')
@dataclass
@ -14,66 +14,75 @@ class ManifestSkill(object):
Mycroft core keeps a manifest off all skills associated with a device.
This manifest shows the status of each skill as it relates to the device.
"""
name: str
installed: bool
method: str
blocked: bool
status: str
beta: bool
failure_message: str
installation: str
installed_on: int
name: str
origin: str
status: str
updated: int
is_installing: bool = field(init=False)
install_failed: bool = field(init=False)
def __post_init__(self):
if self.method == 'installing':
self.is_installing = True
else:
self.is_installing = False
if self.method == 'failed':
self.install_failed = True
else:
self.install_failed = False
class SkillEndpointBase(SeleneEndpoint):
def call_skill_manifest_endpoint(token: str, base_url: str, user_uuid: str):
"""Get the skill manifests from each of a user's devices
The skill manifests will be used to determine the status of each
skill as it relates to the marketplace.
"""
service_request_headers = {'Authorization': 'Bearer ' + token}
service_url = base_url + '/user/' + user_uuid + '/skillJson'
response = service_request.get(
service_url,
headers=service_request_headers
)
return response
def parse_skill_manifest_response(response) -> defaultdict:
skills_in_manifests = defaultdict(list)
response_skills = response.json()
for device in response_skills.get('devices', []):
for skill in device['skills']:
manifest_skill = ManifestSkill(**skill)
skills_in_manifests[manifest_skill.name].append(
manifest_skill
)
return skills_in_manifests
class SkillManifestAggregator(object):
"""Base class containing functionality shared by summary and detail"""
def __init__(self):
super(SkillEndpointBase, self).__init__()
self.skills_in_manifests = defaultdict(list)
def _get_skill_manifests(self):
"""Get the skill manifests from each of a user's devices
def __init__(self, manifest_skills: List[ManifestSkill]):
self.manifest_skills = manifest_skills
self.aggregate_skill = ManifestSkill(**asdict(manifest_skills[0]))
The skill manifests will be used to determine the status of each
skill as it relates to the marketplace.
def aggregate_manifest_skills(self):
"""Aggregate skill data on all devices into a single skill.
Each skill is represented once on the Marketplace, even though it can
be present on multiple devices.
"""
service_request_headers = {
'Authorization': 'Bearer ' + self.tartarus_token
}
service_url = (
self.config['TARTARUS_BASE_URL'] +
'/user/' +
self.user_uuid +
'/skillJson'
)
user_service_response = service_request.get(
service_url,
headers=service_request_headers
)
if user_service_response.status_code != HTTPStatus.OK:
self._check_for_service_errors(user_service_response)
self._validate_install_status()
self._determine_install_status()
if self.aggregate_skill.installation == 'failed':
self._determine_failure_reason()
response_skills = user_service_response.json()
for device in response_skills.get('devices', []):
for skill in device['skills']:
manifest_skill = ManifestSkill(**skill)
self.skills_in_manifests[manifest_skill.name].append(
manifest_skill
def _validate_install_status(self):
for manifest_skill in self.manifest_skills:
if manifest_skill.installation not in VALID_INSTALLATION_VALUES:
raise ValueError(
'"{install_status}" is not a supported value of the '
'installation field in the skill manifest'.format(
install_status=manifest_skill.installation
)
)
def _determine_skill_install_status(self, skill):
"""Use skill data from all devices to determine install status
def _determine_install_status(self):
"""Use skill data from all devices to determine install status.
When a skill is installed via the Marketplace, it is installed to all
devices. The Marketplace will not mark a skill as "installed" until
@ -83,16 +92,39 @@ class SkillEndpointBase(SeleneEndpoint):
If the install fails on any device, the install will be flagged as a
failed install in the Marketplace.
"""
manifest_skill_id = skill['name'] + '.' + skill['github_username']
is_installed = False
is_installing = False
install_failed = False
manifest_skills = self.skills_in_manifests[manifest_skill_id]
if any([s.install_failed for s in manifest_skills]):
install_failed = True
elif any([s.is_installing for s in manifest_skills]):
is_installing = True
elif all([s.is_installed for s in manifest_skills]):
is_installed = True
failed = [s.installation == 'failed' for s in self.manifest_skills]
installing = [
s.installation == 'installing' for s in self.manifest_skills
]
uninstalling = [
s.installation == 'uninstalling' for s in self.manifest_skills
]
installed = [
s.installation == 'installed' for s in self.manifest_skills
]
if any(failed):
self.aggregate_skill.installation = 'failed'
elif any(installing):
self.aggregate_skill.installation = 'installing'
elif any(uninstalling):
self.aggregate_skill.installation = 'uninstalling'
elif all(installed):
self.aggregate_skill.installation = 'installed'
return is_installed, is_installing, install_failed
def _determine_failure_reason(self):
"""When a skill fails to install, determine the reason"""
for manifest_skill in self.manifest_skills:
if manifest_skill.installation == 'failed':
self.aggregate_skill.failure_reason = (
manifest_skill.failure_message
)
break
def aggregate_manifest_skills(
manifest_skills: List[ManifestSkill]
) -> ManifestSkill:
skill_aggregator = SkillManifestAggregator(manifest_skills)
skill_aggregator.aggregate_manifest_skills()
return skill_aggregator.aggregate_skill

View File

@ -4,11 +4,15 @@ from http import HTTPStatus
from markdown import markdown
import requests as service_request
from .common import SkillEndpointBase
from selene_util.api import APIError
from .common import (
aggregate_manifest_skills,
call_skill_manifest_endpoint,
parse_skill_manifest_response
)
from selene_util.api import APIError, SeleneEndpoint
class SkillDetailEndpoint(SkillEndpointBase):
class SkillDetailEndpoint(SeleneEndpoint):
""""Supply the data that will populate the skill detail page."""
authentication_required = False
@ -16,6 +20,7 @@ class SkillDetailEndpoint(SkillEndpointBase):
super(SkillDetailEndpoint, self).__init__()
self.skill_id = None
self.response_skill = None
self.manifest_skills = []
def get(self, skill_id):
"""Process an HTTP GET request"""
@ -40,12 +45,26 @@ class SkillDetailEndpoint(SkillEndpointBase):
self._check_for_service_errors(skill_service_response)
self.response_skill = skill_service_response.json()
def _get_skill_manifests(self):
service_response = call_skill_manifest_endpoint(
self.tartarus_token,
self.config['TARTARUS_BASE_URL'],
self.user_uuid
)
if service_response.status_code != HTTPStatus.OK:
self._check_for_service_errors(service_response)
skills_in_manifest = parse_skill_manifest_response(
service_response
)
self.manifest_skills = skills_in_manifest[
self.response_skill['skill_name']
]
def _build_response_data(self):
"""Make some modifications to the response skill for the marketplace"""
install_status = self._determine_skill_install_status(
self.response_skill
aggregated_manifest_skill = aggregate_manifest_skills(
self.manifest_skills
)
is_installed, is_installing, install_failed = install_status
self.response_skill.update(
description=markdown(
self.response_skill['description'],
@ -55,7 +74,5 @@ class SkillDetailEndpoint(SkillEndpointBase):
self.response_skill['summary'],
output_format='html5'
),
is_installed=is_installed,
is_installing=is_installing,
install_failed=install_failed
install_status=aggregated_manifest_skill.installation,
)

View File

@ -8,8 +8,12 @@ from typing import List
from markdown import markdown
import requests as service_request
from .common import SkillEndpointBase
from selene_util.api import APIError
from .common import (
aggregate_manifest_skills,
call_skill_manifest_endpoint,
parse_skill_manifest_response
)
from selene_util.api import APIError, SeleneEndpoint
DEFAULT_ICON_COLOR = '#6C7A89'
DEFAULT_ICON_NAME = 'comment-alt'
@ -22,18 +26,25 @@ _log = getLogger(__package__)
@dataclass
class RepositorySkill(object):
"""Represents a single skill defined in the Mycroft Skills repository."""
title: str
summary: str
description: str
branch: str
categories: List[str]
triggers: List[str]
created: str
credits: List[str]
tags: List[str]
description: str
id: str
last_update: str
platforms: List[str]
repository_owner: str
repository_url: str
icon_image: str
skill_name: str
summary: str
tags: List[str]
title: str
triggers: List[str]
icon: dict = field(
default=lambda: dict(icon=DEFAULT_ICON_NAME, color=DEFAULT_ICON_COLOR)
)
icon_image: str = field(default=None)
marketplace_category: str = field(init=False, default=UNDEFINED_CATEGORY)
def __post_init__(self):
@ -46,13 +57,14 @@ class RepositorySkill(object):
self.marketplace_category = self.categories[0]
class SkillSummaryEndpoint(SkillEndpointBase):
class SkillSummaryEndpoint(SeleneEndpoint):
authentication_required = False
def __init__(self):
super(SkillSummaryEndpoint, self).__init__()
self.available_skills: List[RepositorySkill] = []
self.response_skills = defaultdict(list)
self.skills_in_manifests = defaultdict(list)
def get(self):
try:
@ -69,8 +81,8 @@ class SkillSummaryEndpoint(SkillEndpointBase):
def _get_skills(self):
"""Retrieve the skill data that will be used to build the response."""
self._get_available_skills()
if self.authenticated:
self._get_skill_manifests()
# if self.authenticated:
# self._get_skill_manifests()
def _get_available_skills(self):
"""Retrieve all skills in the skill repository.
@ -88,6 +100,18 @@ class SkillSummaryEndpoint(SkillEndpointBase):
RepositorySkill(**skill) for skill in skill_service_response.json()
]
def _get_skill_manifests(self):
service_response = call_skill_manifest_endpoint(
self.tartarus_token,
self.config['TARTARUS_BASE_URL'],
self.user_uuid
)
if service_response.status_code != HTTPStatus.OK:
self._check_for_service_errors(service_response)
self.skills_in_manifest = parse_skill_manifest_response(
service_response
)
def _build_response_data(self):
"""Build the data to include in the response."""
if self.request.query_string:
@ -125,17 +149,19 @@ class SkillSummaryEndpoint(SkillEndpointBase):
def _reformat_skills(self, skills_to_include: list):
"""Build the response data from the skill service response"""
for skill in skills_to_include:
install_status = self._determine_skill_install_status(skill)
is_installed, is_installing, install_failed = install_status
install_status = None
manifest_skills = self.skills_in_manifests.get(skill.skill_name)
if manifest_skills is not None:
aggregated_manifest = aggregate_manifest_skills(manifest_skills)
install_status = aggregated_manifest.installation
skill_summary = dict(
credits=skill.credits,
icon=skill.icon,
icon_image=skill.icon_image,
id=skill['id'],
is_installed=is_installed,
is_installing=is_installing,
install_failed=install_failed,
repository_url=skill.repository_url,
iconImage=skill.icon_image,
id=skill.id,
install_status=install_status,
repositoryUrl=skill.repository_url,
summary=markdown(skill.summary, output_format='html5'),
title=skill.title,
triggers=skill.triggers