class GlobalContext:
    lang = Lazy(unset)  # type: str
    msm = Lazy(unset)  # type: MycroftSkillsManager
    use_token = Lazy(unset)  # type: bool
    github = Lazy(
        lambda s: ask_for_github_credentials(s.use_token))  # type: Github
    user = Lazy(lambda s: s.github.get_user())  # type: AuthenticatedUser
class SkillData(GlobalContext):
    def __init__(self, skill: SkillEntry):
        self.entry = skill

    name = property(lambda self: self.entry.name)
    repo = Lazy(lambda s: RepoData())  # type: RepoData
    repo_git = Lazy(lambda s: Git(join(s.repo.msminfo.path, s.submodule_name)))  # type: Git
    git = Lazy(lambda s: Git(s.entry.path))  # type: Git
    hub = Lazy(lambda s: s.github.get_repo(skill_repo_name(s.entry.url)))  # type: Repository

    @Lazy
    def submodule_name(self):
        name_to_path = {name: path for name, path, url, sha in self.repo.msminfo.get_skill_data()}
        if self.name not in name_to_path:
            raise NotUploaded('The skill {} has not yet been uploaded to the skill store'.format(
                self.name
            ))
        return name_to_path[self.name]

    def upgrade(self) -> str:
        skill_module = self.submodule_name
        self.repo.msminfo.update()
        self.repo_git.fetch()
        default_branch = self.repo_git.symbolic_ref('refs/remotes/origin/HEAD')
        self.repo_git.reset(default_branch, hard=True)

        upgrade_branch = 'upgrade/' + self.name
        self.repo.checkout_branch(upgrade_branch)

        if not self.repo.git.diff(skill_module) and self.repo.git.ls_files(skill_module):
            raise AlreadyUpdated(
                'The latest version of {} is already uploaded to the skill repo'.format(
                    self.name
                )
            )
        self.repo.git.add(skill_module)
        self.repo.git.commit(message='Upgrade ' + self.name)
        return upgrade_branch

    def add_to_repo(self) -> str:
        self.repo.msminfo.update()
        elements = [i.split() for i in self.git.ls_tree('HEAD').split('\n')]
        existing_mods = [folder for size, typ, sha, folder in elements]
        if self.name not in existing_mods:
            self.repo.git.submodule('add', self.entry.url, self.name)

        # Upgrade skill in case it is outdated
        self.repo_git.fetch()
        default_branch = self.repo_git.symbolic_ref('refs/remotes/origin/HEAD')
        self.repo_git.reset(default_branch, hard=True)

        branch_name = 'add/' + self.name
        self.repo.checkout_branch(branch_name)
        self.repo.git.add(self.name)
        self.repo.git.commit(message='Add ' + self.name)
        return branch_name

    def init_existing(self):
        self.repo.git.submodule('update', '--init', self.submodule_name)
Example #3
0
class TestCreator(GlobalContext):
    def __init__(self, folder):
        self.folder = folder

    init_file = Lazy(lambda s: join(s.folder, '__init__.py'))
    init_content = Lazy(lambda s: read_file(s.init_file)
                        if isfile(s.init_file) else '')
    utterance = Lazy(
        lambda s: ask_input('Enter an example query:', lambda x: x))
    dialogs = Lazy(lambda s: [
        splitext(basename(i))[0]
        for i in glob(join(s.folder, 'dialog', s.lang, '*.dialog'))
    ])
    expected_dialog = Lazy(
        lambda s: ask_choice('Choose expected dialog (leave empty to skip).',
                             s.dialogs,
                             allow_empty=True,
                             on_empty='No dialogs available. Skipping...'))

    padatious_creator = Lazy(
        lambda s: PadatiousTestCreator(s.folder))  # type: PadatiousTestCreator
    adapt_creator = Lazy(
        lambda s: AdaptTestCreator(s.folder))  # type: AdaptTestCreator
    intent_choices = Lazy(lambda s: list(
        chain(s.adapt_creator.intent_recipes, s.padatious_creator.intent_names)
    ))

    @Lazy
    def intent_name(self):
        return ask_choice(
            'Which intent would you like to test?',
            self.intent_choices,
            on_empty='No existing intents found. Please create some first')
Example #4
0
class ConsoleAction(GlobalContext, metaclass=ABCMeta):
    @staticmethod
    @abstractmethod
    def register(parser: ArgumentParser):
        pass

    @abstractmethod
    def perform(self):
        pass

    repo = Lazy(lambda s: RepoData())  # type: RepoData
Example #5
0
class RepoData(GlobalContext):
    msminfo = Lazy(lambda s: s.msm.repo)  # type: SkillRepo
    git = Lazy(lambda s: Git(s.msminfo.path))  # type: Git
    hub = Lazy(lambda s: s.github.get_repo(skill_repo_name(s.msminfo.url)))  # type: Repository
    fork = Lazy(lambda s: s.github.get_user().create_fork(s.hub))  # type: Repository

    def push_to_fork(self, branch: str):
        remotes = self.git.remote().split('\n')
        command = 'set-url' if 'fork' in remotes else 'add'
        self.git.remote(command, 'fork', self.fork.html_url)

        # Use call to ensure the environment variable GIT_ASKPASS is used
        call(['git', 'push', '-u', 'fork', branch, '--force'], cwd=self.msminfo.path)

    def checkout_branch(self, branch):
        with suppress(GitCommandError):
            self.git.branch('-D', branch)
        try:
            self.git.checkout(b=branch)
        except GitCommandError:
            self.git.checkout(branch)
Example #6
0
class CreateAction(ConsoleAction):
    def __init__(self, args, name: str = None):
        if name:
            self.name = name

    @staticmethod
    def register(parser: ArgumentParser):
        pass

    @Lazy
    def name(self) -> str:
        name_to_skill = {skill.name: skill for skill in self.msm.list()}
        while True:
            name = ask_input(
                'Enter a short unique skill name (ie. "siren alarm" or "pizza orderer"):',
                lambda x: re.match(r'^[a-zA-Z \-]+$', x),
                'Please use only letter and spaces.').strip(
                    ' -').lower().replace(' ', '-')
            skill = name_to_skill.get(
                name, name_to_skill.get('{}-skill'.format(name)))
            if skill:
                print('The skill {} {}already exists'.format(
                    skill.name,
                    'by {} '.format(skill.author) * bool(skill.author)))
                if ask_yes_no('Remove it? (y/N)', False):
                    rmtree(skill.path)
                else:
                    continue
            class_name = '{}Skill'.format(to_camel(name.replace('-', '_')))
            repo_name = '{}-skill'.format(name)
            print()
            print('Class name:', class_name)
            print('Repo name:', repo_name)
            print()
            alright = ask_yes_no('Looks good? (Y/n)', True)
            if alright:
                return name

    path = Lazy(lambda s: join(s.msm.skills_dir, s.name + '-skill'))
    git = Lazy(lambda s: Git(s.path))
    short_description = Lazy(lambda s: ask_input(
        'Enter a one line description for your skill (ie. Orders fresh pizzas from the store):',
    ).capitalize())
    author = Lazy(lambda s: ask_input('Enter author:'))
    intent_lines = Lazy(lambda s: [
        i.capitalize() for i in ask_input_lines(
            'Enter some example phrases to trigger your skill:', '-')
    ])
    dialog_lines = Lazy(lambda s: [
        i.capitalize() for i in ask_input_lines(
            'Enter what your skill should say to respond:', '-')
    ])
    intent_entities = Lazy(lambda s: set(
        re.findall(r'(?<={)[a-z_A-Z]*(?=})', '\n'.join(
            i for i in s.intent_lines))))
    dialog_entities = Lazy(lambda s: set(
        re.findall(r'(?<={)[a-z_A-Z]*(?=})', '\n'.join(s.dialog_lines))))
    long_description = Lazy(
        lambda s: '\n\n'.join(ask_input_lines('Enter a long description:', '>')
                              ).strip().capitalize())
    readme = Lazy(lambda s: readme_template.format(
        title_name=s.name.replace('-', ' ').title(),
        short_description=s.short_description,
        long_description=s.long_description,
        examples=''.join(' - "{}"\n'.format(i) for i in s.intent_lines),
        credits=credits_template.format(author=s.author)))
    init_file = Lazy(lambda s: init_template.format(
        class_name=to_camel(s.name.replace('-', '_')),
        handler_name=s.intent_name.replace('.', '_'),
        handler_code='\n'.join(' ' * 8 * bool(i) + i for i in [
            "{ent} = message.data['{ent}']".format(ent=entity)
            for entity in sorted(s.intent_entities)
        ] + [
            "{ent} = ''".format(ent=entity)
            for entity in sorted(s.dialog_entities - s.intent_entities)
        ] + [''] * bool(
            s.dialog_entities | s.intent_entities
        ) + "self.speak_dialog('{intent}'{args})".format(
            intent=s.intent_name,
            args=", data={{\n{}\n}}".format(
                ',\n'.join("    '{ent}': {ent}".format(ent=entity)
                           for entity in s.dialog_entities | s.intent_entities)
            ) * bool(s.dialog_entities | s.intent_entities)).split('\n')),
        intent_name=s.intent_name))
    intent_name = Lazy(lambda s: '.'.join(reversed(s.name.split('-'))))

    def add_vocab(self):
        makedirs(join(self.path, 'vocab', self.lang))
        with open(
                join(self.path, 'vocab', self.lang,
                     self.intent_name + '.intent'), 'w') as f:
            f.write('\n'.join(self.intent_lines + ['']))

    def add_dialog(self):
        makedirs(join(self.path, 'dialog', self.lang))
        with open(
                join(self.path, 'dialog', self.lang,
                     self.intent_name + '.dialog'), 'w') as f:
            f.write('\n'.join(self.dialog_lines + ['']))

    def initialize_template(self, files: set = None):
        git = Git(self.path)

        skill_template = [
            ('', lambda: makedirs(self.path)), ('vocab', self.add_vocab),
            ('dialog', self.add_dialog),
            ('__init__.py', lambda: self.init_file),
            ('README.md', lambda: self.readme),
            ('.gitignore', lambda: gitignore_template),
            ('settingsmeta.json', lambda: settingsmeta_template.format(
                capital_desc=self.name.replace('-', ' ').capitalize())),
            ('.git', lambda: git.init())
        ]

        def cleanup():
            rmtree(self.path)

        if not isdir(self.path):
            atexit.register(cleanup)
        for file, handler in skill_template:
            if files and file not in files:
                continue
            if not exists(join(self.path, file)):
                result = handler()
                if isinstance(result,
                              str) and not exists(join(self.path, file)):
                    with open(join(self.path, file), 'w') as f:
                        f.write(result)
        atexit.unregister(cleanup)

    def commit_changes(self):
        if self.git.rev_parse('HEAD', with_exceptions=False) == 'HEAD':
            self.git.add('.')
            self.git.commit(message='Initial commit')

    def create_github_repo(self,
                           get_repo_name: Callable = None
                           ) -> Optional[Repository]:
        if 'origin' not in Git(self.path).remote().split('\n'):
            if ask_yes_no(
                    'Would you like to create a GitHub repo for it? (Y/n)',
                    True):
                repo_name = (get_repo_name
                             and get_repo_name()) or (self.name + '-skill')
                try:
                    repo = self.user.create_repo(repo_name,
                                                 self.short_description)
                except GithubException as e:
                    if e.status == 422:
                        raise GithubRepoExists(repo_name) from e
                    raise
                self.git.remote('add', 'origin', repo.html_url)
                call(['git', 'push', '-u', 'origin', 'master'],
                     cwd=self.git.working_dir)
                print('Created GitHub repo:', repo.html_url)
                return repo
        return None

    def perform(self):
        self.initialize_template()
        self.commit_changes()
        with print_error(GithubRepoExists):
            self.create_github_repo()
        print('Created skill at:', self.path)
Example #7
0
class AdaptTestCreator(TestCreator):
    """
    Extracts Adapt intents from the source code

    Adapt's intents are made up of two components:
     - The "vocab definitions": words associated with a vocab name
     - The "intent recipe": list of vocab keywords that are required and optional for an intent
    """

    intent_regex = (
        r'''@(?:\\\n )*intent_handler  (?:\n )*\(  IntentBuilder  \(  ['"][^'"]*['"]  \)((?:  '''
        r'''\.  (?:optionally|require)  \(  ['"][a-zA-Z_]+['"]  \))*)\) \n'''
        r'''(?: \\\n)* def (?:\\\n )*([a-z_]+)''').replace('  ',
                                                           r'[\s\n]*').replace(
                                                               ' ', r'\s*')

    parts_regex = r'''\.  (require|optionally)  \(  ['"]([a-zA-Z_]+)['"]  \)'''.replace(
        '  ', r'[\s\n]*').replace(' ', '\s*')
    intent_recipe = Lazy(lambda s: s.intent_recipes[s.intent_name])

    @Lazy
    def utterance(self):
        while True:
            utterance = ask_input('Enter an example query:', lambda x: x)

            missing_vocabs = [
                i for i in self.intent_recipe['require']
                if not any(j in utterance.lower()
                           for j in self.vocab_defs.get(i, []))
            ]
            if missing_vocabs:
                print('Missing the following vocab:',
                      ', '.join(missing_vocabs))
                if ask_yes_no('Continue anyways? (y/N)', False):
                    return utterance
            else:
                return utterance

    def extract_recipe(self, recipe_str):
        parts = {'require': [], 'optionally': []}
        for part_match in re.finditer(self.parts_regex, recipe_str):
            parts[part_match.group(1)].append(part_match.group(2))
        return parts

    @Lazy
    def intent_recipes(self) -> Dict[str, Dict[str, list]]:
        return {
            match.group(2): self.extract_recipe(match.group(1))
            for match in re.finditer(self.intent_regex, self.init_content)
        }

    @Lazy
    def vocab_defs(self):
        return {
            splitext(basename(content_file))[0]: list(
                chain(*(map(str.strip,
                            i.lower().split('|'))
                        for i in read_lines(content_file))))
            for content_file in
            glob(join(self.folder, 'vocab', self.lang, '*.voc')) +
            glob(join(self.folder, 'locale', self.lang, '*.voc')) +
            glob(join(self.folder, 'regex', self.lang, '*.rx')) +
            glob(join(self.folder, 'locale', self.lang, '*.rx'))
        }

    @Lazy
    def utterance_data(self):
        utterance_left = self.utterance.lower()
        utterance_data = {}

        for key, start_message in [('require', 'Required'),
                                   ('optionally', 'Optional')]:
            if not self.intent_recipe[key]:
                continue

            print()
            print('===', start_message, 'Tags', '===')
            for vocab_name in sorted(self.intent_recipe[key]):
                vocab_value = ask_input(
                    vocab_name + ':',
                    lambda x: not x or x.lower() in utterance_left,
                    'Response must be in the remaining utterance: ' +
                    utterance_left)
                if vocab_value:
                    utterance_data[vocab_name] = vocab_value
                    utterance_left = utterance_left.replace(
                        vocab_value.lower(), '')

        return utterance_data

    @Lazy
    @serialized
    def recipe_str(self):
        for key, name in [('require', 'Required'), ('optionally', 'Optional')]:
            if not self.intent_recipe[key]:
                continue

            yield ''
            yield '===', name, 'Vocab', '==='
            for vocab_name in sorted(self.intent_recipe[key]):
                words = self.vocab_defs.get(vocab_name, ['?'])
                yield '{}: {}'.format(
                    vocab_name,
                    ', '.join(words[:6] + ['...'] * (len(words) > 6)))

    @Lazy
    def test_case(self) -> dict:
        if self.intent_name not in self.intent_recipes:
            return {}

        print(self.recipe_str)
        print()

        test_case = {'utterance': self.utterance}
        if self.utterance_data:
            test_case['intent'] = self.utterance_data
        test_case['intent_type'] = self.intent_name
        if self.expected_dialog:
            test_case['expected_dialog'] = self.expected_dialog
        return test_case
Example #8
0
class PadatiousTestCreator(TestCreator):
    intent_files = Lazy(
        lambda s: glob(join(s.folder, 'vocab', s.lang, '*.intent')) + glob(
            join(s.folder, 'locale', s.lang, '*.intent')))
    intent_names = Lazy(
        lambda s:
        {basename(intent_file): intent_file
         for intent_file in s.intent_files})
    intent_file = Lazy(lambda s: s.intent_names.get(s.intent_name, ''))
    entities = Lazy(
        lambda s: {
            splitext(basename(entity_file))[0]: read_lines(entity_file)
            for entity_file in glob(join(
                s.folder, 'vocab', s.lang, '*.entity')) + glob(
                    join(s.folder, 'locale', s.lang, '*.entity'))
        })

    intent_lines = Lazy(lambda s: read_lines(s.intent_file))
    entity_names = Lazy(lambda s: set(
        re.findall(r'(?<={)[a-z_]+(?=})', '\n'.join(s.intent_lines))))

    @Lazy
    @serialized
    def entities_str(self) -> str:
        if not self.entities:
            return
        yield '=== Entity Examples ==='
        for entity_name, lines in sorted(self.entities.items()):
            sample = ', '.join(lines)
            yield '{}: {}'.format(entity_name,
                                  sample[:50] + '...' * (len(sample) > 50))

    @Lazy
    @serialized
    def intent_str(self) -> str:
        shuffle(self.intent_lines)
        yield '=== Intent Examples ==='
        yield '\n'.join(self.intent_lines[:6] + ['...'] *
                        (len(self.intent_lines) > 6))

    @Lazy
    def utterance_data(self) -> dict:
        utterance_data = {}
        utterance_left = self.utterance

        print()
        print('=== Entity Tags ===')
        for entity_name in sorted(self.entity_names):
            entity_value = ask_input(
                entity_name + ':', lambda x: not x or x in utterance_left,
                'Response must be in the remaining utterance: ' +
                utterance_left)
            if entity_value:
                utterance_data[entity_name] = entity_value
                utterance_left = utterance_left.replace(entity_value, '')
        return utterance_data

    @Lazy
    def test_case(self) -> {}:
        if self.intent_name not in self.intent_names:
            return {}

        print()
        print(self.intent_str)
        print()
        if self.entities_str:
            print(self.entities_str)
            print()

        test_case = {'utterance': self.utterance}
        if self.entity_names and self.utterance_data:
            test_case['intent'] = self.utterance_data
        test_case['intent_type'] = self.intent_name
        if self.expected_dialog:
            test_case['expected_dialog'] = self.expected_dialog
        return test_case
Example #9
0
class CreateAction(ConsoleAction):
    def __init__(self, args, name: str = None):
        colorama_init()
        if name:
            self.name = name

    @staticmethod
    def register(parser: ArgumentParser):
        pass

    @Lazy
    def name(self) -> str:
        name_to_skill = {skill.name: skill for skill in self.msm.list()}
        while True:
            name = ask_input(
                'Enter a short unique skill name (ie. "siren alarm" or "pizza orderer"):',
                lambda x: re.match(r'^[a-zA-Z \-]+$', x),
                'Please use only letter and spaces.').strip(
                    ' -').lower().replace(' ', '-')
            skill = name_to_skill.get(
                name, name_to_skill.get('{}-skill'.format(name)))
            if skill:
                print('The skill {} {}already exists'.format(
                    skill.name,
                    'by {} '.format(skill.author) * bool(skill.author)))
                if ask_yes_no('Remove it? (y/N)', False):
                    rmtree(skill.path)
                else:
                    continue
            class_name = '{}Skill'.format(to_camel(name.replace('-', '_')))
            repo_name = '{}-skill'.format(name)
            print()
            print('Class name:', class_name)
            print('Repo name:', repo_name)
            print()
            alright = ask_yes_no('Looks good? (Y/n)', True)
            if alright:
                return name

    path = Lazy(lambda s: join(s.msm.skills_dir, s.name + '-skill'))
    git = Lazy(lambda s: Git(s.path))
    short_description = Lazy(lambda s: ask_input(
        'Enter a one line description for your skill (ie. Orders fresh pizzas from the store):\n-',
    ).capitalize())
    author = Lazy(lambda s: ask_input('Enter author:'))
    intent_lines = Lazy(lambda s: [
        i.capitalize() for i in ask_input_lines(
            'Enter some example phrases to trigger your skill:', '-')
    ])
    dialog_lines = Lazy(lambda s: [
        i.capitalize() for i in ask_input_lines(
            'Enter what your skill should say to respond:', '-')
    ])
    intent_entities = Lazy(lambda s: set(
        re.findall(r'(?<={)[a-z_A-Z]*(?=})', '\n'.join(
            i for i in s.intent_lines))))
    dialog_entities = Lazy(lambda s: set(
        re.findall(r'(?<={)[a-z_A-Z]*(?=})', '\n'.join(s.dialog_lines))))
    long_description = Lazy(
        lambda s: '\n\n'.join(ask_input_lines('Enter a long description:', '>')
                              ).strip().capitalize())
    icon = Lazy(lambda s: ask_input(
        'Go to Font Awesome ({blue}fontawesome.com/cheatsheet{reset}) and choose an icon.'
        '\nEnter the name of the icon (default: robot):'.format(
            blue=Fore.BLUE + Style.BRIGHT, reset=Style.RESET_ALL),
        validator=lambda x: x == '' or requests.get(
            "https://raw.githack.com/FortAwesome/Font-Awesome/"
            "master/svgs/solid/{x}.svg".format(x=x)).ok,
        on_fail=
        "\n\n{red}Error: The name was not found. Make sure you spelled the icon name right,"
        " and try again.{reset}\n".format(red=Fore.RED + Style.BRIGHT,
                                          reset=Style.RESET_ALL)))
    color = Lazy(lambda s: ask_input(
        "Pick a {yellow}color{reset} for your icon. Find a color that matches the color scheme at"
        " {blue}mycroft.ai/colors{reset}, or pick a color at: {blue}color-hex.com.{reset}"
        "\nEnter the color hex code including the # (default: #22A7F0):".
        format(blue=Fore.BLUE + Style.BRIGHT,
               yellow=Fore.YELLOW,
               reset=Style.RESET_ALL),
        validator=lambda hex_code: hex_code == '' or hex_code[
            0] == "#" and len(hex_code) in [4, 7],
        on_fail=
        "\n{red}Check that you entered a correct hex code, and try again.{reset}\n"
        .format(red=Fore.RED + Style.BRIGHT, reset=Style.RESET_ALL)))

    category_options = [
        'Daily', 'Configuration', 'Entertainment', 'Information', 'IoT',
        'Music & Audio', 'Media', 'Productivity', 'Transport'
    ]
    category_primary = Lazy(lambda s: s.ask_category_primary())
    categories_other = Lazy(
        lambda s: s.ask_categories_other(s.category_primary))

    tags = Lazy(lambda s: [
        i.capitalize() for i in ask_input_lines(
            'Enter tags to make it easier to search for your skill (optional):',
            '-')
    ])

    manifest = Lazy(lambda s: manifest_template if ask_yes_no(
        message=
        "Does this Skill depend on Python Packages (PyPI), System Packages (apt-get/others), or other skills?"
        "\nThis will create a manifest.yml file for you to define the dependencies for your Skill."
        "\nCheck the Mycroft documentation at mycroft.ai/to/skill-dependencies to learn more about including dependencies, and the manifest.yml file, in Skills. (y/N)",
        default=False) else None)

    readme = Lazy(lambda s: readme_template.format(
        title_name=s.name.replace('-', ' ').title(),
        short_description=s.short_description,
        long_description=s.long_description,
        examples=''.join('* "{}"\n'.format(i) for i in s.intent_lines),
        credits=credits_template.format(author=s.author),
        icon=s.icon or 'robot',
        color=s.color.upper() or '#22A7F0',
        category_primary=s.category_primary,
        categories_other=''.join('{}\n'.format(i) for i in s.categories_other),
        tags=''.join('#{}\n'.format(i) for i in s.tags),
    ))
    init_file = Lazy(lambda s: init_template.format(
        class_name=to_camel(s.name.replace('-', '_')),
        handler_name=s.intent_name.replace('.', '_'),
        handler_code='\n'.join(' ' * 8 * bool(i) + i for i in [
            "{ent} = message.data.get('{ent}')".format(ent=entity)
            for entity in sorted(s.intent_entities)
        ] + [
            "{ent} = ''".format(ent=entity)
            for entity in sorted(s.dialog_entities - s.intent_entities)
        ] + [''] * bool(
            s.dialog_entities | s.intent_entities
        ) + "self.speak_dialog('{intent}'{args})".format(
            intent=s.intent_name,
            args=", data={{\n{}\n}}".format(
                ',\n'.join("    '{ent}': {ent}".format(ent=entity)
                           for entity in s.dialog_entities | s.intent_entities)
            ) * bool(s.dialog_entities | s.intent_entities)).split('\n')),
        intent_name=s.intent_name))

    intent_name = Lazy(lambda s: '.'.join(reversed(s.name.split('-'))))

    def add_locale(self):
        makedirs(join(self.path, 'locale', self.lang))
        with open(
                join(self.path, 'locale', self.lang,
                     self.intent_name + '.intent'), 'w') as f:
            f.write('\n'.join(self.intent_lines + ['']))
        with open(
                join(self.path, 'locale', self.lang,
                     self.intent_name + '.dialog'), 'w') as f:
            f.write('\n'.join(self.dialog_lines + ['']))

    def ask_category_primary(self):
        """Ask user to select primary category."""
        category = ask_choice(
            '\nCategories define where the skill will display in the Marketplace. \nEnter the primary category for your skill: ',
            self.category_options,
            allow_empty=False)
        return category

    def ask_categories_other(self, category_primary):
        """Ask user to select aditional categories."""
        categories_other = []
        while True:
            category_options_formatted = []
            for category in self.category_options:
                if (category == category_primary) or (category
                                                      in categories_other):
                    category = '*' + category + '*'
                category_options_formatted.append(category)
            category = ask_choice('Enter additional categories (optional):',
                                  category_options_formatted,
                                  allow_empty=True,
                                  on_empty=None)
            if (category != None) and (category[0] != '*'):
                categories_other.append(category)
            if category == None:
                break
        return categories_other

    def license(self):
        """Ask user to select a license for the repo."""
        license_files = get_licenses()
        print('For uploading a skill a license is required.\n'
              'Choose one of the licenses listed below or add one later.\n')
        for num, pth in zip(range(1, 1 + len(license_files)), license_files):
            print('{}: {}'.format(num, pretty_license(pth)))
        choice = ask_input('Choose license above or press Enter to skip?')
        if choice.isdigit():
            index = int(choice) - 1
            shutil.copy(license_files[index], join(self.path, 'LICENSE.md'))
            print('\nSome of these require that you insert the project name '
                  'and/or author\'s name. Please check the license file and '
                  'add the appropriate information.\n')

    def initialize_template(self, files: set = None):
        git = Git(self.path)

        skill_template = [
            ('', lambda: makedirs(self.path)), ('locale', self.add_locale),
            ('__init__.py', lambda: self.init_file),
            ('README.md', lambda: self.readme), ('LICENSE.md', self.license),
            ('.gitignore', lambda: gitignore_template),
            ('settingsmeta.yaml', lambda: settingsmeta_template.format(
                capital_desc=self.name.replace('-', ' ').capitalize())),
            ('manifest.yml', lambda: self.manifest),
            ('.git', lambda: git.init())
        ]

        def cleanup():
            rmtree(self.path)

        if not isdir(self.path):
            atexit.register(cleanup)
        for file, handler in skill_template:
            if files and file not in files:
                continue
            if not exists(join(self.path, file)):
                result = handler()
                if isinstance(result,
                              str) and not exists(join(self.path, file)):
                    with open(join(self.path, file), 'w') as f:
                        f.write(result)
        atexit.unregister(cleanup)

    def commit_changes(self):
        if self.git.rev_parse('HEAD', with_exceptions=False) == 'HEAD':
            self.git.add('.')
            self.git.commit(message='Initial commit')

    def force_push(self,
                   get_repo_name: Callable = None) -> Optional[Repository]:
        if ask_yes_no(
                'Are you sure you want to overwrite the remote github repo? '
                'This cannot be undone and you will lose your commit '
                'history! (y/N)', False):
            repo_name = (get_repo_name
                         and get_repo_name()) or (self.name + '-skill')
            repo = self.user.get_repo(repo_name)
            self.git.push('origin', 'master', force=True)
            print('Force pushed to GitHub repo:', repo.html_url)
            return repo

    def link_github_repo(self,
                         get_repo_name: Callable = None
                         ) -> Optional[Repository]:
        if 'origin' not in Git(self.path).remote().split('\n'):
            if ask_yes_no(
                    'Would you like to link an existing GitHub repo to it? (Y/n)',
                    True):
                repo_name = (get_repo_name
                             and get_repo_name()) or (self.name + '-skill')
                repo = self.user.get_repo(repo_name)
                self.git.remote('add', 'origin', repo.html_url)
                self.git.fetch()
                try:
                    self.git.pull('origin', 'master')
                except GitCommandError as e:
                    if e.status == 128:
                        raise UnrelatedGithubHistory(repo_name) from e
                    raise
                self.git.push('origin', 'master', set_upstream=True)
                print('Linked and pushed to GitHub repo:', repo.html_url)
                return repo

    def create_github_repo(self,
                           get_repo_name: Callable = None
                           ) -> Optional[Repository]:
        if 'origin' not in Git(self.path).remote().split('\n'):
            if ask_yes_no(
                    'Would you like to create a GitHub repo for it? (Y/n)',
                    True):
                repo_name = (get_repo_name
                             and get_repo_name()) or (self.name + '-skill')
                try:
                    repo = self.user.create_repo(repo_name,
                                                 self.short_description)
                except GithubException as e:
                    if e.status == 422:
                        raise GithubRepoExists(repo_name) from e
                    raise
                self.git.remote('add', 'origin', repo.html_url)
                call(['git', 'push', '-u', 'origin', 'master'],
                     cwd=self.git.working_dir)
                print('Created GitHub repo:', repo.html_url)
                return repo
        return None

    def perform(self):
        self.initialize_template()
        self.commit_changes()
        with print_error(GithubRepoExists):
            self.create_github_repo()
        print('Created skill at:', self.path)
Example #10
0
class UploadAction(ConsoleAction):
    def __init__(self, args):
        folder = abspath(expanduser(args.skill_folder))
        self.entry = SkillEntry.from_folder(folder)
        skills_dir = abspath(expanduser(self.msm.skills_dir))
        if join(skills_dir, basename(folder)) != folder:
            raise MskException('Skill folder, {}, not directly within skills directory, {}.'.format(
                args.skill_folder, self.msm.skills_dir
            ))

    git = Lazy(lambda s: Git(s.entry.path))  # type: Git

    @staticmethod
    def register(parser: ArgumentParser):
        parser.add_argument('skill_folder')

    def perform(self):
        for i in listdir(self.entry.path):
            if i.lower() == 'readme.md' and i != 'README.md':
                shutil.move(join(self.entry.path, i), join(self.entry.path, 'README.md'))

        creator = CreateAction(None, self.entry.name.replace('-skill', ''))
        creator.path = self.entry.path
        creator.initialize_template({'.git', '.gitignore', 'README.md'})
        self.git.add('README.md')
        creator.commit_changes()
        skill_repo = creator.create_github_repo(lambda: input('Repo name:'))
        if skill_repo:
            self.entry.url = skill_repo.html_url
            self.entry.author = self.user.login
        else:
            skill_repo = self.github.get_repo(skill_repo_name(self.entry.url))

        if not skill_repo.permissions.push:
            print('Warning: You do not have write permissions to the provided skill repo.')
            if ask_yes_no('Create a fork and use that instead? (Y/n)', True):
                skill_repo = self.user.create_fork(skill_repo)
                print('Created fork:', skill_repo.html_url)
                self.git.remote('rename', 'origin', 'upstream')
                self.git.remote('add', 'origin', skill_repo.html_url)

        self.entry.name = input('Enter a unique skill name (ie. npr-news or grocery-list): ')

        readme_file = {i.lower(): i for i in os.listdir(self.entry.path)}['readme.md']
        readme = read_file(self.entry.path, readme_file)

        last_section = None
        sections = {last_section: ''}
        for line in readme.split('\n'):
            line = line.strip()
            if line.startswith('#'):
                last_section = line.strip('# ').lower()
                sections[last_section] = ''
            else:
                sections[last_section] += '\n' + line
        del sections[None]

        if 'description' in sections:
            description = sections['description']
        else:
            description = ask_choice(
                'Which section contains the description?', list(sections),
                on_empty='Please create a description section in the README'
            )

        branch = SkillData(self.entry).add_to_repo()
        self.repo.push_to_fork(branch)

        pull = create_or_edit_pr(
            title='Add {}'.format(self.entry.name), body=body_template.format(
                description=description, skill_name=self.entry.name, skill_url=skill_repo.html_url
            ), user=self.user, branch=branch, skills_repo=self.repo.hub
        )

        print('Created pull request: ', pull.html_url)
Example #11
0
class UploadAction(ConsoleAction):
    def __init__(self, args):
        folder = abspath(expanduser(args.skill_folder))
        self.entry = SkillEntry.from_folder(folder)
        skills_dir = abspath(expanduser(self.msm.skills_dir))
        if join(skills_dir, basename(folder)) != folder:
            raise MskException(
                'Skill folder, {}, not directly within skills directory, {}.'.
                format(args.skill_folder, self.msm.skills_dir))
        self.skill_dir = folder

    git = Lazy(lambda s: Git(s.entry.path))  # type: Git

    @staticmethod
    def register(parser: ArgumentParser):
        pass  # Implemented in SubmitAction

    def check_valid(self):
        """Check that the skill contains all required files before uploading.
        """
        results = []
        if not (exists_in_remote(self.git, 'LICENSE.md')
                or exists_in_remote(self.git, 'LICENSE')
                or exists_in_remote(self.git, 'LICENSE.txt')):
            print('To have your Skill available for installation through the '
                  'Skills Marketplace, a license is required.\n'
                  'Please select one and add it to the skill as '
                  '`LICENSE.md.`\n'
                  'See https://opensource.org/licenses for information on '
                  'open source license options.')
            results.append(False)
        else:
            results.append(True)

        if not exists_in_remote(self.git, 'README.md'):
            print('For inclusion in the Mycroft Marketplace a README.md file '
                  'is required. please add the file and retry.')
            results.append(False)
        else:
            results.append(True)

        with open(join(self.skill_dir, 'README.md')) as f:
            readme = f.read()
        if '# About' not in readme and '# Description' not in readme:
            print('README is missing About Section needed by the Marketplace')
            results.append(False)
        else:
            results.append(True)

        if '# Category' not in readme:
            print('README is missing Category section needed by the '
                  'Marketplace')
            results.append(False)
        else:
            results.append(True)
        return all(results)

    def perform(self):
        print('Uploading a new skill to the skill repo...')

        for i in listdir(self.entry.path):
            if i.lower() == 'readme.md' and i != 'README.md':
                shutil.move(join(self.entry.path, i),
                            join(self.entry.path, 'README.md'))

        creator = CreateAction(None, self.entry.name.replace('-skill', ''))
        creator.path = self.entry.path
        creator.initialize_template({'.git', '.gitignore', 'README.md'})
        self.git.add('README.md')
        creator.commit_changes()

        try:
            skill_repo = creator.create_github_repo(
                lambda: input('Repo name:'))
        except GithubRepoExists:
            try:
                print("A repository with that name already exists")
                skill_repo = creator.link_github_repo(
                    lambda: input('Remote repo name:'))
            except UnrelatedGithubHistory:
                print("Repository history does not seem to be related")
                skill_repo = creator.force_push(
                    lambda: input('Confirm repo name:'))
        if skill_repo:
            self.entry.url = skill_repo.html_url
            self.entry.author = self.user.login
        else:
            if not self.entry.url:
                raise NoGitRepository
            skill_repo = self.github.get_repo(skill_repo_name(self.entry.url))

        if not skill_repo.permissions.push:
            print(
                'Warning: You do not have write permissions to the provided skill repo.'
            )
            if ask_yes_no('Create a fork and use that instead? (Y/n)', True):
                skill_repo = self.user.create_fork(skill_repo)
                print('Created fork:', skill_repo.html_url)
                self.git.remote('rename', 'origin', 'upstream')
                self.git.remote('add', 'origin', skill_repo.html_url)

        # verify that the required files exists in origin and contain the
        # required content.
        if not self.check_valid():
            print("Please add the missing information and rerun the command.")
            return

        self.entry.name = input(
            'Enter a unique skill name (ie. npr-news or grocery-list): ')

        readme_file = {i.lower(): i
                       for i in os.listdir(self.entry.path)}['readme.md']
        readme = read_file(self.entry.path, readme_file)

        last_section = None
        sections = {last_section: ''}
        for line in readme.split('\n'):
            line = line.strip()
            if line.startswith('#'):
                last_section = line.strip('# ').lower()
                sections[last_section] = ''
            else:
                sections[last_section] += '\n' + line
        del sections[None]

        if 'about' in sections:
            description = sections['about']
        elif 'description' in sections:
            description = sections['description']

        branch = SkillData(self.entry).add_to_repo()
        self.repo.push_to_fork(branch)

        pull = create_or_edit_pr(title='Add {}'.format(self.entry.name),
                                 body=body_template.format(
                                     description=description,
                                     skill_name=self.entry.name,
                                     skill_url=skill_repo.html_url),
                                 user=self.user,
                                 branch=branch,
                                 skills_repo=self.repo.hub,
                                 repo_branch=self.branch)

        print('Created pull request: ', pull.html_url)