diff --git a/exasol_transformers_extension/deployment/language_container_deployer.py b/exasol_transformers_extension/deployment/language_container_deployer.py index 5aeb83a3..7a52063c 100644 --- a/exasol_transformers_extension/deployment/language_container_deployer.py +++ b/exasol_transformers_extension/deployment/language_container_deployer.py @@ -60,6 +60,19 @@ class LanguageActivationLevel(Enum): System = 'SYSTEM' +def get_language_settings(pyexasol_conn: pyexasol.ExaConnection, alter_type: LanguageActivationLevel) -> str: + """ + Reads the current language settings at the specified level. + + pyexasol_conn - Opened database connection. + alter_type - Activation level - SYSTEM or SESSION. + """ + result = pyexasol_conn.execute( + f"""SELECT "{alter_type.value}_VALUE" FROM SYS.EXA_PARAMETERS WHERE + PARAMETER_NAME='SCRIPT_LANGUAGES'""").fetchall() + return result[0][0] + + class LanguageContainerDeployer: def __init__(self, @@ -189,21 +202,10 @@ def generate_activation_command(self, bucket_file_path: str, f"ALTER {alter_type.value} SET SCRIPT_LANGUAGES='{new_settings}';" return alter_command - def get_language_settings(self, alter_type: LanguageActivationLevel) -> str: - """ - Reads the current language settings at the specified level. - - alter_type - Activation level - SYSTEM or SESSION. - """ - result = self._pyexasol_conn.execute( - f"""SELECT "{alter_type.value}_VALUE" FROM SYS.EXA_PARAMETERS WHERE - PARAMETER_NAME='SCRIPT_LANGUAGES'""").fetchall() - return result[0][0] - def _update_previous_language_settings(self, alter_type: LanguageActivationLevel, allow_override: bool, path_in_udf: PurePosixPath) -> str: - prev_lang_settings = self.get_language_settings(alter_type) + prev_lang_settings = get_language_settings(self._pyexasol_conn, alter_type) prev_lang_aliases = prev_lang_settings.split(" ") self._check_if_requested_language_alias_already_exists( allow_override, prev_lang_aliases) diff --git a/exasol_transformers_extension/deployment/language_container_deployer_cli.py b/exasol_transformers_extension/deployment/language_container_deployer_cli.py index 99b8dd8a..43c497a8 100644 --- a/exasol_transformers_extension/deployment/language_container_deployer_cli.py +++ b/exasol_transformers_extension/deployment/language_container_deployer_cli.py @@ -1,4 +1,9 @@ +######################################################### +# To be migrated to the script-languages-container-tool # +######################################################### +from typing import Optional, Any import os +import re import click from enum import Enum from pathlib import Path @@ -7,32 +12,68 @@ class CustomizableParameters(Enum): + """ + Parameters of the cli that can be programmatically customised by a developer + of a specialised version of the cli. + The names in the enum list should match the parameter names in language_container_deployer_main. + """ container_url = 1 container_name = 2 class _ParameterFormatters: + """ + Class facilitating customization of the cli. + + The idea is that some of the cli parameters can be programmatically customized based + on values of other parameters and externally supplied formatters. For example a specialized + version of the cli may want to provide its own url. Furthermore, this url will depend on + the user supplied parameter called "version". The solution is to set a formatter for the + url, for instance "http://my_stuff/{version}/my_data". If the user specifies non-empty version + parameter the url will be fully formed. + + A formatter may include more than one parameter. In the previous example the url could, + for instance, also include a username: "http://my_stuff/{version}/{user}/my_data". + + Note that customized parameters can only be updated in a callback function. There is no + way to inject them directly into the cli. Also, the current implementation doesn't perform + the update if the value of the parameter dressed with the callback is None. + + IMPORTANT! Please make sure that the formatters are set up before the call to the cli function, + e.g. language_container_deployer_main, is executed. + """ def __init__(self): self._formatters = {} - def __call__(self, ctx, param, value): + def __call__(self, ctx: click.Context, param: click.Parameter, value: Optional[Any]) -> Optional[Any]: def update_parameter(parameter_name: str, formatter: str) -> None: param_formatter = ctx.params.get(parameter_name, formatter) if param_formatter: + # Enclose in double curly brackets all other parameters in the formatting string, + # to avoid the missing parameters' error. + pattern = r'\{(?!' + param.name + r'\})\w+\}' + param_formatter = re.sub(pattern, lambda m: f'{{{m.group(0)}}}', param_formatter) kwargs = {param.name: value} ctx.params[parameter_name] = param_formatter.format(**kwargs) - if value: + if value is not None: for prm_name, prm_formatter in self._formatters.items(): update_parameter(prm_name, prm_formatter) return value def set_formatter(self, custom_parameter: CustomizableParameters, formatter: str) -> None: + """ Sets a formatter for a customizable parameter. """ self._formatters[custom_parameter.name] = formatter + def clear_formatters(self): + """ Deletes all formatters, mainly for testing purposes. """ + self._formatters.clear() + +# Global cli customization object. +# Specialized versions of this cli should use this object to set custom parameter formatters. slc_parameter_formatters = _ParameterFormatters() diff --git a/tests/unit_tests/deployment/test_language_container_deployer.py b/tests/unit_tests/deployment/test_language_container_deployer.py index 49f99605..58f7e1b1 100644 --- a/tests/unit_tests/deployment/test_language_container_deployer.py +++ b/tests/unit_tests/deployment/test_language_container_deployer.py @@ -2,7 +2,7 @@ # To be migrated to the script-languages-container-tool # ######################################################### from pathlib import Path, PurePosixPath -from unittest.mock import create_autospec, MagicMock +from unittest.mock import create_autospec, MagicMock, patch import pytest from pyexasol import ExaConnection from exasol_bucketfs_utils_python.bucketfs_location import BucketFSLocation @@ -50,7 +50,6 @@ def container_deployer(mock_pyexasol_conn, mock_bfs_location, language_alias) -> deployer.upload_container = MagicMock() deployer.activate_container = MagicMock() - deployer.get_language_settings = MagicMock() return deployer @@ -69,16 +68,17 @@ def test_slc_deployer_upload(container_deployer, container_file_name, container_ def test_slc_deployer_activate(container_deployer, container_file_name, container_file_path): - container_deployer.run(bucket_file_path=container_file_path, alter_system=True, allow_override=True) + container_deployer.run(bucket_file_path=container_file_name, alter_system=True, allow_override=True) container_deployer.upload_container.assert_not_called() container_deployer.activate_container.assert_called_once_with(container_file_name, LanguageActivationLevel.System, True) -def test_slc_deployer_generate_activation_command(container_deployer, language_alias, container_file_name, - container_bfs_path): +@patch('exasol_transformers_extension.deployment.language_container_deployer.get_language_settings') +def test_slc_deployer_generate_activation_command(mock_lang_settings, container_deployer, language_alias, + container_file_name, container_bfs_path): - container_deployer.get_language_settings.return_value = 'R=builtin_r JAVA=builtin_java PYTHON3=builtin_python3' + mock_lang_settings.return_value = 'R=builtin_r JAVA=builtin_java PYTHON3=builtin_python3' alter_type = LanguageActivationLevel.Session expected_command = f"ALTER {alter_type.value.upper()} SET SCRIPT_LANGUAGES='" \ @@ -90,11 +90,12 @@ def test_slc_deployer_generate_activation_command(container_deployer, language_a assert command == expected_command -def test_slc_deployer_generate_activation_command_override(container_deployer, language_alias, container_file_name, - container_bfs_path): +@patch('exasol_transformers_extension.deployment.language_container_deployer.get_language_settings') +def test_slc_deployer_generate_activation_command_override(mock_lang_settings, container_deployer, language_alias, + container_file_name, container_bfs_path): current_bfs_path = 'bfsdefault/default/container_abc' - container_deployer.get_language_settings.return_value = \ + mock_lang_settings.return_value = \ 'R=builtin_r JAVA=builtin_java PYTHON3=builtin_python3 ' \ f'{language_alias}=localzmq+protobuf:///{current_bfs_path}?' \ f'lang=python#/buckets/{current_bfs_path}/exaudf/exaudfclient_py3' @@ -109,10 +110,12 @@ def test_slc_deployer_generate_activation_command_override(container_deployer, l assert command == expected_command -def test_slc_deployer_generate_activation_command_failure(container_deployer, language_alias, container_file_name): +@patch('exasol_transformers_extension.deployment.language_container_deployer.get_language_settings') +def test_slc_deployer_generate_activation_command_failure(mock_lang_settings, container_deployer, language_alias, + container_file_name): current_bfs_path = 'bfsdefault/default/container_abc' - container_deployer.get_language_settings.return_value = \ + mock_lang_settings.return_value = \ 'R=builtin_r JAVA=builtin_java PYTHON3=builtin_python3 ' \ f'{language_alias}=localzmq+protobuf:///{current_bfs_path}?' \ f'lang=python#/buckets/{current_bfs_path}/exaudf/exaudfclient_py3' diff --git a/tests/unit_tests/deployment/test_language_container_deployer_cli.py b/tests/unit_tests/deployment/test_language_container_deployer_cli.py new file mode 100644 index 00000000..dcaf837c --- /dev/null +++ b/tests/unit_tests/deployment/test_language_container_deployer_cli.py @@ -0,0 +1,29 @@ +import click +from exasol_transformers_extension.deployment.language_container_deployer_cli import ( + _ParameterFormatters, CustomizableParameters) + + +def test_parameter_formatters_1param(): + cmd = click.Command('a_command') + ctx = click.Context(cmd) + opt = click.Option(['--version']) + formatters = _ParameterFormatters() + formatters.set_formatter(CustomizableParameters.container_url, 'http://my_server/{version}/my_stuff') + formatters.set_formatter(CustomizableParameters.container_name, 'downloaded') + formatters(ctx, opt, '1.3.2') + assert ctx.params[CustomizableParameters.container_url.name] == 'http://my_server/1.3.2/my_stuff' + assert ctx.params[CustomizableParameters.container_name.name] == 'downloaded' + + +def test_parameter_formatters_2params(): + cmd = click.Command('a_command') + ctx = click.Context(cmd) + opt1 = click.Option(['--version']) + opt2 = click.Option(['--user']) + formatters = _ParameterFormatters() + formatters.set_formatter(CustomizableParameters.container_url, 'http://my_server/{version}/{user}/my_stuff') + formatters.set_formatter(CustomizableParameters.container_name, 'downloaded-{version}') + formatters(ctx, opt1, '1.3.2') + formatters(ctx, opt2, 'cezar') + assert ctx.params[CustomizableParameters.container_url.name] == 'http://my_server/1.3.2/cezar/my_stuff' + assert ctx.params[CustomizableParameters.container_name.name] == 'downloaded-1.3.2'