Module cdev.commands.project_initializer

Expand source code
import json
import os
import shutil
from typing import Any, Dict, List, Union, Optional
import uuid

import boto3
from pydantic import FilePath
from pydantic.types import DirectoryPath
from rich.prompt import Prompt, Confirm

from cdev.commands import project_initializer_params
from cdev.default.project import local_project, local_project_info
from cdev.utils.display_manager import SimpleSelectionListPage
from cdev.utils.credential_helper import prompt_write_default_aws_credentials

from core.default.backend import Local_Backend_Configuration
from core.utils import paths as paths_util


from cdev.constructs.project import (
    check_if_project_exists,
    CDEV_FOLDER,
    CDEV_PROJECT_FILE,
)

STATE_FOLDER = "state"
INTERMEDIATE_FOLDER = "intermediate"
CACHE_FOLDER = "cache"
CENTRAL_STATE_FILE = "central_state.json"
SETTINGS_FOLDER_NAME = "settings"
DEFAULT_ENVIRONMENTS = ["prod", "stage", "dev"]
TEMPLATE_LOCATIONS = os.path.join(
    os.path.dirname(os.path.dirname(__file__)), "project_templates"
)

AVAILABLE_TEMPLATES = [
    "quick-start",
    "twilio-bot",
    "resources-test",
    "packages",
    "slack-bot",
    "user-auth",
    "power-tools",
    "raw",
]


def create_project_cli(args) -> None:
    """CLI version of the create project command.

    Args:
        args (_type_): cli arguments
    """
    config = args

    create_project(args.name)

    if args.template:
        template_name = args.template
        if template_name not in AVAILABLE_TEMPLATES:
            print(
                f"{template_name} is not one of the available templates. {AVAILABLE_TEMPLATES}"
            )
            return

        print("")
        print(f"Loading Template {template_name}")
        _load_template(template_name)
        print(f"Created Project From Template: {template_name}")


def _load_template(template_name: str, base_directory: DirectoryPath = None) -> None:
    """Copy the given template name into the provided directory.

    Args:
        template_name (str): Name of the template
        base_directory (DirectoryPath): directory to copy to. defaults to os.cwd().
    """
    if not base_directory:
        base_directory = os.getcwd()

    template_folder_name = template_name.replace("-", "_")

    if not template_folder_name in os.listdir(TEMPLATE_LOCATIONS):
        print(f"Could not finder template for {template_folder_name}")
        return

    template_location = os.path.join(TEMPLATE_LOCATIONS, template_folder_name)
    for x in os.listdir(template_location):

        full_location = os.path.join(template_location, x)
        if os.path.isdir(full_location):
            shutil.copytree(full_location, os.path.join(base_directory, x))
        elif os.path.isfile(full_location):
            shutil.copyfile(full_location, os.path.join(base_directory, x))


def create_project(project_name: str, base_directory: DirectoryPath = None) -> None:
    """Create a new `Project` at the given base_directory (or cwd). This initializes all the files needed
    to create a basic Cdev Project.

    Args:
        project_name (str): name of the project
        base_directory (DirectoryPath, optional): directory to store information. Defaults to cwd().

    Raises:
        Exception: If a Cdev project already exists at the given location.
    """

    if not base_directory:
        base_directory = os.getcwd()

    if check_if_project_exists(base_directory):
        raise Exception("Project Already Created")

    prompt_write_default_aws_credentials()

    base_settings_values = _default_new_project_input_questions()
    _create_folder_structure(
        base_directory,
        base_settings=base_settings_values,
        extra_environments=DEFAULT_ENVIRONMENTS,
    )

    backend_directory = os.path.join(CDEV_FOLDER, STATE_FOLDER)
    backend_configuration = Local_Backend_Configuration(
        {
            "base_folder": backend_directory,
            "central_state_file": os.path.join(backend_directory, CENTRAL_STATE_FILE),
        }
    )

    base_settings_folder = SETTINGS_FOLDER_NAME

    new_project_info = local_project_info(
        project_name=project_name,
        environment_infos=[],
        current_environment_name="",
        default_backend_configuration=backend_configuration,
        settings_directory=base_settings_folder,
        initialization_module="src.cdev_project",
    )

    project_info_location = os.path.join(base_directory, CDEV_FOLDER, CDEV_PROJECT_FILE)
    with open(project_info_location, "w") as fh:
        json.dump(new_project_info.dict(), fh, indent=4)

    new_project = local_project(new_project_info, project_info_location)

    for environment in DEFAULT_ENVIRONMENTS:
        new_project.create_environment(
            environment, backend_configuration=backend_configuration
        )

    new_project.set_current_environment(DEFAULT_ENVIRONMENTS[-1])


def _default_new_project_input_questions() -> Dict[str, str]:
    """Run through the sequence of input questions for a user to properly initialize a project.

    Returns:
        Dict[str, str]: Settings completed by the user
    """
    _artifact_bucket = _select_resources_bucket()

    return {"S3_ARTIFACTS_BUCKET": _artifact_bucket}


def _create_folder_structure(
    base_directory: DirectoryPath,
    base_settings: Dict[str, str] = None,
    extra_environments: List[str] = None,
) -> None:
    """Create a skeleton file structure needed to make a project and additional environments. Apply the Dict of base settings to the generated
    base settings file.

    Args:
        base_directory (DirectoryPath)
        base_settings (Dict[str, str], optional): Settings for base environment. Defaults to None.
        extra_environments (List[str], optional): List of environments to generate. Defaults to None.
    """
    cdev_folder = os.path.join(base_directory, CDEV_FOLDER)
    state_folder = os.path.join(cdev_folder, STATE_FOLDER)
    intermediate_folder = os.path.join(cdev_folder, INTERMEDIATE_FOLDER)
    cache_folder = os.path.join(intermediate_folder, CACHE_FOLDER)
    settings_folder = os.path.join(base_directory, SETTINGS_FOLDER_NAME)
    base_settings_file = os.path.join(settings_folder, f"base_settings.py")

    paths_util.mkdir(cdev_folder)
    paths_util.mkdir(state_folder)
    paths_util.mkdir(intermediate_folder)
    paths_util.mkdir(cache_folder)
    paths_util.mkdir(settings_folder)

    _create_base_settings(base_settings_file, base_settings)

    for environment in extra_environments:
        paths_util.touch_file(
            os.path.join(settings_folder, f"{environment}_settings.py")
        )
        paths_util.mkdir(os.path.join(settings_folder, f"{environment}_secrets"))


def _create_base_settings(
    file_path: Union[FilePath, str], base_settings: Dict[str, str] = None
) -> None:
    """Create the base settings for the project at the provide path. This creates a python module at the provided directory, creates
    a base settings file, and applies any base settings to the generate file.

    Args:
        file_path (FilePath): path of the file
        base_settings (Dict[str, str], optional): Settings for base environment. Defaults to None.
    """
    # The settings are dynamically importable python modules, so the folder needs to be a python module
    paths_util.touch_file(os.path.join(os.path.dirname(file_path), f"__init__.py"))

    paths_util.touch_file(file_path)

    if base_settings:
        _render_settings_file(file_path, base_settings)


def _render_settings_file(file_path: FilePath, base_settings: Dict[str, str]) -> None:
    """Apply the base settings to the given file

    Args:
        file_path (FilePath): path of the file
        base_settings (Dict[str, str]): settings to apply
    """
    with open(file_path, "w") as fh:
        for key, value in base_settings.items():
            fh.write(f'{key} = "{value}"')


def _list_all_available_buckets(s3_client: Any) -> List[str]:
    """List all S3 buckets using the provided client. Return the list of bucket names. Will throw errors related to Aws that should be handled by the caller.

    Args:
        s3_client (Any): boto3 s3 client

    Returns:
        List[str]: bucket names
    """
    bucket_names = [
        bucket.get("Name") for bucket in s3_client.list_buckets().get("Buckets")
    ]

    return list(
        filter(
            lambda x: not any(
                x.startswith(_filter)
                for _filter in project_initializer_params.BUCKET_FILTERS
            ),
            bucket_names,
        )
    )


def _create_artifact_bucket(s3_client: Any) -> str:
    """Create an S3 bucket using the provided client. Return the bucket name. Will throw errors related to Aws that should be handled by the caller.

    Args:
        s3_client (Any): boto3 s3 client

    Returns:
        str: bucket name
    """
    _bucket_random_suffix = uuid.uuid4().hex[
        : project_initializer_params.GENERATED_BUCKET_SUFFIX_LENGTH
    ]
    _bucket_name = (
        f"{project_initializer_params.GENERATED_BUCKET_BASE}-{_bucket_random_suffix}"
    )

    s3_client.create_bucket(
        Bucket=_bucket_name,
    )

    return _bucket_name


def _select_resources_bucket() -> str:
    """Select a bucket name to be used for storing the artifacts created by Cdev. This function will not surface any errors, but instead, will return an empty string.

    Returns:
        str: bucket name
    """
    print(project_initializer_params.ARTIFACT_BUCKET_INTRO_MESSAGE)
    _s3_client = boto3.client("s3")

    try:
        _available_buckets = _list_all_available_buckets(_s3_client)
    except Exception as e:
        print(project_initializer_params.LIST_BUCKETS_FAILED)
        print(e)
        return ""

    if len(_available_buckets) > project_initializer_params.MAXIMUM_BUCKETS_LISTED:
        print(project_initializer_params.TOO_MANY_AVAILABLE_BUCKETS_MESSAGE)

        while True:
            selected_bucket_name = Prompt.ask(
                prompt=project_initializer_params.NAME_OF_BUCKET_PROMPT
            )

            if selected_bucket_name not in _available_buckets:
                print(
                    project_initializer_params.BUCKET_NOT_AVAILABLE_MESSAGE.format(
                        bucket_name=selected_bucket_name
                    )
                )
                print(_available_buckets)
            else:
                break

    elif len(_available_buckets) == 0:
        print(project_initializer_params.NO_BUCKET_SELECT_MESSAGE)

        _create_bucket = Confirm.ask(
            prompt=project_initializer_params.CONFIRM_BUCKET_CREATION
        )

        if _create_bucket:
            try:
                selected_bucket_name = _create_artifact_bucket(_s3_client)
                print(
                    project_initializer_params.CREATE_ARTIFACT_BUCKET_SUCCESS.format(
                        bucket_name=selected_bucket_name
                    )
                )
            except Exception as e:
                print(project_initializer_params.CREATE_ARTIFACT_BUCKET_FAILED)
                print(e)
                selected_bucket_name = ""

        else:
            print(project_initializer_params.DO_NOT_CREATE_BUCKET_SELECT_MESSAGE)
            selected_bucket_name = ""

    else:
        _final_bucket_list = [
            project_initializer_params.CREATE_BUCKET_LABEL
        ] + _available_buckets
        selection_page = SimpleSelectionListPage(_final_bucket_list)
        selected_bucket_name = selection_page.blocking_selection_process()

        if selected_bucket_name == project_initializer_params.CREATE_BUCKET_LABEL:
            selected_bucket_name = _create_artifact_bucket(_s3_client)
            print(
                project_initializer_params.CREATE_ARTIFACT_BUCKET_SUCCESS.format(
                    bucket_name=selected_bucket_name
                )
            )

    return selected_bucket_name

Functions

def create_project(project_name: str, base_directory: pydantic.types.DirectoryPath = None) ‑> None

Create a new Project at the given base_directory (or cwd). This initializes all the files needed to create a basic Cdev Project.

Args

project_name : str
name of the project
base_directory : DirectoryPath, optional
directory to store information. Defaults to cwd().

Raises

Exception
If a Cdev project already exists at the given location.
Expand source code
def create_project(project_name: str, base_directory: DirectoryPath = None) -> None:
    """Create a new `Project` at the given base_directory (or cwd). This initializes all the files needed
    to create a basic Cdev Project.

    Args:
        project_name (str): name of the project
        base_directory (DirectoryPath, optional): directory to store information. Defaults to cwd().

    Raises:
        Exception: If a Cdev project already exists at the given location.
    """

    if not base_directory:
        base_directory = os.getcwd()

    if check_if_project_exists(base_directory):
        raise Exception("Project Already Created")

    prompt_write_default_aws_credentials()

    base_settings_values = _default_new_project_input_questions()
    _create_folder_structure(
        base_directory,
        base_settings=base_settings_values,
        extra_environments=DEFAULT_ENVIRONMENTS,
    )

    backend_directory = os.path.join(CDEV_FOLDER, STATE_FOLDER)
    backend_configuration = Local_Backend_Configuration(
        {
            "base_folder": backend_directory,
            "central_state_file": os.path.join(backend_directory, CENTRAL_STATE_FILE),
        }
    )

    base_settings_folder = SETTINGS_FOLDER_NAME

    new_project_info = local_project_info(
        project_name=project_name,
        environment_infos=[],
        current_environment_name="",
        default_backend_configuration=backend_configuration,
        settings_directory=base_settings_folder,
        initialization_module="src.cdev_project",
    )

    project_info_location = os.path.join(base_directory, CDEV_FOLDER, CDEV_PROJECT_FILE)
    with open(project_info_location, "w") as fh:
        json.dump(new_project_info.dict(), fh, indent=4)

    new_project = local_project(new_project_info, project_info_location)

    for environment in DEFAULT_ENVIRONMENTS:
        new_project.create_environment(
            environment, backend_configuration=backend_configuration
        )

    new_project.set_current_environment(DEFAULT_ENVIRONMENTS[-1])
def create_project_cli(args) ‑> None

CLI version of the create project command.

Args

args : _type_
cli arguments
Expand source code
def create_project_cli(args) -> None:
    """CLI version of the create project command.

    Args:
        args (_type_): cli arguments
    """
    config = args

    create_project(args.name)

    if args.template:
        template_name = args.template
        if template_name not in AVAILABLE_TEMPLATES:
            print(
                f"{template_name} is not one of the available templates. {AVAILABLE_TEMPLATES}"
            )
            return

        print("")
        print(f"Loading Template {template_name}")
        _load_template(template_name)
        print(f"Created Project From Template: {template_name}")