def create_template_with_subdomain(self, domain_path: str,
                                       subdomain: str) -> None:
        """
        Creates a chosen template that **does** have a subdomain.
        Calls cookiecutter on the main chosen template.

        :param domain_path: Path to the template, which is still in cookiecutter format
        :param subdomain: Subdomain of the chosen template
        """
        occupied = os.path.isdir(f'{self.CWD}/{self.creator_ctx.project_slug}')
        if occupied:
            self.directory_exists_warning()

            # Confirm proceeding with overwriting existing directory
            if cookietemple_questionary_or_dot_cookietemple(
                    'confirm', 'Do you really want to continue?',
                    default='Yes'):
                delete_dir_tree(
                    Path(f'{self.CWD}/{self.creator_ctx.project_slug}'))
                cookiecutter(
                    f'{domain_path}/{subdomain}_{self.creator_ctx.language.lower()}',
                    no_input=True,
                    overwrite_if_exists=True,
                    extra_context=self.creator_ctx_to_dict())

            else:
                console.print('[bold red]Aborted! Canceled template creation!')
                sys.exit(0)
        else:
            cookiecutter(
                f'{domain_path}/{subdomain}_{self.creator_ctx.language.lower()}',
                no_input=True,
                overwrite_if_exists=True,
                extra_context=self.creator_ctx_to_dict())
 def check_name_available(self, host, dot_cookietemple) -> None:
     """
     Main function that calls the queries for the project name lookup at PyPi and readthedocs.io
     """
     # if project already exists at either PyPi or readthedocs, ask user for confirmation with the option to change the project name
     while TemplateCreator.query_name_available(
             host, self.creator_ctx.project_name
     ) and not dot_cookietemple:  # type: ignore
         console.print(
             f'[bold red]A project named {self.creator_ctx.project_name} already exists at {host}!'
         )
         # provide the user an option to change the project's name
         if cookietemple_questionary_or_dot_cookietemple(
                 function='confirm',
                 question=
                 'Do you want to choose another name for your project?\n'
                 f'Otherwise you will not be able to host your project at {host}!',
                 default='Yes'):
             self.creator_ctx.project_name = cookietemple_questionary_or_dot_cookietemple(
                 function='text',
                 question='Project name',
                 default='Exploding Springfield')
         # continue if the project should be named anyways
         else:
             break
    def create_template_with_subdomain_framework(self, domain_path: str, subdomain: str, framework: str) -> None:
        """
        Creates a chosen template that **does** have a subdomain.
        Calls cookiecutter on the main chosen template.

        :param domain_path: Path to the template, which is still in cookiecutter format
        :param subdomain: Subdomain of the chosen template
        :param framework: Chosen framework
        """
        occupied = os.path.isdir(f"{self.CWD}/{self.creator_ctx.project_slug}")
        if occupied:
            self.directory_exists_warning()

            # Confirm proceeding with overwriting existing directory
            if cookietemple_questionary_or_dot_cookietemple(
                "confirm", "Do you really want to continue?", default="Yes"
            ):
                cookiecutter(
                    f"{domain_path}/{subdomain}_{self.creator_ctx.language.lower()}/{framework}",
                    no_input=True,
                    overwrite_if_exists=True,
                    extra_context=self.creator_ctx_to_dict(),
                )

            else:
                console.print("[bold red]Aborted! Canceled template creation!")
                sys.exit(0)
        else:
            cookiecutter(
                f"{domain_path}/{subdomain}_{self.creator_ctx.language.lower()}/{framework}",
                no_input=True,
                overwrite_if_exists=True,
                extra_context=self.creator_ctx_to_dict(),
            )
Example #4
0
def bump_version(ctx, new_version, project_dir, downgrade, tag) -> None:
    """
    Bump the version of an existing cookietemple project.

    INFO on valid versions: All versions must match the format like 1.0.0 or 1.1.0-SNAPSHOT; these are the only valid
    version formats cookietemple allows. A valid version therefore contains a three digits (in the range from 0 to however large it will grow)
    separated by two dots.
    Optional is the -SNAPSHOT at the end (for JVM templates especially). NOTE that versions like 1.2.3.4 or 1.2 WILL NOT be recognized as valid versions as
    well as no substring of them will be recognized.

    Unless the user uses downgrade mode via the -d flag, a downgrade of a version is never allowed. Note that bump-version with the new version
    equals the current version is never allowed, either with or without -d.

    Users can create a commit tag for the version bump commit by using the --tag/-t flag.
    """
    create_tag = True if tag else False
    version_bumper = VersionBumper(project_dir, downgrade)
    # suggest valid version if none was given
    if not new_version:
        new_version = version_bumper.choose_valid_version()
    # if the path entered ends with a trailing slash remove it for consistent output
    if str(project_dir).endswith("/"):
        project_dir = Path(
            str(project_dir).replace(
                str(project_dir)[len(str(project_dir)) - 1:], ""))

    # lint before run bump-version
    version_bumper.lint_before_bump()
    # only run bump-version if conditions are met
    if version_bumper.can_run_bump_version(new_version):
        # only run "sanity" checker when the downgrade flag is not set
        if not downgrade:
            # if the check fails, ask the user for confirmation
            if version_bumper.check_bump_range(
                    version_bumper.CURRENT_VERSION.split("-")[0],
                    new_version.split("-")[0]):
                version_bumper.bump_template_version(new_version, project_dir,
                                                     create_tag)
            elif cookietemple_questionary_or_dot_cookietemple(
                    function="confirm",
                    question=
                    f"Bumping from {version_bumper.CURRENT_VERSION} to {new_version} seems not reasonable.\n"
                    f"Do you really want to bump the project version?",
                    default="n",
            ):
                console.print("\n")
                version_bumper.bump_template_version(new_version, project_dir,
                                                     create_tag)
        else:
            version_bumper.bump_template_version(new_version, project_dir,
                                                 create_tag)
    else:
        sys.exit(1)
    def process_common_operations(
            self,
            path: Path,
            skip_common_files=False,
            skip_fix_underline=False,
            domain: Optional[str] = None,
            subdomain: Union[str, bool] = None,
            language: Union[str, bool] = None,
            dot_cookietemple: Optional[dict] = None) -> None:
        """
        Create all stuff that is common for cookietemples template creation process; in detail those things are:
        create and copy common files, fix docs style, lint the project and ask whether the user wants to create a github repo.
        """
        # create the common files and copy them into the templates directory (skip if flag is set)

        if not skip_common_files:
            self.create_common_files()

        self.create_dot_cookietemple(
            template_version=self.creator_ctx.template_version)

        if self.creator_ctx.language == 'python':
            project_path = f'{self.CWD}/{self.creator_ctx.project_slug.replace("-", "_")}'
        else:
            project_path = f'{self.CWD}/{self.creator_ctx.project_slug}'

        # Ensure that docs are looking good (skip if flag is set)
        if not skip_fix_underline:
            fix_short_title_underline(f'{project_path}/docs/index.rst')

        # Lint the project to verify that the new template adheres to all standards
        lint_project(project_path, is_create=True, skip_external=False)

        if self.creator_ctx.is_github_repo and not dot_cookietemple:
            # rename the currently created template to a temporary name, create Github repo, push, remove temporary template
            tmp_project_path = f'{project_path}_cookietemple_tmp'
            os.mkdir(tmp_project_path)
            create_push_github_repository(project_path, self.creator_ctx,
                                          tmp_project_path)
            shutil.rmtree(tmp_project_path, ignore_errors=True)

        if subdomain:
            console.print()
            console.print(
                '[bold blue]Please visit: https://cookietemple.readthedocs.io/en/latest/available_templates/available_templates.html'
                f'#{domain}-{subdomain}-{language} for more information about how to use your chosen template.'
            )
        else:
            console.print()
            console.print(
                '[bold blue]Please visit: https://cookietemple.readthedocs.io/en/latest/available_templates/available_templates.html'
                f'#{domain}-{language} for more information about how to use your chosen template.'
            )

        # do not move if path is current working directory or a directory named like the project in the current working directory (second is default case)
        if path != self.CWD and path != Path(
                self.CWD / self.creator_ctx.project_slug_no_hyphen):
            shutil.move(
                f'{self.CWD}/{self.creator_ctx.project_slug_no_hyphen}',
                f'{path}/{self.creator_ctx.project_slug_no_hyphen}')
Example #6
0
def get_template_handle(
        dot_cookietemple_path: str = ".cookietemple.yml") -> str:
    """
    Reads the .cookietemple file and extracts the template handle
    :param dot_cookietemple_path: path to the .cookietemple file
    :return: found template handle
    """
    path = Path(f"{dot_cookietemple_path}/.cookietemple.yml")
    if not path.exists():
        console.print(
            "[bold red].cookietemple.yml not found. Is this a cookietemple project?"
        )
        sys.exit(1)
    yaml = YAML(typ="safe")
    dot_cookietemple_content = yaml.load(path)

    return dot_cookietemple_content["template_handle"]
Example #7
0
def create_github_labels(repo, labels: list) -> None:
    """
    Create github labels and add them to the repository.
    If failed, print error message.

    :param repo: The repository where the label needs to be added
    :param labels: A list of the new labels to be added
    """
    for label in labels:
        log.debug(f"Creating Github label {label[0]}")
        try:
            repo.create_label(name=label[0], color=label[1])
        except GithubException:
            log.debug(f"Unable to create label {label[0]}")
            console.print(
                f"[bold red]Unable to create label {label[0]} due to permissions"
            )
Example #8
0
def handle_failed_github_repo_creation(
        e: Union[ConnectionError, GithubException]) -> None:
    """
    Called, when the automatic GitHub repo creation process failed during the create process. As this may have various issue sources,
    try to provide the user a detailed error message for the individual exception and inform them about what they should/can do next.

    :param e: The exception that has been thrown
    """
    # output the error dict thrown by PyGitHub due to an error related to GitHub
    if isinstance(e, GithubException):
        console.print(
            "[bold red]\nError while trying to create a Github repo due to an error related to Github API. "
            "See below output for detailed information!\n")
        format_github_exception(e.data)
    # output an error that might occur due to a missing internet connection
    elif isinstance(e, ConnectionError):
        console.print(
            "[bold red]Error while trying to establish a connection to https://github.com. Do you have an active internet connection?"
        )
Example #9
0
def is_git_accessible() -> bool:
    """
    Verifies that git is accessible and in the PATH.

    :return: True if accessible, false if not
    """
    log.debug("Testing whether git is accessible.")
    git_installed = Popen(["git", "--version"],
                          stdout=PIPE,
                          stderr=PIPE,
                          universal_newlines=True)
    (git_installed_stdout, git_installed_stderr) = git_installed.communicate()
    if git_installed.returncode != 0:
        console.print(
            "[bold red]Could not find 'git' in the PATH. Is it installed?")
        console.print("[bold red]Run command was: 'git --version '")
        log.debug("git is not accessible!")
        return False

    return True
Example #10
0
def bump_version(ctx, new_version, project_dir, downgrade) -> None:
    """
    Bump the version of an existing cookietemple project.

    INFO on valid versions: All versions must match the format like 1.0.0 or 1.1.0-SNAPSHOT; these are the only valid
    version formats cookietemple allows. A valid version therefore contains a three digits (in the range from 0 to however large it will grow)
    separated by two dots.
    Optional is the -SNAPSHOT at the end (for JVM templates especially). NOTE that versions like 1.2.3.4 or 1.2 WILL NOT be recognized as valid versions as
    well as no substring of them will be recognized.

    Unless the user uses downgrade mode via the -d flag, a downgrade of a version is never allowed. Note that bump-version with the new version
    equals the current version is never allowed, either with or without -d.
    """
    if not new_version:
        HelpErrorHandling.args_not_provided(ctx, 'bump-version')
    else:
        # if the path entered ends with a trailing slash remove it for consistent output
        if str(project_dir).endswith('/'):
            project_dir = Path(str(project_dir).replace(str(project_dir)[len(str(project_dir)) - 1:], ''))

        version_bumper = VersionBumper(project_dir, downgrade)
        # lint before run bump-version
        version_bumper.lint_before_bump()
        # only run bump-version if conditions are met
        if version_bumper.can_run_bump_version(new_version):
            # only run "sanity" checker when the downgrade flag is not set
            if not downgrade:
                # if the check fails, ask the user for confirmation
                if version_bumper.check_bump_range(version_bumper.CURRENT_VERSION.split('-')[0], new_version.split('-')[0]):
                    version_bumper.bump_template_version(new_version, project_dir)
                elif cookietemple_questionary_or_dot_cookietemple(function='confirm',
                                                                  question=f'Bumping from {version_bumper.CURRENT_VERSION} to {new_version} seems not reasonable.\n'
                                                                           f'Do you really want to bump the project version?',
                                                                  default='n'):
                    console.print('\n')
                    version_bumper.bump_template_version(new_version, project_dir)
            else:
                version_bumper.bump_template_version(new_version, project_dir)
        else:
            sys.exit(1)
Example #11
0
def decrypt_pat() -> str:
    """
    Decrypt the encrypted PAT.

    :return: The decrypted Personal Access Token for GitHub
    """
    log.debug(
        f"Decrypting personal access token using key saved in {ConfigCommand.KEY_PAT_FILE}."
    )
    # read key and encrypted PAT from files
    with open(ConfigCommand.KEY_PAT_FILE, "rb") as f:
        key = f.readline()
    fer = Fernet(key)
    log.debug(
        f"Reading personal access token from {ConfigCommand.CONF_FILE_PATH}.")
    encrypted_pat = load_yaml_file(ConfigCommand.CONF_FILE_PATH)["pat"]
    # decrypt the PAT and decode it to string
    console.print("[bold blue]Decrypting personal access token.")
    log.debug("Successfully decrypted personal access token.")
    decrypted_pat = fer.decrypt(encrypted_pat).decode("utf-8")

    return decrypted_pat
    def query_name_available(host: str, project_name: str) -> bool:
        """
        Make a GET request to the host to check whether a project with this name already exists.
        :param host The host (either PyPi or readthedocs)
        :param project_name Name of the project the user wants to create

        :return: Whether request was successful (name already taken on host) or not
        """
        # check if host is either PyPi or readthedocs.io; only relevant for developers working on this code
        if host not in {"PyPi", "readthedocs.io"}:
            log.debug(
                f'[bold red]Unknown host {host}. Use either PyPi or readthedocs.io!'
            )
            # raise a ValueError if the host name is invalid
            raise ValueError(
                f'check_name_available has been called with the invalid host {host}.\nValid hosts are PyPi and readthedocs.io'
            )
        console.print(f'[bold blue]Looking up {project_name} at {host}!')
        # determine url depending on host being either PyPi or readthedocs.io
        url = 'https://' + (f'pypi.org/project/{project_name.replace(" ", "")}'
                            if host == "PyPi" else
                            f'{project_name.replace(" ", "")}.readthedocs.io')
        log.debug(f'Looking up {url}')
        try:
            request = requests.get(url)
            if request.status_code == 200:
                return True
        # catch exceptions when server may be unavailable or the request timed out
        except (requests.exceptions.ConnectionError,
                requests.exceptions.Timeout) as e:
            log.debug(f'Unable to contact {host}')
            log.debug(f'Error was: {e}')
            console.print(
                f'[bold red]Cannot check whether name already taken on {host} because its unreachable at the moment!'
            )
            return False

        return False
 def directory_exists_warning(self) -> None:
     """
     If the directory is already a git directory within the same project, print error message and exit.
     Otherwise print a warning that a directory already exists and any further action on the directory will overwrite its contents.
     """
     if is_git_repo(Path(f"{self.CWD}/{self.creator_ctx.project_slug}")):
         console.print(
             f"[bold red]Error: A git project named {self.creator_ctx.project_slug} already exists at [green]{self.CWD}\n"
         )
         console.print("[bold red]Aborting!")
         sys.exit(1)
     else:
         console.print(
             f"[bold yellow]WARNING: [red]A directory named {self.creator_ctx.project_slug} already exists at [blue]{self.CWD}\n"
         )
         console.print("Proceeding now will overwrite this directory and its content!")
Example #14
0
def format_github_exception(data: dict) -> None:
    """
    Format the github exception thrown by PyGitHub in a nice way and output it.

    :param data: The exceptions data as a dict
    """
    for section, description in data.items():
        if not isinstance(description, list):
            console.print(f"[bold red]{section.capitalize()}: {description}")
        else:
            console.print(f"[bold red]{section.upper()}: ")
            messages = [
                val if not isinstance(val, dict) and not isinstance(val, set)
                else github_exception_dict_repr(val) for val in description
            ]  # type: ignore
            console.print("[bold red]\n".join(msg for msg in messages))
Example #15
0
def main():
    traceback.install(width=200, word_wrap=True)
    console.print(rf"""[bold blue]
     ██████  ██████   ██████  ██   ██ ██ ███████ ████████ ███████ ███    ███ ██████  ██      ███████ 
    ██      ██    ██ ██    ██ ██  ██  ██ ██         ██    ██      ████  ████ ██   ██ ██      ██      
    ██      ██    ██ ██    ██ █████   ██ █████      ██    █████   ██ ████ ██ ██████  ██      █████ 
    ██      ██    ██ ██    ██ ██  ██  ██ ██         ██    ██      ██  ██  ██ ██      ██      ██    
     ██████  ██████   ██████  ██   ██ ██ ███████    ██    ███████ ██      ██ ██      ███████ ███████ 
        """)

    console.print('[bold blue]Run [green]cookietemple --help [blue]for an overview of all commands\n')

    # Is the latest cookietemple version installed? Upgrade if not!
    if not UpgradeCommand.check_cookietemple_latest():
        console.print('[bold blue]Run [green]cookietemple upgrade [blue]to get the latest version.')
    cookietemple_cli()
    def prompt_general_template_configuration(
            self, dot_cookietemple: Optional[dict]):
        """
        Prompts the user for general options that are required by all templates.
        Options are saved in the creator context manager object.
        """
        try:
            """
            Check, if the dot_cookietemple dictionary contains the full name and email (this happens, when dry creating the template while syncing on
            TEMPLATE branch).
            If that's not the case, try to read them from the config file (created with the config command).

            If none of the approaches above succeed (no config file has been found and its not a dry create run), configure the basic credentials and proceed.
            """
            if dot_cookietemple:
                self.creator_ctx.full_name = dot_cookietemple['full_name']
                self.creator_ctx.email = dot_cookietemple['email']
            else:
                self.creator_ctx.full_name = load_yaml_file(
                    ConfigCommand.CONF_FILE_PATH)['full_name']
                self.creator_ctx.email = load_yaml_file(
                    ConfigCommand.CONF_FILE_PATH)['email']
        except FileNotFoundError:
            # style and automatic use config
            console.print(
                '[bold red]Cannot find a cookietemple config file. Is this your first time using cookietemple?'
            )
            # inform the user and config all settings (with PAT optional)
            console.print(
                '[bold blue]Lets set your name, email and Github username and you´re ready to go!'
            )
            ConfigCommand.all_settings()
            # load mail and full name
            path = Path(ConfigCommand.CONF_FILE_PATH)
            yaml = YAML(typ='safe')
            settings = yaml.load(path)
            # set full name and mail
            self.creator_ctx.full_name = settings['full_name']
            self.creator_ctx.email = settings['email']

        self.creator_ctx.project_name = cookietemple_questionary_or_dot_cookietemple(
            function='text',
            question='Project name',
            default='exploding-springfield',
            dot_cookietemple=dot_cookietemple,
            to_get_property='project_name').lower()  # type: ignore
        if self.creator_ctx.language == 'python':
            self.check_name_available("PyPi", dot_cookietemple)
        self.check_name_available("readthedocs.io", dot_cookietemple)
        self.creator_ctx.project_slug = self.creator_ctx.project_name.replace(
            ' ', '_')  # type: ignore
        self.creator_ctx.project_slug_no_hyphen = self.creator_ctx.project_slug.replace(
            '-', '_')
        self.creator_ctx.project_short_description = cookietemple_questionary_or_dot_cookietemple(
            function='text',
            question='Short description of your project',
            default=f'{self.creator_ctx.project_name}'
            f'. A cookietemple based .',
            dot_cookietemple=dot_cookietemple,
            to_get_property='project_short_description')
        poss_vers = cookietemple_questionary_or_dot_cookietemple(
            function='text',
            question='Initial version of your project',
            default='0.1.0',
            dot_cookietemple=dot_cookietemple,
            to_get_property='version')

        # make sure that the version has the right format
        while not re.match(r'(?<!.)\d+(?:\.\d+){2}(?:-SNAPSHOT)?(?!.)',
                           poss_vers) and not dot_cookietemple:  # type: ignore
            console.print(
                '[bold red]The version number entered does not match semantic versioning.\n'
                +
                'Please enter the version in the format \[number].\[number].\[number]!'
            )  # noqa: W605
            poss_vers = cookietemple_questionary_or_dot_cookietemple(
                function='text',
                question='Initial version of your project',
                default='0.1.0')
        self.creator_ctx.version = poss_vers

        self.creator_ctx.license = cookietemple_questionary_or_dot_cookietemple(
            function='select',
            question='License',
            choices=[
                'MIT', 'BSD', 'ISC', 'Apache2.0', 'GNUv3', 'Boost', 'Affero',
                'CC0', 'CCBY', 'CCBYSA', 'Eclipse', 'WTFPL', 'unlicence',
                'Not open source'
            ],
            default='MIT',
            dot_cookietemple=dot_cookietemple,
            to_get_property='license')
        if dot_cookietemple:
            self.creator_ctx.github_username = dot_cookietemple[
                'github_username']
            self.creator_ctx.creator_github_username = dot_cookietemple[
                'creator_github_username']
        else:
            self.creator_ctx.github_username = load_github_username()
            self.creator_ctx.creator_github_username = self.creator_ctx.github_username
Example #17
0
def create_push_github_repository(project_path: str,
                                  creator_ctx: CookietempleTemplateStruct,
                                  tmp_repo_path: str) -> None:
    """
    Creates a Github repository for the created template and pushes the template to it.
    Prompts the user for the required specifications.

    :param creator_ctx: Full Template Struct. Github username may be updated if an organization repository is warranted.
    :param project_path: The path to the recently created project
    :param tmp_repo_path: Path to the empty cloned repo
    """
    try:
        if not is_git_accessible():
            return

        # the personal access token for GitHub
        access_token = handle_pat_authentification()

        # Login to Github
        log.debug("Logging into Github.")
        console.print("[bold blue]Logging into Github")
        authenticated_github_user = Github(access_token)
        user = authenticated_github_user.get_user()

        # Create new repository
        console.print("[bold blue]Creating Github repository")
        if creator_ctx.is_github_orga:
            log.debug(
                f"Creating a new Github repository for organizaton: {creator_ctx.github_orga}."
            )
            org = authenticated_github_user.get_organization(
                creator_ctx.github_orga)
            repo = org.create_repo(
                creator_ctx.project_slug,
                description=creator_ctx.
                project_short_description,  # type: ignore
                private=creator_ctx.is_repo_private,
            )
            creator_ctx.github_username = creator_ctx.github_orga
        else:
            log.debug(
                f"Creating a new Github repository for user: {creator_ctx.github_username}."
            )
            repo = user.create_repo(
                creator_ctx.project_slug,
                description=creator_ctx.
                project_short_description,  # type: ignore
                private=creator_ctx.is_repo_private,
            )

        console.print("[bold blue]Creating labels and default Github settings")
        create_github_labels(repo=repo, labels=[("DEPENDABOT", "1BB0CE")])

        repository = f"{tmp_repo_path}"

        # NOTE: github_username is the organizations name, if an organization repository is to be created

        # create the repos sync secret
        console.print("[bold blue]Creating repository sync secret")
        create_sync_secret(creator_ctx.github_username,
                           creator_ctx.project_slug, access_token)
        # create repo cookietemple topic
        console.print("[bold blue]Creating repository topic")
        create_ct_topic(creator_ctx.github_username, creator_ctx.project_slug,
                        access_token)

        # git clone
        console.print("[bold blue]Cloning empty Github repository")
        log.debug(
            f"Cloning repository {creator_ctx.github_username}/{creator_ctx.project_slug}"
        )
        Repo.clone_from(
            f"https://{creator_ctx.github_username}:{access_token}@github.com/{creator_ctx.github_username}/{creator_ctx.project_slug}",
            repository,
        )

        log.debug(
            "Copying files from the template into the cloned repository.")
        # Copy files which should be included in the initial commit -> basically the template
        copy_tree(f"{repository}", project_path)

        # the created project repository with the copied .git directory
        cloned_repo = Repo(path=project_path)

        # git add
        log.debug("git add")
        console.print("[bold blue]Staging template")
        cloned_repo.git.add(A=True)

        # git commit
        log.debug("git commit")
        cloned_repo.index.commit(
            f"Create {creator_ctx.project_slug} with {creator_ctx.template_handle} "
            f'template of version {creator_ctx.template_version.replace("# <<COOKIETEMPLE_NO_BUMP>>", "")} using cookietemple.'
        )

        # get the default branch of the repository as default branch of GitHub repositories are nor configurable by the user and can be set to any branch name
        # but cookietemple needs to know which one is the default branch in order to push to the correct remote branch and rename local branch, if necessary
        headers = {"Authorization": f"token {access_token}"}
        url = f"https://api.github.com/repos/{creator_ctx.github_username}/{creator_ctx.project_slug}"
        response = requests.get(url, headers=headers).json()
        default_branch = response["default_branch"]
        log.debug(f"git push origin {default_branch}")
        console.print(
            f"[bold blue]Pushing template to Github origin {default_branch}")
        if default_branch != "master":
            cloned_repo.git.branch("-M", f"{default_branch}")
        cloned_repo.remotes.origin.push(
            refspec=f"{default_branch}:{default_branch}")

        # set branch protection (all WF must pass, dismiss stale PR reviews) only when repo is public
        log.debug("Set branch protection rules.")

        if not creator_ctx.is_repo_private and not creator_ctx.is_github_orga:
            main_branch = (authenticated_github_user.get_user().get_repo(
                name=creator_ctx.project_slug).get_branch(f"{default_branch}"))
            main_branch.edit_protection(dismiss_stale_reviews=True)
        else:
            console.print(
                "[bold blue]Cannot set branch protection rules due to your repository being private or an organization repo!\n"
                "You can set them manually later on.")

        # git create development branch
        log.debug("git checkout -b development")
        console.print("[bold blue]Creating development branch.")
        cloned_repo.git.checkout("-b", "development")

        # git push to origin development
        log.debug("git push origin development")
        console.print(
            "[bold blue]Pushing template to Github origin development.")
        cloned_repo.remotes.origin.push(refspec="development:development")

        # git create TEMPLATE branch
        log.debug("git checkout -b TEMPLATE")
        console.print("[bold blue]Creating TEMPLATE branch.")
        cloned_repo.git.checkout("-b", "TEMPLATE")

        # git push to origin TEMPLATE
        log.debug("git push origin TEMPLATE")
        console.print("[bold blue]Pushing template to Github origin TEMPLATE.")
        cloned_repo.remotes.origin.push(refspec="TEMPLATE:TEMPLATE")

        # finally, checkout to development branch
        log.debug("git checkout development")
        console.print("[bold blue]Checking out development branch.")
        cloned_repo.git.checkout("development")

        # did any errors occur?
        console.print(
            f"[bold green]Successfully created a Github repository at https://github.com/{creator_ctx.github_username}/{creator_ctx.project_slug}"
        )

    except (GithubException, ConnectionError) as e:
        handle_failed_github_repo_creation(e)
Example #18
0
def prompt_github_repo(
        dot_cookietemple: Optional[dict]) -> Tuple[bool, bool, bool, str]:
    """
    Ask user for all settings needed in order to create and push automatically to GitHub repo.

    :param dot_cookietemple: .cookietemple.yml content if passed
    :return if is git repo, if repo should be private, if user is an organization and if so, the organizations name
    """
    # if dot_cookietemple dict was passed -> return the Github related properties and do NOT prompt for them
    try:
        if dot_cookietemple:
            if not dot_cookietemple["is_github_orga"]:
                return dot_cookietemple["is_github_repo"], dot_cookietemple[
                    "is_repo_private"], False, ""
            else:
                return (
                    dot_cookietemple["is_github_repo"],
                    dot_cookietemple["is_repo_private"],
                    dot_cookietemple["is_github_orga"],
                    dot_cookietemple["github_orga"],
                )
    except KeyError:
        console.print(
            "[bold red]Missing required Github properties in .cookietemple.yml file!"
        )

    # No dot_cookietemple_dict was passed -> prompt whether to create a Github repository and the required settings
    create_git_repo, private, is_github_org, github_org = False, False, False, ""
    console.print(
        "[bold blue]Automatically creating a Github repository with cookietemple is strongly recommended. "
        "Otherwise you will not be able to use all of cookietemple's features!\n"
    )

    if cookietemple_questionary_or_dot_cookietemple(
            function="confirm",
            question=
            "Do you want to create a Github repository and push your template to it?",
            default="Yes",
    ):
        create_git_repo = True
        is_github_org = cookietemple_questionary_or_dot_cookietemple(
            function="confirm",  # type: ignore
            question="Do you want to create an organization repository?",
            default="No",
        )
        github_org = (
            cookietemple_questionary_or_dot_cookietemple(
                function="text",  # type: ignore
                question="Please enter the name of the Github organization",
                default="SpringfieldNuclearPowerPlant",
            ) if is_github_org else "")
        console.print(
            "[bold yellow]Be aware that cookietemple runs github actions for various Python versions (see noxfile.py. "
            "Private projects may be run out of build minutes in a matter of hours when "
            " there is a lot activity or Dependabot is enabled.")

        private = cookietemple_questionary_or_dot_cookietemple(
            function="confirm",
            question="Do you want your repository to be private?",
            default="No"  # type: ignore
        )
    return create_git_repo, private, is_github_org, github_org
Example #19
0
def handle_pat_authentification() -> str:
    """
    Try to read the encrypted Personal Access Token for GitHub.
    If this fails (maybe there was no generated key before) notify user to config its credentials for cookietemple.

    :return: The decrypted PAT
    """
    # check if the key and encrypted PAT already exist
    log.debug(
        f"Attempting to read the personal access token from {ConfigCommand.CONF_FILE_PATH}"
    )
    if os.path.exists(ConfigCommand.CONF_FILE_PATH):
        path = Path(ConfigCommand.CONF_FILE_PATH)
        yaml = YAML(typ="safe")
        settings = yaml.load(path)
        if os.path.exists(ConfigCommand.KEY_PAT_FILE) and "pat" in settings:
            pat = decrypt_pat()
            return pat
        else:
            log.debug(
                f"Unable to read the personal access token from {ConfigCommand.CONF_FILE_PATH}"
            )
            console.print(
                "[bold red]Could not find encrypted personal access token!\n")
            console.print(
                "[bold blue]Please navigate to Github -> Your profile -> Settings -> Developer Settings -> Personal access token -> "
                "Generate a new Token")
            console.print(
                "[bold blue]Only tick 'repo'. The token is a hidden input to cookietemple and stored encrypted locally on your machine."
            )
            console.print(
                "[bold blue]For more information please read" +
                "https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line\n\n"
            )
            console.print(
                "[bold blue]Lets move on to set your personal access token for your cookietemple project!"
            )
            # set the PAT
            ConfigCommand.config_pat()
            # if the user wants to create a GitHub repo but accidentally presses no on PAT config prompt
            if not os.path.exists(ConfigCommand.KEY_PAT_FILE):
                console.print(
                    "[bold red]No Github personal access token found. Please set it using [green]cookietemple config github"
                )
                sys.exit(1)
            else:
                pat = decrypt_pat()
            return pat
    else:
        console.print(
            "[bold red]Cannot find a cookietemple config file! Did you delete it?"
        )

    return ""
Example #20
0
    def print_results(self):
        console.print()
        console.rule("[bold green] LINT RESULTS")
        console.print()
        console.print(
            f'     [bold green][[\u2714]] {len(self.passed):>4} tests passed\n     [bold yellow][[!]] {len(self.warned):>4} tests had warnings\n'
            f'     [bold red][[\u2717]] {len(self.failed):>4} tests failed',
            overflow="ellipsis",
            highlight=False,
        )

        # Helper function to format test links nicely
        def format_result(test_results):
            """
            Given an list of error message IDs and the message texts, return a nicely formatted
            string for the terminal with appropriate ASCII colours.
            """
            results = []
            for eid, msg in test_results:
                results.append(
                    f"1. [https://cookietemple.readthedocs.io/en/latest/lint.html#{eid}]"
                    f"(https://cookietemple.readthedocs.io/en/latest/lint.html#{eid}) : {msg}"
                )
            return rich.markdown.Markdown("\n".join(results))

        if len(self.passed) > 0:
            console.print()
            console.rule("[bold green][[\u2714]] Tests Passed", style="green")
            console.print(rich.panel.Panel(format_result(self.passed),
                                           style="green"),
                          overflow="ellipsis")
        if len(self.warned) > 0:
            console.print()
            console.rule("[bold yellow][[!]] Test Warnings", style="yellow")
            console.print(rich.panel.Panel(format_result(self.warned),
                                           style="yellow"),
                          overflow="ellipsis")
        if len(self.failed) > 0:
            console.print()
            console.rule("[bold red][[\u2717]] Test Failures", style="red")
            console.print(rich.panel.Panel(format_result(self.failed),
                                           style="red"),
                          overflow="ellipsis")
Example #21
0
def sync(project_dir, set_token, pat, username, check_update) -> None:
    """
    Sync your project with the latest template release.

    cookietemple regularly updates its templates.
    To ensure that you have the latest changes you can invoke sync, which submits a pull request to your Github repository (if existing).
    If no repository exists the TEMPLATE branch will be updated and you can merge manually.
    """
    project_dir_path = Path(project_dir).resolve()
    log.debug(f'Set project top level path to given path argument {project_dir_path}')
    # if set_token flag is set, update the sync token value and exit
    if set_token:
        log.debug('Running sync to update sync token in repo.')
        try:
            log.debug(f'Loading project information from .cookietemple.yml file located at {project_dir}')
            project_data = load_yaml_file(f'{project_dir}/.cookietemple.yml')
            # if project is an orga repo, pass orga name as username
            if project_data['is_github_repo'] and project_data['is_github_orga']:
                log.debug(f'Project is a Github orga repo. Using {project_data["github_orga"]} as username.')
                TemplateSync.update_sync_token(project_name=project_data['project_slug'], gh_username=project_data['github_orga'])
            # if not, use default username
            elif project_data['is_github_repo']:
                log.debug(f'Project is not a Github orga repo.')
                TemplateSync.update_sync_token(project_name=project_data['project_slug'])
            else:
                console.print('[bold red]Your current project does not seem to have a Github repository!')
                sys.exit(1)
        except (FileNotFoundError, KeyError):
            console.print(f'[bold red]Your token value is not a valid personal access token for your account or there exists no .cookietemple.yml file at '
                          f'{project_dir_path}. Is this a cookietemple project?')
            sys.exit(1)
        sys.exit(0)

    log.debug(f'Initializing syncer object.')
    syncer = TemplateSync(new_template_version='', project_dir=project_dir_path, gh_username=username, token=pat)
    # check for template version updates
    log.debug(f'Checking for major/minor or patch version changes in cookietemple templates.')
    major_change, minor_change, patch_change, proj_template_version, ct_template_version = TemplateSync.has_template_version_changed(project_dir_path)
    syncer.new_template_version = ct_template_version
    # check for user without actually syncing
    if check_update:
        log.debug('Running snyc to manually check whether a new template version is available.')
        # a template update has been released by cookietemple
        if any(change for change in (major_change, minor_change, patch_change)):
            console.print(f'[bold blue]Your templates version received an update from {proj_template_version} to {ct_template_version}!\n'
                          f' Use [green]cookietemple sync [blue]to sync your project')
        # no updates were found
        else:
            console.print('[bold blue]Using the latest template version. No sync required.')
        # exit without syncing
        sys.exit(0)
    # set sync flags indicating a major, minor or patch update
    syncer.major_update = major_change
    syncer.minor_update = minor_change
    syncer.patch_update = patch_change
    log.debug('Major template update found.' if major_change else 'Minor template update found.' if minor_change else 'Patch template update found.' if
              patch_change else 'No template update found.')
    # sync the project if any changes
    if any(change for change in (major_change, minor_change, patch_change)):
        if syncer.should_run_sync():
            # check if a pull request should be created according to set level constraints
            log.debug('Starting sync.')
            syncer.sync()
        else:
            console.print('[bold red]Aborting sync due to set level constraints or sync being disabled. You can set the level any time in your cookietemple.cfg'
                          ' in the sync_level section and sync again.')
    else:
        console.print('[bold blue]No changes detected. Your template is up to date.')
Example #22
0
def lint_project(project_dir: str) -> Optional[TemplateLinter]:
    """
    Verifies the integrity of a project to best coding and practices.
    Runs a set of general linting functions, which all templates share and afterwards runs template specific linting functions.
    All results are collected and presented to the user.

    :param project_dir: The path to the .cookietemple.yml file.
    """
    # Detect which template the project is based on
    template_handle = get_template_handle(project_dir)
    log.debug(f"Detected handle {template_handle}")

    switcher = {
        "cli-python": CliPythonLint,
        "cli-java": CliJavaLint,
        "web-website-python": WebWebsitePythonLint,
        "gui-java": GuiJavaLint,
        "lib-cpp": LibCppLint,
        "pub-thesis-latex": PubLatexLint,
    }

    try:
        lint_obj: Union[TemplateLinter, Any] = switcher.get(template_handle)(
            project_dir)  # type: ignore
    except TypeError:
        console.print(
            f"[bold red]Unable to find linter for handle {template_handle}! Aborting..."
        )
        sys.exit(1)

    # Run the linting tests
    try:
        # Disable check files?
        disable_check_files_templates = ["pub-thesis-latex"]
        if template_handle in disable_check_files_templates:
            disable_check_files = True
        else:
            disable_check_files = False
        # Run non project specific linting
        log.debug("Running general linting.")
        console.print("[bold blue]Running general linting")
        lint_obj.lint_project(super(lint_obj.__class__, lint_obj),
                              custom_check_files=disable_check_files,
                              is_subclass_calling=False)

        # Run the project specific linting
        log.debug(f"Running linting of {template_handle}")
        console.print(f"[bold blue]Running {template_handle} linting")

        lint_obj.lint()  # type: ignore
    except AssertionError as e:
        console.print(f"[bold red]Critical error: {e}")
        console.print("[bold red] Stopping tests...")
        return lint_obj

    # Print the results
    lint_obj.print_results()

    # Exit code
    if len(lint_obj.failed) > 0:
        console.print(
            f"[bold red] {len(lint_obj.failed)} tests failed! Exiting with non-zero error code."
        )
        sys.exit(1)

    return None
    def prompt_general_template_configuration(self, dot_cookietemple: Optional[dict]):
        """
        Prompts the user for general options that are required by all templates.
        Options are saved in the creator context manager object.
        """
        try:
            """
            Check, if the dot_cookietemple dictionary contains the full name and email (this happens, when dry creating the template while syncing on
            TEMPLATE branch).
            If that's not the case, try to read them from the config file (created with the config command).

            If none of the approaches above succeed (no config file has been found and its not a dry create run), configure the basic credentials and proceed.
            """
            if dot_cookietemple:
                self.creator_ctx.full_name = dot_cookietemple["full_name"]
                self.creator_ctx.email = dot_cookietemple["email"]
            else:
                self.creator_ctx.full_name = load_yaml_file(ConfigCommand.CONF_FILE_PATH)["full_name"]
                self.creator_ctx.email = load_yaml_file(ConfigCommand.CONF_FILE_PATH)["email"]
        except FileNotFoundError:
            # style and automatic use config
            console.print(
                "[bold red]Cannot find a cookietemple config file. Is this your first time using cookietemple?"
            )
            # inform the user and config all settings (with PAT optional)
            console.print("[bold blue]Lets set your name, email and Github username and you´re ready to go!")
            ConfigCommand.all_settings()
            # load mail and full name
            path = Path(ConfigCommand.CONF_FILE_PATH)
            yaml = YAML(typ="safe")
            settings = yaml.load(path)
            # set full name and mail
            self.creator_ctx.full_name = settings["full_name"]
            self.creator_ctx.email = settings["email"]

        self.creator_ctx.project_name = cookietemple_questionary_or_dot_cookietemple(
            function="text",
            question="Project name",
            default="exploding-springfield",
            dot_cookietemple=dot_cookietemple,
            to_get_property="project_name",
        ).lower()  # type: ignore
        if self.creator_ctx.language == "python":
            self.check_name_available("PyPi", dot_cookietemple)
        self.check_name_available("readthedocs.io", dot_cookietemple)
        self.creator_ctx.project_slug = self.creator_ctx.project_name.replace(" ", "_")  # type: ignore
        self.creator_ctx.project_slug_no_hyphen = self.creator_ctx.project_slug.replace("-", "_")
        self.creator_ctx.project_short_description = cookietemple_questionary_or_dot_cookietemple(
            function="text",
            question="Short description of your project",
            default=f"{self.creator_ctx.project_name}" f". A cookietemple based .",
            dot_cookietemple=dot_cookietemple,
            to_get_property="project_short_description",
        )
        poss_vers = cookietemple_questionary_or_dot_cookietemple(
            function="text",
            question="Initial version of your project",
            default="0.1.0",
            dot_cookietemple=dot_cookietemple,
            to_get_property="version",
        )

        # make sure that the version has the right format
        while not re.match(r"(?<!.)\d+(?:\.\d+){2}(?:-SNAPSHOT)?(?!.)", poss_vers) and not dot_cookietemple:  # type: ignore
            console.print(
                "[bold red]The version number entered does not match semantic versioning.\n"
                + r"Please enter the version in the format \[number].\[number].\[number]!"
            )  # noqa: W605
            poss_vers = cookietemple_questionary_or_dot_cookietemple(
                function="text", question="Initial version of your project", default="0.1.0"
            )
        self.creator_ctx.version = poss_vers

        self.creator_ctx.license = cookietemple_questionary_or_dot_cookietemple(
            function="select",
            question="License",
            choices=[
                "MIT",
                "BSD",
                "ISC",
                "Apache2.0",
                "GNUv3",
                "Boost",
                "Affero",
                "CC0",
                "CCBY",
                "CCBYSA",
                "Eclipse",
                "WTFPL",
                "unlicence",
                "Not open source",
            ],
            default="MIT",
            dot_cookietemple=dot_cookietemple,
            to_get_property="license",
        )
        if dot_cookietemple:
            self.creator_ctx.github_username = dot_cookietemple["github_username"]
            self.creator_ctx.creator_github_username = dot_cookietemple["creator_github_username"]
        else:
            self.creator_ctx.github_username = load_github_username()
            self.creator_ctx.creator_github_username = self.creator_ctx.github_username
Example #24
0
def cookietemple_questionary_or_dot_cookietemple(
        function: str,
        question: str,
        choices: Optional[List[str]] = None,
        default: Optional[str] = None,
        dot_cookietemple: Dict = None,
        to_get_property: str = None) -> Union[str, bool]:
    """
    Custom selection based on Questionary. Handles keyboard interrupts and default values.

    :param function: The function of questionary to call (e.g. select or text). See https://github.com/tmbo/questionary for all available functions.
    :param choices: List of all possible choices.
    :param question: The question to prompt for. Should not include default values or colons.
    :param default: A set default value, which will be chosen if the user does not enter anything.
    :param dot_cookietemple: A dictionary, which contains the whole .cookietemple.yml content
    :param to_get_property: A key, which must be in the dot_cookietemple file, which is used to fetch the read in value from the .cookietemple.yml file
    :return: The chosen answer.
    """
    # First check whether a dot_cookietemple was passed and whether it contains the desired property -> return it if so
    try:
        if dot_cookietemple:
            if to_get_property in dot_cookietemple:
                return dot_cookietemple[to_get_property]
    except KeyError:
        log.debug(
            f'.cookietemple.yml file was passed when creating a project, but key {to_get_property}'
            f' does not exist in the dot_cookietemple dictionary! Assigning default {default} to {to_get_property}.'
        )
        return default  # type: ignore

    # There is no .cookietemple.yml file aka dot_cookietemple dict passed -> ask for the properties
    answer: Optional[str] = ''
    try:
        if function == 'select':
            if default not in choices:  # type: ignore
                log.debug(
                    f'Default value {default} is not in the set of choices!')
            answer = getattr(questionary,
                             function)(f'{question}: ',
                                       choices=choices,
                                       style=cookietemple_style).unsafe_ask()
        elif function == 'password':
            while not answer or answer == '':
                answer = getattr(questionary, function)(
                    f'{question}: ', style=cookietemple_style).unsafe_ask()
        elif function == 'text':
            if not default:
                log.debug(
                    'Tried to utilize default value in questionary prompt, but is None! Please set a default value.'
                )
                default = ''
            answer = getattr(questionary,
                             function)(f'{question} [{default}]: ',
                                       style=cookietemple_style).unsafe_ask()
        elif function == 'confirm':
            default_value_bool = True if default == 'Yes' or default == 'yes' else False
            answer = getattr(questionary, function)(
                f'{question} [{default}]: ',
                style=cookietemple_style,
                default=default_value_bool).unsafe_ask()
        else:
            log.debug(f'Unsupported questionary function {function} used!')

    except KeyboardInterrupt:
        console.print('[bold red] Aborted!')
        sys.exit(1)
    if answer is None or answer == '':
        answer = default

    log.debug(f'User was asked the question: ||{question}|| as: {function}')
    log.debug(f'User selected {answer}')

    return answer  # type: ignore
Example #25
0
def lint_project(project_dir: str,
                 skip_external: bool,
                 is_create: bool = False) -> Optional[TemplateLinter]:
    """
    Verifies the integrity of a project to best coding and practices.
    Runs a set of general linting functions, which all templates share and afterwards runs template specific linting functions.
    All results are collected and presented to the user.

    :param project_dir: The path to the .cookietemple.yml file.
    :param skip_external: Whether to skip external linters such as autopep8
    :param is_create: Whether linting is called during project creation
    """
    # Detect which template the project is based on
    template_handle = get_template_handle(project_dir)
    log.debug(f'Detected handle {template_handle}')

    switcher = {
        'cli-python': CliPythonLint,
        'cli-java': CliJavaLint,
        'web-website-python': WebWebsitePythonLint,
        'gui-java': GuiJavaLint,
        'lib-cpp': LibCppLint,
        'pub-thesis-latex': PubLatexLint
    }

    try:
        lint_obj: Union[TemplateLinter, Any] = switcher.get(template_handle)(
            project_dir)  # type: ignore
    except TypeError:
        console.print(
            f'[bold red]Unable to find linter for handle {template_handle}! Aborting...'
        )
        sys.exit(1)

    # Run the linting tests
    try:
        # Disable check files?
        disable_check_files_templates = ['pub-thesis-latex']
        if template_handle in disable_check_files_templates:
            disable_check_files = True
        else:
            disable_check_files = False
        # Run non project specific linting
        log.debug('Running general linting.')
        console.print('[bold blue]Running general linting')
        lint_obj.lint_project(super(lint_obj.__class__, lint_obj),
                              custom_check_files=disable_check_files,
                              is_subclass_calling=False)

        # Run the project specific linting
        log.debug(f'Running linting of {template_handle}')
        console.print(f'[bold blue]Running {template_handle} linting')

        # for every python project that is created autopep8 will run one time
        # when linting en existing python cookietemple project, autopep8 should be now optional,
        # since (for example) it messes up Jinja syntax (if included in project)
        if 'python' in template_handle:
            lint_obj.lint(is_create, skip_external)  # type: ignore
        else:
            lint_obj.lint(skip_external)  # type: ignore
    except AssertionError as e:
        console.print(f'[bold red]Critical error: {e}')
        console.print('[bold red] Stopping tests...')
        return lint_obj

    # Print the results
    lint_obj.print_results()

    # Exit code
    if len(lint_obj.failed) > 0:
        console.print(
            f'[bold red] {len(lint_obj.failed)} tests failed! Exiting with non-zero error code.'
        )
        sys.exit(1)

    return None