diff --git a/python/hopsworks/__init__.py b/python/hopsworks/__init__.py index 23dcea9df..836b21da9 100644 --- a/python/hopsworks/__init__.py +++ b/python/hopsworks/__init__.py @@ -25,6 +25,8 @@ from hopsworks.client.exceptions import RestAPIError, ProjectException from hopsworks import version, constants, client from hopsworks.connection import Connection +from hopsworks.core import project_api, secret_api +from hopsworks.decorators import NoHopsworksConnectionError # Needs to run before import of hsml and hsfs warnings.filterwarnings(action="ignore", category=UserWarning, module=r".*psycopg2") @@ -39,7 +41,8 @@ _hw_connection = Connection.connection _connected_project = None - +_secrets_api = None +_project_api = None def hw_formatwarning(message, category, filename, lineno, line=None): return "{}: {}\n".format(category.__name__, message) @@ -110,6 +113,7 @@ def login( if "REST_ENDPOINT" in os.environ: _hw_connection = _hw_connection() _connected_project = _hw_connection.get_project() + _initialize_module_apis() print("\nLogged in to project, explore it here " + _connected_project.get_url()) return _connected_project @@ -173,6 +177,7 @@ def login( "\nLogged in to project, explore it here " + _connected_project.get_url() ) + _initialize_module_apis() return _connected_project except RestAPIError: logout() @@ -200,7 +205,12 @@ def login( logout() raise e - print("\nLogged in to project, explore it here " + _connected_project.get_url()) + if _connected_project is None: + print("Could not find any project, use hopsworks.create_project('my_project') to create one") + else: + print("\nLogged in to project, explore it here " + _connected_project.get_url()) + + _initialize_module_apis() return _connected_project @@ -246,7 +256,7 @@ def _prompt_project(valid_connection, project): saas_projects = valid_connection.get_projects() if project is None: if len(saas_projects) == 0: - raise ProjectException("Could not find any project") + return None elif len(saas_projects) == 1: return saas_projects[0] else: @@ -283,7 +293,72 @@ def _prompt_project(valid_connection, project): def logout(): global _hw_connection - if isinstance(_hw_connection, Connection): + global _project_api + global _secrets_api + + if _is_connection_active(): _hw_connection.close() + client.stop() + _project_api = None + _secrets_api = None _hw_connection = Connection.connection + +def _is_connection_active(): + global _hw_connection + return isinstance(_hw_connection, Connection) + +def _initialize_module_apis(): + global _project_api + global _secrets_api + _project_api = project_api.ProjectApi() + _secrets_api = secret_api.SecretsApi() + +def create_project( + name: str, description: str = None, feature_store_topic: str = None +): + """Create a new project. + + Example for creating a new project + + ```python + + import hopsworks + + hopsworks.login() + + hopsworks.create_project("my_hopsworks_project", description="An example Hopsworks project") + + ``` + # Arguments + name: The name of the project. + description: optional description of the project + feature_store_topic: optional feature store topic name + + # Returns + `Project`. A project handle object to perform operations on. + """ + global _hw_connection + global _connected_project + + if not _is_connection_active(): + raise NoHopsworksConnectionError() + + new_project = _hw_connection._project_api._create_project(name, description, feature_store_topic) + if _connected_project is None: + _connected_project = new_project + print("Setting {} as the active project".format(_connected_project.name)) + return _connected_project + else: + print("You are already using the project {}, to access the new project use hopsworks.login(project='{}')".format(_connected_project.name, new_project.name)) + +def get_secrets_api(): + """Get the secrets api. + + # Returns + `SecretsApi`: The Secrets Api handle + """ + global _secrets_api + if not _is_connection_active(): + raise NoHopsworksConnectionError() + return _secrets_api \ No newline at end of file diff --git a/python/hopsworks/core/secret_api.py b/python/hopsworks/core/secret_api.py index a39a2dca0..60a1f9415 100644 --- a/python/hopsworks/core/secret_api.py +++ b/python/hopsworks/core/secret_api.py @@ -14,9 +14,11 @@ # limitations under the License. # -from hopsworks import client, secret +from hopsworks import client, secret, util from hopsworks.core import project_api import json +import getpass +from hopsworks.client.exceptions import RestAPIError class SecretsApi: @@ -46,7 +48,7 @@ def get_secret(self, name: str, owner: str = None): """Get a secret. # Arguments - name: Name of the project. + name: Name of the secret. owner: email of the owner for a secret shared with the current project. # Returns `Secret`: The Secret object @@ -69,9 +71,33 @@ def get_secret(self, name: str, owner: str = None): "shared", ] - return secret.Secret.from_response_json( - _client._send_request("GET", path_params, query_params=query_params) - )[0] + return secret.Secret.from_response_json(_client._send_request("GET", path_params, query_params=query_params))[0] + + def get(self, name: str, owner: str = None): + """Get the secret's value. + If the secret does not exist, it prompts the user to create the secret if the application is running interactively + + # Arguments + name: Name of the secret. + owner: email of the owner for a secret shared with the current project. + # Returns + `str`: The secret value + # Raises + `RestAPIError`: If unable to get the secret + """ + try: + return self.get_secret(name=name, owner=owner).value + except RestAPIError as e: + print(e.response.json()) + if ( + e.response.json().get("errorCode", "") == 160048 + and e.response.status_code == 404 + and util.is_interactive() + ): + secret_input = getpass.getpass(prompt="\nCould not find secret, enter value here to create it: ") + return self.create_secret(name, secret_input).value + else: + raise e def create_secret(self, name: str, value: str, project: str = None): """Create a new secret. diff --git a/python/hopsworks/project.py b/python/hopsworks/project.py index 1ae45fb0a..30358ffcd 100644 --- a/python/hopsworks/project.py +++ b/python/hopsworks/project.py @@ -107,6 +107,8 @@ def get_feature_store(self, name: str = None): Defaulting to the project name of default feature store. To get a shared feature store, the project name of the feature store is required. + # Arguments + name: Project name of the feature store. # Returns `hsfs.feature_store.FeatureStore`: The Feature Store API # Raises diff --git a/python/hopsworks/util.py b/python/hopsworks/util.py index 70712431a..66ad8be70 100644 --- a/python/hopsworks/util.py +++ b/python/hopsworks/util.py @@ -79,3 +79,7 @@ def get_hostname_replaced_url(sub_path: str): href = urljoin(client.get_instance()._base_url, sub_path) url_parsed = client.get_instance().replace_public_host(urlparse(href)) return url_parsed.geturl() + +def is_interactive(): + import __main__ as main + return not hasattr(main, '__file__') \ No newline at end of file