From 17f44ac919431da7b503a84f941364250655e006 Mon Sep 17 00:00:00 2001 From: mibe Date: Tue, 26 Mar 2024 09:20:50 +0000 Subject: [PATCH 1/3] Add documentation build folder to .gitignore --- .gitignore | 1 + {changes => doc/changes}/changelog.md | 0 {changes => doc/changes}/changes_0.1.0.md | 0 exasol/python_extension_common/__init__.py | 0 .../deployment/language_container_deployer.py | 291 ++++++++++++++++++ .../language_container_deployer_cli.py | 168 ++++++++++ pyproject.toml | 15 + .../test_language_container_deployer.py | 133 ++++++++ .../test_language_container_deployer_cli.py | 29 ++ 9 files changed, 637 insertions(+) create mode 100644 .gitignore rename {changes => doc/changes}/changelog.md (100%) rename {changes => doc/changes}/changes_0.1.0.md (100%) create mode 100644 exasol/python_extension_common/__init__.py create mode 100644 exasol/python_extension_common/deployment/language_container_deployer.py create mode 100644 exasol/python_extension_common/deployment/language_container_deployer_cli.py create mode 100644 pyproject.toml create mode 100644 test/unit/deployment/test_language_container_deployer.py create mode 100644 test/unit/deployment/test_language_container_deployer_cli.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8568120 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.html-documentation diff --git a/changes/changelog.md b/doc/changes/changelog.md similarity index 100% rename from changes/changelog.md rename to doc/changes/changelog.md diff --git a/changes/changes_0.1.0.md b/doc/changes/changes_0.1.0.md similarity index 100% rename from changes/changes_0.1.0.md rename to doc/changes/changes_0.1.0.md diff --git a/exasol/python_extension_common/__init__.py b/exasol/python_extension_common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/exasol/python_extension_common/deployment/language_container_deployer.py b/exasol/python_extension_common/deployment/language_container_deployer.py new file mode 100644 index 0000000..c260965 --- /dev/null +++ b/exasol/python_extension_common/deployment/language_container_deployer.py @@ -0,0 +1,291 @@ +from enum import Enum +from textwrap import dedent +from typing import List, Optional +from pathlib import Path, PurePosixPath +import logging +import tempfile +import requests +import ssl +import pyexasol +from exasol_bucketfs_utils_python.bucketfs_location import BucketFSLocation +from exasol_bucketfs_utils_python.bucket_config import BucketConfig, BucketFSConfig +from exasol_bucketfs_utils_python.bucketfs_connection_config import BucketFSConnectionConfig + +logger = logging.getLogger(__name__) + + +def create_bucketfs_location( + bucketfs_name: str, bucketfs_host: str, bucketfs_port: int, + bucketfs_use_https: bool, bucketfs_user: str, bucketfs_password: str, + bucket: str, path_in_bucket: str) -> BucketFSLocation: + _bucketfs_connection = BucketFSConnectionConfig( + host=bucketfs_host, port=bucketfs_port, user=bucketfs_user, + pwd=bucketfs_password, is_https=bucketfs_use_https) + _bucketfs_config = BucketFSConfig( + bucketfs_name=bucketfs_name, connection_config=_bucketfs_connection) + _bucket_config = BucketConfig( + bucket_name=bucket, bucketfs_config=_bucketfs_config) + return BucketFSLocation( + bucket_config=_bucket_config, + base_path=PurePosixPath(path_in_bucket)) + + +def get_websocket_sslopt(use_ssl_cert_validation: bool = True, + ssl_trusted_ca: Optional[str] = None, + ssl_client_certificate: Optional[str] = None, + ssl_private_key: Optional[str] = None) -> dict: + """ + Returns a dictionary in the winsocket-client format + (see https://websocket-client.readthedocs.io/en/latest/faq.html#what-else-can-i-do-with-sslopts) + """ + + # Is server certificate validation required? + sslopt: dict[str, object] = {"cert_reqs": ssl.CERT_REQUIRED if use_ssl_cert_validation else ssl.CERT_NONE} + + # Is a bundle with trusted CAs provided? + if ssl_trusted_ca: + trusted_ca_path = Path(ssl_trusted_ca) + if trusted_ca_path.is_dir(): + sslopt["ca_cert_path"] = ssl_trusted_ca + elif trusted_ca_path.is_file(): + sslopt["ca_certs"] = ssl_trusted_ca + else: + raise ValueError(f"Trusted CA location {ssl_trusted_ca} doesn't exist.") + + # Is client's own certificate provided? + if ssl_client_certificate: + if not Path(ssl_client_certificate).is_file(): + raise ValueError(f"Certificate file {ssl_client_certificate} doesn't exist.") + sslopt["certfile"] = ssl_client_certificate + if ssl_private_key: + if not Path(ssl_private_key).is_file(): + raise ValueError(f"Private key file {ssl_private_key} doesn't exist.") + sslopt["keyfile"] = ssl_private_key + + return sslopt + + +class LanguageActivationLevel(Enum): + f""" + Language activation level, i.e. + ALTER SET SCRIPT_LANGUAGES=... + """ + Session = 'SESSION' + 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, + pyexasol_connection: pyexasol.ExaConnection, + language_alias: str, + bucketfs_location: BucketFSLocation) -> None: + + self._bucketfs_location = bucketfs_location + self._language_alias = language_alias + self._pyexasol_conn = pyexasol_connection + logger.debug(f"Init {LanguageContainerDeployer.__name__}") + + def download_and_run(self, url: str, + bucket_file_path: str, + alter_system: bool = True, + allow_override: bool = False) -> None: + """ + Downloads the language container from the provided url to a temporary file and then deploys it. + See docstring on the `run` method for details on what is involved in the deployment. + + url - Address where the container will be downloaded from. + bucket_file_path - Path within the designated bucket where the container should be uploaded. + alter_system - If True will try to activate the container at the System level. + allow_override - If True the activation of a language container with the same alias will be + overriden, otherwise a RuntimeException will be thrown. + """ + + with tempfile.NamedTemporaryFile() as tmp_file: + response = requests.get(url, stream=True) + response.raise_for_status() + tmp_file.write(response.content) + + self.run(Path(tmp_file.name), bucket_file_path, alter_system, allow_override) + + def run(self, container_file: Optional[Path] = None, + bucket_file_path: Optional[str] = None, + alter_system: bool = True, + allow_override: bool = False) -> None: + """ + Deploys the language container. This includes two steps, both of which are optional: + - Uploading the container into the database. This step can be skipped if the container + has already been uploaded. + - Activating the container. This step may have to be skipped if the user does not have + System Privileges in the database. In that case two alternative activation SQL commands + will be printed on the console. + + container_file - Path of the container tar.gz file in a local file system. + If not provided the container is assumed to be uploaded already. + bucket_file_path - Path within the designated bucket where the container should be uploaded. + If not specified the name of the container file will be used instead. + alter_system - If True will try to activate the container at the System level. + allow_override - If True the activation of a language container with the same alias will be + overriden, otherwise a RuntimeException will be thrown. + """ + + if not bucket_file_path: + if not container_file: + raise ValueError('Either a container file or a bucket file path must be specified.') + bucket_file_path = container_file.name + + if container_file: + self.upload_container(container_file, bucket_file_path) + + if alter_system: + self.activate_container(bucket_file_path, LanguageActivationLevel.System, allow_override) + else: + message = dedent(f""" + In SQL, you can activate the SLC of the Transformers Extension + by using the following statements: + + To activate the SLC only for the current session: + {self.generate_activation_command(bucket_file_path, LanguageActivationLevel.Session, True)} + + To activate the SLC on the system: + {self.generate_activation_command(bucket_file_path, LanguageActivationLevel.System, True)} + """) + print(message) + + def upload_container(self, container_file: Path, + bucket_file_path: Optional[str] = None) -> None: + """ + Upload the language container to the BucketFS. + + container_file - Path of the container tar.gz file in a local file system. + bucket_file_path - Path within the designated bucket where the container should be uploaded. + """ + if not container_file.is_file(): + raise RuntimeError(f"Container file {container_file} " + f"is not a file.") + with open(container_file, "br") as f: + self._bucketfs_location.upload_fileobj_to_bucketfs( + fileobj=f, bucket_file_path=bucket_file_path) + logging.debug("Container is uploaded to bucketfs") + + def activate_container(self, bucket_file_path: str, + alter_type: LanguageActivationLevel = LanguageActivationLevel.Session, + allow_override: bool = False) -> None: + """ + Activates the language container at the required level. + + bucket_file_path - Path within the designated bucket where the container is uploaded. + alter_type - Language activation level, defaults to the SESSION. + allow_override - If True the activation of a language container with the same alias will be overriden, + otherwise a RuntimeException will be thrown. + """ + alter_command = self.generate_activation_command(bucket_file_path, alter_type, allow_override) + self._pyexasol_conn.execute(alter_command) + logging.debug(alter_command) + + def generate_activation_command(self, bucket_file_path: str, + alter_type: LanguageActivationLevel, + allow_override: bool = False) -> str: + """ + Generates an SQL command to activate the SLC container at the required level. The command will + preserve existing activations of other containers identified by different language aliases. + Activation of a container with the same alias, if exists, will be overwritten. + + bucket_file_path - Path within the designated bucket where the container is uploaded. + alter_type - Activation level - SYSTEM or SESSION. + allow_override - If True the activation of a language container with the same alias will be overriden, + otherwise a RuntimeException will be thrown. + """ + path_in_udf = self._bucketfs_location.generate_bucket_udf_path(bucket_file_path) + new_settings = \ + self._update_previous_language_settings(alter_type, allow_override, path_in_udf) + alter_command = \ + f"ALTER {alter_type.value} SET SCRIPT_LANGUAGES='{new_settings}';" + return alter_command + + def _update_previous_language_settings(self, alter_type: LanguageActivationLevel, + allow_override: bool, + path_in_udf: PurePosixPath) -> str: + 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) + new_definitions_str = self._generate_new_language_settings( + path_in_udf, prev_lang_aliases) + return new_definitions_str + + def get_language_definition(self, bucket_file_path: str): + """ + Generate a language definition (ALIAS=URL) for the specified bucket file path. + + bucket_file_path - Path within the designated bucket where the container is uploaded. + """ + path_in_udf = self._bucketfs_location.generate_bucket_udf_path(bucket_file_path) + result = self._generate_new_language_settings(path_in_udf=path_in_udf, prev_lang_aliases=[]) + return result + + def _generate_new_language_settings(self, path_in_udf: PurePosixPath, + prev_lang_aliases: List[str]) -> str: + other_definitions = [ + alias_definition for alias_definition in prev_lang_aliases + if not alias_definition.startswith(self._language_alias + "=")] + path_in_udf_without_buckets = PurePosixPath(*path_in_udf.parts[2:]) + new_language_alias_definition = \ + f"{self._language_alias}=localzmq+protobuf:///" \ + f"{path_in_udf_without_buckets}?lang=python#" \ + f"{path_in_udf}/exaudf/exaudfclient_py3" + new_definitions = other_definitions + [new_language_alias_definition] + new_definitions_str = " ".join(new_definitions) + return new_definitions_str + + def _check_if_requested_language_alias_already_exists( + self, allow_override: bool, + prev_lang_aliases: List[str]) -> None: + definition_for_requested_alias = [ + alias_definition for alias_definition in prev_lang_aliases + if alias_definition.startswith(self._language_alias + "=")] + if not len(definition_for_requested_alias) == 0: + warning_message = f"The requested language alias {self._language_alias} is already in use." + if allow_override: + logging.warning(warning_message) + else: + raise RuntimeError(warning_message) + + @classmethod + def create(cls, bucketfs_name: str, bucketfs_host: str, bucketfs_port: int, + bucketfs_use_https: bool, bucketfs_user: str, + bucketfs_password: str, bucket: str, path_in_bucket: str, + dsn: str, db_user: str, db_password: str, language_alias: str, + use_ssl_cert_validation: bool = True, ssl_trusted_ca: Optional[str] = None, + ssl_client_certificate: Optional[str] = None, + ssl_private_key: Optional[str] = None) -> "LanguageContainerDeployer": + + websocket_sslopt = get_websocket_sslopt(use_ssl_cert_validation, ssl_trusted_ca, + ssl_client_certificate, ssl_private_key) + + pyexasol_conn = pyexasol.connect( + dsn=dsn, + user=db_user, + password=db_password, + encryption=True, + websocket_sslopt=websocket_sslopt + ) + + bucketfs_location = create_bucketfs_location( + bucketfs_name, bucketfs_host, bucketfs_port, bucketfs_use_https, + bucketfs_user, bucketfs_password, bucket, path_in_bucket) + + return cls(pyexasol_conn, language_alias, bucketfs_location) diff --git a/exasol/python_extension_common/deployment/language_container_deployer_cli.py b/exasol/python_extension_common/deployment/language_container_deployer_cli.py new file mode 100644 index 0000000..fb0b222 --- /dev/null +++ b/exasol/python_extension_common/deployment/language_container_deployer_cli.py @@ -0,0 +1,168 @@ +from typing import Optional, Any +import os +import re +import click +from enum import Enum +from pathlib import Path +from exasol.python_transformers_extension.deployment import deployment_utils as utils +from exasol_transformers_extension.deployment.language_container_deployer import LanguageContainerDeployer + + +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: 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. Below is an example of a formatter string + # before and after applying the regex, assuming the current parameter is 'version'. + # 'something-with-{version}/tailored-for-{user}' => 'something-with-{version}/tailored-for-{{user}}' + # We were looking for all occurrences of a pattern '{some_name}', where some_name is not version. + 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 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() + + +@click.command(name="language-container") +@click.option('--bucketfs-name', type=str, required=True) +@click.option('--bucketfs-host', type=str, required=True) +@click.option('--bucketfs-port', type=int, required=True) +@click.option('--bucketfs-use-https', type=bool, default=False) +@click.option('--bucketfs-user', type=str, required=True, default="w") +@click.option('--bucketfs-password', prompt='bucketFS password', hide_input=True, + default=lambda: os.environ.get(utils.BUCKETFS_PASSWORD_ENVIRONMENT_VARIABLE, "")) +@click.option('--bucket', type=str, required=True) +@click.option('--path-in-bucket', type=str, required=True, default=None) +@click.option('--container-file', + type=click.Path(exists=True, file_okay=True), default=None) +@click.option('--version', type=str, default=None, expose_value=False, + callback=slc_parameter_formatters) +@click.option('--dsn', type=str, required=True) +@click.option('--db-user', type=str, required=True) +@click.option('--db-pass', prompt='db password', hide_input=True, + default=lambda: os.environ.get(utils.DB_PASSWORD_ENVIRONMENT_VARIABLE, "")) +@click.option('--language-alias', type=str, default="PYTHON3_TE") +@click.option('--ssl-cert-path', type=str, default="") +@click.option('--ssl-client-cert-path', type=str, default="") +@click.option('--ssl-client-private-key', type=str, default="") +@click.option('--use-ssl-cert-validation/--no-use-ssl-cert-validation', type=bool, default=True) +@click.option('--upload-container/--no-upload_container', type=bool, default=True) +@click.option('--alter-system/--no-alter-system', type=bool, default=True) +@click.option('--allow-override/--disallow-override', type=bool, default=False) +def language_container_deployer_main( + bucketfs_name: str, + bucketfs_host: str, + bucketfs_port: int, + bucketfs_use_https: bool, + bucketfs_user: str, + bucketfs_password: str, + bucket: str, + path_in_bucket: str, + container_file: str, + dsn: str, + db_user: str, + db_pass: str, + language_alias: str, + ssl_cert_path: str, + ssl_client_cert_path: str, + ssl_client_private_key: str, + use_ssl_cert_validation: bool, + upload_container: bool, + alter_system: bool, + allow_override: bool, + container_url: str = None, + container_name: str = None): + + deployer = LanguageContainerDeployer.create( + bucketfs_name=bucketfs_name, + bucketfs_host=bucketfs_host, + bucketfs_port=bucketfs_port, + bucketfs_use_https=bucketfs_use_https, + bucketfs_user=bucketfs_user, + bucketfs_password=bucketfs_password, + bucket=bucket, + path_in_bucket=path_in_bucket, + dsn=dsn, + db_user=db_user, + db_password=db_pass, + language_alias=language_alias, + ssl_trusted_ca=ssl_cert_path, + ssl_client_certificate=ssl_client_cert_path, + ssl_private_key=ssl_client_private_key, + use_ssl_cert_validation=use_ssl_cert_validation) + + if not upload_container: + deployer.run(alter_system=alter_system, allow_override=allow_override) + elif container_file: + deployer.run(container_file=Path(container_file), alter_system=alter_system, allow_override=allow_override) + elif container_url and container_name: + deployer.download_and_run(container_url, container_name, alter_system=alter_system, + allow_override=allow_override) + else: + # The error message should mention the parameters which the callback is specified for being missed. + raise ValueError("To upload a language container you should specify either its " + "release version or a path of the already downloaded container file.") + + +if __name__ == '__main__': + import logging + + logging.basicConfig( + format='%(asctime)s - %(module)s - %(message)s', + level=logging.DEBUG) + + language_container_deployer_main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..aa8c864 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "python-extension-common" +version = "0.1.0" +description = "A collection of common utilities for Exasol extensions." +authors = ["Your Name "] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "[^3.8]" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/test/unit/deployment/test_language_container_deployer.py b/test/unit/deployment/test_language_container_deployer.py new file mode 100644 index 0000000..746c442 --- /dev/null +++ b/test/unit/deployment/test_language_container_deployer.py @@ -0,0 +1,133 @@ +######################################################### +# To be migrated to the script-languages-container-tool # +######################################################### +from pathlib import Path, PurePosixPath +from unittest.mock import create_autospec, MagicMock, patch + +import pytest +from exasol_bucketfs_utils_python.bucketfs_location import BucketFSLocation +from pyexasol import ExaConnection + +from exasol_transformers_extension.deployment.language_container_deployer import ( + LanguageContainerDeployer, LanguageActivationLevel) + + +@pytest.fixture(scope='module') +def container_file_name() -> str: + return 'container_xyz.tag.gz' + + +@pytest.fixture(scope='module') +def container_file_path(container_file_name) -> Path: + return Path(container_file_name) + + +@pytest.fixture(scope='module') +def language_alias() -> str: + return 'PYTHON3_TEST' + + +@pytest.fixture(scope='module') +def container_bfs_path(container_file_name) -> str: + return f'bfsdefault/default/container/{container_file_name[:-7]}' + + +@pytest.fixture(scope='module') +def mock_pyexasol_conn() -> ExaConnection: + return create_autospec(ExaConnection) + + +@pytest.fixture(scope='module') +def mock_bfs_location(container_bfs_path) -> BucketFSLocation: + mock_loc = create_autospec(BucketFSLocation) + mock_loc.generate_bucket_udf_path.return_value = PurePosixPath(f'/buckets/{container_bfs_path}') + return mock_loc + + +@pytest.fixture +def container_deployer(mock_pyexasol_conn, mock_bfs_location, language_alias) -> LanguageContainerDeployer: + deployer = LanguageContainerDeployer(pyexasol_connection=mock_pyexasol_conn, + language_alias=language_alias, + bucketfs_location=mock_bfs_location) + + deployer.upload_container = MagicMock() + deployer.activate_container = MagicMock() + return deployer + + +def test_slc_deployer_deploy(container_deployer, container_file_name, container_file_path): + container_deployer.run(container_file=container_file_path, bucket_file_path=container_file_name, alter_system=True, + allow_override=True) + container_deployer.upload_container.assert_called_once_with(container_file_path, container_file_name) + container_deployer.activate_container.assert_called_once_with(container_file_name, LanguageActivationLevel.System, + True) + + +def test_slc_deployer_upload(container_deployer, container_file_name, container_file_path): + container_deployer.run(container_file=container_file_path, alter_system=False) + container_deployer.upload_container.assert_called_once_with(container_file_path, container_file_name) + container_deployer.activate_container.assert_not_called() + + +def test_slc_deployer_activate(container_deployer, container_file_name, container_file_path): + 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) + + +@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): + 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='" \ + "R=builtin_r JAVA=builtin_java PYTHON3=builtin_python3 " \ + f"{language_alias}=localzmq+protobuf:///{container_bfs_path}?" \ + f"lang=python#/buckets/{container_bfs_path}/exaudf/exaudfclient_py3';" + + command = container_deployer.generate_activation_command(container_file_name, alter_type) + assert command == expected_command + + +@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' + 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' + + alter_type = LanguageActivationLevel.Session + expected_command = f"ALTER {alter_type.value.upper()} SET SCRIPT_LANGUAGES='" \ + "R=builtin_r JAVA=builtin_java PYTHON3=builtin_python3 " \ + f"{language_alias}=localzmq+protobuf:///{container_bfs_path}?" \ + f"lang=python#/buckets/{container_bfs_path}/exaudf/exaudfclient_py3';" + + command = container_deployer.generate_activation_command(container_file_name, alter_type, allow_override=True) + assert command == expected_command + + +@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' + 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' + + with pytest.raises(RuntimeError): + container_deployer.generate_activation_command(container_file_name, LanguageActivationLevel.Session, + allow_override=False) + + +def test_slc_deployer_get_language_definition(container_deployer, language_alias, + container_file_name, container_bfs_path): + expected_command = f"{language_alias}=localzmq+protobuf:///{container_bfs_path}?" \ + f"lang=python#/buckets/{container_bfs_path}/exaudf/exaudfclient_py3" + + command = container_deployer.get_language_definition(container_file_name) + assert command == expected_command diff --git a/test/unit/deployment/test_language_container_deployer_cli.py b/test/unit/deployment/test_language_container_deployer_cli.py new file mode 100644 index 0000000..dcaf837 --- /dev/null +++ b/test/unit/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' From fd32726b7613f922e97c3a81ac38c2295f664b92 Mon Sep 17 00:00:00 2001 From: mibe Date: Thu, 25 Jun 2026 15:41:57 +0100 Subject: [PATCH 2/3] Added support for Python3.14 --- .github/workflows/checks.yml | 6 +- .github/workflows/fast-tests.yml | 2 +- doc/changes/changelog.md | 2 + doc/changes/changes_1.0.0.md | 15 +++ noxconfig.py | 2 +- poetry.lock | 191 +++++++++++++++++++------------ pyproject.toml | 5 +- 7 files changed, 140 insertions(+), 83 deletions(-) create mode 100644 doc/changes/changes_1.0.0.md diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 701bc9a..c11540e 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -61,7 +61,7 @@ jobs: strategy: fail-fast: false matrix: - python-versions: ["3.10", "3.11", "3.12", "3.13"] + python-versions: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Check out Repository id: check-out-repository @@ -96,7 +96,7 @@ jobs: strategy: fail-fast: false matrix: - python-versions: ["3.10", "3.11", "3.12", "3.13"] + python-versions: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Check out Repository @@ -122,7 +122,7 @@ jobs: strategy: fail-fast: false matrix: - python-versions: ["3.10", "3.11", "3.12", "3.13"] + python-versions: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Check out Repository diff --git a/.github/workflows/fast-tests.yml b/.github/workflows/fast-tests.yml index 647414d..13fa06f 100644 --- a/.github/workflows/fast-tests.yml +++ b/.github/workflows/fast-tests.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - python-versions: ["3.10", "3.11", "3.12", "3.13"] + python-versions: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Check out Repository diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md index dc68d80..ff7a87d 100644 --- a/doc/changes/changelog.md +++ b/doc/changes/changelog.md @@ -1,6 +1,7 @@ # 📝 Changes * [unreleased](unreleased.md) +* [1.0.0](changes_1.0.0.md) * [0.16.0](changes_0.16.0.md) * [0.15.0](changes_0.15.0.md) * [0.14.0](changes_0.14.0.md) @@ -26,6 +27,7 @@ hidden: --- unreleased +changes_1.0.0 changes_0.16.0 changes_0.15.0 changes_0.14.0 diff --git a/doc/changes/changes_1.0.0.md b/doc/changes/changes_1.0.0.md new file mode 100644 index 0000000..35cd03f --- /dev/null +++ b/doc/changes/changes_1.0.0.md @@ -0,0 +1,15 @@ +# 1.0.0 - 2026-06-25 + +## Summary + +Added support for Python3.14 + +## Dependencies + +* #163: Added support for Python3.14 + +## Dependency Updates + +### `dev` + +* Updated dependency `pytest-exasol-backend:1.4.1` to `1.5.0` diff --git a/noxconfig.py b/noxconfig.py index 07b8efd..5f8960d 100644 --- a/noxconfig.py +++ b/noxconfig.py @@ -5,5 +5,5 @@ PROJECT_CONFIG = BaseConfig( root_path=Path(__file__).parent, project_name="python_extension_common", - python_versions=("3.10", "3.11", "3.12", "3.13"), + python_versions=("3.10", "3.11", "3.12", "3.13", "3.14"), ) diff --git a/poetry.lock b/poetry.lock index a2e2f00..b28a7cd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1127,6 +1127,25 @@ typing-extensions = ">=3.7.4.1" all = ["pytz (>=2019.1)"] dates = ["pytz (>=2019.1)"] +[[package]] +name = "exasol-bucketfs" +version = "2.1.0" +description = "BucketFS utilities for the Python programming language" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main", "dev"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "exasol_bucketfs-2.1.0-py3-none-any.whl", hash = "sha256:82d0f87d756921846ce60b7af112a71a491019aa46976693046399805c303a12"}, + {file = "exasol_bucketfs-2.1.0.tar.gz", hash = "sha256:55583af02e8a65b40bb15bc462e8bcdf1c0195e00f8c1e70148724869fb6da29"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +exasol-saas-api = ">2.0.0,<3.0" +httpx = ">=0.27.0" +requests = ">=2.32.4" + [[package]] name = "exasol-bucketfs" version = "2.2.0" @@ -1134,6 +1153,7 @@ description = "BucketFS utilities for the Python programming language" optional = false python-versions = "<3.14,>=3.10" groups = ["main", "dev"] +markers = "python_version < \"3.12\"" files = [ {file = "exasol_bucketfs-2.2.0-py3-none-any.whl", hash = "sha256:cc1e1331467f64b951ee484fa0e51ba40d1e28bd7963cd09a021b793ba66531c"}, {file = "exasol_bucketfs-2.2.0.tar.gz", hash = "sha256:a66fe0e80756e034adc665dc1bc7f9c2115ecbb4d83ffd214099c35c6c62e539"}, @@ -1156,6 +1176,39 @@ files = [ ] markers = {main = "extra == \"slc-tool\""} +[[package]] +name = "exasol-integration-test-docker-environment" +version = "4.4.1" +description = "Integration Test Docker Environment for Exasol" +optional = false +python-versions = "<3.15,>=3.10" +groups = ["main", "dev"] +files = [ + {file = "exasol_integration_test_docker_environment-4.4.1-py3-none-any.whl", hash = "sha256:1aaaac92ca0165435e617394d3ad2192b629a459356f227ac8ef6cf22bdd9863"}, + {file = "exasol_integration_test_docker_environment-4.4.1.tar.gz", hash = "sha256:0f2cd9247189fdacfd54ab43653d97496b103a03fc7431499c80a38b7b4b315c"}, +] +markers = {main = "python_version >= \"3.12\" and extra == \"slc-tool\"", dev = "python_version >= \"3.12\""} + +[package.dependencies] +click = ">=7.0" +docker = {version = ">=4.0.0,<7.0.0 || >7.0.0", markers = "sys_platform != \"win32\""} +docutils = ">=0.21.2" +exasol-error-reporting = ">=1.0.0,<2.0.0" +fabric = ">=3.0.1,<4.0.0" +gitpython = ">=2.1.0" +humanfriendly = ">=4.18" +importlib_resources = ">=5.4.0" +jinja2 = ">=2.10.1" +jsonpickle = ">=1.1" +luigi = ">=2.8.4" +netaddr = ">=0.7.19" +networkx = ">=2.3" +portalocker = ">=2.7.0,<3.0.0" +pydot = ">=1.4.0" +requests = ">=2.32.4" +simplejson = ">=3.16.0" +"stopwatch.py" = ">=1.0.0" + [[package]] name = "exasol-integration-test-docker-environment" version = "6.2.0" @@ -1167,7 +1220,7 @@ files = [ {file = "exasol_integration_test_docker_environment-6.2.0-py3-none-any.whl", hash = "sha256:6bae833b3b304f2fdb5bbbda70438840583a086b9040f380a799e322718c34bc"}, {file = "exasol_integration_test_docker_environment-6.2.0.tar.gz", hash = "sha256:690960ce28768c371725a394d0b03c1f768cd656c3f3670bc75bce9d10c013f0"}, ] -markers = {main = "extra == \"slc-tool\""} +markers = {main = "python_version < \"3.12\" and extra == \"slc-tool\"", dev = "python_version < \"3.12\""} [package.dependencies] click = ">=7.0" @@ -1209,6 +1262,28 @@ requests = ">=2.32.4,<3.0.0" tenacity = ">=8,<10" types-requests = ">=2.31.0.6,<3.0.0.0" +[[package]] +name = "exasol-script-languages-container-tool" +version = "3.4.1" +description = "Script Languages Container Tool" +optional = false +python-versions = "<4,>=3.10" +groups = ["main", "dev"] +files = [ + {file = "exasol_script_languages_container_tool-3.4.1-py3-none-any.whl", hash = "sha256:d0fabc5a8bfab38c2ad6e13309ebce813051f5e4a13ecfd6e50775bffb733af4"}, + {file = "exasol_script_languages_container_tool-3.4.1.tar.gz", hash = "sha256:43264a85f2dbc6455993923583a0b73a58c5fb7e987852779fd83d89e3c63e2b"}, +] +markers = {main = "python_version >= \"3.12\" and extra == \"slc-tool\"", dev = "python_version >= \"3.12\""} + +[package.dependencies] +exasol-bucketfs = ">=1,<3" +exasol-integration-test-docker-environment = ">=4.1.0,<5.0.0" +importlib_metadata = ">=4.6.0" +importlib-resources = ">=6.4.0" +jsonschema = ">=4.23.0,<5.0.0" +networkx = ">=3.3.0,<4.0.0" +pydantic = ">=2.10.2,<3.0.0" + [[package]] name = "exasol-script-languages-container-tool" version = "4.1.0" @@ -1220,7 +1295,7 @@ files = [ {file = "exasol_script_languages_container_tool-4.1.0-py3-none-any.whl", hash = "sha256:67be6fe190144983b8e1c9f55e850364895b70d5aa770ee8c22411159bcf3d15"}, {file = "exasol_script_languages_container_tool-4.1.0.tar.gz", hash = "sha256:cca58883d576251ea4e5fc7b1eeaf134315cc76d91ada6c646f31c2fd8971bc9"}, ] -markers = {main = "extra == \"slc-tool\""} +markers = {main = "python_version < \"3.12\" and extra == \"slc-tool\"", dev = "python_version < \"3.12\""} [package.dependencies] click = ">=8.2.1,<9" @@ -1244,7 +1319,7 @@ files = [ {file = "exasol_script_languages_package_management-1.3.1-py3-none-any.whl", hash = "sha256:f8a38c8f024dfcb229c08d1de34894d0ab04057d4e513803a510a40531baa59a"}, {file = "exasol_script_languages_package_management-1.3.1.tar.gz", hash = "sha256:833ed226880474aa629b720acc1cc42ca7cc80b464da44b51fd38424ebfdc68b"}, ] -markers = {main = "extra == \"slc-tool\""} +markers = {main = "python_version < \"3.12\" and extra == \"slc-tool\"", dev = "python_version < \"3.12\""} [package.dependencies] click = ">=8.3.2,<9.0.0" @@ -1739,12 +1814,12 @@ version = "9.0.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +groups = ["main", "dev"] files = [ {file = "importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7"}, {file = "importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc"}, ] +markers = {main = "python_version >= \"3.12\" and extra == \"slc-tool\"", dev = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" or python_version >= \"3.12\""} [package.dependencies] zipp = ">=3.20" @@ -1764,11 +1839,12 @@ version = "7.1.0" description = "Read resources from Python packages" optional = false python-versions = ">=3.10" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1"}, {file = "importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708"}, ] +markers = {main = "python_version >= \"3.12\" and extra == \"slc-tool\""} [package.extras] check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] @@ -2592,7 +2668,7 @@ files = [ {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, ] -markers = {main = "python_version == \"3.10\" and extra == \"slc-tool\"", dev = "python_version == \"3.10\""} +markers = {main = "(python_version == \"3.10\" or python_version >= \"3.12\") and extra == \"slc-tool\"", dev = "python_version == \"3.10\" or python_version >= \"3.12\""} [package.extras] default = ["matplotlib (>=3.7)", "numpy (>=1.24)", "pandas (>=2.0)", "scipy (>=1.10,!=1.11.0,!=1.11.1)"] @@ -2613,7 +2689,7 @@ files = [ {file = "networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762"}, {file = "networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509"}, ] -markers = {main = "python_version >= \"3.11\" and extra == \"slc-tool\"", dev = "python_version >= \"3.11\""} +markers = {main = "python_version == \"3.11\" and extra == \"slc-tool\"", dev = "python_version == \"3.11\""} [package.extras] benchmarking = ["asv", "virtualenv"] @@ -2851,61 +2927,6 @@ files = [ ] markers = {main = "python_version == \"3.11\" and extra == \"slc-tool\"", dev = "python_version == \"3.11\""} -[[package]] -name = "numpy" -version = "2.5.0" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.12" -groups = ["main", "dev"] -files = [ - {file = "numpy-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:489780423903667933b4ed6197b6ec3b75ea5dd17d1d8f0f38d798feb6921561"}, - {file = "numpy-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ece55976ced6bca95a03ae2839e2e5ccffe8eb6a3e7022415645eb154a81e4e6"}, - {file = "numpy-2.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c83b664b0e6eee9594fa920cf0639d8af796606d3fad6cc70180c87e4b97c7be"}, - {file = "numpy-2.5.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bf80333980bf37f523341ddd72c783f39d6829ec7736b9eb99086388a2d52cc2"}, - {file = "numpy-2.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a4874217b36d5ac8fc876f52e39df56f8182c88463e9e2dceabf7ca8b7efb8"}, - {file = "numpy-2.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaa760137137e8d3c920d27927748215b56014f92667dc9b6c27dfc61249255a"}, - {file = "numpy-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7174ce8265fc7f7417d171c9ea8fe905220748893ea67a2a7abe726ec331c4b0"}, - {file = "numpy-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b8c3daaf99de52415d20b42f8e8155c78642cb04207d02f9d317a0dcf1b3fb54"}, - {file = "numpy-2.5.0-cp312-cp312-win32.whl", hash = "sha256:6206db0af545d73d068add6d992279145f158428d1da6cc49adc4b630c5d6ee5"}, - {file = "numpy-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:6f2d6873e2940c860a309d21e25b1e69af6aaffdd80aa056b04c16380db1c4f2"}, - {file = "numpy-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:a55e1eb2bca2cfd17a16b213c99dfc8502d47b0d494224d2122277d0400935ca"}, - {file = "numpy-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:520e6b8be0a4b65840ac8090d4f51cef4bed66e2b0894d5a520f099adc24a9b2"}, - {file = "numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:146b81cdd3967fdb6beca8ba25f00c58741d8f3cbd797f55af0fbe0bfec3469c"}, - {file = "numpy-2.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:126b88d95e8ff9b00c9e717aa540469f21d6180162f84c0caec51b16215d49cd"}, - {file = "numpy-2.5.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d4313cef1594c5ce46c31b6e54e918338f63f16ee9322304e8c9114d6d81c8bd"}, - {file = "numpy-2.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:750fb097caf26fa878746d9d119f6f9da12dedcbff1eea966c3e3447647c4a9e"}, - {file = "numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3893adc2dc7c0412ba76777db55a049215d99c9aa3113003be8f49f4f1290ab9"}, - {file = "numpy-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:835e454dd99b238cdc5a3f63bce2371296f5ebc53ca1e0f8e6ddbb6d92a29aab"}, - {file = "numpy-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f9836778081a0a3c02a6a21493f3e9f5b311f8d2541934f31f05583dc999ea4"}, - {file = "numpy-2.5.0-cp313-cp313-win32.whl", hash = "sha256:0b525be4744b60bb0557ac872d53ef07d085b5f39622bc579c98d3809d05b988"}, - {file = "numpy-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:44353e2878930039db472b99dc353d749826e4010bd4d2a7f835e94a97a5c748"}, - {file = "numpy-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:48f54b00711f83a5f796b70c518e8c2b3c5848dda03a54911f23eb68519b9b60"}, - {file = "numpy-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f27582c55ba4c750b7c58c8faf021d2cd9324a662b466229db8a417b41368af9"}, - {file = "numpy-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:28e7137057d551e4a83c4ae414e3451f50568409db7569aacc7f9811ee06a446"}, - {file = "numpy-2.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e1da54b53e75cd9fcfc23efcc7edab2c6aecf97b6037566d8a0fe804af8ec57c"}, - {file = "numpy-2.5.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:694d8f74e156f7fd01179f1aa8faa2f648ab6ae0f70b6c3fe57a03249aea2303"}, - {file = "numpy-2.5.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a7569a7b53c77716f036bb28cb1c91f166a26ec7d9502cd1e4bdfe502fdec22"}, - {file = "numpy-2.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a0433bd4086ebd462960cf375e19195bb07b53dc1d87dd5fcf47ad78576f03"}, - {file = "numpy-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:929f0c79ac38bcbd7154fe631dc907abfeddbcc5027a896bd1f7767323271e7a"}, - {file = "numpy-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cc4f247a47bbf070bfd70be53ccdcf47b800af563535e7bbe172322197c30e21"}, - {file = "numpy-2.5.0-cp314-cp314-win32.whl", hash = "sha256:5dc71423499fab3f46f7a7201155ade1669ea101f2f429d332df9e72f8161731"}, - {file = "numpy-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ebb81d9d5443e0309d6c54894c3fbed74ad7da0714352a67b6d773cd189eae73"}, - {file = "numpy-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3b94d0d0deceebfad3e67ae5c0e5eb87371e8f7a0581cd04a779928c2450cf1e"}, - {file = "numpy-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:22f3d43e362d650bc39db1f17851302874a148ca95ba6981c1dfb5fa6862f35b"}, - {file = "numpy-2.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:243563efb4cd7528a264567e9fd206c87826457322521d06206a00bfa316c927"}, - {file = "numpy-2.5.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:84881d825ca75249b189bbee875fcfe3238aa5c479e6100893cda566e8e86826"}, - {file = "numpy-2.5.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cda12aa4779d42b8771180aba759c96f527d43446d8f380ab59e2b35e8489efd"}, - {file = "numpy-2.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c0121101093d2bd74981b10f8837d78e794a8ff57834eb27179f49e1ba11ac6"}, - {file = "numpy-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d371c92cfa09da00022f501ab67fafaea813d752eb30ac44336d45b1e5b0268a"}, - {file = "numpy-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9990713e9c38154c6861e7547f1e3fc7a87e75ff09bab24ef1cc81d81c2835e9"}, - {file = "numpy-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:edadfbd4794b1086c0d822f81863e8a68fc129d132fd0bb9e31e955d7fbbbdb7"}, - {file = "numpy-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f7e5fa4382967ae6548bd2f174219afb908e294b0d5f625af01166edd5f7d9aa"}, - {file = "numpy-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:016623417bb330d719d579daf2d6b9a01ddc52e41a9ed61a47f39fde46dcd865"}, - {file = "numpy-2.5.0.tar.gz", hash = "sha256:5a129578019311b6e56bdd714250f19b518f7dceeeb8d1af5490f4942d3f891c"}, -] -markers = {main = "python_version >= \"3.12\" and extra == \"slc-tool\"", dev = "python_version >= \"3.12\""} - [[package]] name = "packageurl-python" version = "0.17.6" @@ -3000,13 +3021,12 @@ files = [ {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, ] -markers = {main = "extra == \"slc-tool\""} +markers = {main = "python_version < \"3.12\" and extra == \"slc-tool\"", dev = "python_version < \"3.12\""} [package.dependencies] numpy = [ {version = ">=1.22.4", markers = "python_version < \"3.11\""}, {version = ">=1.23.2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -3675,6 +3695,7 @@ description = "" optional = false python-versions = "<3.14,>=3.10" groups = ["dev"] +markers = "python_version < \"3.12\"" files = [ {file = "pytest_exasol_backend-1.4.1-py3-none-any.whl", hash = "sha256:208b5f6e3da31105a8e692c6aaa04775d3f2d29e3985f69092e2ed805c3285ca"}, {file = "pytest_exasol_backend-1.4.1.tar.gz", hash = "sha256:c89af2b959377cc2ac6fda81b6d433ecc66e9e9b6608f2a0048f3f1890223470"}, @@ -3685,6 +3706,24 @@ exasol-integration-test-docker-environment = ">=4.3,<7" exasol-saas-api = ">=2.8,<3" pytest = ">=7,<10" +[[package]] +name = "pytest-exasol-backend" +version = "1.5.0" +description = "" +optional = false +python-versions = "<3.15,>=3.10" +groups = ["dev"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "pytest_exasol_backend-1.5.0-py3-none-any.whl", hash = "sha256:5dcf9acffbb16f5fc6cab9886749a46574023dce47410ee1f91c8f1f14c9f111"}, + {file = "pytest_exasol_backend-1.5.0.tar.gz", hash = "sha256:fa8b46be7b112ea6f8cce31f7e2680c8eab6ec75605eb928bb951a80cb369d32"}, +] + +[package.dependencies] +exasol-integration-test-docker-environment = ">=4.3,<7" +exasol-saas-api = ">=2.8,<3" +pytest = ">=9,<10" + [[package]] name = "python-daemon" version = "3.1.2" @@ -3809,7 +3848,7 @@ files = [ {file = "pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126"}, {file = "pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a"}, ] -markers = {main = "extra == \"slc-tool\""} +markers = {main = "python_version < \"3.12\" and extra == \"slc-tool\"", dev = "python_version < \"3.12\""} [[package]] name = "pyupgrade" @@ -3953,7 +3992,7 @@ files = [ {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] -markers = {main = "extra == \"slc-tool\""} +markers = {main = "python_version < \"3.12\" and extra == \"slc-tool\""} [[package]] name = "readme-renderer" @@ -4369,7 +4408,7 @@ description = "C version of reader, parser and emitter for ruamel.yaml derived f optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "platform_python_implementation == \"CPython\"" +markers = "python_version < \"3.14\" and platform_python_implementation == \"CPython\"" files = [ {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03"}, {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77"}, @@ -5084,7 +5123,7 @@ description = "Standard library imghdr redistribution. \"dead battery\"." optional = false python-versions = "*" groups = ["dev"] -markers = "python_version == \"3.13\"" +markers = "python_version >= \"3.13\"" files = [ {file = "standard_imghdr-3.10.14-py3-none-any.whl", hash = "sha256:cdf6883163349624dee9a81d2853a20260337c4cd41c04e99c082e01833a08e2"}, {file = "standard_imghdr-3.10.14.tar.gz", hash = "sha256:2598fe2e7c540dbda34b233295e10957ab8dc8ac6f3bd9eaa8d38be167232e52"}, @@ -5141,7 +5180,7 @@ files = [ {file = "tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3"}, {file = "tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d"}, ] -markers = {main = "extra == \"slc-tool\""} +markers = {main = "python_version < \"3.12\" and extra == \"slc-tool\""} [package.extras] widechars = ["wcwidth"] @@ -5350,7 +5389,7 @@ files = [ {file = "types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd"}, {file = "types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466"}, ] -markers = {main = "extra == \"slc-tool\""} +markers = {main = "python_version < \"3.12\" and extra == \"slc-tool\"", dev = "python_version < \"3.12\""} [[package]] name = "types-requests" @@ -5407,7 +5446,7 @@ files = [ {file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"}, {file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"}, ] -markers = {main = "extra == \"slc-tool\""} +markers = {main = "python_version < \"3.12\" and extra == \"slc-tool\"", dev = "python_version < \"3.12\""} [[package]] name = "urllib3" @@ -5597,12 +5636,12 @@ version = "4.1.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.10" -groups = ["dev"] -markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +groups = ["main", "dev"] files = [ {file = "zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f"}, {file = "zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602"}, ] +markers = {main = "python_version >= \"3.12\" and extra == \"slc-tool\"", dev = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" or python_version >= \"3.12\""} [package.extras] check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] @@ -5617,5 +5656,5 @@ slc-tool = ["exasol-script-languages-container-tool"] [metadata] lock-version = "2.1" -python-versions = ">=3.10,<3.14" -content-hash = "1a34e2c12b159711550cd3fe1642f1e523c9a3a1217c76abd5efc94d5d4c7f76" +python-versions = ">=3.10,<3.15" +content-hash = "a522cae9a5c050c2781d40de45e367d890f96ea0c0e7f0ef482e36b67cf7c0ef" diff --git a/pyproject.toml b/pyproject.toml index 8e526d6..db5d556 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "exasol-python-extension-common" -version = "0.16.0" +version = "1.0.0" description = "A collection of common utilities for Exasol extensions." authors = [{ name = "Mikhail Beck", email = "mikhail.beck@exasol.com" }] -requires-python = ">=3.10,<3.14" +requires-python = ">=3.10,<3.15" readme = "README.md" license = "MIT" classifiers = [ @@ -12,6 +12,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = [ "pyexasol>=0.25.0, <3", From ed3d938f75c420c6f439bcbc0aaaf78a745b9f1b Mon Sep 17 00:00:00 2001 From: mibe Date: Thu, 25 Jun 2026 15:46:46 +0100 Subject: [PATCH 3/3] Merged changes from main --- poetry.lock | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/poetry.lock b/poetry.lock index b28a7cd..0c54772 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1330,14 +1330,14 @@ types-pyyaml = ">=6.0.12.20260408,<7.0.0.0" [[package]] name = "exasol-toolbox" -version = "8.2.0" +version = "10.0.0" description = "Your one-stop solution for managing all standard tasks and core workflows of your Python project." optional = false python-versions = "<4.0,>=3.10" groups = ["dev"] files = [ - {file = "exasol_toolbox-8.2.0-py3-none-any.whl", hash = "sha256:27fb716e04c76d25d347904490b077bbf92d1f40192a6ebdbeed87ba8d19f1dd"}, - {file = "exasol_toolbox-8.2.0.tar.gz", hash = "sha256:a15cba09b1390a92ad28ae22449a16e3f7cd4612830ef6f4261f858d47d73256"}, + {file = "exasol_toolbox-10.0.0-py3-none-any.whl", hash = "sha256:98c0456e7526ccffdfe74c727e74b706f9e613a0e42d6fffa8a0cbd2412299b8"}, + {file = "exasol_toolbox-10.0.0.tar.gz", hash = "sha256:c2002f4cf7e8f94c9c72289a4acb98bfe6fa123942f488ecb5c84d82e92dd770"}, ] [package.dependencies] @@ -1373,6 +1373,7 @@ sphinxcontrib-mermaid = ">=2.0.0,<3.0.0" structlog = ">=25.5.0,<26.0.0" twine = ">=6.1.0,<7" typer = {version = ">=0.7.0", extras = ["all"]} +zizmor = ">=1.25.2,<2.0.0" [[package]] name = "exceptiongroup" @@ -3688,24 +3689,6 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytest-exasol-backend" -version = "1.4.1" -description = "" -optional = false -python-versions = "<3.14,>=3.10" -groups = ["dev"] -markers = "python_version < \"3.12\"" -files = [ - {file = "pytest_exasol_backend-1.4.1-py3-none-any.whl", hash = "sha256:208b5f6e3da31105a8e692c6aaa04775d3f2d29e3985f69092e2ed805c3285ca"}, - {file = "pytest_exasol_backend-1.4.1.tar.gz", hash = "sha256:c89af2b959377cc2ac6fda81b6d433ecc66e9e9b6608f2a0048f3f1890223470"}, -] - -[package.dependencies] -exasol-integration-test-docker-environment = ">=4.3,<7" -exasol-saas-api = ">=2.8,<3" -pytest = ">=7,<10" - [[package]] name = "pytest-exasol-backend" version = "1.5.0" @@ -3713,7 +3696,6 @@ description = "" optional = false python-versions = "<3.15,>=3.10" groups = ["dev"] -markers = "python_version >= \"3.12\"" files = [ {file = "pytest_exasol_backend-1.5.0-py3-none-any.whl", hash = "sha256:5dcf9acffbb16f5fc6cab9886749a46574023dce47410ee1f91c8f1f14c9f111"}, {file = "pytest_exasol_backend-1.5.0.tar.gz", hash = "sha256:fa8b46be7b112ea6f8cce31f7e2680c8eab6ec75605eb928bb951a80cb369d32"}, @@ -5651,10 +5633,31 @@ enabler = ["pytest-enabler (>=3.4)"] test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] +[[package]] +name = "zizmor" +version = "1.26.1" +description = "Static analysis for GitHub Actions" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "zizmor-1.26.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7ea21ca959c8e888de238fee81d73a1fdf89a82067eac75b8f1acdbd23e2eeaf"}, + {file = "zizmor-1.26.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:78083b495593f8b0b9dec14036a0836a5afcddda8a40738336ff4e399476b741"}, + {file = "zizmor-1.26.1-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:bb7ebbe565a3742eb49a590352127ad549bb122b9b4ff9424ebab7525fa3b6b6"}, + {file = "zizmor-1.26.1-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:d3049010b6bd6f849413b6d20c28e0c677b90e0a5b2bc73cbee7f7bd86dc5828"}, + {file = "zizmor-1.26.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6a958d8a0941d7e1d0de8436670b5cb7fc64c8028b4d16e3f519ccc77f953cef"}, + {file = "zizmor-1.26.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d2744cdf944436ca7a009ae8b626a017a40381ec990216abd6cf6b8beb23323a"}, + {file = "zizmor-1.26.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:44099f426af9da750ff9f548a0084e11d7d83e0158fe1a2778672398d728efdd"}, + {file = "zizmor-1.26.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8313cc264dec792f00a7328eb7c8e89e7d62d54f950fc897d1e6a5a6e5762203"}, + {file = "zizmor-1.26.1-py3-none-win32.whl", hash = "sha256:c96d7787d69fb298eae939e00dfdf7f534d7dfbd9cc17ab442c0650a56851415"}, + {file = "zizmor-1.26.1-py3-none-win_amd64.whl", hash = "sha256:0a05acf6068609fb6df3b137276cf18a686226a1e0e207941cb34a85929f16cf"}, + {file = "zizmor-1.26.1.tar.gz", hash = "sha256:0c2cc575007a4db99d89d5acc6120cfa7b61504bc2394c3b50af348c73f1916e"}, +] + [extras] slc-tool = ["exasol-script-languages-container-tool"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.15" -content-hash = "a522cae9a5c050c2781d40de45e367d890f96ea0c0e7f0ef482e36b67cf7c0ef" +content-hash = "85954a9c44f75dfd2da9c508241c7210a2fbc9d91192fe56e69b1022ccf47ef9"