diff --git a/scp_udg_client_app/cli.py b/scp_udg_client_app/cli.py index 8936604361c5f22d0180cf08b07e6ddeceffc105..07e48cb09a6ae7480f4ab5459998f309f2276ddc 100644 --- a/scp_udg_client_app/cli.py +++ b/scp_udg_client_app/cli.py @@ -41,6 +41,7 @@ def configure_logger(log_file, log_max_bytes, log_backup_count): @click.version_option() @click.pass_context def cli(ctx, log_file, log_max_bytes, log_backup_count): + ctx.ensure_object(dict) if log_file: configure_logger(log_file, log_max_bytes, log_backup_count) diff --git a/scp_udg_client_app/commands/authenticate.py b/scp_udg_client_app/commands/authenticate.py index d33081bd53769d1700e81e37479ca4c0751de73e..32ca75e59347928dc010473608b07654360a7326 100755 --- a/scp_udg_client_app/commands/authenticate.py +++ b/scp_udg_client_app/commands/authenticate.py @@ -1,24 +1,13 @@ #!/usr/bin/env python3 -import json -import os -import shutil import click import click_log -from click import ClickException from click_config_file import configuration_option -from scp_udg_client_rest import Configuration, ApiClient, UrbanDatasetGatewayApi, LoginRequest -from scp_udg_client_app.helpers import javaproperties_provider +from scp_udg_client_app import helpers +from scp_udg_client_app.helpers import javaproperties_provider, get_api_instance from scp_udg_client_app.options import udg_endpoint_option -# Definizione delle costanti per i codici di ritorno -SUCCESS_CODE_GENERIC = "00" -SUCCESS_CODE_AUTHENTICATION = "01" -BACKUP_SUFFIX = ".bak" -FAILED_BACKUP_TOKEN_FILE = "Failed to create a backup of the token file: " -AUTH_FAILED_RESPONSE = "Authentication failed. Full response: " - # Logger configuration logger = click_log.basic_config(__name__) @@ -40,90 +29,7 @@ def authenticate(ctx, udg_endpoint, token_file, is_json, username, password): Command to authenticate a user and save the authentication token. """ api_instance = get_api_instance(ctx, udg_endpoint) - token = read_token_from_file(token_file, is_json) - - if token and is_token_alive(api_instance, token): - logger.info(f"Token from {token_file} is still valid.") - else: - logger.info("Token is invalid or expired. Proceeding to acquire a new token via login.") - authenticate_user(api_instance, token_file, is_json, username, password) - - -def get_api_instance(ctx, udg_endpoint): - if ctx.obj and 'api_instance' in ctx.obj: - return ctx.obj.get('api_instance') - return UrbanDatasetGatewayApi(ApiClient(Configuration(host=udg_endpoint))) - - -def is_token_alive(api_instance, token): - api_instance.api_client.configuration.access_token = token - try: - return api_instance.is_alive().code == SUCCESS_CODE_GENERIC - except Exception as e: - logger.error(f"Error verifying token: {str(e)}") - return False - - -def authenticate_user(api_instance, token_file, is_json, username, password): - if not username or not password: - raise click.ClickException("Token is invalid and no credentials provided for re-authentication.") - backup_token_file(token_file) - perform_login(api_instance, token_file, is_json, username, password) - - -def backup_token_file(token_file): - try: - shutil.copy2(token_file, f"{token_file}{BACKUP_SUFFIX}") - logger.info(f"Backup of the token file created at {token_file}{BACKUP_SUFFIX}") - except Exception as e: - logger.error(f"{FAILED_BACKUP_TOKEN_FILE}{str(e)}") - - -def read_token_from_file(token_file: str, is_json: bool) -> str: - """ - Reads the authentication token from the specified file. - - :param token_file: The path to the token file. - :param is_json: A flag indicating if the token file is in JSON format. - :return: The token as a string, or an empty string if the file does not exist or cannot be read. - """ - if not os.path.exists(token_file): - logger.warning(f"Token file {token_file} does not exist.") - return "" - - try: - logger.info(f"Reading token from file: {token_file}") - with open(token_file, 'r') as file: - if token_file.endswith('.json') or is_json: - data = json.load(file) - return data.get("token", "") - else: - return file.read().strip() - except Exception as e: - error_message = f"Failed to read token file: {str(e)}" - logger.error(error_message) - raise click.ClickException(error_message) - - -def perform_login(api_instance, token_file, is_json, username, password): - try: - login_response = api_instance.login(LoginRequest(username=username, password=password)) - if login_response.code != SUCCESS_CODE_AUTHENTICATION or not login_response.token: - raise click.ClickException(f"{AUTH_FAILED_RESPONSE}{login_response.to_str()}") - - with open(token_file, 'w') as file: - if token_file.endswith('.json') or is_json: - json.dump(login_response.to_dict(), file) - else: - file.write(login_response.token) - api_instance.api_client.configuration.access_token = login_response.token - logger.info("New token obtained and saved successfully.") - except ClickException as ce: - logger.error(ce) - raise ce - except Exception as e: - logger.error(f"Failed to authenticate and obtain token: {str(e)}") - raise click.ClickException(f"Failed to authenticate and obtain token: {str(e)}") + helpers.authenticate(api_instance, token_file, is_json, username, password) if __name__ == '__main__': diff --git a/scp_udg_client_app/commands/last_request.py b/scp_udg_client_app/commands/last_request.py index 84b0d73dd77a856bcc53af5e03ca3adcdceb520c..44a5cfbee273cbb3b1387eba5844c8db6c369684 100755 --- a/scp_udg_client_app/commands/last_request.py +++ b/scp_udg_client_app/commands/last_request.py @@ -6,7 +6,9 @@ from click import ClickException from click_config_file import configuration_option from scp_udg_client_rest import LastRequest, Configuration, ApiClient, UrbanDatasetGatewayApi -from scp_udg_client_app.helpers import save_dataset, javaproperties_provider, authenticate +from scp_udg_client_app import helpers +from scp_udg_client_app.commands.authenticate import authenticate +from scp_udg_client_app.helpers import save_dataset, javaproperties_provider, get_api_instance from scp_udg_client_app.options import udg_endpoint_option, resource_id_option, inbox_dir_option, token_options # Logger configuration @@ -17,13 +19,16 @@ logger = click_log.basic_config(__name__) @configuration_option(implicit=False, provider=javaproperties_provider, default="config.properties", help="Read configuration from a Java .properties FILE.") @udg_endpoint_option -@token_options +@click.option('--token-file', type=click.Path(readable=True, writable=True, dir_okay=False), + required=True, default='token.json', show_envvar=True, + help='Path to the file containing the login response (either in JSON or plain text format depending on --is-json).') +@click.option('--is-json', is_flag=True, default=False, show_envvar=True, help='Flag to handle token as JSON.') @click.option('--username', prompt=True, prompt_required=False, show_envvar=True) @click.password_option(show_default=False, prompt_required=False, show_envvar=True) @resource_id_option @inbox_dir_option @click.pass_context -def last_request(ctx, token, token_file, is_json, username, password, udg_endpoint, resource_id, inbox_dir): +def last_request(ctx, udg_endpoint, token_file, is_json, username, password, resource_id, inbox_dir): """ This function is a Click command for making the last request to the UrbanDatasetGateway API. It allows the user to either use an authentication token or enter credentials for access. @@ -31,9 +36,8 @@ def last_request(ctx, token, token_file, is_json, username, password, udg_endpoi for a specified resource_id, and saves the dataset to a given directory. """ - # Authentication - api_instance = authenticate(UrbanDatasetGatewayApi(ApiClient(Configuration(host=udg_endpoint))), - token, token_file, is_json, username, password) + api_instance = get_api_instance(ctx, udg_endpoint) + helpers.authenticate(api_instance, token_file, is_json, username, password) try: response = api_instance.last(LastRequest(resource_id=resource_id)) @@ -53,17 +57,6 @@ def last_request(ctx, token, token_file, is_json, username, password, udg_endpoi error_message = f"Unexpected error occurred when fetching the dataset: {str(e)}" logger.error(error_message) raise click.ClickException(error_message) - finally: - if not token: - logger.info("Disconnecting from API as we authenticated with username and password.") - try: - response = api_instance.logout() - message = f"The response of UrbanDatasetGatewayApi->logout: {response.to_str()}" - logger.debug(message) - except Exception as e: - error_message = f"Unexpected error occurred when logging out: {str(e)}" - logger.error(error_message) - raise click.ClickException(error_message) if __name__ == '__main__': diff --git a/scp_udg_client_app/commands/searching_request.py b/scp_udg_client_app/commands/searching_request.py index c648ee4cf9cb374596ac5c2cf57af2f0067e4149..ce6cea2558d1bf4cb6c74f1860f719c795756800 100755 --- a/scp_udg_client_app/commands/searching_request.py +++ b/scp_udg_client_app/commands/searching_request.py @@ -8,7 +8,9 @@ from click import FloatRange from click_config_file import configuration_option from scp_udg_client_rest import SearchingRequest -from scp_udg_client_app.helpers import create_api_client_instance, active_login, save_dataset, javaproperties_provider +from scp_udg_client_app import helpers +from scp_udg_client_app.helpers import create_api_client_instance, save_dataset, javaproperties_provider, \ + get_api_instance from scp_udg_client_app.options import login_options, udg_endpoint_option, resource_id_option, token_options # Setting up Logger @@ -32,7 +34,10 @@ def fetch_and_save_datasets(api_client, request, resource_id, inbox_dir): help="Read configuration from a Java .properties FILE.") @udg_endpoint_option @login_options -@token_options +@click.option('--token-file', type=click.Path(readable=True, writable=True, dir_okay=False), + required=True, default='token.json', show_envvar=True, + help='Path to the file containing the login response (either in JSON or plain text format depending on --is-json).') +@click.option('--is-json', is_flag=True, default=False, show_envvar=True, help='Flag to handle token as JSON.') @resource_id_option @click.option('--inbox-dir', type=click.Path(resolve_path=True, file_okay=False), default="Inbox", show_envvar=True) @@ -51,19 +56,12 @@ def fetch_and_save_datasets(api_client, request, resource_id, inbox_dir): @click.option('--distance', type=click.FloatRange(min=0), help="Radius of the circle, in meters, on which space research will be carried out.", show_envvar=True) @click.pass_context -def searching_request(ctx, udg_endpoint, resource_id, token, token_file, is_json, username, password, inbox_dir, +def searching_request(ctx, udg_endpoint, token_file, is_json, username, password, resource_id, inbox_dir, period_start: datetime, period_end: datetime, center_latitude: float, center_longitude: float, distance: float): """Fetch and save datasets from an UDG endpoint.""" - - if not token and not token_file and not (username and password): - raise click.MissingParameter("Neither --token, --token-file nor credentials provided (username and password).") - - api_client = create_api_client_instance(udg_endpoint, token) - - if username and not active_login(api_client, username, password): - logger.info("Login fallito!") - return + api_instance = get_api_instance(ctx, udg_endpoint) + helpers.authenticate(api_instance, token_file, is_json, username, password) try: request = SearchingRequest( @@ -74,7 +72,7 @@ def searching_request(ctx, udg_endpoint, resource_id, token, token_file, is_json center_longitude=center_longitude if center_longitude is not None else None, distance=distance if distance is not None else None) logger.debug("Request: %s", request.to_str()) - fetch_and_save_datasets(api_client, request, resource_id, inbox_dir) + fetch_and_save_datasets(api_instance, request, resource_id, inbox_dir) except Exception as e: logger.error(f"Errore durante il recupero dei dataset: {str(e)}") diff --git a/scp_udg_client_app/helpers.py b/scp_udg_client_app/helpers.py index 55c8e5552e2fa73f5154a0b6f4e7c4ad3971f19b..103025336530c9d505b8d23dbed4d2fdb2197727 100644 --- a/scp_udg_client_app/helpers.py +++ b/scp_udg_client_app/helpers.py @@ -9,6 +9,7 @@ from typing import Optional import click import javaproperties +from click import ClickException from dateutil.parser import parse from scp_udg_client_rest import LoginRequest, Configuration, ApiClient, UrbanDatasetGatewayApi from tocase.for_strings import ToCase @@ -20,6 +21,11 @@ logger = logging.getLogger() DATE_FORMAT = "%Y%m%d%H%M%S" DEFAULT_UDG_ENDPOINT = "https://scp-casaccia.bologna.enea.it:8443/webservices/rest/UrbanDatasetGateway" NO_CONFIG_WARNING = 'No configuration file found in [{}]' +BACKUP_SUFFIX = ".bak" + +# Definizione delle costanti per i codici di ritorno +SUCCESS_CODE_GENERIC = "00" +SUCCESS_CODE_AUTHENTICATION = "01" # Functions @@ -107,15 +113,6 @@ def is_api_alive(api_instance: UrbanDatasetGatewayApi) -> bool: return False -def authenticate_user(api_instance: UrbanDatasetGatewayApi, user_name: str, user_password: str) -> bool: - login_request = LoginRequest(username=user_name, password=user_password) - login_response = api_instance.login(login_request) - if login_response.code == "01": - api_instance.api_client.configuration.access_token = login_response.token - return True - return False - - def save_dataset(dataset, resource_id: str, destination_dir: str) -> None: timestamp = parse(dataset.urban_dataset.context.timestamp) Path(destination_dir).mkdir(parents=True, exist_ok=True) @@ -155,19 +152,6 @@ def create_api_client_instance(udg_endpoint: str, access_token: Optional[str] = return UrbanDatasetGatewayApi(api_client) -def active_login(api_instance: UrbanDatasetGatewayApi, username: str, password: str) -> bool: - logger.info("Checking if the API is alive.") - api_alive = is_api_alive(api_instance) - if api_alive: - logger.info("API is alive; no need to authenticate user.") - return True - if not username or not password: - logger.warning("Username or password not set; cannot authenticate user.") - return False - logger.info(f"API is not alive; authenticating user '{username}'.") - return authenticate_user(api_instance, username, password) - - def count_files_recursively(folder_path: str) -> dict: """ Counts the number of files recursively in each subfolder. @@ -240,57 +224,6 @@ def retrieve_token(token, token_file, is_json): raise click.ClickException("Neither --token nor --token-file provided.") -def read_token_from_file(token_file, is_json): - """Read token from the provided file and parse if necessary.""" - try: - if is_json or token_file.name.endswith('.json'): - token_data = json.load(token_file) - if 'token' not in token_data: - raise KeyError("JSON file does not contain 'token' field.") - return token_data['token'] - else: - token = token_file.read().strip() - if not token: - raise ValueError("Token file is empty.") - return token - except json.JSONDecodeError: - raise click.ClickException("Failed to parse file as JSON.") - except (KeyError, ValueError) as e: - raise click.ClickException(str(e)) - except Exception as e: - raise click.ClickException(f"Failed to read token from file: {e}") - - -def authenticate(api_instance, token, token_file, is_json, username, password): - try: - token = get_token(token, token_file, is_json) - except Exception as e: - error_message = f"Failed to retrieve token: {e}" - logger.error(error_message) - raise click.ClickException(error_message) - if token: - api_instance.api_client.configuration.access_token = token - logger.info("A token was provided and will be used for authentication.") - check_api_alive(api_instance) - elif not username or not password: - error_message = "Neither --token, --token-file nor credentials provided (username and password)." - logger.error(error_message) - raise click.ClickException(error_message) - else: - try: - response = api_instance.login(LoginRequest(username=username, password=password)) - api_instance.api_client.configuration.access_token = response.token - if response.token: - logger.info("Login successful. Access token retrieved and set.") - else: - error_message = f"No access token received. Full response: {response.to_str()}" - raise click.ClickException(error_message) - except Exception as e: - logger.error(e) - raise click.ClickException(f"{e}") - return api_instance - - def check_api_alive(api_instance): try: response = api_instance.is_alive() @@ -304,3 +237,104 @@ def check_api_alive(api_instance): error_message = f"IsAlive request failed: {e}" logger.error(error_message) raise click.ClickException(error_message) + + +def get_api_instance(ctx, udg_endpoint) -> UrbanDatasetGatewayApi: + """ + Returns an instance of UrbanDatasetGatewayApi. If the instance already exists in the context, reuse it. + Otherwise, create a new instance and store it in the context for future use. + + :param ctx: Click context + :param udg_endpoint: UDG endpoint URL + :return: Instance of UrbanDatasetGatewayApi + """ + if ctx.obj is None: + ctx.obj = {} + + if 'api_instance' not in ctx.obj: + ctx.obj['api_instance'] = UrbanDatasetGatewayApi(ApiClient(Configuration(host=udg_endpoint))) + + return ctx.obj['api_instance'] + + +def authenticate(api_instance, token_file, is_json, username, password): + """ + Command to authenticate a user and save the authentication token. + """ + token = read_token_from_file(token_file, is_json) + + if token and is_token_alive(api_instance, token): + logger.info(f"Token from {token_file} is still valid.") + else: + logger.info("Token is invalid or expired. Proceeding to acquire a new token via login.") + authenticate_user(api_instance, token_file, is_json, username, password) + + +def is_token_alive(api_instance, token): + api_instance.api_client.configuration.access_token = token + try: + return api_instance.is_alive().code == SUCCESS_CODE_GENERIC + except Exception as e: + logger.error(f"Error verifying token: {str(e)}") + return False + + +def authenticate_user(api_instance, token_file, is_json, username, password): + if not username or not password: + raise click.ClickException("No credentials provided for re-authentication.") + backup_token_file(token_file) + perform_login(api_instance, token_file, is_json, username, password) + + +def backup_token_file(token_file): + try: + shutil.copy2(token_file, f"{token_file}{BACKUP_SUFFIX}") + logger.info(f"Backup of the token file created at {token_file}{BACKUP_SUFFIX}") + except Exception as e: + logger.error(f"Failed to create a backup of the token file: {str(e)}") + + +def read_token_from_file(token_file: str, is_json: bool) -> str: + """ + Reads the authentication token from the specified file. + + :param token_file: The path to the token file. + :param is_json: A flag indicating if the token file is in JSON format. + :return: The token as a string, or an empty string if the file does not exist or cannot be read. + """ + if not os.path.exists(token_file): + logger.warning(f"Token file {token_file} does not exist.") + return "" + try: + logger.info(f"Reading token from file: {token_file}") + with open(token_file, 'r') as file: + if token_file.endswith('.json') or is_json: + data = json.load(file) + return data.get("token", "") + else: + return file.read().strip() + except Exception as e: + error_message = f"Failed to read token file: {str(e)}" + logger.error(error_message) + raise click.ClickException(error_message) + + +def perform_login(api_instance, token_file, is_json, username, password): + try: + login_response = api_instance.login(LoginRequest(username=username, password=password)) + if login_response.code != SUCCESS_CODE_AUTHENTICATION or not login_response.token: + raise click.ClickException(f"Authentication failed. Full response: {login_response.to_str()}") + + with open(token_file, 'w') as file: + if token_file.endswith('.json') or is_json: + json.dump(login_response.to_dict(), file) + else: + file.write(login_response.token) + api_instance.api_client.configuration.access_token = login_response.token + logger.info("New token obtained and saved successfully.") + except ClickException as ce: + logger.error(ce) + raise ce + except Exception as e: + logger.error(f"Failed to authenticate and obtain token: {str(e)}") + raise click.ClickException(f"Failed to authenticate and obtain token: {str(e)}")