From 2ca488e1ca6ecc2872c04952f06a3a5bbb524ea0 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Thu, 11 Jul 2024 07:02:40 -0300 Subject: [PATCH 01/19] Added slct_manager --- exasol/nb_connector/slct_manager.py | 82 +++++++++++++++++++++++++++++ pyproject.toml | 1 + 2 files changed, 83 insertions(+) create mode 100644 exasol/nb_connector/slct_manager.py diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py new file mode 100644 index 0000000..428fba7 --- /dev/null +++ b/exasol/nb_connector/slct_manager.py @@ -0,0 +1,82 @@ +import os +import re +import contextlib +from pathlib import Path +from exasol_script_languages_container_tool.lib import api as exaslct_api +from exasol.nb_connector.ai_lab_config import AILabConfig as CKey, StorageBackend +from exasol.nb_connector.language_container_activation import ACTIVATION_KEY_PREFIX + +EXPORT_PATH = Path() / "container" +OUTPUT_PATH = Path() / "output" +RELEASE_NAME = "current" +PATH_IN_BUCKET = "container" + +SLC_SOURCE_FLAVOR_STORE_KEY = "slc_flavor" +SLC_TARGET_DIR_STORE_KEY = "slc_target_dir" + + +# Activation SQL for the Custom SLC will be saved in the secret +# store with this key. +ACTIVATION_KEY = ACTIVATION_KEY_PREFIX + "slc" + +@contextlib.contextmanager +def working_directory(path: Path): + """Changes working directory and returns to previous on exit.""" + prev_cwd = Path.cwd() + os.chdir(path) + try: + yield + finally: + os.chdir(prev_cwd) + +def check_slc_config(secrets) -> bool: + slc_dir = Path(secrets.get(SLC_TARGET_DIR_STORE_KEY)) + selected_flavor = secrets.get(SLC_SOURCE_FLAVOR_STORE_KEY) + print(f"Script-languages repository path is '{slc_dir}'") + print(f"Selected flavor is '{selected_flavor}'") + if not (Path(slc_dir) / "flavors" / selected_flavor).is_dir(): + return False + + return True + + +def get_flavor_path(secrets): + selected_flavor = secrets.get(SLC_SOURCE_FLAVOR_STORE_KEY) + return Path("flavors") / selected_flavor + +def export(secrets): + slc_dir = Path(secrets.get(SLC_TARGET_DIR_STORE_KEY)) + flavor_path = get_flavor_path(secrets) + with working_directory(slc_dir): + export_result = exaslct_api.export(flavor_path=(str(flavor_path),),export_path=EXPORT_PATH, + output_directory=OUTPUT_PATH) + +def upload(secrets): + slc_dir = Path(secrets.get(SLC_TARGET_DIR_STORE_KEY)) + selected_flavor = secrets.get(SLC_SOURCE_FLAVOR_STORE_KEY) + flavor_path = get_flavor_path(secrets) + + bucketfs_name = secrets.get(CKey.bfs_service) + bucket_name = secrets.get(CKey.bfs_bucket) + database_host = secrets.get(CKey.bfs_host_name) + bucketfs_port = secrets.get(CKey.bfs_port) + bucketfs_username = secrets.get(CKey.bfs_user) + bucketfs_password = secrets.get(CKey.bfs_password) + + with working_directory(slc_dir): + upload_result = exaslct_api.upload(flavor_path=(str(flavor_path),), database_host=database_host, + bucketfs_name=bucketfs_name, + bucket_name=bucket_name, bucketfs_port=bucketfs_port, + bucketfs_username=bucketfs_username, + bucketfs_password=bucketfs_password, path_in_bucket=PATH_IN_BUCKET, + release_name=RELEASE_NAME) + container_name = f"{selected_flavor}-release-{RELEASE_NAME}" + result = exaslct_api.generate_language_activation(flavor_path=flavor_path, bucketfs_name=bucketfs_name, + bucket_name=bucket_name, container_name=container_name, + path_in_bucket=PATH_IN_BUCKET) + + alter_session_cmd = result[0] + re_res = re.search(r"ALTER SESSION SET SCRIPT_LANGUAGES='(.*)'", alter_session_cmd) + secrets.save(ACTIVATION_KEY, re_res.groups()[0]) + + diff --git a/pyproject.toml b/pyproject.toml index 1275caa..8e08d55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ types-requests = "^2.31.0.6" ifaddr = "^0.2.0" exasol-saas-api = {git = "https://github.com/exasol/saas-api-python.git", branch = "main"} ibis-framework = {extras = ["exasol"], version = "^9.1.0"} +exasol-script-languages-container-tool = ">=0.20.0" [build-system] From fb9dd3f4a76a1f3f172c9d015f6e443aed2515ce Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:36:16 -0300 Subject: [PATCH 02/19] 1. Added dependencies 2. Added class SlctManager 3. Added integration test 4. Added a helper function for creating an pyexasol connection which also runs the ALTER SESSION command for all registered languages --- exasol/nb_connector/ai_lab_config.py | 1 + .../language_container_activation.py | 11 + exasol/nb_connector/slct_manager.py | 213 ++++++++++++------ pyproject.toml | 2 + test/integration/test_slct_manager.py | 121 ++++++++++ 5 files changed, 280 insertions(+), 68 deletions(-) create mode 100644 test/integration/test_slct_manager.py diff --git a/exasol/nb_connector/ai_lab_config.py b/exasol/nb_connector/ai_lab_config.py index bec507b..56dc215 100644 --- a/exasol/nb_connector/ai_lab_config.py +++ b/exasol/nb_connector/ai_lab_config.py @@ -42,6 +42,7 @@ class AILabConfig(Enum): saas_database_id = auto() saas_database_name = auto() storage_backend = auto() + slc_target_dir = auto() class StorageBackend(Enum): diff --git a/exasol/nb_connector/language_container_activation.py b/exasol/nb_connector/language_container_activation.py index 1736418..f458993 100644 --- a/exasol/nb_connector/language_container_activation.py +++ b/exasol/nb_connector/language_container_activation.py @@ -1,5 +1,7 @@ from typing import Dict +import pyexasol + from exasol.nb_connector.secret_store import Secrets from exasol.nb_connector.connections import open_pyexasol_connection @@ -89,3 +91,12 @@ def get_activation_sql(conf: Secrets) -> str: # Build and return an SQL command for the language container activation. merged_langs_str = " ".join(f"{key}={value}" for key, value in lang_definitions.items()) return f"ALTER SESSION SET SCRIPT_LANGUAGES='{merged_langs_str}';" + + +def open_pyexasol_connection_with_lang_definitions(conf: Secrets, **kwargs) -> pyexasol.ExaConnection: + """ + Opens a `pyexasol` connection and applies the `ALTER SESSION` command using all registered languages. + """ + conn = open_pyexasol_connection(conf, **kwargs) + conn.execute(get_activation_sql(conf)) + return conn diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index 428fba7..18cc677 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -1,82 +1,159 @@ import os import re import contextlib +from collections import namedtuple +from typing import Optional, List + +from git import Repo from pathlib import Path from exasol_script_languages_container_tool.lib import api as exaslct_api -from exasol.nb_connector.ai_lab_config import AILabConfig as CKey, StorageBackend +from exasol.nb_connector.ai_lab_config import AILabConfig as CKey, AILabConfig from exasol.nb_connector.language_container_activation import ACTIVATION_KEY_PREFIX -EXPORT_PATH = Path() / "container" -OUTPUT_PATH = Path() / "output" RELEASE_NAME = "current" PATH_IN_BUCKET = "container" -SLC_SOURCE_FLAVOR_STORE_KEY = "slc_flavor" -SLC_TARGET_DIR_STORE_KEY = "slc_target_dir" - - # Activation SQL for the Custom SLC will be saved in the secret # store with this key. ACTIVATION_KEY = ACTIVATION_KEY_PREFIX + "slc" -@contextlib.contextmanager -def working_directory(path: Path): - """Changes working directory and returns to previous on exit.""" - prev_cwd = Path.cwd() - os.chdir(path) - try: - yield - finally: - os.chdir(prev_cwd) - -def check_slc_config(secrets) -> bool: - slc_dir = Path(secrets.get(SLC_TARGET_DIR_STORE_KEY)) - selected_flavor = secrets.get(SLC_SOURCE_FLAVOR_STORE_KEY) - print(f"Script-languages repository path is '{slc_dir}'") - print(f"Selected flavor is '{selected_flavor}'") - if not (Path(slc_dir) / "flavors" / selected_flavor).is_dir(): - return False - - return True - - -def get_flavor_path(secrets): - selected_flavor = secrets.get(SLC_SOURCE_FLAVOR_STORE_KEY) - return Path("flavors") / selected_flavor - -def export(secrets): - slc_dir = Path(secrets.get(SLC_TARGET_DIR_STORE_KEY)) - flavor_path = get_flavor_path(secrets) - with working_directory(slc_dir): - export_result = exaslct_api.export(flavor_path=(str(flavor_path),),export_path=EXPORT_PATH, - output_directory=OUTPUT_PATH) - -def upload(secrets): - slc_dir = Path(secrets.get(SLC_TARGET_DIR_STORE_KEY)) - selected_flavor = secrets.get(SLC_SOURCE_FLAVOR_STORE_KEY) - flavor_path = get_flavor_path(secrets) - - bucketfs_name = secrets.get(CKey.bfs_service) - bucket_name = secrets.get(CKey.bfs_bucket) - database_host = secrets.get(CKey.bfs_host_name) - bucketfs_port = secrets.get(CKey.bfs_port) - bucketfs_username = secrets.get(CKey.bfs_user) - bucketfs_password = secrets.get(CKey.bfs_password) - - with working_directory(slc_dir): - upload_result = exaslct_api.upload(flavor_path=(str(flavor_path),), database_host=database_host, - bucketfs_name=bucketfs_name, - bucket_name=bucket_name, bucketfs_port=bucketfs_port, - bucketfs_username=bucketfs_username, - bucketfs_password=bucketfs_password, path_in_bucket=PATH_IN_BUCKET, - release_name=RELEASE_NAME) - container_name = f"{selected_flavor}-release-{RELEASE_NAME}" - result = exaslct_api.generate_language_activation(flavor_path=flavor_path, bucketfs_name=bucketfs_name, - bucket_name=bucket_name, container_name=container_name, - path_in_bucket=PATH_IN_BUCKET) - - alter_session_cmd = result[0] - re_res = re.search(r"ALTER SESSION SET SCRIPT_LANGUAGES='(.*)'", alter_session_cmd) - secrets.save(ACTIVATION_KEY, re_res.groups()[0]) - - +# This is the flavor customers are supposed to use for modifications. +REQUIRED_FLAVOR = "template-Exasol-all-python-3.10" + +# Path to the used flavor within the script-languages-release repository +FLAVOR_PATH_IN_SLC_REPO = Path("flavors") / REQUIRED_FLAVOR + +PipPackageDefinition = namedtuple('PipPackageDefinition', ['pkg', 'version']) + + +class SlctManager: + def __init__(self, secrets, working_path: Optional[Path] = None): + if not working_path: + self.working_path = Path.cwd() + else: + self.working_path = working_path + self._secrets = secrets + + @property + def export_path(self): + """ + Returns the export path for script-languages-container + """ + return self.working_path / "container" + + @property + def output_path(self): + """ + Returns the output path containing caches and logs. + """ + return self.working_path / "output" + + @contextlib.contextmanager + def _slc_working_directory(self): + """Changes working directory and returns to previous on exit.""" + slc_dir = Path(self._secrets.get(AILabConfig.slc_target_dir)) + prev_cwd = Path.cwd() + os.chdir(slc_dir) + try: + yield + finally: + os.chdir(prev_cwd) + + def check_slc_repo_complete(self) -> bool: + """ + Checks if the target dir for the script-languages repository is present and correct. + """ + slc_dir = Path(self._secrets.get(AILabConfig.slc_target_dir)) + print(f"Script-languages repository path is '{slc_dir}'") + if not (Path(slc_dir) / FLAVOR_PATH_IN_SLC_REPO).is_dir(): + return False + return True + + def clone_slc_repo(self): + """ + Clones the script-languages-release repository from Github into the target dir configured in the secret store. + """ + slc_dir = Path(self._secrets.get(AILabConfig.slc_target_dir)) + if not slc_dir.is_dir(): + print(f"Cloning into {slc_dir}...") + repo = Repo.clone_from("https://github.com/exasol/script-languages-release", slc_dir) + print("Fetching submodules...") + repo.submodule_update(recursive=True) + else: + print(f"Directory '{slc_dir}' already exists. Skipping cloning....") + + def export(self): + """ + Exports the current script-languages-container to the export directory. + """ + with self._slc_working_directory(): + export_result = exaslct_api.export(flavor_path=(str(FLAVOR_PATH_IN_SLC_REPO),), + export_path=str(self.export_path), + output_directory=str(self.output_path)) + + def upload(self): + """ + Uploads the current script-languages-container to the database + and stores the activation string in the secret store. + """ + bucketfs_name = self._secrets.get(CKey.bfs_service) + bucket_name = self._secrets.get(CKey.bfs_bucket) + database_host = self._secrets.get(CKey.bfs_host_name) + bucketfs_port = self._secrets.get(CKey.bfs_port) + bucketfs_username = self._secrets.get(CKey.bfs_user) + bucketfs_password = self._secrets.get(CKey.bfs_password) + + with self._slc_working_directory(): + exaslct_api.upload(flavor_path=(str(FLAVOR_PATH_IN_SLC_REPO),), + database_host=database_host, + bucketfs_name=bucketfs_name, + bucket_name=bucket_name, bucketfs_port=bucketfs_port, + bucketfs_username=bucketfs_username, + bucketfs_password=bucketfs_password, path_in_bucket=PATH_IN_BUCKET, + release_name=RELEASE_NAME) + container_name = f"{REQUIRED_FLAVOR}-release-{RELEASE_NAME}" + result = exaslct_api.generate_language_activation(flavor_path=str(FLAVOR_PATH_IN_SLC_REPO), + bucketfs_name=bucketfs_name, + bucket_name=bucket_name, container_name=container_name, + path_in_bucket=PATH_IN_BUCKET) + + alter_session_cmd = result[0] + re_res = re.search(r"ALTER SESSION SET SCRIPT_LANGUAGES='(.*)'", alter_session_cmd) + self._secrets.save(ACTIVATION_KEY, re_res.groups()[0]) + + @property + def activation_key(self) -> str: + """ + Returns the language activation string for the uploaded script-language-container. + Can be used in `ALTER SESSION` or `ALTER_SYSTEM` SQL commands to activate + the language of the uploaded script-language-container. + Must not be called after an initial upload. + """ + return self._secrets.get(ACTIVATION_KEY) + + @property + def language_alias(self) -> str: + """ + Returns the language alias of the uploaded script-language-container. + Must not be called after an initial upload. + """ + activation_key = self.activation_key + alias, _ = activation_key.split("=", maxsplit=1) + return alias + + @property + def custom_pip_file(self) -> Path: + """ + Returns the path to the custom pip file of the flavor + """ + return Path(self._secrets.get( + AILabConfig.slc_target_dir)) / FLAVOR_PATH_IN_SLC_REPO / "flavor_customization" / "packages" / "python3_pip_packages" + + def append_custom_packages(self, pip_packages: List[PipPackageDefinition]): + """ + Appends packages to the custom pip file. + Note: This method is not idempotent: Multiple calls with the same package definitions will result in duplicated entries. + """ + with open(self.custom_pip_file, "a") as f: + for p in pip_packages: + print(f"{p.pkg}|{p.version}", file=f) diff --git a/pyproject.toml b/pyproject.toml index 8e08d55..ffca850 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ ifaddr = "^0.2.0" exasol-saas-api = {git = "https://github.com/exasol/saas-api-python.git", branch = "main"} ibis-framework = {extras = ["exasol"], version = "^9.1.0"} exasol-script-languages-container-tool = ">=0.20.0" +GitPython = ">=2.1.0" [build-system] @@ -52,6 +53,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.dev-dependencies] pytest = "^7.1.1" pytest-mock = "^3.7.0" +pytest_dependency = ">=0.6.0" exasol-toolbox = "^0.5.0" diff --git a/test/integration/test_slct_manager.py b/test/integration/test_slct_manager.py new file mode 100644 index 0000000..4ac390d --- /dev/null +++ b/test/integration/test_slct_manager.py @@ -0,0 +1,121 @@ +import textwrap +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import List, Tuple + +import pytest + +from exasol.nb_connector.ai_lab_config import AILabConfig +from exasol.nb_connector.itde_manager import bring_itde_up +from exasol.nb_connector.language_container_activation import open_pyexasol_connection_with_lang_definitions +from exasol.nb_connector.secret_store import Secrets +from exasol.nb_connector.slct_manager import SlctManager, PipPackageDefinition +from test.integration.test_itde_manager import remove_itde + + +@pytest.fixture(scope="module") +def working_path() -> Path: + with TemporaryDirectory() as d: + yield Path(d) + + +@pytest.fixture(scope="module") +def secrets_file(working_path: Path) -> Path: + return working_path / "sample_database.db" + + +@pytest.fixture(scope="module") +def slc_secrets(secrets_file, working_path) -> Secrets: + secrets = Secrets(secrets_file, master_password="abc") + secrets.save(AILabConfig.slc_target_dir, str(working_path / "script_languages_release")) + return secrets + + +@pytest.fixture(scope="module") +def slct_manager(slc_secrets: Secrets, working_path: Path) -> SlctManager: + return SlctManager(slc_secrets, working_path) + + +@pytest.fixture(scope="module") +def itde(slc_secrets: Secrets): + bring_itde_up(slc_secrets) + yield + remove_itde() + + +@pytest.fixture +def custom_packages() -> List[Tuple[str, str, str]]: + return [("xgboost", "2.0.3", "xgboost"), ("scikit-learn", "1.5.0", "sklearn")] + + +@pytest.mark.dependency(name="clone") +def test_clone_slc(slct_manager): + slct_manager.clone_slc_repo() + + +@pytest.mark.dependency( + name="check_config", depends=["clone"] +) +def test_check_slc_config(slct_manager): + config_ok = slct_manager.check_slc_repo_complete() + assert config_ok + + +@pytest.mark.dependency( + name="export_slc", depends=["check_config"] +) +def test_export_slc(slct_manager): + slct_manager.export() + export_path = slct_manager.export_path + assert export_path.exists() + tgz = [f for f in export_path.glob("*.tar.gz")] + assert len(tgz) == 1 + assert tgz[0].is_file() + tgz_sum = [f for f in export_path.glob("*.tar.gz.sha512sum")] + assert len(tgz_sum) == 1 + assert tgz_sum[0].is_file() + + +@pytest.mark.dependency( + name="upload_slc", depends=["check_config"] +) +def test_upload(slct_manager: SlctManager, itde): + slct_manager.upload() + assert slct_manager.activation_key == "PYTHON3=localzmq+protobuf:///bfsdefault/default/container/template-Exasol-all-python-3.10-release-current?lang=python#buckets/bfsdefault/default/container/template-Exasol-all-python-3.10-release-current/exaudf/exaudfclient_py3" + + +@pytest.mark.dependency( + name="append_custom_packages", depends=["upload_slc"] +) +def test_append_custom_packages(slct_manager: SlctManager, custom_packages: List[Tuple[str, str, str]]): + slct_manager.append_custom_packages([PipPackageDefinition(pkg, version) for pkg, version, _ in custom_packages]) + with open(slct_manager.custom_pip_file, "r") as f: + pip_content = f.read() + for custom_package, version, _ in custom_packages: + assert f"{custom_package}|{version}" in pip_content + + +@pytest.mark.dependency( + name="check_new_packages", depends=["append_custom_packages"] +) +def test_check_new_packages(slc_secrets: Secrets, slct_manager: SlctManager, + custom_packages: List[Tuple[str, str, str]]): + alias = slct_manager.language_alias + + import_statements = "\n".join(f" import {module}" for pkg, version, module in custom_packages) + udf = textwrap.dedent(f""" +CREATE OR REPLACE {alias} SET SCRIPT test_custom_packages(i integer) +EMITS (o VARCHAR(2000000)) AS +def run(ctx): +{import_statements} + + ctx.emit("success") +/ + """) + slct_manager.upload() + con = open_pyexasol_connection_with_lang_definitions(slc_secrets) + con.execute("CREATE SCHEMA TEST") + con.execute(udf) + res = con.execute("select test_custom_packages(1)") + rows = res.fetchall() + assert rows == [('success',)] From a0fe93ae524e26a3c4747f6195b3ff52159cbd8c Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:46:29 -0300 Subject: [PATCH 03/19] Fixed output path for upload --- exasol/nb_connector/slct_manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index 18cc677..8af5632 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -87,9 +87,9 @@ def export(self): Exports the current script-languages-container to the export directory. """ with self._slc_working_directory(): - export_result = exaslct_api.export(flavor_path=(str(FLAVOR_PATH_IN_SLC_REPO),), - export_path=str(self.export_path), - output_directory=str(self.output_path)) + exaslct_api.export(flavor_path=(str(FLAVOR_PATH_IN_SLC_REPO),), + export_path=str(self.export_path), + output_directory=str(self.output_path)) def upload(self): """ @@ -110,7 +110,8 @@ def upload(self): bucket_name=bucket_name, bucketfs_port=bucketfs_port, bucketfs_username=bucketfs_username, bucketfs_password=bucketfs_password, path_in_bucket=PATH_IN_BUCKET, - release_name=RELEASE_NAME) + release_name=RELEASE_NAME, + output_directory=str(self.output_path)) container_name = f"{REQUIRED_FLAVOR}-release-{RELEASE_NAME}" result = exaslct_api.generate_language_activation(flavor_path=str(FLAVOR_PATH_IN_SLC_REPO), bucketfs_name=bucketfs_name, From 67d411bf815d9f5195f9ba4dea42375203d0dd86 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:51:27 -0300 Subject: [PATCH 04/19] Decreased exasol-script-languages-container-tool requiored version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ffca850..e743c7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ types-requests = "^2.31.0.6" ifaddr = "^0.2.0" exasol-saas-api = {git = "https://github.com/exasol/saas-api-python.git", branch = "main"} ibis-framework = {extras = ["exasol"], version = "^9.1.0"} -exasol-script-languages-container-tool = ">=0.20.0" +exasol-script-languages-container-tool = ">=0.19.0" GitPython = ">=2.1.0" From 53cd102c9cc8481db944b76357da368a4fc5cf06 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:20:30 -0300 Subject: [PATCH 05/19] Added AiLabConfig.slc_source --- exasol/nb_connector/ai_lab_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/exasol/nb_connector/ai_lab_config.py b/exasol/nb_connector/ai_lab_config.py index 56dc215..f8dd64e 100644 --- a/exasol/nb_connector/ai_lab_config.py +++ b/exasol/nb_connector/ai_lab_config.py @@ -43,6 +43,7 @@ class AILabConfig(Enum): saas_database_name = auto() storage_backend = auto() slc_target_dir = auto() + slc_source = auto() class StorageBackend(Enum): From de132f7d7230d1f7af88cd8e3203e183c1d813ed Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Thu, 11 Jul 2024 14:13:42 -0300 Subject: [PATCH 06/19] Refactored slct_manager. --- exasol/nb_connector/slct_manager.py | 85 ++++++++++++++++----------- test/integration/test_slct_manager.py | 2 +- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index 8af5632..9afc5ae 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -9,6 +9,7 @@ from exasol_script_languages_container_tool.lib import api as exaslct_api from exasol.nb_connector.ai_lab_config import AILabConfig as CKey, AILabConfig from exasol.nb_connector.language_container_activation import ACTIVATION_KEY_PREFIX +from exasol.nb_connector.secret_store import Secrets RELEASE_NAME = "current" PATH_IN_BUCKET = "container" @@ -26,46 +27,66 @@ PipPackageDefinition = namedtuple('PipPackageDefinition', ['pkg', 'version']) -class SlctManager: - def __init__(self, secrets, working_path: Optional[Path] = None): - if not working_path: - self.working_path = Path.cwd() - else: - self.working_path = working_path +class SlcDir: + def __init__(self, secrets: Secrets): self._secrets = secrets + @property + def root_dir(self) -> Path: + return Path(self._secrets.get(AILabConfig.slc_target_dir)) + + @property + def flavor_dir(self) -> Path: + return self.root_dir / FLAVOR_PATH_IN_SLC_REPO + + @contextlib.contextmanager + def enter(self): + """Changes working directory and returns to previous on exit.""" + prev_cwd = Path.cwd() + os.chdir(self.root_dir) + try: + yield + finally: + os.chdir(prev_cwd) + + def __str__(self): + return str(self.root_dir) + + +class WorkingDir: + def __init__(self, p: Optional[Path]): + if p is None: + self.root_dir = Path.cwd() + else: + self.root_dir = p + @property def export_path(self): """ Returns the export path for script-languages-container """ - return self.working_path / "container" + return self.root_dir / "container" @property def output_path(self): """ Returns the output path containing caches and logs. """ - return self.working_path / "output" + return self.root_dir / "output" - @contextlib.contextmanager - def _slc_working_directory(self): - """Changes working directory and returns to previous on exit.""" - slc_dir = Path(self._secrets.get(AILabConfig.slc_target_dir)) - prev_cwd = Path.cwd() - os.chdir(slc_dir) - try: - yield - finally: - os.chdir(prev_cwd) + +class SlctManager: + def __init__(self, secrets: Secrets, working_path: Optional[Path] = None): + self.working_path = WorkingDir(working_path) + self.slc_dir = SlcDir(secrets) + self._secrets = secrets def check_slc_repo_complete(self) -> bool: """ Checks if the target dir for the script-languages repository is present and correct. """ - slc_dir = Path(self._secrets.get(AILabConfig.slc_target_dir)) - print(f"Script-languages repository path is '{slc_dir}'") - if not (Path(slc_dir) / FLAVOR_PATH_IN_SLC_REPO).is_dir(): + print(f"Script-languages repository path is '{self.slc_dir}'") + if not self.slc_dir.flavor_dir.is_dir(): return False return True @@ -73,23 +94,22 @@ def clone_slc_repo(self): """ Clones the script-languages-release repository from Github into the target dir configured in the secret store. """ - slc_dir = Path(self._secrets.get(AILabConfig.slc_target_dir)) - if not slc_dir.is_dir(): - print(f"Cloning into {slc_dir}...") - repo = Repo.clone_from("https://github.com/exasol/script-languages-release", slc_dir) + if not self.slc_dir.root_dir.is_dir(): + print(f"Cloning into {self.slc_dir}...") + repo = Repo.clone_from("https://github.com/exasol/script-languages-release", self.slc_dir.root_dir) print("Fetching submodules...") repo.submodule_update(recursive=True) else: - print(f"Directory '{slc_dir}' already exists. Skipping cloning....") + print(f"Directory '{self.slc_dir}' already exists. Skipping cloning....") def export(self): """ Exports the current script-languages-container to the export directory. """ - with self._slc_working_directory(): + with self.slc_dir.enter(): exaslct_api.export(flavor_path=(str(FLAVOR_PATH_IN_SLC_REPO),), - export_path=str(self.export_path), - output_directory=str(self.output_path)) + export_path=str(self.working_path.export_path), + output_directory=str(self.working_path.output_path)) def upload(self): """ @@ -103,7 +123,7 @@ def upload(self): bucketfs_username = self._secrets.get(CKey.bfs_user) bucketfs_password = self._secrets.get(CKey.bfs_password) - with self._slc_working_directory(): + with self.slc_dir.enter(): exaslct_api.upload(flavor_path=(str(FLAVOR_PATH_IN_SLC_REPO),), database_host=database_host, bucketfs_name=bucketfs_name, @@ -111,7 +131,7 @@ def upload(self): bucketfs_username=bucketfs_username, bucketfs_password=bucketfs_password, path_in_bucket=PATH_IN_BUCKET, release_name=RELEASE_NAME, - output_directory=str(self.output_path)) + output_directory=str(self.working_path.output_path)) container_name = f"{REQUIRED_FLAVOR}-release-{RELEASE_NAME}" result = exaslct_api.generate_language_activation(flavor_path=str(FLAVOR_PATH_IN_SLC_REPO), bucketfs_name=bucketfs_name, @@ -147,8 +167,7 @@ def custom_pip_file(self) -> Path: """ Returns the path to the custom pip file of the flavor """ - return Path(self._secrets.get( - AILabConfig.slc_target_dir)) / FLAVOR_PATH_IN_SLC_REPO / "flavor_customization" / "packages" / "python3_pip_packages" + return self.slc_dir.flavor_dir / "flavor_customization" / "packages" / "python3_pip_packages" def append_custom_packages(self, pip_packages: List[PipPackageDefinition]): """ diff --git a/test/integration/test_slct_manager.py b/test/integration/test_slct_manager.py index 4ac390d..c0b02fd 100644 --- a/test/integration/test_slct_manager.py +++ b/test/integration/test_slct_manager.py @@ -66,7 +66,7 @@ def test_check_slc_config(slct_manager): ) def test_export_slc(slct_manager): slct_manager.export() - export_path = slct_manager.export_path + export_path = slct_manager.working_path.export_path assert export_path.exists() tgz = [f for f in export_path.glob("*.tar.gz")] assert len(tgz) == 1 From 164b2c47fe0dfc2dac6e213c3f5e1e9a0edd7c6c Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:55:26 -0300 Subject: [PATCH 07/19] Fixed MyPy findings --- exasol/nb_connector/language_container_activation.py | 2 +- exasol/nb_connector/slct_manager.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/exasol/nb_connector/language_container_activation.py b/exasol/nb_connector/language_container_activation.py index f458993..23effaa 100644 --- a/exasol/nb_connector/language_container_activation.py +++ b/exasol/nb_connector/language_container_activation.py @@ -1,6 +1,6 @@ from typing import Dict -import pyexasol +import pyexasol # type: ignore from exasol.nb_connector.secret_store import Secrets from exasol.nb_connector.connections import open_pyexasol_connection diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index 9afc5ae..e8b8c2b 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -6,7 +6,7 @@ from git import Repo from pathlib import Path -from exasol_script_languages_container_tool.lib import api as exaslct_api +from exasol_script_languages_container_tool.lib import api as exaslct_api # type: ignore from exasol.nb_connector.ai_lab_config import AILabConfig as CKey, AILabConfig from exasol.nb_connector.language_container_activation import ACTIVATION_KEY_PREFIX from exasol.nb_connector.secret_store import Secrets @@ -33,7 +33,10 @@ def __init__(self, secrets: Secrets): @property def root_dir(self) -> Path: - return Path(self._secrets.get(AILabConfig.slc_target_dir)) + target_dir = self._secrets.get(AILabConfig.slc_target_dir) + if not target_dir: + raise RuntimeError("slc target dir is not defined in secrets.") + return Path(target_dir) @property def flavor_dir(self) -> Path: @@ -150,7 +153,10 @@ def activation_key(self) -> str: the language of the uploaded script-language-container. Must not be called after an initial upload. """ - return self._secrets.get(ACTIVATION_KEY) + activation_key = self._secrets.get(ACTIVATION_KEY) + if not activation_key: + raise RuntimeError("SLC activation key not defined in secrets.") + return activation_key @property def language_alias(self) -> str: From f709c7c8199d465576e43febd1227d0c2fb0b31e Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:21:01 -0300 Subject: [PATCH 08/19] Fixed test_itde_manager_in_docker_container.py --- test/integration/test_itde_manager_in_docker_container.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/test_itde_manager_in_docker_container.py b/test/integration/test_itde_manager_in_docker_container.py index 5268ffb..b31a62d 100644 --- a/test/integration/test_itde_manager_in_docker_container.py +++ b/test/integration/test_itde_manager_in_docker_container.py @@ -265,6 +265,7 @@ def docker_container(wheel_path, docker_image, f"python3 -m pip install /tmp/{wheel_path.name} " f"--extra-index-url https://download.pytorch.org/whl/cpu" ) + print(output) assert exit_code == 0, output yield container finally: From 927045305701180accab4b3ae11015b952ac1541 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Mon, 15 Jul 2024 07:45:39 -0300 Subject: [PATCH 09/19] Fixes from review --- exasol/nb_connector/slct_manager.py | 7 ++++--- test/integration/test_itde_manager_in_docker_container.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index e8b8c2b..0a8da7f 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -1,3 +1,4 @@ +import logging import os import re import contextlib @@ -98,12 +99,12 @@ def clone_slc_repo(self): Clones the script-languages-release repository from Github into the target dir configured in the secret store. """ if not self.slc_dir.root_dir.is_dir(): - print(f"Cloning into {self.slc_dir}...") + logging.info(f"Cloning into {self.slc_dir}...") repo = Repo.clone_from("https://github.com/exasol/script-languages-release", self.slc_dir.root_dir) - print("Fetching submodules...") + logging.info("Fetching submodules...") repo.submodule_update(recursive=True) else: - print(f"Directory '{self.slc_dir}' already exists. Skipping cloning....") + logging.warning(f"Directory '{self.slc_dir}' already exists. Skipping cloning....") def export(self): """ diff --git a/test/integration/test_itde_manager_in_docker_container.py b/test/integration/test_itde_manager_in_docker_container.py index b31a62d..5268ffb 100644 --- a/test/integration/test_itde_manager_in_docker_container.py +++ b/test/integration/test_itde_manager_in_docker_container.py @@ -265,7 +265,6 @@ def docker_container(wheel_path, docker_image, f"python3 -m pip install /tmp/{wheel_path.name} " f"--extra-index-url https://download.pytorch.org/whl/cpu" ) - print(output) assert exit_code == 0, output yield container finally: From 3a92c260117498cbf830109243da6db0463ddb40 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Mon, 15 Jul 2024 10:19:33 -0300 Subject: [PATCH 10/19] Fixed closing SQL connection at end of integration test --- test/integration/test_slct_manager.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/integration/test_slct_manager.py b/test/integration/test_slct_manager.py index c0b02fd..8a44602 100644 --- a/test/integration/test_slct_manager.py +++ b/test/integration/test_slct_manager.py @@ -114,8 +114,11 @@ def run(ctx): """) slct_manager.upload() con = open_pyexasol_connection_with_lang_definitions(slc_secrets) - con.execute("CREATE SCHEMA TEST") - con.execute(udf) - res = con.execute("select test_custom_packages(1)") - rows = res.fetchall() - assert rows == [('success',)] + try: + con.execute("CREATE SCHEMA TEST") + con.execute(udf) + res = con.execute("select test_custom_packages(1)") + rows = res.fetchall() + assert rows == [('success',)] + finally: + con.close() From 6426d04b7f88836321fe0bc55738804d77040de8 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Tue, 16 Jul 2024 07:03:55 -0300 Subject: [PATCH 11/19] Allow custom definition of alias --- exasol/nb_connector/slct_manager.py | 7 +++++-- test/integration/test_slct_manager.py | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index 0a8da7f..539c93c 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -115,10 +115,11 @@ def export(self): export_path=str(self.working_path.export_path), output_directory=str(self.working_path.output_path)) - def upload(self): + def upload(self, alias: str): """ Uploads the current script-languages-container to the database and stores the activation string in the secret store. + @param alias: The alias used for the script-language-container activation """ bucketfs_name = self._secrets.get(CKey.bfs_service) bucket_name = self._secrets.get(CKey.bfs_bucket) @@ -144,7 +145,9 @@ def upload(self): alter_session_cmd = result[0] re_res = re.search(r"ALTER SESSION SET SCRIPT_LANGUAGES='(.*)'", alter_session_cmd) - self._secrets.save(ACTIVATION_KEY, re_res.groups()[0]) + activation_key = re_res.groups()[0] + _, url = activation_key.split("=", maxsplit=1) + self._secrets.save(ACTIVATION_KEY, f"{alias}={url}") @property def activation_key(self) -> str: diff --git a/test/integration/test_slct_manager.py b/test/integration/test_slct_manager.py index 8a44602..18fd4c7 100644 --- a/test/integration/test_slct_manager.py +++ b/test/integration/test_slct_manager.py @@ -80,8 +80,8 @@ def test_export_slc(slct_manager): name="upload_slc", depends=["check_config"] ) def test_upload(slct_manager: SlctManager, itde): - slct_manager.upload() - assert slct_manager.activation_key == "PYTHON3=localzmq+protobuf:///bfsdefault/default/container/template-Exasol-all-python-3.10-release-current?lang=python#buckets/bfsdefault/default/container/template-Exasol-all-python-3.10-release-current/exaudf/exaudfclient_py3" + slct_manager.upload("my_python") + assert slct_manager.activation_key == "my_python=localzmq+protobuf:///bfsdefault/default/container/template-Exasol-all-python-3.10-release-current?lang=python#buckets/bfsdefault/default/container/template-Exasol-all-python-3.10-release-current/exaudf/exaudfclient_py3" @pytest.mark.dependency( @@ -100,7 +100,7 @@ def test_append_custom_packages(slct_manager: SlctManager, custom_packages: List ) def test_check_new_packages(slc_secrets: Secrets, slct_manager: SlctManager, custom_packages: List[Tuple[str, str, str]]): - alias = slct_manager.language_alias + alias = "my_python" import_statements = "\n".join(f" import {module}" for pkg, version, module in custom_packages) udf = textwrap.dedent(f""" @@ -112,7 +112,7 @@ def run(ctx): ctx.emit("success") / """) - slct_manager.upload() + slct_manager.upload(alias) con = open_pyexasol_connection_with_lang_definitions(slc_secrets) try: con.execute("CREATE SCHEMA TEST") From bf50b7322544bd726f008c88a5ff29b5d0e7f135 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Tue, 16 Jul 2024 09:06:08 -0300 Subject: [PATCH 12/19] Provide configuration of alias --- exasol/nb_connector/ai_lab_config.py | 1 + exasol/nb_connector/slct_manager.py | 38 ++++++++++++-------- test/integration/test_slct_manager.py | 50 +++++++++++++++++++++------ 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/exasol/nb_connector/ai_lab_config.py b/exasol/nb_connector/ai_lab_config.py index f8dd64e..f7a7740 100644 --- a/exasol/nb_connector/ai_lab_config.py +++ b/exasol/nb_connector/ai_lab_config.py @@ -44,6 +44,7 @@ class AILabConfig(Enum): storage_backend = auto() slc_target_dir = auto() slc_source = auto() + slc_alias = auto() class StorageBackend(Enum): diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index 539c93c..b6a72ff 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -17,7 +17,7 @@ # Activation SQL for the Custom SLC will be saved in the secret # store with this key. -ACTIVATION_KEY = ACTIVATION_KEY_PREFIX + "slc" +SLC_ACTIVATION_KEY_PREFIX = ACTIVATION_KEY_PREFIX + "slc_" # This is the flavor customers are supposed to use for modifications. REQUIRED_FLAVOR = "template-Exasol-all-python-3.10" @@ -115,11 +115,10 @@ def export(self): export_path=str(self.working_path.export_path), output_directory=str(self.working_path.output_path)) - def upload(self, alias: str): + def upload(self): """ Uploads the current script-languages-container to the database and stores the activation string in the secret store. - @param alias: The alias used for the script-language-container activation """ bucketfs_name = self._secrets.get(CKey.bfs_service) bucket_name = self._secrets.get(CKey.bfs_bucket) @@ -132,12 +131,12 @@ def upload(self, alias: str): exaslct_api.upload(flavor_path=(str(FLAVOR_PATH_IN_SLC_REPO),), database_host=database_host, bucketfs_name=bucketfs_name, - bucket_name=bucket_name, bucketfs_port=bucketfs_port, + bucket_name=bucket_name, bucketfs_port=int(bucketfs_port), bucketfs_username=bucketfs_username, bucketfs_password=bucketfs_password, path_in_bucket=PATH_IN_BUCKET, - release_name=RELEASE_NAME, + release_name=self.language_alias, output_directory=str(self.working_path.output_path)) - container_name = f"{REQUIRED_FLAVOR}-release-{RELEASE_NAME}" + container_name = f"{REQUIRED_FLAVOR}-release-{self.language_alias}" result = exaslct_api.generate_language_activation(flavor_path=str(FLAVOR_PATH_IN_SLC_REPO), bucketfs_name=bucketfs_name, bucket_name=bucket_name, container_name=container_name, @@ -147,7 +146,11 @@ def upload(self, alias: str): re_res = re.search(r"ALTER SESSION SET SCRIPT_LANGUAGES='(.*)'", alter_session_cmd) activation_key = re_res.groups()[0] _, url = activation_key.split("=", maxsplit=1) - self._secrets.save(ACTIVATION_KEY, f"{alias}={url}") + self._secrets.save(self._alias_key, f"{self.language_alias}={url}") + + @property + def _alias_key(self): + return SLC_ACTIVATION_KEY_PREFIX + self.language_alias @property def activation_key(self) -> str: @@ -155,9 +158,8 @@ def activation_key(self) -> str: Returns the language activation string for the uploaded script-language-container. Can be used in `ALTER SESSION` or `ALTER_SYSTEM` SQL commands to activate the language of the uploaded script-language-container. - Must not be called after an initial upload. """ - activation_key = self._secrets.get(ACTIVATION_KEY) + activation_key = self._secrets.get(self._alias_key) if not activation_key: raise RuntimeError("SLC activation key not defined in secrets.") return activation_key @@ -165,12 +167,20 @@ def activation_key(self) -> str: @property def language_alias(self) -> str: """ - Returns the language alias of the uploaded script-language-container. - Must not be called after an initial upload. + Returns the stored language alias. + """ + language_alias = self._secrets.get(AILabConfig.slc_alias) + if not language_alias: + raise RuntimeError("SLC language alias key not defined in secrets.") + return language_alias + + @language_alias.setter + def language_alias(self, alias: str): + """ + Stores the language alias in the secret store. """ - activation_key = self.activation_key - alias, _ = activation_key.split("=", maxsplit=1) - return alias + self._secrets.save(AILabConfig.slc_alias, alias) + @property def custom_pip_file(self) -> Path: diff --git a/test/integration/test_slct_manager.py b/test/integration/test_slct_manager.py index 18fd4c7..214788e 100644 --- a/test/integration/test_slct_manager.py +++ b/test/integration/test_slct_manager.py @@ -80,8 +80,9 @@ def test_export_slc(slct_manager): name="upload_slc", depends=["check_config"] ) def test_upload(slct_manager: SlctManager, itde): - slct_manager.upload("my_python") - assert slct_manager.activation_key == "my_python=localzmq+protobuf:///bfsdefault/default/container/template-Exasol-all-python-3.10-release-current?lang=python#buckets/bfsdefault/default/container/template-Exasol-all-python-3.10-release-current/exaudf/exaudfclient_py3" + slct_manager.language_alias = "my_python" + slct_manager.upload() + assert slct_manager.activation_key == "my_python=localzmq+protobuf:///bfsdefault/default/container/template-Exasol-all-python-3.10-release-my_python?lang=python#buckets/bfsdefault/default/container/template-Exasol-all-python-3.10-release-my_python/exaudf/exaudfclient_py3" @pytest.mark.dependency( @@ -96,23 +97,30 @@ def test_append_custom_packages(slct_manager: SlctManager, custom_packages: List @pytest.mark.dependency( - name="check_new_packages", depends=["append_custom_packages"] + name="upload_slc_with_new_packages", depends=["append_custom_packages"] ) -def test_check_new_packages(slc_secrets: Secrets, slct_manager: SlctManager, - custom_packages: List[Tuple[str, str, str]]): - alias = "my_python" +def test_upload_slc_with_new_packages(slc_secrets: Secrets, slct_manager: SlctManager, + custom_packages: List[Tuple[str, str, str]]): + slct_manager.language_alias = "my_new_python" + slct_manager.upload() + assert slct_manager.activation_key == "my_new_python=localzmq+protobuf:///bfsdefault/default/container/template-Exasol-all-python-3.10-release-my_new_python?lang=python#buckets/bfsdefault/default/container/template-Exasol-all-python-3.10-release-my_new_python/exaudf/exaudfclient_py3" + +@pytest.mark.dependency( + name="udf_with_new_packages", depends=["upload_slc_with_new_packages"] +) +def test_udf_with_new_packages(slc_secrets: Secrets, slct_manager: SlctManager, + custom_packages: List[Tuple[str, str, str]]): import_statements = "\n".join(f" import {module}" for pkg, version, module in custom_packages) udf = textwrap.dedent(f""" -CREATE OR REPLACE {alias} SET SCRIPT test_custom_packages(i integer) +CREATE OR REPLACE {slct_manager.language_alias} SET SCRIPT test_custom_packages(i integer) EMITS (o VARCHAR(2000000)) AS def run(ctx): {import_statements} - + ctx.emit("success") / - """) - slct_manager.upload(alias) + """) con = open_pyexasol_connection_with_lang_definitions(slc_secrets) try: con.execute("CREATE SCHEMA TEST") @@ -122,3 +130,25 @@ def run(ctx): assert rows == [('success',)] finally: con.close() + + +@pytest.mark.dependency( + name="test_old_alias", depends=["udf_with_new_packages"] +) +def test_old_alias(slc_secrets: Secrets, slct_manager: SlctManager): + + udf = textwrap.dedent(f""" +CREATE OR REPLACE my_python SET SCRIPT test_old_slc(i integer) +EMITS (o VARCHAR(2000000)) AS +def run(ctx): + ctx.emit("success") +/ + """) + con = open_pyexasol_connection_with_lang_definitions(slc_secrets, schema='TEST') + try: + con.execute(udf) + res = con.execute("select test_old_slc(1)") + rows = res.fetchall() + assert rows == [('success',)] + finally: + con.close() From fec53a69a17920c15fa59e713769498b43b1069b Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Tue, 16 Jul 2024 10:01:43 -0300 Subject: [PATCH 13/19] Use language_alias also for export --- exasol/nb_connector/slct_manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index b6a72ff..2f66bb3 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -12,7 +12,7 @@ from exasol.nb_connector.language_container_activation import ACTIVATION_KEY_PREFIX from exasol.nb_connector.secret_store import Secrets -RELEASE_NAME = "current" +DEFAULT_ALIAS = "my_language" PATH_IN_BUCKET = "container" # Activation SQL for the Custom SLC will be saved in the secret @@ -113,7 +113,8 @@ def export(self): with self.slc_dir.enter(): exaslct_api.export(flavor_path=(str(FLAVOR_PATH_IN_SLC_REPO),), export_path=str(self.working_path.export_path), - output_directory=str(self.working_path.output_path)) + output_directory=str(self.working_path.output_path), + release_name=self.language_alias,) def upload(self): """ @@ -169,9 +170,9 @@ def language_alias(self) -> str: """ Returns the stored language alias. """ - language_alias = self._secrets.get(AILabConfig.slc_alias) + language_alias = self._secrets.get(AILabConfig.slc_alias, DEFAULT_ALIAS) if not language_alias: - raise RuntimeError("SLC language alias key not defined in secrets.") + return DEFAULT_ALIAS return language_alias @language_alias.setter From 157a3e8a5c03e81d017e7af08e319b294120d518 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:48:16 -0300 Subject: [PATCH 14/19] Added clean up functions --- exasol/nb_connector/slct_manager.py | 36 ++++++++++++++++++++------- test/integration/test_slct_manager.py | 32 +++++++++++++++++++++++- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index 2f66bb3..ceb5a06 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -2,6 +2,7 @@ import os import re import contextlib +import shutil from collections import namedtuple from typing import Optional, List @@ -43,6 +44,13 @@ def root_dir(self) -> Path: def flavor_dir(self) -> Path: return self.root_dir / FLAVOR_PATH_IN_SLC_REPO + @property + def custom_pip_file(self) -> Path: + """ + Returns the path to the custom pip file of the flavor + """ + return self.flavor_dir / "flavor_customization" / "packages" / "python3_pip_packages" + @contextlib.contextmanager def enter(self): """Changes working directory and returns to previous on exit.""" @@ -78,6 +86,18 @@ def output_path(self): """ return self.root_dir / "output" + def cleanup_output_path(self): + """ + Remove the output path recursively. + """ + shutil.rmtree(self.output_path) + + def cleanup_export_path(self): + """ + Remove the export path recursively + """ + shutil.rmtree(self.export_path) + class SlctManager: def __init__(self, secrets: Secrets, working_path: Optional[Path] = None): @@ -182,19 +202,17 @@ def language_alias(self, alias: str): """ self._secrets.save(AILabConfig.slc_alias, alias) - - @property - def custom_pip_file(self) -> Path: - """ - Returns the path to the custom pip file of the flavor - """ - return self.slc_dir.flavor_dir / "flavor_customization" / "packages" / "python3_pip_packages" - def append_custom_packages(self, pip_packages: List[PipPackageDefinition]): """ Appends packages to the custom pip file. Note: This method is not idempotent: Multiple calls with the same package definitions will result in duplicated entries. """ - with open(self.custom_pip_file, "a") as f: + with open(self.slc_dir.custom_pip_file, "a") as f: for p in pip_packages: print(f"{p.pkg}|{p.version}", file=f) + + def clean_all_images(self): + """ + Deletes all local docker images. + """ + exaslct_api.clean_all_images(output_directory=str(self.working_path.output_path)) diff --git a/test/integration/test_slct_manager.py b/test/integration/test_slct_manager.py index 214788e..ab4a134 100644 --- a/test/integration/test_slct_manager.py +++ b/test/integration/test_slct_manager.py @@ -4,6 +4,7 @@ from typing import List, Tuple import pytest +from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient from exasol.nb_connector.ai_lab_config import AILabConfig from exasol.nb_connector.itde_manager import bring_itde_up @@ -90,7 +91,7 @@ def test_upload(slct_manager: SlctManager, itde): ) def test_append_custom_packages(slct_manager: SlctManager, custom_packages: List[Tuple[str, str, str]]): slct_manager.append_custom_packages([PipPackageDefinition(pkg, version) for pkg, version, _ in custom_packages]) - with open(slct_manager.custom_pip_file, "r") as f: + with open(slct_manager.slc_dir.custom_pip_file, "r") as f: pip_content = f.read() for custom_package, version, _ in custom_packages: assert f"{custom_package}|{version}" in pip_content @@ -152,3 +153,32 @@ def run(ctx): assert rows == [('success',)] finally: con.close() + + +@pytest.mark.dependency( + name="clean_up_images", depends=["upload_slc_with_new_packages"] +) +def test_clean_up_images(slct_manager: SlctManager): + slct_manager.clean_all_images() + with ContextDockerClient() as docker_client: + images = docker_client.images.list(name="exasol/script-language-container") + assert len(images) == 0 + + +@pytest.mark.dependency( + name="clean_up_output_path", depends=["clean_up_images"] +) +def test_clean_output(slct_manager: SlctManager): + slct_manager.working_path.cleanup_output_path() + p = Path(slct_manager.working_path.output_path) + assert not p.is_dir() + + +@pytest.mark.dependency( + name="clean_up_export_path", depends=["clean_up_images"] +) +def test_clean_export(slct_manager: SlctManager): + slct_manager.working_path.cleanup_export_path() + p = Path(slct_manager.working_path.export_path) + assert not p.is_dir() + From a061a6fc5132d71c2a77d3fe9cfe44855b8691ea Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:32:59 -0300 Subject: [PATCH 15/19] Added a function to get slc docker images --- exasol/nb_connector/slct_manager.py | 8 ++++++++ test/integration/test_slct_manager.py | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index ceb5a06..a06fe6c 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -6,6 +6,7 @@ from collections import namedtuple from typing import Optional, List +from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient from git import Repo from pathlib import Path from exasol_script_languages_container_tool.lib import api as exaslct_api # type: ignore @@ -211,6 +212,13 @@ def append_custom_packages(self, pip_packages: List[PipPackageDefinition]): for p in pip_packages: print(f"{p.pkg}|{p.version}", file=f) + @property + def slc_docker_images(self): + with ContextDockerClient() as docker_client: + images = docker_client.images.list(name="exasol/script-language-container") + image_tags = [img.tags[0] for img in images] + return image_tags + def clean_all_images(self): """ Deletes all local docker images. diff --git a/test/integration/test_slct_manager.py b/test/integration/test_slct_manager.py index ab4a134..c7f5ca3 100644 --- a/test/integration/test_slct_manager.py +++ b/test/integration/test_slct_manager.py @@ -76,6 +76,15 @@ def test_export_slc(slct_manager): assert len(tgz_sum) == 1 assert tgz_sum[0].is_file() +@pytest.mark.dependency( + name="slc_images", depends=["export_slc"] +) +def test_slc_images(slct_manager): + images = slct_manager.slc_docker_images + assert len(images) > 0 + for img in images: + assert "exasol/script-language-container" in img + @pytest.mark.dependency( name="upload_slc", depends=["check_config"] From f9ecc6fbdeaae764950fd68861835499e6856d78 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:51:24 -0300 Subject: [PATCH 16/19] Ignore type --- exasol/nb_connector/slct_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index a06fe6c..ae85dba 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -6,7 +6,7 @@ from collections import namedtuple from typing import Optional, List -from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient +from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient # type: ignore from git import Repo from pathlib import Path from exasol_script_languages_container_tool.lib import api as exaslct_api # type: ignore From f4cf8b73508b84ff4bfe8f3d9421071196b145e9 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:07:06 -0300 Subject: [PATCH 17/19] Updated lock-file --- poetry.lock | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 6fa49cf..b784391 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2656,6 +2656,20 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-dependency" +version = "0.6.0" +description = "Manage dependencies of tests" +optional = false +python-versions = ">=3.4" +files = [ + {file = "pytest-dependency-0.6.0.tar.gz", hash = "sha256:934b0e6a39d95995062c193f7eaeed8a8ffa06ff1bcef4b62b0dc74a708bacc1"}, +] + +[package.dependencies] +pytest = ">=3.7.0" +setuptools = "*" + [[package]] name = "pytest-mock" version = "3.14.0" @@ -2780,6 +2794,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -4417,4 +4432,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "6cc43a6605086a2a64e1f4812d49b176ce33425adb671003d3cf65c4b44e5f2d" +content-hash = "db3d47a8e79f2f260c3eb06165a2f39391a066796aa35f4f2fd83076db194ea6" From 190ad2cfb0a4e595574bd500a13e9e067d56ded7 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:18:53 -0300 Subject: [PATCH 18/19] Changed default-alias --- exasol/nb_connector/slct_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index ae85dba..3c3d32c 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -14,7 +14,7 @@ from exasol.nb_connector.language_container_activation import ACTIVATION_KEY_PREFIX from exasol.nb_connector.secret_store import Secrets -DEFAULT_ALIAS = "my_language" +DEFAULT_ALIAS = "ai-lab-default" PATH_IN_BUCKET = "container" # Activation SQL for the Custom SLC will be saved in the secret From 2be5218475c0f76848a9efc0805c759ecdada214 Mon Sep 17 00:00:00 2001 From: Thomas Ubensee <34603111+tomuben@users.noreply.github.com> Date: Fri, 19 Jul 2024 07:21:34 -0300 Subject: [PATCH 19/19] Renamed default alias --- exasol/nb_connector/slct_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exasol/nb_connector/slct_manager.py b/exasol/nb_connector/slct_manager.py index 3c3d32c..07137f3 100644 --- a/exasol/nb_connector/slct_manager.py +++ b/exasol/nb_connector/slct_manager.py @@ -14,7 +14,7 @@ from exasol.nb_connector.language_container_activation import ACTIVATION_KEY_PREFIX from exasol.nb_connector.secret_store import Secrets -DEFAULT_ALIAS = "ai-lab-default" +DEFAULT_ALIAS = "ai_lab_default" PATH_IN_BUCKET = "container" # Activation SQL for the Custom SLC will be saved in the secret