From e2d1ca1502e9cfb95395b1e09a1689022559066d Mon Sep 17 00:00:00 2001 From: richardbryan Date: Mon, 4 Jul 2022 22:01:50 +0700 Subject: [PATCH 01/29] minor refractor on global config --- deploifai/context.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/deploifai/context.py b/deploifai/context.py index f6bd485..f59a5ca 100644 --- a/deploifai/context.py +++ b/deploifai/context.py @@ -35,20 +35,16 @@ def __init__(self): pass def read_config(self): - global_config = configparser.ConfigParser() - # read the global config file - global_config.read(global_config_filepath) + self.global_config.read(global_config_filepath) # initialise sections if they don't exist already for section in global_config_sections: - if section not in global_config.sections(): - global_config[section] = {} + if section not in self.global_config.sections(): + self.global_config[section] = {} self.debug_msg(f"Read global config file from {global_config_filepath}") - self.global_config = global_config - # read local config file self.local_config = local_config.read_config_file() From af984b4778e13b99c246022637beb4e9344d73d2 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Tue, 5 Jul 2022 10:53:44 +0700 Subject: [PATCH 02/29] traverse up recursively and read config file if in child dir of client's project dir --- deploifai/utilities/local_config.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 8950dc1..9be583c 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -69,10 +69,16 @@ def read_config_file() -> configparser.ConfigParser: Read the config file in the existing .deploifai/local.cfg file :return: A ConfigParser that contains all the configs in the config file """ + path = pathlib.Path.cwd() + while not path.joinpath(".deploifai").exists() and path != path.parent: + path = path.parent + + file_path = path.joinpath(".deploifai/local.cfg") + try: config = configparser.ConfigParser() # read the config file - config.read(config_file_path) + config.read(file_path) for section in config_sections: if section not in config.sections(): From b4863a8d19a664a726266c9491f6b97f95c6ccc4 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Tue, 5 Jul 2022 12:58:24 +0700 Subject: [PATCH 03/29] refractor local config file manager --- deploifai/utilities/local_config.py | 37 ++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 9be583c..73a1dbd 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -1,8 +1,33 @@ import configparser import pathlib import click +from PyInquirer import prompt -config_file_path = pathlib.Path(".deploifai").joinpath("local.cfg") + +def find_local_config_dir(): + """ + Traverse up the file system and checks for a .deplooifai directory. + If does not exist, raise error not found. + :return: if local config file exists, return pathlib.Path object that points to the config file + """ + path = pathlib.Path.cwd() + while not path.joinpath(".deploifai").exists() and path != path.parent: + path = path.parent + + # True if .deplofai is not created yet + + # 2 cases: + # 1. user has not initialized a project + # 2. project is initialized but user is in the parent directory + + # TODO: not yet handle the case where user is in the parent directory, which means user is assumed to be creating a new project + if path == path.parent: + return pathlib.Path.cwd().joinpath(".deploifai/local.cfg") + + return path.joinpath(".deploifai/local.cfg") + + +config_file_path = find_local_config_dir() # pathlib.Path(".deploifai").joinpath("local.cfg") """ Manages a .deploifai/local.cfg file to store configuration info about a project. @@ -42,7 +67,7 @@ def create_config_files(): Creates the folder .deploifai that stores all the config files. :return: None """ - if pathlib.Path(".deploifai").exists(): + if config_file_path.parent.exists(): raise DeploifaiAlreadyInitialisedError( "Deploifai has already been initialised in this directory." ) @@ -69,16 +94,10 @@ def read_config_file() -> configparser.ConfigParser: Read the config file in the existing .deploifai/local.cfg file :return: A ConfigParser that contains all the configs in the config file """ - path = pathlib.Path.cwd() - while not path.joinpath(".deploifai").exists() and path != path.parent: - path = path.parent - - file_path = path.joinpath(".deploifai/local.cfg") - try: config = configparser.ConfigParser() # read the config file - config.read(file_path) + config.read(config_file_path) for section in config_sections: if section not in config.sections(): From 4011f3b9872565be46545b3ee383a80657bc8b30 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Tue, 5 Jul 2022 14:10:24 +0700 Subject: [PATCH 04/29] minor typo --- deploifai/utilities/local_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 73a1dbd..974e3fa 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -6,7 +6,7 @@ def find_local_config_dir(): """ - Traverse up the file system and checks for a .deplooifai directory. + Traverse up the file system and checks for a .deploifai directory. If does not exist, raise error not found. :return: if local config file exists, return pathlib.Path object that points to the config file """ From 9412cd6daa7984648980d2aa6e9ae1d4a4f81614 Mon Sep 17 00:00:00 2001 From: Sean Chok Date: Tue, 5 Jul 2022 16:18:03 +0800 Subject: [PATCH 05/29] WIP --- deploifai/project/create.py | 6 +++++- deploifai/utilities/local_config.py | 33 ++++++++++++++--------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 47a56e1..55297f5 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -134,4 +134,8 @@ def create(context: DeploifaiContextObj, name: str, workspace): click.secho(f"Successfully created new project named {name}.", fg="green") - local_config.set_project_config(project_id, context.local_config) + # local_config.set_project_config(project_id, context.local_config) + + # create a project directory, and create .deploifai directory within this + + # tell the user that a new project directory called [...] has been created diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 974e3fa..3033bed 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -1,10 +1,9 @@ import configparser import pathlib import click -from PyInquirer import prompt -def find_local_config_dir(): +def _find_local_config_dir(): """ Traverse up the file system and checks for a .deploifai directory. If does not exist, raise error not found. @@ -14,20 +13,13 @@ def find_local_config_dir(): while not path.joinpath(".deploifai").exists() and path != path.parent: path = path.parent - # True if .deplofai is not created yet - - # 2 cases: - # 1. user has not initialized a project - # 2. project is initialized but user is in the parent directory - - # TODO: not yet handle the case where user is in the parent directory, which means user is assumed to be creating a new project if path == path.parent: - return pathlib.Path.cwd().joinpath(".deploifai/local.cfg") + raise DeploifaiNotInitialisedError("Deploifai project not found.") - return path.joinpath(".deploifai/local.cfg") + return path.joinpath(".deploifai", "local.cfg") -config_file_path = find_local_config_dir() # pathlib.Path(".deploifai").joinpath("local.cfg") +config_file_path = _find_local_config_dir() """ Manages a .deploifai/local.cfg file to store configuration info about a project. @@ -62,7 +54,16 @@ def __init__(self, message): super(DeploifaiDataAlreadyInitialisedError, self).__init__(message) -def create_config_files(): +class DeploifaiNotInitialisedError(Exception): + """ + Exception when Deploifai project is not found. + """ + + def __init__(self, message): + super(DeploifaiNotInitialisedError, self).__init__(message) + + +def create_config_files(new_project_dir: str): """ Creates the folder .deploifai that stores all the config files. :return: None @@ -72,6 +73,7 @@ def create_config_files(): "Deploifai has already been initialised in this directory." ) + pathlib.Path(new_project_dir).join pathlib.Path(".deploifai").mkdir() config_file_path.touch(exist_ok=True) @@ -112,11 +114,8 @@ def read_config_file() -> configparser.ConfigParser: def save_config_file(config: configparser.ConfigParser): """ Save the local config from the context in the local.cfg file. - :param context: The deploifai context object + :param config: A ConfigParser object representing config data """ - if not config_file_path.exists(): - create_config_files() - with config_file_path.open("w") as config_file: config.write(config_file) From 594072c28dc23b8a9cb9245894ee9077f9ae6054 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 13:26:22 +0700 Subject: [PATCH 06/29] change flow of project create command, minor refractor in local config --- deploifai/project/create.py | 17 ++++++++++++++--- deploifai/utilities/local_config.py | 13 +++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 55297f5..234f0f6 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -9,6 +9,7 @@ from deploifai.api import DeploifaiAPIError from deploifai.utilities.user import parse_user_profiles from PyInquirer import prompt +import os @click.command() @@ -134,8 +135,18 @@ def create(context: DeploifaiContextObj, name: str, workspace): click.secho(f"Successfully created new project named {name}.", fg="green") - # local_config.set_project_config(project_id, context.local_config) + # create a project directory, along with .deploifai directory within this project + try: + os.mkdir(name) + except FileExistsError: + click.secho("A directory exists with the same name as the project name you just specified", fg="yellow") + except OSError: + click.secho("An error when creating the project locally", fg="red") + + project_path = os.path.join(os.getcwd(), name) + local_config.create_config_files(project_path) - # create a project directory, and create .deploifai directory within this + click.secho(f"A new directory named {name} has been created locally.", fg="green") - # tell the user that a new project directory called [...] has been created + # set id in local config file + local_config.set_project_config(project_id, context.local_config) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 3033bed..24848ba 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -68,13 +68,11 @@ def create_config_files(new_project_dir: str): Creates the folder .deploifai that stores all the config files. :return: None """ - if config_file_path.parent.exists(): - raise DeploifaiAlreadyInitialisedError( - "Deploifai has already been initialised in this directory." - ) + local_config_dir = pathlib.Path(new_project_dir).joinpath(".deploifai") + local_config_dir.mkdir(exist_ok=True) + + config_file_path = local_config_dir.joinpath("local.cfg") - pathlib.Path(new_project_dir).join - pathlib.Path(".deploifai").mkdir() config_file_path.touch(exist_ok=True) # initialise sections if they don't exist already @@ -96,6 +94,9 @@ def read_config_file() -> configparser.ConfigParser: Read the config file in the existing .deploifai/local.cfg file :return: A ConfigParser that contains all the configs in the config file """ + if config_file_path is None: + return None + try: config = configparser.ConfigParser() # read the config file From bb58cc44c44fac8cb70b99ddabd77bf6a2052150 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 13:46:32 +0700 Subject: [PATCH 07/29] change flow of cli, if project not found, set local config to none instead --- deploifai/utilities/local_config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 24848ba..d98e303 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -14,7 +14,11 @@ def _find_local_config_dir(): path = path.parent if path == path.parent: - raise DeploifaiNotInitialisedError("Deploifai project not found.") + click.echo(click.style("Project not found. To create a project: ", fg="red") + + click.style("deploifai project create NAME", fg="blue") + ) + return None + # raise DeploifaiNotInitialisedError("Deploifai project not found.") return path.joinpath(".deploifai", "local.cfg") From 4de74b4e0219664a190ad46e759625476791105d Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 14:29:36 +0700 Subject: [PATCH 08/29] bug fix when user initializes a project --- deploifai/project/create.py | 1 + deploifai/utilities/local_config.py | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 234f0f6..d066f27 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -148,5 +148,6 @@ def create(context: DeploifaiContextObj, name: str, workspace): click.secho(f"A new directory named {name} has been created locally.", fg="green") + context.local_config = local_config.read_config_file() # set id in local config file local_config.set_project_config(project_id, context.local_config) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index d98e303..67c8df1 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -14,9 +14,6 @@ def _find_local_config_dir(): path = path.parent if path == path.parent: - click.echo(click.style("Project not found. To create a project: ", fg="red") + - click.style("deploifai project create NAME", fg="blue") - ) return None # raise DeploifaiNotInitialisedError("Deploifai project not found.") @@ -68,6 +65,7 @@ def __init__(self, message): def create_config_files(new_project_dir: str): + global config_file_path """ Creates the folder .deploifai that stores all the config files. :return: None @@ -87,7 +85,7 @@ def create_config_files(new_project_dir: str): click.secho( """A .deploifai directory has been created, which contains configuration that Deploifai requires. - You should version control this directory. + You should version control this directory. """, fg="blue", ) From f853949b86a19c062b8a6110b5f182e7e93ca072 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 14:42:46 +0700 Subject: [PATCH 09/29] change flow of command, create project locally b4 doing it in the backend --- deploifai/project/create.py | 19 ++++++++++--------- deploifai/utilities/local_config.py | 3 +-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index d066f27..933b12f 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -127,26 +127,27 @@ def create(context: DeploifaiContextObj, name: str, workspace): cloud_profile = choose_cloud_profile["cloud_profile"] - try: - project_id = deploifai_api.create_project(name, cloud_profile)["id"] - except DeploifaiAPIError as err: - click.echo("Could not create project. Please try again.") - return - - click.secho(f"Successfully created new project named {name}.", fg="green") - # create a project directory, along with .deploifai directory within this project try: os.mkdir(name) except FileExistsError: click.secho("A directory exists with the same name as the project name you just specified", fg="yellow") + raise click.Abort() except OSError: click.secho("An error when creating the project locally", fg="red") + click.secho(f"A new directory named {name} has been created locally.", fg="green") + project_path = os.path.join(os.getcwd(), name) local_config.create_config_files(project_path) - click.secho(f"A new directory named {name} has been created locally.", fg="green") + try: + project_id = deploifai_api.create_project(name, cloud_profile)["id"] + except DeploifaiAPIError as err: + click.echo("Could not create project. Please try again.") + return + + click.secho(f"Successfully created new project named {name}.", fg="green") context.local_config = local_config.read_config_file() # set id in local config file diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 67c8df1..b5677e8 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -85,8 +85,7 @@ def create_config_files(new_project_dir: str): click.secho( """A .deploifai directory has been created, which contains configuration that Deploifai requires. - You should version control this directory. - """, + You should version control this directory.""", fg="blue", ) From a4802ae0c03b94f0654f30b740e1a9e640636159 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 15:12:12 +0700 Subject: [PATCH 10/29] refractor for commands using local config --- deploifai/data/info.py | 4 ++++ deploifai/project/browse.py | 3 +++ deploifai/utilities/local_config.py | 3 +++ 3 files changed, 10 insertions(+) diff --git a/deploifai/data/info.py b/deploifai/data/info.py index 3701297..516c2b3 100644 --- a/deploifai/data/info.py +++ b/deploifai/data/info.py @@ -1,11 +1,15 @@ import click from deploifai.context import pass_deploifai_context_obj, DeploifaiContextObj +from deploifai.utilities.local_config import DeploifaiNotInitialisedError @click.command() @pass_deploifai_context_obj def info(context: DeploifaiContextObj): + if context.local_config is None: + raise DeploifaiNotInitialisedError("Deploifai project not found") + data_storage_config = context.local_config["DATA_STORAGE"] if "id" not in data_storage_config: diff --git a/deploifai/project/browse.py b/deploifai/project/browse.py index 7bb009c..5dbd86f 100644 --- a/deploifai/project/browse.py +++ b/deploifai/project/browse.py @@ -5,6 +5,7 @@ from deploifai.api import DeploifaiAPIError from deploifai.context import pass_deploifai_context_obj, DeploifaiContextObj from deploifai.utilities.frontend_routing import get_project_route +from deploifai.utilities.local_config import DeploifaiNotInitialisedError @click.command() @@ -56,6 +57,8 @@ def browse(context: DeploifaiContextObj, project: str, workspace="unassigned"): else: # assume that the user should be in a project directory, that contains local configuration file + if context.local_config is None: + raise DeploifaiNotInitialisedError("Deploifai project not found") if "id" not in context.local_config["PROJECT"]: click.secho("Missing workspace name and project name", fg='yellow') diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index b5677e8..8b23302 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -61,6 +61,9 @@ class DeploifaiNotInitialisedError(Exception): """ def __init__(self, message): + click.echo(click.style("Project not found. To create a project: ", fg="red") + + click.style("deploifai project create NAME", fg="blue") + ) super(DeploifaiNotInitialisedError, self).__init__(message) From d146b372f13d7ca3bfd3bc2c8dc1452bc62e849d Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 16:36:50 +0700 Subject: [PATCH 11/29] added a decorator to check if deploifai project found --- deploifai/context.py | 12 ++++++++++++ deploifai/data/info.py | 11 ++++++----- deploifai/project/browse.py | 10 ++++++---- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/deploifai/context.py b/deploifai/context.py index f59a5ca..1a83d45 100644 --- a/deploifai/context.py +++ b/deploifai/context.py @@ -141,3 +141,15 @@ def wrapper(click_context, *args, **kwargs): raise click.Abort() return functools.update_wrapper(wrapper, f) + + +def project_found(f): + @pass_context + def wrapper(click_context, *args, **kwargs): + deploifai_context = click_context.find_object(DeploifaiContextObj) + if deploifai_context.local_config is not None: + return click_context.invoke(f, *args, **kwargs) + + raise local_config.DeploifaiNotInitialisedError("Deploifai project not found") + + return functools.update_wrapper(wrapper, f) diff --git a/deploifai/data/info.py b/deploifai/data/info.py index 516c2b3..a2f8e10 100644 --- a/deploifai/data/info.py +++ b/deploifai/data/info.py @@ -1,15 +1,16 @@ import click -from deploifai.context import pass_deploifai_context_obj, DeploifaiContextObj -from deploifai.utilities.local_config import DeploifaiNotInitialisedError +from deploifai.context import ( + pass_deploifai_context_obj, + DeploifaiContextObj, + project_found, +) @click.command() @pass_deploifai_context_obj +@project_found def info(context: DeploifaiContextObj): - if context.local_config is None: - raise DeploifaiNotInitialisedError("Deploifai project not found") - data_storage_config = context.local_config["DATA_STORAGE"] if "id" not in data_storage_config: diff --git a/deploifai/project/browse.py b/deploifai/project/browse.py index 5dbd86f..a140bdf 100644 --- a/deploifai/project/browse.py +++ b/deploifai/project/browse.py @@ -3,13 +3,17 @@ import requests from deploifai.api import DeploifaiAPIError -from deploifai.context import pass_deploifai_context_obj, DeploifaiContextObj +from deploifai.context import ( + pass_deploifai_context_obj, + DeploifaiContextObj, + project_found, +) from deploifai.utilities.frontend_routing import get_project_route -from deploifai.utilities.local_config import DeploifaiNotInitialisedError @click.command() @pass_deploifai_context_obj +@project_found @click.option("--workspace", help="Workspace name", type=str) @click.option("--project", help="Project name", type=str) def browse(context: DeploifaiContextObj, project: str, workspace="unassigned"): @@ -57,8 +61,6 @@ def browse(context: DeploifaiContextObj, project: str, workspace="unassigned"): else: # assume that the user should be in a project directory, that contains local configuration file - if context.local_config is None: - raise DeploifaiNotInitialisedError("Deploifai project not found") if "id" not in context.local_config["PROJECT"]: click.secho("Missing workspace name and project name", fg='yellow') From aca161002c503c69f8946feb2c08e3d483204eac Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 22:16:55 +0700 Subject: [PATCH 12/29] change flow of commmand again, bug fix for duplicate local file names --- deploifai/api/__init__.py | 12 +++++------ deploifai/project/create.py | 42 +++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/deploifai/api/__init__.py b/deploifai/api/__init__.py index 83a13eb..bbe2fa3 100644 --- a/deploifai/api/__init__.py +++ b/deploifai/api/__init__.py @@ -327,9 +327,9 @@ def get_projects(self, workspace, fragment: str, where_project=None): api_data = r.json() return api_data["data"]["projects"] - except TypeError as err: + except TypeError: raise DeploifaiAPIError("Could not get projects. Please try again.") - except KeyError as err: + except KeyError: raise DeploifaiAPIError("Could not get projects. Please try again.") def get_project(self, project_id: str, fragment: str): @@ -357,9 +357,9 @@ def get_project(self, project_id: str, fragment: str): api_data = r.json() return api_data["data"]["project"] - except TypeError as err: + except TypeError: raise DeploifaiAPIError("Could not get project. Please try again.") - except KeyError as err: + except KeyError: raise DeploifaiAPIError("Could not get project. Please try again.") def create_project(self, project_name: str, cloud_profile: CloudProfile): @@ -392,7 +392,7 @@ def create_project(self, project_name: str, cloud_profile: CloudProfile): create_mutation_data = r.json() return create_mutation_data["data"]["createProject"] - except TypeError as err: + except TypeError: raise DeploifaiAPIError("Could not create project. Please try again.") - except KeyError as err: + except KeyError: raise DeploifaiAPIError("Could not create project. Please try again.") diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 933b12f..6f0d823 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -69,14 +69,19 @@ def create(context: DeploifaiContextObj, name: str, workspace): click.echo("An error occurred when fetching projects. Please try again.") return - err_msg = "Project name taken. Choose a unique project name:" - project_names = [project["name"] for project in projects] + user_pwd_dir_names = os.listdir() + + dup_local_err_msg = f"There are existing files/directories in your computer also named {name}" + dup_backend_err_msg = f"Project name taken. Existing names in chosen workspace: {' '.join(project_names)}\nChoose a unique project name:" - name_taken_err_msg = f"Project name taken. Existing names in chosen workspace: {' '.join(project_names)}\nChoose a unique project name:" - err_msg = name_taken_err_msg + err_msg = "" + if name in user_pwd_dir_names: + err_msg = dup_local_err_msg + elif name in project_names: + err_msg = dup_backend_err_msg - is_valid_name = not (name in project_names) + is_valid_name = not (name in project_names or name in user_pwd_dir_names) while not is_valid_name: prompt_name = prompt( @@ -89,12 +94,14 @@ def create(context: DeploifaiContextObj, name: str, workspace): new_project_name = prompt_name["project_name"] - if len(new_project_name) == 0: + if len(new_project_name) == 0 or new_project_name.isspace(): err_msg = "Project name cannot be empty.\nChoose a non-empty project name:" elif not new_project_name.isalnum(): err_msg = f"Project name should only contain alphanumeric characters.\nChoose a valid project name:" + elif new_project_name in user_pwd_dir_names: + err_msg = dup_local_err_msg elif new_project_name in project_names: - err_msg = name_taken_err_msg + err_msg = dup_backend_err_msg else: name = new_project_name is_valid_name = True @@ -127,11 +134,18 @@ def create(context: DeploifaiContextObj, name: str, workspace): cloud_profile = choose_cloud_profile["cloud_profile"] - # create a project directory, along with .deploifai directory within this project + # create proejct in the backend + try: + project_id = deploifai_api.create_project(name, cloud_profile)["id"] + except DeploifaiAPIError as err: + click.secho(err, fg="red") + raise click.Abort() + + click.secho(f"Successfully created new project named {name}.", fg="green") + + # create a project directory locally, along with .deploifai directory within this project try: os.mkdir(name) - except FileExistsError: - click.secho("A directory exists with the same name as the project name you just specified", fg="yellow") raise click.Abort() except OSError: click.secho("An error when creating the project locally", fg="red") @@ -141,14 +155,6 @@ def create(context: DeploifaiContextObj, name: str, workspace): project_path = os.path.join(os.getcwd(), name) local_config.create_config_files(project_path) - try: - project_id = deploifai_api.create_project(name, cloud_profile)["id"] - except DeploifaiAPIError as err: - click.echo("Could not create project. Please try again.") - return - - click.secho(f"Successfully created new project named {name}.", fg="green") - context.local_config = local_config.read_config_file() # set id in local config file local_config.set_project_config(project_id, context.local_config) From af6495369b699fbc0b48c1545605fb8563fa3f88 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Thu, 7 Jul 2022 12:55:17 +0700 Subject: [PATCH 13/29] change command flow, no more loops if invalid name --- deploifai/project/create.py | 44 ++++++++++++------------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 6f0d823..3f7fae1 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -72,43 +72,27 @@ def create(context: DeploifaiContextObj, name: str, workspace): project_names = [project["name"] for project in projects] user_pwd_dir_names = os.listdir() - dup_local_err_msg = f"There are existing files/directories in your computer also named {name}" - dup_backend_err_msg = f"Project name taken. Existing names in chosen workspace: {' '.join(project_names)}\nChoose a unique project name:" + is_valid_name = True err_msg = "" + if name in user_pwd_dir_names: - err_msg = dup_local_err_msg + is_valid_name = False + err_msg = f"There are existing files/directories in your computer also named {name}" elif name in project_names: - err_msg = dup_backend_err_msg - - is_valid_name = not (name in project_names or name in user_pwd_dir_names) - - while not is_valid_name: - prompt_name = prompt( - { - "type": "input", - "name": "project_name", - "message": err_msg, - } - ) - - new_project_name = prompt_name["project_name"] - - if len(new_project_name) == 0 or new_project_name.isspace(): - err_msg = "Project name cannot be empty.\nChoose a non-empty project name:" - elif not new_project_name.isalnum(): - err_msg = f"Project name should only contain alphanumeric characters.\nChoose a valid project name:" - elif new_project_name in user_pwd_dir_names: - err_msg = dup_local_err_msg - elif new_project_name in project_names: - err_msg = dup_backend_err_msg - else: - name = new_project_name - is_valid_name = True + is_valid_name = False + err_msg = f"Project name taken. Existing names in chosen workspace: {' '.join(project_names)}." + elif name.isalnum(): + is_valid_name = False + err_msg = "Project name should only contain alphanumeric characters." + + if not is_valid_name: + click.secho(err_msg, fg="red") + raise click.Abort() try: cloud_profiles = deploifai_api.get_cloud_profiles(workspace=command_workspace) - except DeploifaiAPIError as err: + except DeploifaiAPIError: click.echo("Could not fetch cloud profiles. Please try again.") return From 0ca958a8abfa9f9c443f11e1d38b02348259ca7a Mon Sep 17 00:00:00 2001 From: richardbryan Date: Thu, 7 Jul 2022 13:48:10 +0700 Subject: [PATCH 14/29] minor bug fix --- deploifai/project/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 3f7fae1..2959d38 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -130,9 +130,9 @@ def create(context: DeploifaiContextObj, name: str, workspace): # create a project directory locally, along with .deploifai directory within this project try: os.mkdir(name) - raise click.Abort() except OSError: click.secho("An error when creating the project locally", fg="red") + raise click.Abort() click.secho(f"A new directory named {name} has been created locally.", fg="green") From 7a2e87c58b1f6504f025e545a4b177f837e3891e Mon Sep 17 00:00:00 2001 From: Richard-B18 <78733143+Richard-B18@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:33:07 +0700 Subject: [PATCH 15/29] project command: create cloud profile (#17) * support gcp for the feature, added some enums, minor bug fix * Project create command (#16) * enable create command * prompt workspace and query cloud profile in create command * calls backend graphql api to createProject * fix minor bugs of wrong api call, add some msg after api call * add a validation for project name * move cli msg to command callback instead of during api calling, minor bug fix * push everything * add is_authenticated decorator and refactor to use graphql fragments * bump package version to 0.1.2 * refractor auth checking, minor bug fix Co-authored-by: Sean Chok * gcp cloud profile creation, refractor auth checking * parse gcp key json file as string * rm unnecessary imports * Project Browse Command (#19) * Project Browse Command Adding project browse command to cli and get_workspace function to api * Changes Improved the browse and get project function * final refactor and test * bump package version to 0.1.3 Co-authored-by: Sean Chok * refractor cloud profile creation * change app structure for cloud profile creation * minor bug fix on passing variables for querying graphql * minor bug fix * minor issue Co-authored-by: Sean Chok Co-authored-by: ExtinctWolf83 <89630581+ExtinctWolf83@users.noreply.github.com> --- deploifai/api/__init__.py | 51 +++++- deploifai/cli.py | 3 +- deploifai/cloud_profile/__init__.py | 13 ++ deploifai/cloud_profile/create.py | 165 ++++++++++++++++++ deploifai/project/create.py | 13 +- .../cloud_profile.py | 7 + 6 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 deploifai/cloud_profile/create.py rename deploifai/{cloud_profile => utilities}/cloud_profile.py (65%) diff --git a/deploifai/api/__init__.py b/deploifai/api/__init__.py index 83a13eb..9a00cc1 100644 --- a/deploifai/api/__init__.py +++ b/deploifai/api/__init__.py @@ -5,7 +5,7 @@ from deploifai.api.errors import DeploifaiAPIError from deploifai.utilities.credentials import get_auth_token -from deploifai.cloud_profile.cloud_profile import CloudProfile +from deploifai.utilities.cloud_profile import CloudProfile from deploifai.utilities import environment @@ -298,6 +298,49 @@ def get_cloud_profiles(self, workspace=None) -> typing.List[CloudProfile]: ] return cloud_profiles + def create_cloud_profile(self, provider, name, credentials, workspace, fragment): + mutation = """ + mutation( + $whereAccount: AccountWhereUniqueInput! + $data: CloudProfileCreateInput! + ) { + createCloudProfile(whereAccount: $whereAccount, data: $data) { + ...cloud_profile + } + } + """ + + variables = { + "whereAccount": {"username": workspace["username"]}, + "data": { + "name": name, + "provider": provider.value, + }, + } + + if provider.value == "AWS": + variables["data"]["awsCredentials"] = credentials + elif provider.value == "AZURE": + variables["data"]["azureCredentials"] = credentials + elif provider.value == "GCP": + variables["data"]["gcpCredentials"] = credentials + + try: + r = requests.post( + self.uri, + json={"query": mutation + fragment, "variables": variables}, + headers=self.headers, + ) + + create_mutation_data = r.json() + + return create_mutation_data["data"]["createCloudProfile"]["id"] + except TypeError as err: + raise DeploifaiAPIError("Could not create cloud profile. Please try again.") + except KeyError as err: + raise DeploifaiAPIError("Could not create cloud profile. Please try again.") + + def get_projects(self, workspace, fragment: str, where_project=None): query = ( """ @@ -362,14 +405,14 @@ def get_project(self, project_id: str, fragment: str): except KeyError as err: raise DeploifaiAPIError("Could not get project. Please try again.") - def create_project(self, project_name: str, cloud_profile: CloudProfile): + def create_project(self, project_name: str, cloud_profile: CloudProfile, fragment): mutation = """ mutation( $whereAccount: AccountWhereUniqueInput! $data: CreateProjectInput! ) { createProject(whereAccount: $whereAccount, data: $data) { - id + ...project } } """ @@ -385,7 +428,7 @@ def create_project(self, project_name: str, cloud_profile: CloudProfile): try: r = requests.post( self.uri, - json={"query": mutation, "variables": variables}, + json={"query": mutation + fragment, "variables": variables}, headers=self.headers, ) diff --git a/deploifai/cli.py b/deploifai/cli.py index 582682d..6c3c874 100644 --- a/deploifai/cli.py +++ b/deploifai/cli.py @@ -10,8 +10,9 @@ from .application import application from .project import project from .data import data +from .cloud_profile import cloud_profile -commands = {"auth": auth, "project": project, "data": data, "application": application} +commands = {"auth": auth, "project": project, "data": data, "application": application, "cloud-profile": cloud_profile} @click.group(commands=commands) diff --git a/deploifai/cloud_profile/__init__.py b/deploifai/cloud_profile/__init__.py index e69de29..42ba2e1 100644 --- a/deploifai/cloud_profile/__init__.py +++ b/deploifai/cloud_profile/__init__.py @@ -0,0 +1,13 @@ +import click + +from .create import create + + +@click.group() +def cloud_profile(): + """ + Manage user's cloud profiles + """ + + +cloud_profile.add_command(create) diff --git a/deploifai/cloud_profile/create.py b/deploifai/cloud_profile/create.py new file mode 100644 index 0000000..20a36a4 --- /dev/null +++ b/deploifai/cloud_profile/create.py @@ -0,0 +1,165 @@ +import click +from PyInquirer import prompt +from deploifai.utilities.cloud_profile import Provider +from deploifai.context import ( + pass_deploifai_context_obj, + DeploifaiContextObj, + is_authenticated, +) +from deploifai.utilities.user import parse_user_profiles +from deploifai.api import DeploifaiAPIError + + +@click.command() +@click.option("--name", "-n", help="Cloud profile name", prompt="Choose a cloud profile name") +@click.option("--workspace", "-w", help="Workspace name", type=str) +@pass_deploifai_context_obj +@is_authenticated +def create(context: DeploifaiContextObj, name: str, workspace: str): + """ + Create a new cloud profile + """ + deploifai_api = context.api + + user_data = deploifai_api.get_user() + personal_workspace, team_workspaces = parse_user_profiles(user_data) + + workspaces_from_api = [personal_workspace] + team_workspaces + + # checking validity of workspace, and prompting workspaces choices if not specified + if workspace and len(workspace): + if any(ws["username"] == workspace for ws in workspaces_from_api): + for w in workspaces_from_api: + if w["username"] == workspace: + command_workspace = w + break + else: + # the workspace user input does not match with any of the workspaces the user has access to + click.secho( + f"{workspace} cannot be found. Please put in a workspace you have access to.", + fg="red", + ) + raise click.Abort() + else: + _choose_workspace = prompt( + { + "type": "list", + "name": "workspace", + "message": "Choose a workspace", + "choices": [ + {"name": ws["username"], "value": ws} for ws in workspaces_from_api + ], + } + ) + if _choose_workspace == {}: + raise click.Abort() + command_workspace = _choose_workspace["workspace"] + + try: + cloud_profiles = deploifai_api.get_cloud_profiles(workspace=command_workspace) + except DeploifaiAPIError as err: + click.secho(err, fg="red") + return + + existing_names = [cloud_profile.name for cloud_profile in cloud_profiles] + + if name in existing_names: + click.echo(click.style("Cloud profile name taken. Existing names: ", fg="red") + ' '.join(existing_names)) + raise click.Abort() + + provider = prompt( + { + "type": "list", + "name": "provider", + "message": "Choose a provider for the new cloud profile", + "choices": [{"name": provider.value, "value": provider} for provider in Provider] + } + )["provider"] + + cloud_credentials = {} + if provider == Provider.AWS: + cloud_credentials["awsAccessKey"] = prompt( + { + "type": "input", + "name": "awsAccessKey", + "message": "AWS Access Key ID (We'll keep these secured and encrypted)", + } + )["awsAccessKey"] + cloud_credentials["awsSecretAccessKey"] = prompt( + { + "type": "input", + "name": "awsSecretAccessKey", + "message": "AWS Secret Access Key (We'll keep these secured and encrypted)", + } + )["awsSecretAccessKey"] + elif provider == Provider.AZURE: + cloud_credentials["azureSubscriptionId"] = prompt( + { + "type": "input", + "name": "azureSubscriptionId", + "message": "Azure Account Subscription ID (We'll keep these secured and encrypted)", + } + )["azureSubscriptionId"] + cloud_credentials["azureTenantId"] = prompt( + { + "type": "input", + "name": "azureTenantId", + "message": "Azure Active Directory Tenant ID (We'll keep these secured and encrypted)", + } + )["azureTenantId"] + cloud_credentials["azureClientId"] = prompt( + { + "type": "input", + "name": "azureClientId", + "message": "Azure Client ID (We'll keep these secured and encrypted)", + } + )["azureClientId"] + cloud_credentials["azureClientSecret"] = prompt( + { + "type": "input", + "name": "azureClientSecret", + "message": "Azure Client Secret / Password (We'll keep these secured and encrypted)", + } + )["azureClientSecret"] + else: + cloud_credentials["gcpProjectId"] = prompt( + { + "type": "input", + "name": "gcpProjectId", + "message": "GCP Project ID (We'll keep these secured and encrypted)", + } + )["gcpProjectId"] + + gcp_service_account_key_file = prompt( + { + "type": "input", + "name": "gcp_service_account_key_file", + "message": "File path for the GCP Service Account Key File (We'll keep these secured and encrypted)", + } + )["gcp_service_account_key_file"] + + try: + with open(gcp_service_account_key_file) as gcp_service_account_key_json: + cloud_credentials["gcpServiceAccountKey"] = gcp_service_account_key_json.read() + except FileNotFoundError: + click.secho("File not found. Please input the correct file path.", fg="red") + raise click.Abort() + + try: + cloud_profile_fragment = """ + fragment cloud_profile on CloudProfile { + id + } + """ + _ = deploifai_api.create_cloud_profile( + provider, + name, + cloud_credentials, + command_workspace, + cloud_profile_fragment + ) + except DeploifaiAPIError as err: + click.secho(err, fg="red") + raise click.Abort() + + click.secho(f"Successfully created a new cloud profile named {name}.", fg="green") diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 47a56e1..6a64137 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -104,7 +104,10 @@ def create(context: DeploifaiContextObj, name: str, workspace): click.echo("Could not fetch cloud profiles. Please try again.") return - # TODO: prompt user if no existing cloud profiles exist + if not cloud_profiles: + click.secho("No cloud profiles found. To create a cloud profile: deploifai cloud-profile create", fg="yellow") + raise click.Abort() + choose_cloud_profile = prompt( { "type": "list", @@ -125,9 +128,13 @@ def create(context: DeploifaiContextObj, name: str, workspace): ) cloud_profile = choose_cloud_profile["cloud_profile"] - try: - project_id = deploifai_api.create_project(name, cloud_profile)["id"] + project_fragment = """ + fragment project on Project { + id + } + """ + project_id = deploifai_api.create_project(name, cloud_profile, project_fragment)["id"] except DeploifaiAPIError as err: click.echo("Could not create project. Please try again.") return diff --git a/deploifai/cloud_profile/cloud_profile.py b/deploifai/utilities/cloud_profile.py similarity index 65% rename from deploifai/cloud_profile/cloud_profile.py rename to deploifai/utilities/cloud_profile.py index d78c884..06646d2 100644 --- a/deploifai/cloud_profile/cloud_profile.py +++ b/deploifai/utilities/cloud_profile.py @@ -1,3 +1,10 @@ +from enum import Enum + +class Provider(Enum): + AWS = "AWS" + AZURE = "AZURE" + GCP = "GCP" + class CloudProfile: def __init__(self, id, name, provider, workspace): self.id = id From cf40025a6175a7c3ad97c8a2bd30b6b7add26370 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Mon, 4 Jul 2022 22:01:50 +0700 Subject: [PATCH 16/29] minor refractor on global config --- deploifai/context.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/deploifai/context.py b/deploifai/context.py index f6bd485..f59a5ca 100644 --- a/deploifai/context.py +++ b/deploifai/context.py @@ -35,20 +35,16 @@ def __init__(self): pass def read_config(self): - global_config = configparser.ConfigParser() - # read the global config file - global_config.read(global_config_filepath) + self.global_config.read(global_config_filepath) # initialise sections if they don't exist already for section in global_config_sections: - if section not in global_config.sections(): - global_config[section] = {} + if section not in self.global_config.sections(): + self.global_config[section] = {} self.debug_msg(f"Read global config file from {global_config_filepath}") - self.global_config = global_config - # read local config file self.local_config = local_config.read_config_file() From d42b138229cd3fa129208204775d186c07b643fb Mon Sep 17 00:00:00 2001 From: richardbryan Date: Tue, 5 Jul 2022 10:53:44 +0700 Subject: [PATCH 17/29] traverse up recursively and read config file if in child dir of client's project dir --- deploifai/utilities/local_config.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 8950dc1..9be583c 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -69,10 +69,16 @@ def read_config_file() -> configparser.ConfigParser: Read the config file in the existing .deploifai/local.cfg file :return: A ConfigParser that contains all the configs in the config file """ + path = pathlib.Path.cwd() + while not path.joinpath(".deploifai").exists() and path != path.parent: + path = path.parent + + file_path = path.joinpath(".deploifai/local.cfg") + try: config = configparser.ConfigParser() # read the config file - config.read(config_file_path) + config.read(file_path) for section in config_sections: if section not in config.sections(): From 53df07e594515dca823a1574e6e19296a271b237 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Tue, 5 Jul 2022 12:58:24 +0700 Subject: [PATCH 18/29] refractor local config file manager --- deploifai/utilities/local_config.py | 37 ++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 9be583c..73a1dbd 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -1,8 +1,33 @@ import configparser import pathlib import click +from PyInquirer import prompt -config_file_path = pathlib.Path(".deploifai").joinpath("local.cfg") + +def find_local_config_dir(): + """ + Traverse up the file system and checks for a .deplooifai directory. + If does not exist, raise error not found. + :return: if local config file exists, return pathlib.Path object that points to the config file + """ + path = pathlib.Path.cwd() + while not path.joinpath(".deploifai").exists() and path != path.parent: + path = path.parent + + # True if .deplofai is not created yet + + # 2 cases: + # 1. user has not initialized a project + # 2. project is initialized but user is in the parent directory + + # TODO: not yet handle the case where user is in the parent directory, which means user is assumed to be creating a new project + if path == path.parent: + return pathlib.Path.cwd().joinpath(".deploifai/local.cfg") + + return path.joinpath(".deploifai/local.cfg") + + +config_file_path = find_local_config_dir() # pathlib.Path(".deploifai").joinpath("local.cfg") """ Manages a .deploifai/local.cfg file to store configuration info about a project. @@ -42,7 +67,7 @@ def create_config_files(): Creates the folder .deploifai that stores all the config files. :return: None """ - if pathlib.Path(".deploifai").exists(): + if config_file_path.parent.exists(): raise DeploifaiAlreadyInitialisedError( "Deploifai has already been initialised in this directory." ) @@ -69,16 +94,10 @@ def read_config_file() -> configparser.ConfigParser: Read the config file in the existing .deploifai/local.cfg file :return: A ConfigParser that contains all the configs in the config file """ - path = pathlib.Path.cwd() - while not path.joinpath(".deploifai").exists() and path != path.parent: - path = path.parent - - file_path = path.joinpath(".deploifai/local.cfg") - try: config = configparser.ConfigParser() # read the config file - config.read(file_path) + config.read(config_file_path) for section in config_sections: if section not in config.sections(): From d817930f5680b7bbf3a2ee82260c651c93b9572b Mon Sep 17 00:00:00 2001 From: richardbryan Date: Tue, 5 Jul 2022 14:10:24 +0700 Subject: [PATCH 19/29] minor typo --- deploifai/utilities/local_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 73a1dbd..974e3fa 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -6,7 +6,7 @@ def find_local_config_dir(): """ - Traverse up the file system and checks for a .deplooifai directory. + Traverse up the file system and checks for a .deploifai directory. If does not exist, raise error not found. :return: if local config file exists, return pathlib.Path object that points to the config file """ From bf22a0e7b0876f69dba4fbfcd4cafe82a9b0f291 Mon Sep 17 00:00:00 2001 From: Sean Chok Date: Tue, 5 Jul 2022 16:18:03 +0800 Subject: [PATCH 20/29] WIP --- deploifai/project/create.py | 6 +++++- deploifai/utilities/local_config.py | 33 ++++++++++++++--------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 6a64137..bce04f3 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -141,4 +141,8 @@ def create(context: DeploifaiContextObj, name: str, workspace): click.secho(f"Successfully created new project named {name}.", fg="green") - local_config.set_project_config(project_id, context.local_config) + # local_config.set_project_config(project_id, context.local_config) + + # create a project directory, and create .deploifai directory within this + + # tell the user that a new project directory called [...] has been created diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 974e3fa..3033bed 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -1,10 +1,9 @@ import configparser import pathlib import click -from PyInquirer import prompt -def find_local_config_dir(): +def _find_local_config_dir(): """ Traverse up the file system and checks for a .deploifai directory. If does not exist, raise error not found. @@ -14,20 +13,13 @@ def find_local_config_dir(): while not path.joinpath(".deploifai").exists() and path != path.parent: path = path.parent - # True if .deplofai is not created yet - - # 2 cases: - # 1. user has not initialized a project - # 2. project is initialized but user is in the parent directory - - # TODO: not yet handle the case where user is in the parent directory, which means user is assumed to be creating a new project if path == path.parent: - return pathlib.Path.cwd().joinpath(".deploifai/local.cfg") + raise DeploifaiNotInitialisedError("Deploifai project not found.") - return path.joinpath(".deploifai/local.cfg") + return path.joinpath(".deploifai", "local.cfg") -config_file_path = find_local_config_dir() # pathlib.Path(".deploifai").joinpath("local.cfg") +config_file_path = _find_local_config_dir() """ Manages a .deploifai/local.cfg file to store configuration info about a project. @@ -62,7 +54,16 @@ def __init__(self, message): super(DeploifaiDataAlreadyInitialisedError, self).__init__(message) -def create_config_files(): +class DeploifaiNotInitialisedError(Exception): + """ + Exception when Deploifai project is not found. + """ + + def __init__(self, message): + super(DeploifaiNotInitialisedError, self).__init__(message) + + +def create_config_files(new_project_dir: str): """ Creates the folder .deploifai that stores all the config files. :return: None @@ -72,6 +73,7 @@ def create_config_files(): "Deploifai has already been initialised in this directory." ) + pathlib.Path(new_project_dir).join pathlib.Path(".deploifai").mkdir() config_file_path.touch(exist_ok=True) @@ -112,11 +114,8 @@ def read_config_file() -> configparser.ConfigParser: def save_config_file(config: configparser.ConfigParser): """ Save the local config from the context in the local.cfg file. - :param context: The deploifai context object + :param config: A ConfigParser object representing config data """ - if not config_file_path.exists(): - create_config_files() - with config_file_path.open("w") as config_file: config.write(config_file) From 3cbca698ac72565f385456b4d78dbe18ef4fdd49 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 13:26:22 +0700 Subject: [PATCH 21/29] change flow of project create command, minor refractor in local config --- deploifai/project/create.py | 17 ++++++++++++++--- deploifai/utilities/local_config.py | 13 +++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index bce04f3..90f4eb6 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -9,6 +9,7 @@ from deploifai.api import DeploifaiAPIError from deploifai.utilities.user import parse_user_profiles from PyInquirer import prompt +import os @click.command() @@ -141,8 +142,18 @@ def create(context: DeploifaiContextObj, name: str, workspace): click.secho(f"Successfully created new project named {name}.", fg="green") - # local_config.set_project_config(project_id, context.local_config) + # create a project directory, along with .deploifai directory within this project + try: + os.mkdir(name) + except FileExistsError: + click.secho("A directory exists with the same name as the project name you just specified", fg="yellow") + except OSError: + click.secho("An error when creating the project locally", fg="red") + + project_path = os.path.join(os.getcwd(), name) + local_config.create_config_files(project_path) - # create a project directory, and create .deploifai directory within this + click.secho(f"A new directory named {name} has been created locally.", fg="green") - # tell the user that a new project directory called [...] has been created + # set id in local config file + local_config.set_project_config(project_id, context.local_config) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 3033bed..24848ba 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -68,13 +68,11 @@ def create_config_files(new_project_dir: str): Creates the folder .deploifai that stores all the config files. :return: None """ - if config_file_path.parent.exists(): - raise DeploifaiAlreadyInitialisedError( - "Deploifai has already been initialised in this directory." - ) + local_config_dir = pathlib.Path(new_project_dir).joinpath(".deploifai") + local_config_dir.mkdir(exist_ok=True) + + config_file_path = local_config_dir.joinpath("local.cfg") - pathlib.Path(new_project_dir).join - pathlib.Path(".deploifai").mkdir() config_file_path.touch(exist_ok=True) # initialise sections if they don't exist already @@ -96,6 +94,9 @@ def read_config_file() -> configparser.ConfigParser: Read the config file in the existing .deploifai/local.cfg file :return: A ConfigParser that contains all the configs in the config file """ + if config_file_path is None: + return None + try: config = configparser.ConfigParser() # read the config file From 0376fe9516a358d812972c9cabcbb6e32a45408c Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 13:46:32 +0700 Subject: [PATCH 22/29] change flow of cli, if project not found, set local config to none instead --- deploifai/utilities/local_config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 24848ba..d98e303 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -14,7 +14,11 @@ def _find_local_config_dir(): path = path.parent if path == path.parent: - raise DeploifaiNotInitialisedError("Deploifai project not found.") + click.echo(click.style("Project not found. To create a project: ", fg="red") + + click.style("deploifai project create NAME", fg="blue") + ) + return None + # raise DeploifaiNotInitialisedError("Deploifai project not found.") return path.joinpath(".deploifai", "local.cfg") From bf468cc145681521913750958126ef3072d96cd5 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 14:29:36 +0700 Subject: [PATCH 23/29] bug fix when user initializes a project --- deploifai/project/create.py | 1 + deploifai/utilities/local_config.py | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 90f4eb6..9fc9c41 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -155,5 +155,6 @@ def create(context: DeploifaiContextObj, name: str, workspace): click.secho(f"A new directory named {name} has been created locally.", fg="green") + context.local_config = local_config.read_config_file() # set id in local config file local_config.set_project_config(project_id, context.local_config) diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index d98e303..67c8df1 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -14,9 +14,6 @@ def _find_local_config_dir(): path = path.parent if path == path.parent: - click.echo(click.style("Project not found. To create a project: ", fg="red") + - click.style("deploifai project create NAME", fg="blue") - ) return None # raise DeploifaiNotInitialisedError("Deploifai project not found.") @@ -68,6 +65,7 @@ def __init__(self, message): def create_config_files(new_project_dir: str): + global config_file_path """ Creates the folder .deploifai that stores all the config files. :return: None @@ -87,7 +85,7 @@ def create_config_files(new_project_dir: str): click.secho( """A .deploifai directory has been created, which contains configuration that Deploifai requires. - You should version control this directory. + You should version control this directory. """, fg="blue", ) From 67075b93f633eb4331ecb1e116d266666d8be2d3 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 14:42:46 +0700 Subject: [PATCH 24/29] change flow of command, create project locally b4 doing it in the backend --- deploifai/project/create.py | 13 +++---------- deploifai/utilities/local_config.py | 3 +-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 9fc9c41..d066f27 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -105,10 +105,7 @@ def create(context: DeploifaiContextObj, name: str, workspace): click.echo("Could not fetch cloud profiles. Please try again.") return - if not cloud_profiles: - click.secho("No cloud profiles found. To create a cloud profile: deploifai cloud-profile create", fg="yellow") - raise click.Abort() - + # TODO: prompt user if no existing cloud profiles exist choose_cloud_profile = prompt( { "type": "list", @@ -129,13 +126,9 @@ def create(context: DeploifaiContextObj, name: str, workspace): ) cloud_profile = choose_cloud_profile["cloud_profile"] + try: - project_fragment = """ - fragment project on Project { - id - } - """ - project_id = deploifai_api.create_project(name, cloud_profile, project_fragment)["id"] + project_id = deploifai_api.create_project(name, cloud_profile)["id"] except DeploifaiAPIError as err: click.echo("Could not create project. Please try again.") return diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index 67c8df1..b5677e8 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -85,8 +85,7 @@ def create_config_files(new_project_dir: str): click.secho( """A .deploifai directory has been created, which contains configuration that Deploifai requires. - You should version control this directory. - """, + You should version control this directory.""", fg="blue", ) From 18f22ddaea024a7c6bfbe70054047374a50c3cdf Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 15:12:12 +0700 Subject: [PATCH 25/29] refractor for commands using local config --- deploifai/data/info.py | 4 ++++ deploifai/project/browse.py | 3 +++ deploifai/utilities/local_config.py | 3 +++ 3 files changed, 10 insertions(+) diff --git a/deploifai/data/info.py b/deploifai/data/info.py index 3701297..516c2b3 100644 --- a/deploifai/data/info.py +++ b/deploifai/data/info.py @@ -1,11 +1,15 @@ import click from deploifai.context import pass_deploifai_context_obj, DeploifaiContextObj +from deploifai.utilities.local_config import DeploifaiNotInitialisedError @click.command() @pass_deploifai_context_obj def info(context: DeploifaiContextObj): + if context.local_config is None: + raise DeploifaiNotInitialisedError("Deploifai project not found") + data_storage_config = context.local_config["DATA_STORAGE"] if "id" not in data_storage_config: diff --git a/deploifai/project/browse.py b/deploifai/project/browse.py index 7bb009c..5dbd86f 100644 --- a/deploifai/project/browse.py +++ b/deploifai/project/browse.py @@ -5,6 +5,7 @@ from deploifai.api import DeploifaiAPIError from deploifai.context import pass_deploifai_context_obj, DeploifaiContextObj from deploifai.utilities.frontend_routing import get_project_route +from deploifai.utilities.local_config import DeploifaiNotInitialisedError @click.command() @@ -56,6 +57,8 @@ def browse(context: DeploifaiContextObj, project: str, workspace="unassigned"): else: # assume that the user should be in a project directory, that contains local configuration file + if context.local_config is None: + raise DeploifaiNotInitialisedError("Deploifai project not found") if "id" not in context.local_config["PROJECT"]: click.secho("Missing workspace name and project name", fg='yellow') diff --git a/deploifai/utilities/local_config.py b/deploifai/utilities/local_config.py index b5677e8..8b23302 100644 --- a/deploifai/utilities/local_config.py +++ b/deploifai/utilities/local_config.py @@ -61,6 +61,9 @@ class DeploifaiNotInitialisedError(Exception): """ def __init__(self, message): + click.echo(click.style("Project not found. To create a project: ", fg="red") + + click.style("deploifai project create NAME", fg="blue") + ) super(DeploifaiNotInitialisedError, self).__init__(message) From 6c566a80f35ad8ccc86571826dcc0f4c90bbe7da Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 16:36:50 +0700 Subject: [PATCH 26/29] added a decorator to check if deploifai project found --- deploifai/context.py | 12 ++++++++++++ deploifai/data/info.py | 11 ++++++----- deploifai/project/browse.py | 10 ++++++---- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/deploifai/context.py b/deploifai/context.py index f59a5ca..1a83d45 100644 --- a/deploifai/context.py +++ b/deploifai/context.py @@ -141,3 +141,15 @@ def wrapper(click_context, *args, **kwargs): raise click.Abort() return functools.update_wrapper(wrapper, f) + + +def project_found(f): + @pass_context + def wrapper(click_context, *args, **kwargs): + deploifai_context = click_context.find_object(DeploifaiContextObj) + if deploifai_context.local_config is not None: + return click_context.invoke(f, *args, **kwargs) + + raise local_config.DeploifaiNotInitialisedError("Deploifai project not found") + + return functools.update_wrapper(wrapper, f) diff --git a/deploifai/data/info.py b/deploifai/data/info.py index 516c2b3..a2f8e10 100644 --- a/deploifai/data/info.py +++ b/deploifai/data/info.py @@ -1,15 +1,16 @@ import click -from deploifai.context import pass_deploifai_context_obj, DeploifaiContextObj -from deploifai.utilities.local_config import DeploifaiNotInitialisedError +from deploifai.context import ( + pass_deploifai_context_obj, + DeploifaiContextObj, + project_found, +) @click.command() @pass_deploifai_context_obj +@project_found def info(context: DeploifaiContextObj): - if context.local_config is None: - raise DeploifaiNotInitialisedError("Deploifai project not found") - data_storage_config = context.local_config["DATA_STORAGE"] if "id" not in data_storage_config: diff --git a/deploifai/project/browse.py b/deploifai/project/browse.py index 5dbd86f..a140bdf 100644 --- a/deploifai/project/browse.py +++ b/deploifai/project/browse.py @@ -3,13 +3,17 @@ import requests from deploifai.api import DeploifaiAPIError -from deploifai.context import pass_deploifai_context_obj, DeploifaiContextObj +from deploifai.context import ( + pass_deploifai_context_obj, + DeploifaiContextObj, + project_found, +) from deploifai.utilities.frontend_routing import get_project_route -from deploifai.utilities.local_config import DeploifaiNotInitialisedError @click.command() @pass_deploifai_context_obj +@project_found @click.option("--workspace", help="Workspace name", type=str) @click.option("--project", help="Project name", type=str) def browse(context: DeploifaiContextObj, project: str, workspace="unassigned"): @@ -57,8 +61,6 @@ def browse(context: DeploifaiContextObj, project: str, workspace="unassigned"): else: # assume that the user should be in a project directory, that contains local configuration file - if context.local_config is None: - raise DeploifaiNotInitialisedError("Deploifai project not found") if "id" not in context.local_config["PROJECT"]: click.secho("Missing workspace name and project name", fg='yellow') From 4b8cdf6ebfb41af81c0d442ce8bc64b3aecf6753 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Wed, 6 Jul 2022 22:16:55 +0700 Subject: [PATCH 27/29] change flow of commmand again, bug fix for duplicate local file names --- deploifai/api/__init__.py | 12 ++++++------ deploifai/project/create.py | 35 +++++++++++++++++++++-------------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/deploifai/api/__init__.py b/deploifai/api/__init__.py index 9a00cc1..17884a3 100644 --- a/deploifai/api/__init__.py +++ b/deploifai/api/__init__.py @@ -370,9 +370,9 @@ def get_projects(self, workspace, fragment: str, where_project=None): api_data = r.json() return api_data["data"]["projects"] - except TypeError as err: + except TypeError: raise DeploifaiAPIError("Could not get projects. Please try again.") - except KeyError as err: + except KeyError: raise DeploifaiAPIError("Could not get projects. Please try again.") def get_project(self, project_id: str, fragment: str): @@ -400,9 +400,9 @@ def get_project(self, project_id: str, fragment: str): api_data = r.json() return api_data["data"]["project"] - except TypeError as err: + except TypeError: raise DeploifaiAPIError("Could not get project. Please try again.") - except KeyError as err: + except KeyError: raise DeploifaiAPIError("Could not get project. Please try again.") def create_project(self, project_name: str, cloud_profile: CloudProfile, fragment): @@ -435,7 +435,7 @@ def create_project(self, project_name: str, cloud_profile: CloudProfile, fragmen create_mutation_data = r.json() return create_mutation_data["data"]["createProject"] - except TypeError as err: + except TypeError: raise DeploifaiAPIError("Could not create project. Please try again.") - except KeyError as err: + except KeyError: raise DeploifaiAPIError("Could not create project. Please try again.") diff --git a/deploifai/project/create.py b/deploifai/project/create.py index d066f27..6f0d823 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -69,14 +69,19 @@ def create(context: DeploifaiContextObj, name: str, workspace): click.echo("An error occurred when fetching projects. Please try again.") return - err_msg = "Project name taken. Choose a unique project name:" - project_names = [project["name"] for project in projects] + user_pwd_dir_names = os.listdir() + + dup_local_err_msg = f"There are existing files/directories in your computer also named {name}" + dup_backend_err_msg = f"Project name taken. Existing names in chosen workspace: {' '.join(project_names)}\nChoose a unique project name:" - name_taken_err_msg = f"Project name taken. Existing names in chosen workspace: {' '.join(project_names)}\nChoose a unique project name:" - err_msg = name_taken_err_msg + err_msg = "" + if name in user_pwd_dir_names: + err_msg = dup_local_err_msg + elif name in project_names: + err_msg = dup_backend_err_msg - is_valid_name = not (name in project_names) + is_valid_name = not (name in project_names or name in user_pwd_dir_names) while not is_valid_name: prompt_name = prompt( @@ -89,12 +94,14 @@ def create(context: DeploifaiContextObj, name: str, workspace): new_project_name = prompt_name["project_name"] - if len(new_project_name) == 0: + if len(new_project_name) == 0 or new_project_name.isspace(): err_msg = "Project name cannot be empty.\nChoose a non-empty project name:" elif not new_project_name.isalnum(): err_msg = f"Project name should only contain alphanumeric characters.\nChoose a valid project name:" + elif new_project_name in user_pwd_dir_names: + err_msg = dup_local_err_msg elif new_project_name in project_names: - err_msg = name_taken_err_msg + err_msg = dup_backend_err_msg else: name = new_project_name is_valid_name = True @@ -127,27 +134,27 @@ def create(context: DeploifaiContextObj, name: str, workspace): cloud_profile = choose_cloud_profile["cloud_profile"] + # create proejct in the backend try: project_id = deploifai_api.create_project(name, cloud_profile)["id"] except DeploifaiAPIError as err: - click.echo("Could not create project. Please try again.") - return + click.secho(err, fg="red") + raise click.Abort() click.secho(f"Successfully created new project named {name}.", fg="green") - # create a project directory, along with .deploifai directory within this project + # create a project directory locally, along with .deploifai directory within this project try: os.mkdir(name) - except FileExistsError: - click.secho("A directory exists with the same name as the project name you just specified", fg="yellow") + raise click.Abort() except OSError: click.secho("An error when creating the project locally", fg="red") + click.secho(f"A new directory named {name} has been created locally.", fg="green") + project_path = os.path.join(os.getcwd(), name) local_config.create_config_files(project_path) - click.secho(f"A new directory named {name} has been created locally.", fg="green") - context.local_config = local_config.read_config_file() # set id in local config file local_config.set_project_config(project_id, context.local_config) From d02f6564ebdaf0ebfc82586fabf768486b2d0619 Mon Sep 17 00:00:00 2001 From: richardbryan Date: Thu, 7 Jul 2022 12:55:17 +0700 Subject: [PATCH 28/29] change command flow, no more loops if invalid name --- deploifai/project/create.py | 44 ++++++++++++------------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 6f0d823..3f7fae1 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -72,43 +72,27 @@ def create(context: DeploifaiContextObj, name: str, workspace): project_names = [project["name"] for project in projects] user_pwd_dir_names = os.listdir() - dup_local_err_msg = f"There are existing files/directories in your computer also named {name}" - dup_backend_err_msg = f"Project name taken. Existing names in chosen workspace: {' '.join(project_names)}\nChoose a unique project name:" + is_valid_name = True err_msg = "" + if name in user_pwd_dir_names: - err_msg = dup_local_err_msg + is_valid_name = False + err_msg = f"There are existing files/directories in your computer also named {name}" elif name in project_names: - err_msg = dup_backend_err_msg - - is_valid_name = not (name in project_names or name in user_pwd_dir_names) - - while not is_valid_name: - prompt_name = prompt( - { - "type": "input", - "name": "project_name", - "message": err_msg, - } - ) - - new_project_name = prompt_name["project_name"] - - if len(new_project_name) == 0 or new_project_name.isspace(): - err_msg = "Project name cannot be empty.\nChoose a non-empty project name:" - elif not new_project_name.isalnum(): - err_msg = f"Project name should only contain alphanumeric characters.\nChoose a valid project name:" - elif new_project_name in user_pwd_dir_names: - err_msg = dup_local_err_msg - elif new_project_name in project_names: - err_msg = dup_backend_err_msg - else: - name = new_project_name - is_valid_name = True + is_valid_name = False + err_msg = f"Project name taken. Existing names in chosen workspace: {' '.join(project_names)}." + elif name.isalnum(): + is_valid_name = False + err_msg = "Project name should only contain alphanumeric characters." + + if not is_valid_name: + click.secho(err_msg, fg="red") + raise click.Abort() try: cloud_profiles = deploifai_api.get_cloud_profiles(workspace=command_workspace) - except DeploifaiAPIError as err: + except DeploifaiAPIError: click.echo("Could not fetch cloud profiles. Please try again.") return From 8a7dc38d01661e36235f18a7ae364b91b8c07b9a Mon Sep 17 00:00:00 2001 From: richardbryan Date: Thu, 7 Jul 2022 13:48:10 +0700 Subject: [PATCH 29/29] minor bug fix --- deploifai/project/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploifai/project/create.py b/deploifai/project/create.py index 3f7fae1..2959d38 100644 --- a/deploifai/project/create.py +++ b/deploifai/project/create.py @@ -130,9 +130,9 @@ def create(context: DeploifaiContextObj, name: str, workspace): # create a project directory locally, along with .deploifai directory within this project try: os.mkdir(name) - raise click.Abort() except OSError: click.secho("An error when creating the project locally", fg="red") + raise click.Abort() click.secho(f"A new directory named {name} has been created locally.", fg="green")