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 collections import defaultdict
from dataclasses import dataclass, field from dataclasses import asdict, dataclass
from http import HTTPStatus from typing import List
import requests as service_request import requests as service_request
from selene_util.api import SeleneEndpoint VALID_INSTALLATION_VALUES = ('failed', 'installed', 'installing', 'uninstalled')
@dataclass @dataclass
@ -14,66 +14,75 @@ class ManifestSkill(object):
Mycroft core keeps a manifest off all skills associated with a device. 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. 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 beta: bool
failure_message: str
installation: str
installed_on: int installed_on: int
name: str
origin: str
status: str
updated: int 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""" """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): def __init__(self, manifest_skills: List[ManifestSkill]):
"""Get the skill manifests from each of a user's devices 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 def aggregate_manifest_skills(self):
skill as it relates to the marketplace. """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 = { self._validate_install_status()
'Authorization': 'Bearer ' + self.tartarus_token self._determine_install_status()
} if self.aggregate_skill.installation == 'failed':
service_url = ( self._determine_failure_reason()
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)
response_skills = user_service_response.json() def _validate_install_status(self):
for device in response_skills.get('devices', []): for manifest_skill in self.manifest_skills:
for skill in device['skills']: if manifest_skill.installation not in VALID_INSTALLATION_VALUES:
manifest_skill = ManifestSkill(**skill) raise ValueError(
self.skills_in_manifests[manifest_skill.name].append( '"{install_status}" is not a supported value of the '
manifest_skill 'installation field in the skill manifest'.format(
install_status=manifest_skill.installation
)
) )
def _determine_skill_install_status(self, skill): def _determine_install_status(self):
"""Use skill data from all devices to determine install status """Use skill data from all devices to determine install status.
When a skill is installed via the Marketplace, it is installed to all When a skill is installed via the Marketplace, it is installed to all
devices. The Marketplace will not mark a skill as "installed" until 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 If the install fails on any device, the install will be flagged as a
failed install in the Marketplace. failed install in the Marketplace.
""" """
manifest_skill_id = skill['name'] + '.' + skill['github_username'] failed = [s.installation == 'failed' for s in self.manifest_skills]
is_installed = False installing = [
is_installing = False s.installation == 'installing' for s in self.manifest_skills
install_failed = False ]
manifest_skills = self.skills_in_manifests[manifest_skill_id] uninstalling = [
if any([s.install_failed for s in manifest_skills]): s.installation == 'uninstalling' for s in self.manifest_skills
install_failed = True ]
elif any([s.is_installing for s in manifest_skills]): installed = [
is_installing = True s.installation == 'installed' for s in self.manifest_skills
elif all([s.is_installed for s in manifest_skills]): ]
is_installed = True 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 from markdown import markdown
import requests as service_request import requests as service_request
from .common import SkillEndpointBase from .common import (
from selene_util.api import APIError 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.""" """"Supply the data that will populate the skill detail page."""
authentication_required = False authentication_required = False
@ -16,6 +20,7 @@ class SkillDetailEndpoint(SkillEndpointBase):
super(SkillDetailEndpoint, self).__init__() super(SkillDetailEndpoint, self).__init__()
self.skill_id = None self.skill_id = None
self.response_skill = None self.response_skill = None
self.manifest_skills = []
def get(self, skill_id): def get(self, skill_id):
"""Process an HTTP GET request""" """Process an HTTP GET request"""
@ -40,12 +45,26 @@ class SkillDetailEndpoint(SkillEndpointBase):
self._check_for_service_errors(skill_service_response) self._check_for_service_errors(skill_service_response)
self.response_skill = skill_service_response.json() 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): def _build_response_data(self):
"""Make some modifications to the response skill for the marketplace""" """Make some modifications to the response skill for the marketplace"""
install_status = self._determine_skill_install_status( aggregated_manifest_skill = aggregate_manifest_skills(
self.response_skill self.manifest_skills
) )
is_installed, is_installing, install_failed = install_status
self.response_skill.update( self.response_skill.update(
description=markdown( description=markdown(
self.response_skill['description'], self.response_skill['description'],
@ -55,7 +74,5 @@ class SkillDetailEndpoint(SkillEndpointBase):
self.response_skill['summary'], self.response_skill['summary'],
output_format='html5' output_format='html5'
), ),
is_installed=is_installed, install_status=aggregated_manifest_skill.installation,
is_installing=is_installing,
install_failed=install_failed
) )

View File

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