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
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)
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 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 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 perform(self): if not isdir(self.folder): raise MskException('Skill folder at {} does not exist'.format(self.folder)) if not isfile(join(self.folder, '__init__.py')): if not ask_yes_no("Folder doesn't appear to be a skill. Continue? (y/N)", False): return makedirs(join(self.folder, 'test', 'intent'), exist_ok=True) creator = TestCreator(self.folder) test_case = creator.adapt_creator.test_case or creator.padatious_creator.test_case intent_test_file = self.find_intent_test_file(creator.intent_name) with open(intent_test_file, 'w') as f: json.dump(test_case, f, indent=4, sort_keys=True) print('Generated test file:', intent_test_file)
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
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)
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)