Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring language_container_deployer #162

Merged
merged 10 commits into from
Dec 11, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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+\}'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an example how the pattern looks at the end and what it matches might be useful

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()


Expand Down
25 changes: 14 additions & 11 deletions tests/unit_tests/deployment/test_language_container_deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand All @@ -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='" \
Expand All @@ -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'
Expand All @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import click
from exasol_transformers_extension.deployment.language_container_deployer_cli import (
_ParameterFormatters, CustomizableParameters)


def test_parameter_formatters_1param():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow, I like that

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'
Loading