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)
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')
class ConsoleAction(GlobalContext, metaclass=ABCMeta): @staticmethod @abstractmethod def register(parser: ArgumentParser): pass @abstractmethod def perform(self): pass repo = Lazy(lambda s: RepoData()) # type: RepoData
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)
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)
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
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
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)
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)
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)