From d63c0a624ab375681792d585ab05449f12d2a76f Mon Sep 17 00:00:00 2001 From: German Date: Mon, 24 Jun 2024 17:19:18 +0200 Subject: [PATCH 01/15] First approach to login --- src/ansys/mapdl/core/cli/login.py | 146 ++++++++++++++++++++ src/ansys/mapdl/core/hpc/login.py | 217 ++++++++++++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 src/ansys/mapdl/core/cli/login.py create mode 100644 src/ansys/mapdl/core/hpc/login.py diff --git a/src/ansys/mapdl/core/cli/login.py b/src/ansys/mapdl/core/cli/login.py new file mode 100644 index 0000000000..672a6037d1 --- /dev/null +++ b/src/ansys/mapdl/core/cli/login.py @@ -0,0 +1,146 @@ +# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Login into a PyHPS cluster""" +from getpass import getpass +import logging + +import click + +logger = logging.getLogger() + + +@click.command( + name="login", + short_help="Login into an HPS cluster.", + help="""Login into an HPS cluster. + +It does store credentials (cluster url, password and username) in the OS credential manager. +If you want to change any credential, just issue the command again with the new values. + +""", +) +@click.option("--user", default=None, type=str, help="The username to login.") +@click.option("--password", default=None, type=str, help="The password to login.") +@click.option( + "--url", + default=None, + type=str, + help="The HPS cluster URL. For instance 'https://10.231.106.1:3000/hps'.", +) +@click.option( + "--default", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Set the default user, password and URL. These credentials are not tested.""", +) +@click.option( + "--test_token", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Test if the token is valid. This argument is ignored if '--default' argument is ``True``.""", +) +def login(user, password, url, default, test_token): + """ + Command Line Interface for logging in and getting an access token. + + Parameters + user (str): Username for login + password (str): Password for login + default (bool): If used, the input data is set as default and it will be used for any login which does not specify the url, user or password. + """ + if not default: + logger.debug("Storing non-default credentials.") + if not user and not click.prompt("Username: "): + raise ValueError("No user was provided.") + + if not password and not getpass("Password: "): + raise ValueError("No password was provided.") + + if not url and not click.prompt("HPS cluster URL: "): + raise ValueError("No password was provided.") + + try: + token = login(user, password, url) + click.echo(f"Login successful") + except Exception as e: + click.echo(f"Login failed: {str(e)}") + + if test_token: + logger.debug("Testing token") + from requests import ConnectionError + + from ansys.mapdl.core.hpc.login import token_is_valid + + if not token_is_valid(token): + raise ConnectionError("The retrieved token is not valid.") + + logger.info(f"Stored credentials: {user}, {password}, {url}") + store_credentials(user, password, url) + + +@click.command( + short_help="Logout from an HPS cluster.", + help="""Logout from an HPS cluster. + +It deletes credentials stored on the system. + +""", +) +@click.option( + "--url", + prompt="HPS cluster URL", + help="The HPS cluster URL. For instance 'https://10.231.106.1:3000/hps'.", + default=None, + type=str, +) +@click.option( + "--default", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Whether PyMAPDL is to print debug logging to the console.""", +) +def logout(url, default): + + # TODO: keyrings library seems to not being able to list the credentials + # under a service name. We might need to keep track of those in a file or + # something. + + from ansys.mapdl.core.hpc.login import delete_credentials + + if not url and not default: + raise ValueError("An URL needs to be used.") + + if url and default: + raise ValueError("The argument '--default' cannot be used with an URL.") + + if default: + url = "default" + + delete_credentials(url) + click.echo(f"The credentials for '{url}' have been deleted.") diff --git a/src/ansys/mapdl/core/hpc/login.py b/src/ansys/mapdl/core/hpc/login.py new file mode 100644 index 0000000000..f27311411f --- /dev/null +++ b/src/ansys/mapdl/core/hpc/login.py @@ -0,0 +1,217 @@ +# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.hps.client import AuthApi, Client +from ansys.hps.client.authenticate import authenticate +import keyring +from requests import ConnectionError + +DEFAULT_IDENTIFIER = "defaultconfig" +SERVICE_NAME = "pymapdl-pyhps" +EXPIRATION_TIME = 4 * 24 * 60 # 2 days in minutes + + +def login(user, password, url): + """ + Authenticate with the server and return the access token. + + Parameters + ---------- + user : str + Username. + password : str + Password. + url : str + URL. + + Returns + ------- + str + Access token. + """ + access_token = authenticate( + url=url, username=user, password=password, scope="openid", verify=False + )["access_token"] + return access_token + + +def store_credentials( + user: str = None, + password: str = None, + url: str = None, + default=False, + expiration_time: float = EXPIRATION_TIME, +): + """ + Store user credentials and the current timestamp in the keyring. + + If ``default`` argument is ``True``, you can store a default password, + default user, or/and default URL. + + Parameters + ---------- + user : str, optional + Username. + password : str, optional + Password + url : str, optional + URL of the HPS cluster + + """ + if default: + identifier = DEFAULT_IDENTIFIER + else: + identifier = url + + if not default and (not url or not user or not password): + raise ValueError( + "To store non-default credentials, an URL, an user and a password are needed." + ) + + if url: + keyring.set_password(SERVICE_NAME, f"{identifier}_url", url) + if user: + keyring.set_password(SERVICE_NAME, f"{identifier}_user", user) + if password: + keyring.set_password(SERVICE_NAME, f"{identifier}_password", password) + if expiration_time: + keyring.set_password( + SERVICE_NAME, f"{identifier}_expiration_time", str(expiration_time) + ) + + keyring.set_password(SERVICE_NAME, f"{identifier}_timestamp", str(time.time())) + + +def get_stored_credentials(identifier): + """ + Retrieve stored credentials and timestamp from the keyring. + + Returns: + tuple: (user, password, timestamp) or (None, None, None) if not found + """ + + url = keyring.get_password(SERVICE_NAME, f"{identifier}_url") + user = keyring.get_password(SERVICE_NAME, f"{identifier}_user") + password = keyring.get_password(SERVICE_NAME, f"{identifier}_password") + timestamp = keyring.get_password(SERVICE_NAME, f"{identifier}_timestamp") + expiration_time = keyring.get_password( + SERVICE_NAME, f"{identifier}_expiration_time" + ) + + if timestamp: + timestamp = float(timestamp) + if expiration_time: + expiration_time = float(expiration_time) + + return url, user, password, timestamp, expiration_time + + +def credentials_expired(timestamp, expiration_time: float = EXPIRATION_TIME): + """ + Check if the stored credentials have expired. + + Parameters: + timestamp (float): Timestamp of when the credentials were stored + + Returns: + bool: True if credentials have expired, False otherwise + """ + return time.time() - timestamp > expiration_time * 60 + + +def delete_credentials(identifier): + keyring.delete_password(SERVICE_NAME, f"{identifier}_url") + keyring.delete_password(SERVICE_NAME, f"{identifier}_user") + keyring.delete_password(SERVICE_NAME, f"{identifier}_password") + keyring.delete_password(SERVICE_NAME, f"{identifier}_timestamp") + keyring.delete_password(SERVICE_NAME, f"{identifier}_expiration_time") + + +def token_is_valid(token): + client = Client(access_token=token, verify=False) + auth_api = AuthApi(client) + + try: + auth_api.get_users() + return True + except ConnectionError: + return False + except Exception as e: + raise e + + +def access(url: str = None, user: str = None, password: str = None): + + if not url or not user or not password: + if not url: + identifier = DEFAULT_IDENTIFIER + else: + identifier = url + + ( + url_default, + user_default, + password_default, + timestamp_default, + expiration_default, + ) = get_stored_credentials(identifier=identifier) + + if not url_default or not user_default or not password_default: + raise ConnectionError( + f"There are no credentials stored for '{identifier}'." + ) + + if credentials_expired(timestamp_default, expiration_time=expiration_default): + delete_credentials(identifier) + + raise ConnectionError(f"The stored '{identifier}' credentials are expired.") + + if not url: + if url_default: + url = url_default + else: + raise ValueError( + f"No 'URL' is given nor stored for '{identifier}'. You must input one." + ) + + if not user: + if user: + user = user_default + else: + raise ValueError( + f"No 'user' is given nor stored for '{identifier}'. You must input one." + ) + + if not password: + if password_default: + password = password_default + else: + raise ValueError( + f"No 'password' is given nor stored for '{identifier}'. You must input one." + ) + + return login(user=user, password=password, url=url) + + +def get_default_url(): + """Return the default URL""" + return keyring.get_password(SERVICE_NAME, f"{DEFAULT_IDENTIFIER}_url") From 4319e268f9c07b6c05b107d6100165d2f06544f6 Mon Sep 17 00:00:00 2001 From: German Date: Mon, 24 Jun 2024 17:21:11 +0200 Subject: [PATCH 02/15] Adding HPS dependencies --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b9000b7a67..b6a1f9573b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,11 @@ doc = [ "vtk==9.3.0", ] +hps =[ + "ansys-hps-client==0.8.0", + "keyring==25.2.1", +] + [tool.flit.module] name = "ansys.mapdl.core" From d1838cd2ab795dc6b92e3b5ff37c77b08a2aa0cd Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot Date: Mon, 24 Jun 2024 15:22:01 +0000 Subject: [PATCH 03/15] Adding changelog entry: 3205.miscellaneous.md --- doc/changelog.d/3205.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/3205.miscellaneous.md diff --git a/doc/changelog.d/3205.miscellaneous.md b/doc/changelog.d/3205.miscellaneous.md new file mode 100644 index 0000000000..44cf5dc554 --- /dev/null +++ b/doc/changelog.d/3205.miscellaneous.md @@ -0,0 +1 @@ +feat: Detaching logging from main logic \ No newline at end of file From 2b3451d53f306898fae8d183aed964acfef5c973 Mon Sep 17 00:00:00 2001 From: German Date: Mon, 24 Jun 2024 17:29:26 +0200 Subject: [PATCH 04/15] feat: Coupling login code to current implementation. Allowing login using token which is now the preferred method. --- src/ansys/mapdl/core/cli/hpc.py | 19 +++++++++++++------ src/ansys/mapdl/core/hpc/pyhps.py | 30 ++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/ansys/mapdl/core/cli/hpc.py b/src/ansys/mapdl/core/cli/hpc.py index 0d79ed4ddb..719fbaeae5 100644 --- a/src/ansys/mapdl/core/cli/hpc.py +++ b/src/ansys/mapdl/core/cli/hpc.py @@ -28,12 +28,12 @@ import click -from ansys.mapdl.core.cli import main +from ansys.mapdl.core.hpc.login import access, get_default_url logger = logging.getLogger() -@main.command( +@click.command( short_help="Submit jobs to an HPC cluster using PyHPS.", help=""" Submit jobs to an HPC cluster using PyHPS. @@ -256,9 +256,17 @@ def submit( config_file = os.path.join(os.getcwd(), "hps_config.json") logger.debug(f"Using default HPS configuration file: {config_file}") - url = get_value_from_json_or_default(url, config_file, "url", None) + # Getting cluster login configuration from CLI or file + url = get_value_from_json_or_default( + url, config_file, "url", None, raise_if_none=False + ) + url = url or get_default_url() # using default URL stored. user = get_value_from_json_or_default(user, config_file, "user", None) - password = get_value_from_json_or_default(password, config_file, "password", None) + + # Getting access token + token = access(url, user, password) + + # Getting other configuration from CLI or file python = get_value_from_json_or_default(python, config_file, "python", 3) name = get_value_from_json_or_default(name, config_file, "name", "My PyMAPDL job") @@ -276,8 +284,7 @@ def submit( job = PyMAPDLJobSubmission( url=url, - user=user, - password=password, + token=token, main_file=main_file, mode=mode, inputs=inputs, diff --git a/src/ansys/mapdl/core/hpc/pyhps.py b/src/ansys/mapdl/core/hpc/pyhps.py index 6cf0de8787..8ced7593c7 100644 --- a/src/ansys/mapdl/core/hpc/pyhps.py +++ b/src/ansys/mapdl/core/hpc/pyhps.py @@ -49,7 +49,11 @@ def get_value_from_json_or_default( - arg: str, json_file: str, key: str, default_value: Optional[Union[str, Any]] = None + arg: str, + json_file: str, + key: str, + default_value: Optional[Union[str, Any]] = None, + raise_if_none: Optional[bool] = True, ): if arg is not None: logger.debug(f"Using '{arg}' for {key}") @@ -64,7 +68,7 @@ def get_value_from_json_or_default( logger.debug(f"Using '{config[key]}' for {key}") return config[key] - if default_value is None: + if default_value is None and raise_if_none: raise ValueError( f"The argument {arg} is not given through the CLI or config file." ) @@ -106,10 +110,11 @@ class JobSubmission: def __init__( self, - url, - user, - password, main_file, + url, + user: Optional[str] = None, + password: Optional[str] = None, + token: Optional[str] = None, mode: Optional[str] = None, inputs: Optional[Union[list[str]]] = None, outputs: Optional[Union[list[str]]] = None, @@ -125,9 +130,15 @@ def __init__( max_execution_time: Optional[int] = None, name: Optional[str] = None, ): + + if not token and (not user or not password): + raise ValueError("An access token or an user-password pair must be used.") + self._url = url self._user = user self._password = password + self._token = token + self._main_file = self._validate_main_file(main_file) self._mode = self._validate_mode(mode) @@ -938,9 +949,12 @@ def _load_results(self): self._output_values.append(each_job.values) def _connect_client(self): - self._client: Client = Client( - url=self.url, username=self.user, password=self.password, verify=False - ) + from ansys.mapdl.core.hpc.login import access + + if not self._token: + self._token = access(url=self.url, user=self.user, password=self.password) + + self._client: Client = Client(access_token=self._token, verify=False) def close_client(self): self._client.session.close() From e1c6347d5956d7692ef37c3d76431c13042cfa8e Mon Sep 17 00:00:00 2001 From: German Date: Mon, 24 Jun 2024 17:33:07 +0200 Subject: [PATCH 05/15] coupling cli --- src/ansys/mapdl/core/cli/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ansys/mapdl/core/cli/__init__.py b/src/ansys/mapdl/core/cli/__init__.py index b2936f4558..04589d3a7a 100644 --- a/src/ansys/mapdl/core/cli/__init__.py +++ b/src/ansys/mapdl/core/cli/__init__.py @@ -39,7 +39,6 @@ def main(ctx): pass from ansys.mapdl.core.cli.convert import convert - from ansys.mapdl.core.cli.hpc import submit from ansys.mapdl.core.cli.list_instances import list_instances from ansys.mapdl.core.cli.start import start from ansys.mapdl.core.cli.stop import stop @@ -50,10 +49,16 @@ def main(ctx): main.add_command(list_instances, name="list") # HPC commands - # pymapdl hpc submit - # pymapdl hpc list - # pymapdl hpc stop + # pymapdl (hpc) login + # pymapdl (hpc) submit + # pymapdl (hpc) list #To be implemented + # pymapdl (hpc) stop #To be implemented + from ansys.mapdl.core.cli.hpc import submit + from ansys.mapdl.core.cli.login import login, logout + + main.add_command(login) main.add_command(submit) + main.add_command(logout) def old_pymapdl_convert_script_entry_point(): print( From bf5f92e29cadcd2076940ff57f5383c72b8f07a1 Mon Sep 17 00:00:00 2001 From: German Date: Mon, 24 Jun 2024 17:43:39 +0200 Subject: [PATCH 06/15] fix: command name in CLI --- src/ansys/mapdl/core/cli/list_instances.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ansys/mapdl/core/cli/list_instances.py b/src/ansys/mapdl/core/cli/list_instances.py index a98c72ec89..f9ebf4ab25 100644 --- a/src/ansys/mapdl/core/cli/list_instances.py +++ b/src/ansys/mapdl/core/cli/list_instances.py @@ -28,6 +28,7 @@ @main.command( short_help="List MAPDL running instances.", help="""This command list MAPDL instances""", + name="list", ) @click.option( "--instances", From 186687b241d9fb3854814c4a0a2aafb9f6a89ca4 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot Date: Mon, 24 Jun 2024 15:50:01 +0000 Subject: [PATCH 07/15] Adding changelog entry: 3205.added.md --- doc/changelog.d/{3205.miscellaneous.md => 3205.added.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changelog.d/{3205.miscellaneous.md => 3205.added.md} (100%) diff --git a/doc/changelog.d/3205.miscellaneous.md b/doc/changelog.d/3205.added.md similarity index 100% rename from doc/changelog.d/3205.miscellaneous.md rename to doc/changelog.d/3205.added.md From f68eae8d5e02196b67b49dfb74a666e4d8f4b7a6 Mon Sep 17 00:00:00 2001 From: German Date: Mon, 24 Jun 2024 18:05:00 +0200 Subject: [PATCH 08/15] fix: wrong argument that avoid having the input file as argument --- src/ansys/mapdl/core/cli/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/mapdl/core/cli/convert.py b/src/ansys/mapdl/core/cli/convert.py index 61224e49e2..a999b0ad6f 100644 --- a/src/ansys/mapdl/core/cli/convert.py +++ b/src/ansys/mapdl/core/cli/convert.py @@ -63,7 +63,7 @@ def get_input_source(ctx, param, value): File mapdl.dat successfully converted to mapdl.out.""", ) -@click.argument("filename_in", callback=get_input_source, required=False) +@click.argument("filename_in", callback=get_input_source, required=True) @click.option("-o", default=None, help="Name of the output Python script.") @click.option("--filename_out", default=None, help="Name of the output Python script.") @click.option( From 120b3c369bc7b6379e432570314ef254271a539b Mon Sep 17 00:00:00 2001 From: German Date: Wed, 26 Jun 2024 12:52:47 +0200 Subject: [PATCH 09/15] chore: checking config file in submit function. --- src/ansys/mapdl/core/cli/hpc.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/ansys/mapdl/core/cli/hpc.py b/src/ansys/mapdl/core/cli/hpc.py index 719fbaeae5..3e969fc8fc 100644 --- a/src/ansys/mapdl/core/cli/hpc.py +++ b/src/ansys/mapdl/core/cli/hpc.py @@ -55,16 +55,35 @@ "--url", default=None, type=str, - help="""URL where the HPS cluster is deployed. For example: "https://myserver:3000/hps" """, + help="""URL where the HPS cluster is deployed. For example: "https://myserver:3000/hps". +If it is not input, there is a chain of places where PyMAPDL looks for an URL. +First, it checks if the URL is given in the file specified by the argument ``--config_file``. +If that file does not have an URL or does not exist, then it checks the default user credentials stored with ``pymapdl login --default`` CLI command. +If no URL is found, an exception is raised.""", ) @click.option( - "--user", default=None, type=str, help="Username for logging into the HPC cluster." + "--user", + default=None, + type=str, + help="""Username for logging into the HPC cluster. +If it is not input, there is a chain of places where PyMAPDL looks for an username. +First, it checks if the username is given in the file specified by the argument ``--config_file``. +If that file does not have an username or does not exist, then it checks the username configured using ``pymapdl login`` CLI command, for the given HPS cluster URL. +If there is no user credential stored for that HPS cluster URL, then it checks the default user credentials stored with ``pymapdl login --default`` CLI command. +If no user is found, an exception is raised. +""", ) @click.option( "--password", default=None, type=str, - help="Password for logging into the HPC cluster.", + help="""Password for logging into the HPC cluster. +If it is not input, there is a chain of places where PyMAPDL looks for a password. +First, it checks if the password is given in the file specified by the argument ``--config_file``. +If that file does not have a password or does not exist, then it checks the password configured using ``pymapdl login`` CLI command, for the given HPS cluster URL. +If there is no user credential stored for that HPS cluster URL, then it checks the default user credentials stored with ``pymapdl login --default`` CLI command. +If no password is found, an exception is raised. +""", ) @click.option( "--python", @@ -254,6 +273,8 @@ def submit( if config_file is None: config_file = os.path.join(os.getcwd(), "hps_config.json") + if not os.path.exists(config_file): + config_file = None logger.debug(f"Using default HPS configuration file: {config_file}") # Getting cluster login configuration from CLI or file @@ -261,7 +282,11 @@ def submit( url, config_file, "url", None, raise_if_none=False ) url = url or get_default_url() # using default URL stored. - user = get_value_from_json_or_default(user, config_file, "user", None) + + # allow retrieving user from the configuration + user = get_value_from_json_or_default( + user, config_file, "user", raise_if_none=False + ) # Getting access token token = access(url, user, password) From 012ddb261e1e48c0e5b942231f23a5dc67b28e73 Mon Sep 17 00:00:00 2001 From: German Date: Wed, 26 Jun 2024 13:15:34 +0200 Subject: [PATCH 10/15] feat: avoid venv creation of ``requirements_file`` is False. --- src/ansys/mapdl/core/hpc/pyhps.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/ansys/mapdl/core/hpc/pyhps.py b/src/ansys/mapdl/core/hpc/pyhps.py index 8ced7593c7..aacb1be647 100644 --- a/src/ansys/mapdl/core/hpc/pyhps.py +++ b/src/ansys/mapdl/core/hpc/pyhps.py @@ -50,7 +50,7 @@ def get_value_from_json_or_default( arg: str, - json_file: str, + json_file: Optional[str], key: str, default_value: Optional[Union[str, Any]] = None, raise_if_none: Optional[bool] = True, @@ -59,7 +59,7 @@ def get_value_from_json_or_default( logger.debug(f"Using '{arg}' for {key}") return arg - if os.path.exists(json_file): + if json_file and os.path.exists(json_file): if os.path.getsize(json_file) > 0: with open(json_file, "r") as fid: config = json.load(fid) @@ -633,7 +633,6 @@ def _create_task(self, file_input_ids, file_output_ids): executable=os.path.basename(executable) ) - print(f"Using executable: '{execution_command}'") logger.debug(f"Using executable: '{execution_command}'") # Process step @@ -642,6 +641,7 @@ def _create_task(self, file_input_ids, file_output_ids): TaskDefinition( execution_command=execution_command, resource_requirements=ResourceRequirements( + platform="linux", num_cores=self.num_cores, memory=self.memory * 1024 * 1024, disk_space=self.disk_space * 1024 * 1024, @@ -827,7 +827,8 @@ def _add_files(self): self.output_files.append(self._output_parms_file) if self.mode == "python": - self.input_files.append(self.requirements_file) + if self._requirements_file is not False: + self.input_files.append(self.requirements_file) self.input_files.append(self.shell_file) if self.mode == "python" and (self.inputs or self.outputs): @@ -842,14 +843,18 @@ def _add_files(self): def _prepare_shell_file(self): content = f""" echo "Starting" +""" + if self._requirements_file: + content += f""" # Start venv python{self.python} -m venv .venv source .venv/bin/activate # Install requirements pip install -r {os.path.basename(self.requirements_file)} - +""" + content += f""" # Run script python {self._executed_pyscript} """ @@ -858,6 +863,10 @@ def _prepare_shell_file(self): logger.debug(f"Shell file in: {self.shell_file}") def _prepare_requirements_file(self): + + if self._requirements_file is False: + return + import pkg_resources content = "\n".join( @@ -949,12 +958,16 @@ def _load_results(self): self._output_values.append(each_job.values) def _connect_client(self): - from ansys.mapdl.core.hpc.login import access - if not self._token: + logger.debug("Getting a valid token") + from ansys.mapdl.core.hpc.login import access + self._token = access(url=self.url, user=self.user, password=self.password) - self._client: Client = Client(access_token=self._token, verify=False) + logger.debug("Using a token to authenticate the user.") + self._client: Client = Client( + url=self._url, access_token=self._token, verify=False + ) def close_client(self): self._client.session.close() From 9a101e2c9493dfe1149dedbf60d65e580dc1ee20 Mon Sep 17 00:00:00 2001 From: German Date: Wed, 26 Jun 2024 13:23:45 +0200 Subject: [PATCH 11/15] feat: login CLI finished --- src/ansys/mapdl/core/cli/login.py | 117 ++++++++++++++++++++++++------ src/ansys/mapdl/core/hpc/login.py | 76 ++++++++++++++----- 2 files changed, 149 insertions(+), 44 deletions(-) diff --git a/src/ansys/mapdl/core/cli/login.py b/src/ansys/mapdl/core/cli/login.py index 672a6037d1..b4342b448a 100644 --- a/src/ansys/mapdl/core/cli/login.py +++ b/src/ansys/mapdl/core/cli/login.py @@ -23,11 +23,21 @@ """Login into a PyHPS cluster""" from getpass import getpass import logging +from typing import Optional import click logger = logging.getLogger() +# logging.basicConfig( +# level=logging.DEBUG, +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# # handlers=[ +# # logging.FileHandler("pymapdl.log"), +# # logging.StreamHandler() +# # ] +# ) + @click.command( name="login", @@ -63,7 +73,22 @@ flag_value=True, help="""Test if the token is valid. This argument is ignored if '--default' argument is ``True``.""", ) -def login(user, password, url, default, test_token): +@click.option( + "--quiet", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Suppress al console printout.""", +) +def login( + user: Optional[str] = None, + password: Optional[str] = None, + url: Optional[str] = None, + default: bool = False, + test_token: bool = False, + quiet: bool = False, +): """ Command Line Interface for logging in and getting an access token. @@ -72,34 +97,55 @@ def login(user, password, url, default, test_token): password (str): Password for login default (bool): If used, the input data is set as default and it will be used for any login which does not specify the url, user or password. """ - if not default: - logger.debug("Storing non-default credentials.") - if not user and not click.prompt("Username: "): + from ansys.mapdl.core.hpc.login import login_in_cluster, store_credentials + + if quiet: + import urllib3 + + urllib3.disable_warnings() + + logger.debug("Storing non-default credentials.") + if not user: + user = click.prompt("Username") + + if not user: raise ValueError("No user was provided.") - if not password and not getpass("Password: "): + if not password: + password = getpass("Password: ") + if not password: raise ValueError("No password was provided.") - if not url and not click.prompt("HPS cluster URL: "): + if not default and not url: + url = click.prompt("HPS cluster URL") + if not url: raise ValueError("No password was provided.") - try: - token = login(user, password, url) - click.echo(f"Login successful") - except Exception as e: - click.echo(f"Login failed: {str(e)}") + token = login_in_cluster(user, password, url) + logger.debug(f"Login successful") - if test_token: - logger.debug("Testing token") - from requests import ConnectionError + if test_token: + logger.debug("Testing token") + from requests import ConnectionError - from ansys.mapdl.core.hpc.login import token_is_valid + from ansys.mapdl.core.hpc.login import token_is_valid - if not token_is_valid(token): - raise ConnectionError("The retrieved token is not valid.") + if not token_is_valid(url, token): + raise ConnectionError("The retrieved token is not valid.") + else: + if not quiet: + click.echo("Token has been verified with the HPC cluster.") logger.info(f"Stored credentials: {user}, {password}, {url}") - store_credentials(user, password, url) + store_credentials(user, password, url, default=default) + + if not quiet: + if default: + click.echo("Stored default credentials.") + else: + click.echo( + f"Stored credentials:\n User : '{user}'\n Cluster URL : '{url}'" + ) @click.command( @@ -112,10 +158,9 @@ def login(user, password, url, default, test_token): ) @click.option( "--url", - prompt="HPS cluster URL", - help="The HPS cluster URL. For instance 'https://10.231.106.1:3000/hps'.", default=None, type=str, + help="The HPS cluster URL. For instance 'https://10.231.106.1:3000/hps'.", ) @click.option( "--default", @@ -123,7 +168,7 @@ def login(user, password, url, default, test_token): type=bool, is_flag=False, flag_value=True, - help="""Whether PyMAPDL is to print debug logging to the console.""", + help="""Deletes the default login configuration.""", ) def logout(url, default): @@ -131,6 +176,8 @@ def logout(url, default): # under a service name. We might need to keep track of those in a file or # something. + import keyring + from ansys.mapdl.core.hpc.login import delete_credentials if not url and not default: @@ -140,7 +187,29 @@ def logout(url, default): raise ValueError("The argument '--default' cannot be used with an URL.") if default: - url = "default" + logger.debug("Deleting credentials for the default profile.") + url = None - delete_credentials(url) - click.echo(f"The credentials for '{url}' have been deleted.") + try: + delete_credentials(url) + except keyring.errors.PasswordDeleteError: + click.echo("The default credentials do not exist.") + return + + if default: + click.echo(f"The default credentials have been deleted.") + else: + click.echo(f"The credentials for the HPS cluster '{url}' have been deleted.") + + +if __name__ == "__main__": + # login(default=True) + # user, password, url, default, test_token, quiet + login( + user="repuser", + password="repuser", + url="https://10.231.106.32:3000/hps", + default=False, + test_token=True, + quiet=True, + ) diff --git a/src/ansys/mapdl/core/hpc/login.py b/src/ansys/mapdl/core/hpc/login.py index f27311411f..97564f11e5 100644 --- a/src/ansys/mapdl/core/hpc/login.py +++ b/src/ansys/mapdl/core/hpc/login.py @@ -20,6 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import logging +import time + from ansys.hps.client import AuthApi, Client from ansys.hps.client.authenticate import authenticate import keyring @@ -29,8 +32,28 @@ SERVICE_NAME = "pymapdl-pyhps" EXPIRATION_TIME = 4 * 24 * 60 # 2 days in minutes +logger = logging.getLogger() +# logging.basicConfig( +# level=logging.DEBUG, +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# # handlers=[ +# # logging.FileHandler("pymapdl.log"), +# # logging.StreamHandler() +# # ] +# ) + + +def get_password(*args, **kwargs): + logger.debug(f"Getting password from service '{args[0]}' and key '{args[1]}'") + return keyring.get_password(*args, **kwargs) + -def login(user, password, url): +def set_password(*args, **kwargs): + logger.debug(f"Setting password from service '{args[0]}' and key '{args[1]}'") + return keyring.set_password(*args, **kwargs) + + +def login_in_cluster(user, password, url): """ Authenticate with the server and return the access token. @@ -48,6 +71,7 @@ def login(user, password, url): str Access token. """ + logger.debug(f"Authenticating on cluster '{url}' using user '{user}'.") access_token = authenticate( url=url, username=user, password=password, scope="openid", verify=False )["access_token"] @@ -81,6 +105,7 @@ def store_credentials( identifier = DEFAULT_IDENTIFIER else: identifier = url + logger.debug(f"Using identifier: '{identifier}'") if not default and (not url or not user or not password): raise ValueError( @@ -88,17 +113,17 @@ def store_credentials( ) if url: - keyring.set_password(SERVICE_NAME, f"{identifier}_url", url) + set_password(SERVICE_NAME, f"{identifier}_url", url) if user: - keyring.set_password(SERVICE_NAME, f"{identifier}_user", user) + set_password(SERVICE_NAME, f"{identifier}_user", user) if password: - keyring.set_password(SERVICE_NAME, f"{identifier}_password", password) + set_password(SERVICE_NAME, f"{identifier}_password", password) if expiration_time: - keyring.set_password( + set_password( SERVICE_NAME, f"{identifier}_expiration_time", str(expiration_time) ) - keyring.set_password(SERVICE_NAME, f"{identifier}_timestamp", str(time.time())) + set_password(SERVICE_NAME, f"{identifier}_timestamp", str(time.time())) def get_stored_credentials(identifier): @@ -108,20 +133,21 @@ def get_stored_credentials(identifier): Returns: tuple: (user, password, timestamp) or (None, None, None) if not found """ - - url = keyring.get_password(SERVICE_NAME, f"{identifier}_url") - user = keyring.get_password(SERVICE_NAME, f"{identifier}_user") - password = keyring.get_password(SERVICE_NAME, f"{identifier}_password") - timestamp = keyring.get_password(SERVICE_NAME, f"{identifier}_timestamp") - expiration_time = keyring.get_password( - SERVICE_NAME, f"{identifier}_expiration_time" - ) + logger.debug(f"Retrieving info for '{identifier}'") + url = get_password(SERVICE_NAME, f"{identifier}_url") + user = get_password(SERVICE_NAME, f"{identifier}_user") + password = get_password(SERVICE_NAME, f"{identifier}_password") + timestamp = get_password(SERVICE_NAME, f"{identifier}_timestamp") + expiration_time = get_password(SERVICE_NAME, f"{identifier}_expiration_time") if timestamp: timestamp = float(timestamp) if expiration_time: expiration_time = float(expiration_time) + logger.debug( + f"Retrieved info for '{identifier}': {url}, {user}, {password}, {timestamp}, {expiration_time} " + ) return url, user, password, timestamp, expiration_time @@ -138,7 +164,12 @@ def credentials_expired(timestamp, expiration_time: float = EXPIRATION_TIME): return time.time() - timestamp > expiration_time * 60 -def delete_credentials(identifier): +def delete_credentials(identifier=None): + if not identifier: + identifier = DEFAULT_IDENTIFIER + + logger.debug(f"Deleting credentials for identifier: {identifier}") + keyring.delete_password(SERVICE_NAME, f"{identifier}_url") keyring.delete_password(SERVICE_NAME, f"{identifier}_user") keyring.delete_password(SERVICE_NAME, f"{identifier}_password") @@ -146,8 +177,8 @@ def delete_credentials(identifier): keyring.delete_password(SERVICE_NAME, f"{identifier}_expiration_time") -def token_is_valid(token): - client = Client(access_token=token, verify=False) +def token_is_valid(url, token): + client = Client(url=url, access_token=token, verify=False) auth_api = AuthApi(client) try: @@ -194,7 +225,7 @@ def access(url: str = None, user: str = None, password: str = None): ) if not user: - if user: + if user_default: user = user_default else: raise ValueError( @@ -209,9 +240,14 @@ def access(url: str = None, user: str = None, password: str = None): f"No 'password' is given nor stored for '{identifier}'. You must input one." ) - return login(user=user, password=password, url=url) + return login_in_cluster(user=user, password=password, url=url) def get_default_url(): """Return the default URL""" - return keyring.get_password(SERVICE_NAME, f"{DEFAULT_IDENTIFIER}_url") + return get_password(SERVICE_NAME, f"{DEFAULT_IDENTIFIER}_url") + + +def get_token(url: str = None, user: str = None, password: str = None): + """Wrapper around `access`` function""" + return access(url=url, user=user, password=password) From f15be4e2ec50b68771eb8601334b66c00254a025 Mon Sep 17 00:00:00 2001 From: German Date: Wed, 26 Jun 2024 13:42:22 +0200 Subject: [PATCH 12/15] feat: making sure we don't get import errors when PyHPS is not installed. --- src/ansys/mapdl/core/cli/__init__.py | 19 +++++++++---- src/ansys/mapdl/core/cli/hpc.py | 4 +-- src/ansys/mapdl/core/hpc/login.py | 14 +++++++--- src/ansys/mapdl/core/hpc/pyhps.py | 40 +++++++++++++++++----------- 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/ansys/mapdl/core/cli/__init__.py b/src/ansys/mapdl/core/cli/__init__.py index 04589d3a7a..f0e70da863 100644 --- a/src/ansys/mapdl/core/cli/__init__.py +++ b/src/ansys/mapdl/core/cli/__init__.py @@ -29,6 +29,13 @@ _HAS_CLICK = False +try: + from ansys.hps.client import Client + + _HAS_HPS = True +except ModuleNotFoundError: + _HAS_HPS = False + if _HAS_CLICK: ################################### # PyMAPDL CLI @@ -53,12 +60,14 @@ def main(ctx): # pymapdl (hpc) submit # pymapdl (hpc) list #To be implemented # pymapdl (hpc) stop #To be implemented - from ansys.mapdl.core.cli.hpc import submit - from ansys.mapdl.core.cli.login import login, logout - main.add_command(login) - main.add_command(submit) - main.add_command(logout) + if _HAS_HPS: + from ansys.mapdl.core.cli.hpc import submit + from ansys.mapdl.core.cli.login import login, logout + + main.add_command(login) + main.add_command(submit) + main.add_command(logout) def old_pymapdl_convert_script_entry_point(): print( diff --git a/src/ansys/mapdl/core/cli/hpc.py b/src/ansys/mapdl/core/cli/hpc.py index 3e969fc8fc..ce92b54294 100644 --- a/src/ansys/mapdl/core/cli/hpc.py +++ b/src/ansys/mapdl/core/cli/hpc.py @@ -28,8 +28,6 @@ import click -from ansys.mapdl.core.hpc.login import access, get_default_url - logger = logging.getLogger() @@ -254,7 +252,7 @@ def submit( mode: Optional[Union["python", "shell", "apdl"]] = None, to_json: Optional[bool] = False, ): - import json + from ansys.mapdl.core.hpc.login import access, get_default_url if to_json: import json diff --git a/src/ansys/mapdl/core/hpc/login.py b/src/ansys/mapdl/core/hpc/login.py index 97564f11e5..7252e49aa1 100644 --- a/src/ansys/mapdl/core/hpc/login.py +++ b/src/ansys/mapdl/core/hpc/login.py @@ -23,10 +23,16 @@ import logging import time -from ansys.hps.client import AuthApi, Client -from ansys.hps.client.authenticate import authenticate -import keyring -from requests import ConnectionError +try: + from ansys.hps.client import AuthApi, Client + from ansys.hps.client.authenticate import authenticate + import keyring + from requests import ConnectionError +except ModuleNotFoundError: + raise ModuleNotFoundError( + """Some of the dependencies required for login into an HPS cluster are not installed. +Please install them using "pip install 'ansys-mapdl-core[hps]".""" + ) DEFAULT_IDENTIFIER = "defaultconfig" SERVICE_NAME = "pymapdl-pyhps" diff --git a/src/ansys/mapdl/core/hpc/pyhps.py b/src/ansys/mapdl/core/hpc/pyhps.py index aacb1be647..b90272298f 100644 --- a/src/ansys/mapdl/core/hpc/pyhps.py +++ b/src/ansys/mapdl/core/hpc/pyhps.py @@ -28,22 +28,30 @@ from typing import Any, Optional, Union from warnings import warn -from ansys.hps.client import Client -from ansys.hps.client.jms import ( - File, - FloatParameterDefinition, - HpcResources, - JmsApi, - Job, - JobDefinition, - ParameterMapping, - Project, - ProjectApi, - ResourceRequirements, - Software, - StringParameterDefinition, - TaskDefinition, -) +try: + from ansys.hps.client import Client + from ansys.hps.client.jms import ( + File, + FloatParameterDefinition, + HpcResources, + JmsApi, + Job, + JobDefinition, + ParameterMapping, + Project, + ProjectApi, + ResourceRequirements, + Software, + StringParameterDefinition, + TaskDefinition, + ) + +except ModuleNotFoundError: + raise ModuleNotFoundError( + """Some of the dependencies required for submit jobs into an HPS cluster are not installed. +Please install them using "pip install 'ansys-mapdl-core[hps]".""" + ) + logger = logging.getLogger() From 36480147a6c753630970a8b42010901768f67fe4 Mon Sep 17 00:00:00 2001 From: German Date: Wed, 26 Jun 2024 17:02:18 +0200 Subject: [PATCH 13/15] feat: Adding docstrings --- src/ansys/mapdl/core/cli/login.py | 149 ++++++++++++++++++++++-------- src/ansys/mapdl/core/hpc/login.py | 139 +++++++++++++++++++++++----- 2 files changed, 223 insertions(+), 65 deletions(-) diff --git a/src/ansys/mapdl/core/cli/login.py b/src/ansys/mapdl/core/cli/login.py index b4342b448a..4e9e1e9fc7 100644 --- a/src/ansys/mapdl/core/cli/login.py +++ b/src/ansys/mapdl/core/cli/login.py @@ -27,17 +27,11 @@ import click +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) logger = logging.getLogger() -# logging.basicConfig( -# level=logging.DEBUG, -# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -# # handlers=[ -# # logging.FileHandler("pymapdl.log"), -# # logging.StreamHandler() -# # ] -# ) - @click.command( name="login", @@ -47,6 +41,41 @@ It does store credentials (cluster url, password and username) in the OS credential manager. If you want to change any credential, just issue the command again with the new values. +Examples +-------- + +Prompt the values for user, password and HPC cluster URL: + +$ pymapdl login +Username: myuser +Password: mypassword +HPS cluster URL: https://123.456.789.1:3000/hps +Stored credentials: + User : 'user' + Cluster URL : 'https://123.456.789.1:3000/hps' + +Use the CLI arguments to supply the values + +$ pymapdl login --user myuser --password mypassword --url "https://123.456.789.1:3000/hps" +Stored credentials: + User : 'user' + Cluster URL : 'https://123.456.789.1:3000/hps' + +Set the defaults user, password and URL. They will be used when one of them is +missing. + +$ pymapdl login --default --user myuser --password mypassword --url "https://123.456.789.1:3000/hps" +Stored default credentials. + +It is possible to input some arguments using the CLI arguments, and other using +the prompt: + +$ pymapdl login --user myuser --url "https://123.456.789.1:3000/hps" +Password: mypassword +Stored credentials: + User : 'user' + Cluster URL : 'https://123.456.789.1:3000/hps' + """, ) @click.option("--user", default=None, type=str, help="The username to login.") @@ -55,7 +84,7 @@ "--url", default=None, type=str, - help="The HPS cluster URL. For instance 'https://10.231.106.1:3000/hps'.", + help="The HPS cluster URL. For instance 'https://123.456.789.1:3000/hps'.", ) @click.option( "--default", @@ -63,7 +92,7 @@ type=bool, is_flag=False, flag_value=True, - help="""Set the default user, password and URL. These credentials are not tested.""", + help="""Set the default user, password and URL. These credentials are not tested against any HPC.""", ) @click.option( "--test_token", @@ -79,7 +108,15 @@ type=bool, is_flag=False, flag_value=True, - help="""Suppress al console printout.""", + help="""Suppress all console printout.""", +) +@click.option( + "--debug", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Activate debugging printout. It might show the input password!""", ) def login( user: Optional[str] = None, @@ -88,17 +125,13 @@ def login( default: bool = False, test_token: bool = False, quiet: bool = False, + debug: bool = False, ): - """ - Command Line Interface for logging in and getting an access token. - - Parameters - user (str): Username for login - password (str): Password for login - default (bool): If used, the input data is set as default and it will be used for any login which does not specify the url, user or password. - """ from ansys.mapdl.core.hpc.login import login_in_cluster, store_credentials + if debug: + logger.setLevel(logging.DEBUG) + if quiet: import urllib3 @@ -154,6 +187,24 @@ def login( It deletes credentials stored on the system. +Examples +-------- + +Delete the credentials associated to an specific URL + +$ pymapdl logout --url "https://123.456.789.1:3000/hps" +The HPS cluster 'https://123.456.789.1:3000/hps' credentials have been deleted. + +Delete the default credentials. + +$ pymapdl logout --default +The default credentials have been deleted. + +Notes +----- +- If the credentials do not exist, the CLI notifies and exits cleanly. + No exception is raised. If you want to raise an exception (exit 1), then pass + the argument ``--strict``. """, ) @click.option( @@ -170,7 +221,31 @@ def login( flag_value=True, help="""Deletes the default login configuration.""", ) -def logout(url, default): +@click.option( + "--quiet", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Suppress all console printout.""", +) +@click.option( + "--debug", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Activate debugging printout.""", +) +@click.option( + "--strict", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Raise an issue if the credentials do not exist.""", +) +def logout(url, default, quiet, debug, strict): # TODO: keyrings library seems to not being able to list the credentials # under a service name. We might need to keep track of those in a file or @@ -180,6 +255,9 @@ def logout(url, default): from ansys.mapdl.core.hpc.login import delete_credentials + if debug: + logger.setLevel(logging.DEBUG) + if not url and not default: raise ValueError("An URL needs to be used.") @@ -192,24 +270,15 @@ def logout(url, default): try: delete_credentials(url) + success_message = "The {0} credentials have been deleted.".format( + "default" if default else f"HPS cluster '{url}'" + ) except keyring.errors.PasswordDeleteError: - click.echo("The default credentials do not exist.") - return + success_message = "The {0} credentials do not exist.".format( + "default" if default else f"HPS cluster '{url}'" + ) + if strict: + raise keyring.errors.PasswordDeleteError(success_message) - if default: - click.echo(f"The default credentials have been deleted.") - else: - click.echo(f"The credentials for the HPS cluster '{url}' have been deleted.") - - -if __name__ == "__main__": - # login(default=True) - # user, password, url, default, test_token, quiet - login( - user="repuser", - password="repuser", - url="https://10.231.106.32:3000/hps", - default=False, - test_token=True, - quiet=True, - ) + if not quiet: + click.echo(success_message) diff --git a/src/ansys/mapdl/core/hpc/login.py b/src/ansys/mapdl/core/hpc/login.py index 7252e49aa1..ccee2a5ab3 100644 --- a/src/ansys/mapdl/core/hpc/login.py +++ b/src/ansys/mapdl/core/hpc/login.py @@ -22,6 +22,7 @@ import logging import time +from typing import Optional try: from ansys.hps.client import AuthApi, Client @@ -39,14 +40,6 @@ EXPIRATION_TIME = 4 * 24 * 60 # 2 days in minutes logger = logging.getLogger() -# logging.basicConfig( -# level=logging.DEBUG, -# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -# # handlers=[ -# # logging.FileHandler("pymapdl.log"), -# # logging.StreamHandler() -# # ] -# ) def get_password(*args, **kwargs): @@ -132,12 +125,19 @@ def store_credentials( set_password(SERVICE_NAME, f"{identifier}_timestamp", str(time.time())) -def get_stored_credentials(identifier): +def get_stored_credentials(identifier: str): """ - Retrieve stored credentials and timestamp from the keyring. + Retrieve stored credentials, timestamp and expiration time from the keyring. - Returns: - tuple: (user, password, timestamp) or (None, None, None) if not found + Parameters + ---------- + identifier: str + Identifier for the credentials + + Returns + ------- + tuple + (user, password, timestamp) or (None, None, None) if not found """ logger.debug(f"Retrieving info for '{identifier}'") url = get_password(SERVICE_NAME, f"{identifier}_url") @@ -157,20 +157,40 @@ def get_stored_credentials(identifier): return url, user, password, timestamp, expiration_time -def credentials_expired(timestamp, expiration_time: float = EXPIRATION_TIME): +def credentials_expired(timestamp: float, expiration_time: float = EXPIRATION_TIME): """ Check if the stored credentials have expired. - Parameters: - timestamp (float): Timestamp of when the credentials were stored + Parameters + ---------- + timestamp, float + Timestamp of when the credentials were stored + + expiration_time float + Amount of time before the credentials expires. - Returns: - bool: True if credentials have expired, False otherwise + Returns + ------- + bool + True if credentials have expired, False otherwise """ return time.time() - timestamp > expiration_time * 60 -def delete_credentials(identifier=None): +def delete_credentials(identifier: Optional[str] = None): + """ + Delete stored credentials. + + Parameters + ---------- + identifier: str, Optional + Identifier for the credentials. If it is ``None``, the + default credentials are deleted. + + Returns + ------- + None + """ if not identifier: identifier = DEFAULT_IDENTIFIER @@ -184,6 +204,22 @@ def delete_credentials(identifier=None): def token_is_valid(url, token): + """Check if a token is valid. + + The validation is performed by requesting the number of users to the HPS cluster. + + Parameters + ---------- + url : str + HPS cluster URL. + token : str + Authentication token. + + Returns + ------- + bool + Whether the token is valid or not. + """ client = Client(url=url, access_token=token, verify=False) auth_api = AuthApi(client) @@ -196,8 +232,66 @@ def token_is_valid(url, token): raise e -def access(url: str = None, user: str = None, password: str = None): +def get_token_access(url: str = None, user: str = None, password: str = None): + """ + Access an HPS cluster by logging in with the provided or stored credentials. + + This function attempts to log in to a cluster using the provided URL, username, + and password. + If any of these parameters are not provided, it attempts to retrieve stored + credentials associated with the given URL. + If no URL is provided, then it retrieves the default credentials. + + If the credentials are expired or not found, appropriate errors are raised. + Parameters + ---------- + url : str, optional + The URL of the cluster to log in to. If not provided, a stored URL + associated with the default or given identifier is used. + user : str, optional + The username for logging in. If not provided, a stored username associated + with the default or given identifier is used. + password : str, optional + The password for logging in. If not provided, a stored password associated + with the default or given identifier is used. + + Returns + ------- + str + It returns the authentication token for the session. + + Raises + ------ + ConnectionError + If there are no stored credentials for the given identifier, or if the stored credentials + are expired. + ValueError + If a URL, username, or password is not provided and cannot be found in the stored + credentials. + + Notes + ----- + - If credentials are expired, they are deleted from storage. + - The credentials can be stored using ``pymapdl login`` CLI. + + Examples + -------- + Using url, user and password: + + >>> get_token_access(url='https://cluster.example.com', user='admin', password='securepass') + 'eyJhbGciOiJSUzI1NiIsI...' + + Using the stored credential for that URL. If those credentials do not exists, + the default credentials are used. + + >>> get_token_access(url='https://cluster.example.com') + 'bGciOiJSeyJhUzI1NiIsI...' + + Login using the default stored credentials: + >>> get_token_access() + 'iJSeyJhUzI1bGciONiIsI...' + """ if not url or not user or not password: if not url: identifier = DEFAULT_IDENTIFIER @@ -250,10 +344,5 @@ def access(url: str = None, user: str = None, password: str = None): def get_default_url(): - """Return the default URL""" + """Return the default credentials URL""" return get_password(SERVICE_NAME, f"{DEFAULT_IDENTIFIER}_url") - - -def get_token(url: str = None, user: str = None, password: str = None): - """Wrapper around `access`` function""" - return access(url=url, user=user, password=password) From de4f9f7bb5d4251885c9dc45750b2d03112769a1 Mon Sep 17 00:00:00 2001 From: German Date: Wed, 26 Jun 2024 17:05:04 +0200 Subject: [PATCH 14/15] chore: renaming 'access' to 'get_token_access'. --- src/ansys/mapdl/core/cli/hpc.py | 4 ++-- src/ansys/mapdl/core/hpc/pyhps.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ansys/mapdl/core/cli/hpc.py b/src/ansys/mapdl/core/cli/hpc.py index ce92b54294..a3b765dc58 100644 --- a/src/ansys/mapdl/core/cli/hpc.py +++ b/src/ansys/mapdl/core/cli/hpc.py @@ -252,7 +252,7 @@ def submit( mode: Optional[Union["python", "shell", "apdl"]] = None, to_json: Optional[bool] = False, ): - from ansys.mapdl.core.hpc.login import access, get_default_url + from ansys.mapdl.core.hpc.login import get_default_url, get_token_access if to_json: import json @@ -287,7 +287,7 @@ def submit( ) # Getting access token - token = access(url, user, password) + token = get_token_access(url, user, password) # Getting other configuration from CLI or file python = get_value_from_json_or_default(python, config_file, "python", 3) diff --git a/src/ansys/mapdl/core/hpc/pyhps.py b/src/ansys/mapdl/core/hpc/pyhps.py index b90272298f..457a83950d 100644 --- a/src/ansys/mapdl/core/hpc/pyhps.py +++ b/src/ansys/mapdl/core/hpc/pyhps.py @@ -52,7 +52,6 @@ Please install them using "pip install 'ansys-mapdl-core[hps]".""" ) - logger = logging.getLogger() @@ -968,9 +967,11 @@ def _load_results(self): def _connect_client(self): if not self._token: logger.debug("Getting a valid token") - from ansys.mapdl.core.hpc.login import access + from ansys.mapdl.core.hpc.login import get_token_access - self._token = access(url=self.url, user=self.user, password=self.password) + self._token = get_token_access( + url=self.url, user=self.user, password=self.password + ) logger.debug("Using a token to authenticate the user.") self._client: Client = Client( From 9818a161a878a8a9741470d7ec3e82575b88b243 Mon Sep 17 00:00:00 2001 From: German Date: Wed, 26 Jun 2024 18:04:49 +0200 Subject: [PATCH 15/15] fix: failing piping on the CLI. --- src/ansys/mapdl/core/cli/convert.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ansys/mapdl/core/cli/convert.py b/src/ansys/mapdl/core/cli/convert.py index a999b0ad6f..4e6c8c09bf 100644 --- a/src/ansys/mapdl/core/cli/convert.py +++ b/src/ansys/mapdl/core/cli/convert.py @@ -63,7 +63,7 @@ def get_input_source(ctx, param, value): File mapdl.dat successfully converted to mapdl.out.""", ) -@click.argument("filename_in", callback=get_input_source, required=True) +@click.argument("filename_in", callback=get_input_source, required=False) @click.option("-o", default=None, help="Name of the output Python script.") @click.option("--filename_out", default=None, help="Name of the output Python script.") @click.option( @@ -212,6 +212,9 @@ def convert( ) else: + if not filename_in: + raise ValueError("A file path must be provided.") + convert_script( filename_in, filename_out,