Repository: exasol/integration-test-docker-environment (language: python)

Issue:
Move implementations of all click commands in separate methods
## Background

Similar to https://github.com/exasol/script-languages-container-tool/pull/162 we need to separate click commands and implementation. This provides an access to ITDE functions from other Python libraries (and not only via CLI as now)

## Acceptance Criteria

Add a new API layer  
Move implementations of all click commands in separate methods
## Background

Similar to https://github.com/exasol/script-languages-container-tool/pull/162 we need to separate click commands and implementation. This provides an access to ITDE functions from other Python libraries (and not only via CLI as now)

## Acceptance Criteria

Add a new API layer  


Discussion / hints:

Fix patch:
diff --git a/doc/changes/changes_0.12.0.md b/doc/changes/changes_0.12.0.md
index 325c301e1..e0441a592 100644
--- a/doc/changes/changes_0.12.0.md
+++ b/doc/changes/changes_0.12.0.md
@@ -28,7 +28,8 @@ TBD
 - #223:  Updated docker-db versions
 
 ## Refactoring:
-- TBD
+
+- #235: Moved implementations of all click commands in separate methods
 
 ## Documentation:
 - TBD
diff --git a/setup.py b/setup.py
index 62443b10f..5ede934a7 100644
--- a/setup.py
+++ b/setup.py
@@ -7,6 +7,7 @@
  'exasol_integration_test_docker_environment.cli.commands',
  'exasol_integration_test_docker_environment.cli.options',
  'exasol_integration_test_docker_environment.lib',
+ 'exasol_integration_test_docker_environment.lib.api',
  'exasol_integration_test_docker_environment.lib.base',
  'exasol_integration_test_docker_environment.lib.config',
  'exasol_integration_test_docker_environment.lib.data',


Test patch:
diff --git a/exasol_integration_test_docker_environment/cli/commands/__init__.py b/exasol_integration_test_docker_environment/cli/commands/__init__.py
index e69de29bb..3a8eeed7a 100644
--- a/exasol_integration_test_docker_environment/cli/commands/__init__.py
+++ b/exasol_integration_test_docker_environment/cli/commands/__init__.py
@@ -0,0 +1,4 @@
+from .build_test_container import build_test_container
+from .health import health
+from .push_test_container import push_test_container
+from .spawn_test_environment import spawn_test_environment
diff --git a/exasol_integration_test_docker_environment/cli/commands/build_test_container.py b/exasol_integration_test_docker_environment/cli/commands/build_test_container.py
index 6396ff77e..75c0af289 100644
--- a/exasol_integration_test_docker_environment/cli/commands/build_test_container.py
+++ b/exasol_integration_test_docker_environment/cli/commands/build_test_container.py
@@ -1,13 +1,11 @@
-from typing import Tuple
+from typing import Tuple, Optional
 
 from exasol_integration_test_docker_environment.cli.cli import cli
-from exasol_integration_test_docker_environment.cli.common import add_options, set_build_config, \
-    set_docker_repository_config, run_task, generate_root_task
+from exasol_integration_test_docker_environment.cli.common import add_options
 from exasol_integration_test_docker_environment.cli.options.build_options import build_options
 from exasol_integration_test_docker_environment.cli.options.docker_repository_options import docker_repository_options
 from exasol_integration_test_docker_environment.cli.options.system_options import system_options
-from exasol_integration_test_docker_environment.lib.test_environment.analyze_test_container import AnalyzeTestContainer, \
-    DockerTestContainerBuild
+from exasol_integration_test_docker_environment.lib import api
 
 
 @cli.command()
@@ -21,39 +19,41 @@ def build_test_container(
         output_directory: str,
         temporary_base_directory: str,
         log_build_context_content: bool,
-        cache_directory: str,
-        build_name: str,
+        cache_directory: Optional[str],
+        build_name: Optional[str],
         source_docker_repository_name: str,
         source_docker_tag_prefix: str,
-        source_docker_username: str,
-        source_docker_password: str,
+        source_docker_username: Optional[str],
+        source_docker_password: Optional[str],
         target_docker_repository_name: str,
         target_docker_tag_prefix: str,
-        target_docker_username: str,
-        target_docker_password: str,
+        target_docker_username: Optional[str],
+        target_docker_password: Optional[str],
         workers: int,
-        task_dependencies_dot_file: str):
+        task_dependencies_dot_file: Optional[str]):
     """
     This command builds all stages of the test container for the test environment.
     If stages are cached in a docker registry, they command is going to pull them,
     instead of building them.
-    """
-    set_build_config(force_rebuild,
-                     force_rebuild_from,
-                     force_pull,
-                     log_build_context_content,
-                     output_directory,
-                     temporary_base_directory,
-                     cache_directory,
-                     build_name)
-    # Use AnalyzeTestContainer to ensure that all luigi processes got it loaded
-    analyze_task = AnalyzeTestContainer.__class__.__name__
 
-    set_docker_repository_config(source_docker_password, source_docker_repository_name, source_docker_username,
-                                 source_docker_tag_prefix, "source")
-    set_docker_repository_config(target_docker_password, target_docker_repository_name, target_docker_username,
-                                 target_docker_tag_prefix, "target")
-    task_creator = lambda: generate_root_task(task_class=DockerTestContainerBuild)
-    success, task = run_task(task_creator, workers, task_dependencies_dot_file)
+    """
+    success = api.build_test_container(force_rebuild,
+                                       force_rebuild_from,
+                                       force_pull,
+                                       output_directory,
+                                       temporary_base_directory,
+                                       log_build_context_content,
+                                       cache_directory,
+                                       build_name,
+                                       source_docker_repository_name,
+                                       source_docker_tag_prefix,
+                                       source_docker_username,
+                                       source_docker_password,
+                                       target_docker_repository_name,
+                                       target_docker_tag_prefix,
+                                       target_docker_username,
+                                       target_docker_password,
+                                       workers,
+                                       task_dependencies_dot_file)
     if not success:
         exit(1)
diff --git a/exasol_integration_test_docker_environment/cli/commands/health.py b/exasol_integration_test_docker_environment/cli/commands/health.py
index 6fdd6a1fb..885c0a29e 100644
--- a/exasol_integration_test_docker_environment/cli/commands/health.py
+++ b/exasol_integration_test_docker_environment/cli/commands/health.py
@@ -1,11 +1,8 @@
 import sys
-from inspect import cleandoc
 
 from exasol_integration_test_docker_environment.cli.cli import cli
-from exasol_integration_test_docker_environment.doctor import (
-    health_checkup,
-    recommend_mitigation,
-)
+from exasol_integration_test_docker_environment.lib import api
+from exasol_integration_test_docker_environment.lib.api.api_errors import HealthProblem
 
 
 @cli.command()
@@ -16,36 +13,12 @@ def health():
     If no issues have been found, using the library or executing the test should work just fine.
     For all found issues there will be a proposed fix/solution.
 
-    If the environment was found to be healthy the exit code will be 0.
+    If the environment was found to be healthy the exit code will be 0, otherwise -1.
     """
     success, failure = 0, -1
-
-    problems = set(health_checkup())
-    if not problems:
+    try:
+        api.health()
         sys.exit(success)
-
-    suggestion_template = cleandoc(
-        """
-        * {problem}
-          Fix: {suggestion}
-        """
-    )
-    message = cleandoc(
-        """
-        {count} problem(s) have been identified.
-        
-        {problems}
-        """
-    ).format(
-        count=len(problems),
-        problems="\n".join(
-            (
-                suggestion_template.format(
-                    problem=icd.value, suggestion=recommend_mitigation(icd)
-                )
-                for icd in problems
-            )
-        ),
-    )
-    print(message)
-    sys.exit(failure)
+    except HealthProblem as e:
+        print(e.args[0])
+        sys.exit(failure)
diff --git a/exasol_integration_test_docker_environment/cli/commands/push_test_container.py b/exasol_integration_test_docker_environment/cli/commands/push_test_container.py
index ff78b6409..d98056943 100644
--- a/exasol_integration_test_docker_environment/cli/commands/push_test_container.py
+++ b/exasol_integration_test_docker_environment/cli/commands/push_test_container.py
@@ -1,14 +1,12 @@
-from typing import Tuple
+from typing import Tuple, Optional
 
 from exasol_integration_test_docker_environment.cli.cli import cli
-from exasol_integration_test_docker_environment.cli.common import add_options, set_docker_repository_config, \
-    run_task, set_build_config, generate_root_task
+from exasol_integration_test_docker_environment.cli.common import add_options
 from exasol_integration_test_docker_environment.cli.options.build_options import build_options
 from exasol_integration_test_docker_environment.cli.options.docker_repository_options import docker_repository_options
 from exasol_integration_test_docker_environment.cli.options.push_options import push_options
 from exasol_integration_test_docker_environment.cli.options.system_options import system_options
-from exasol_integration_test_docker_environment.lib.test_environment.analyze_test_container import AnalyzeTestContainer, \
-    DockerTestContainerPush
+from exasol_integration_test_docker_environment.lib import api
 
 
 @cli.command()
@@ -25,40 +23,41 @@ def push_test_container(
         output_directory: str,
         temporary_base_directory: str,
         log_build_context_content: bool,
-        cache_directory: str,
-        build_name: str,
+        cache_directory: Optional[str],
+        build_name: Optional[str],
         source_docker_repository_name: str,
         source_docker_tag_prefix: str,
-        source_docker_username: str,
-        source_docker_password: str,
+        source_docker_username: Optional[str],
+        source_docker_password: Optional[str],
         target_docker_repository_name: str,
         target_docker_tag_prefix: str,
-        target_docker_username: str,
-        target_docker_password: str,
+        target_docker_username: Optional[str],
+        target_docker_password: Optional[str],
         workers: int,
-        task_dependencies_dot_file: str):
+        task_dependencies_dot_file: Optional[str]):
     """
     This command pushs all stages of the test container for the test environment.
     If the stages do not exists locally, the system will build or pull them before the push.
     """
-    set_build_config(force_rebuild,
-                     force_rebuild_from,
-                     force_pull,
-                     log_build_context_content,
-                     output_directory,
-                     temporary_base_directory,
-                     cache_directory,
-                     build_name)
-    # Use AnalyzeTestContainer to ensure that all luigi processes got it loaded
-    analyze_task = AnalyzeTestContainer.__class__.__name__
-
-    set_docker_repository_config(source_docker_password, source_docker_repository_name, source_docker_username,
-                                 source_docker_tag_prefix, "source")
-    set_docker_repository_config(target_docker_password, target_docker_repository_name, target_docker_username,
-                                 target_docker_tag_prefix, "target")
-    task_creator = lambda: generate_root_task(task_class=DockerTestContainerPush,
-                                                   force_push=force_push,
-                                                   push_all=push_all)
-    success, task = run_task(task_creator, workers, task_dependencies_dot_file)
+    success = api.push_test_container(force_push,
+                                      push_all,
+                                      force_rebuild,
+                                      force_rebuild_from,
+                                      force_pull,
+                                      output_directory,
+                                      temporary_base_directory,
+                                      log_build_context_content,
+                                      cache_directory,
+                                      build_name,
+                                      source_docker_repository_name,
+                                      source_docker_tag_prefix,
+                                      source_docker_username,
+                                      source_docker_password,
+                                      target_docker_repository_name,
+                                      target_docker_tag_prefix,
+                                      target_docker_username,
+                                      target_docker_password,
+                                      workers,
+                                      task_dependencies_dot_file)
     if not success:
         exit(1)
diff --git a/exasol_integration_test_docker_environment/cli/commands/spawn_test_environment.py b/exasol_integration_test_docker_environment/cli/commands/spawn_test_environment.py
index 926f9b255..03e9ddcfa 100644
--- a/exasol_integration_test_docker_environment/cli/commands/spawn_test_environment.py
+++ b/exasol_integration_test_docker_environment/cli/commands/spawn_test_environment.py
@@ -1,16 +1,14 @@
-from typing import List
+from typing import Optional, Tuple
 import click
-import humanfriendly
 
 from exasol_integration_test_docker_environment.cli.cli import cli
-from exasol_integration_test_docker_environment.cli.common import add_options, set_build_config, run_task, \
-    set_docker_repository_config, run_task, generate_root_task
+from exasol_integration_test_docker_environment.cli.common import add_options
 from exasol_integration_test_docker_environment.cli.options.system_options import system_options, \
     output_directory_option, tempory_base_directory_option
 from exasol_integration_test_docker_environment.cli.options.test_environment_options import docker_db_options
-from exasol_integration_test_docker_environment.lib.test_environment.spawn_test_environment_with_docker_db import \
-    SpawnTestEnvironmentWithDockerDB
+from exasol_integration_test_docker_environment.lib import api
 from exasol_integration_test_docker_environment.cli.options.docker_repository_options import docker_repository_options
+from exasol_integration_test_docker_environment.lib.api.api_errors import ArgumentConstraintError
 
 
 @cli.command()
@@ -37,72 +35,66 @@
 @add_options(system_options)
 def spawn_test_environment(
         environment_name: str,
-        database_port_forward: int,
-        bucketfs_port_forward: int,
+        database_port_forward: Optional[int],
+        bucketfs_port_forward: Optional[int],
         db_mem_size: str,
         db_disk_size: str,
-        nameserver:List[str],
+        nameserver: Tuple[str,...],
         deactivate_database_setup: bool,
-        docker_runtime: str,
+        docker_runtime: Optional[str],
         docker_db_image_version: str,
         docker_db_image_name: str,
+        create_certificates: bool,
         source_docker_repository_name: str,
         source_docker_tag_prefix: str,
-        source_docker_username: str,
-        source_docker_password: str,
+        source_docker_username: Optional[str],
+        source_docker_password: Optional[str],
         target_docker_repository_name: str,
         target_docker_tag_prefix: str,
-        target_docker_username: str,
-        target_docker_password: str,
+        target_docker_username: Optional[str],
+        target_docker_password: Optional[str],
         output_directory: str,
         temporary_base_directory: str,
         workers: int,
-        task_dependencies_dot_file: str,
-        create_certificates: bool):
+        task_dependencies_dot_file: Optional[str]):
     """
     This command spawn a test environment with a docker-db container and a connected test-container.
     The test-container is reachable by the database for output redirects of UDFs.
     """
-    parsed_db_mem_size = humanfriendly.parse_size(db_mem_size)
-    if parsed_db_mem_size < humanfriendly.parse_size("1 GiB"):
-        print("The --db-mem-size needs to be at least 1 GiB.")
-        exit(1)
-    parsed_db_disk_size = humanfriendly.parse_size(db_disk_size)
-    if parsed_db_disk_size < humanfriendly.parse_size("100 MiB"):
-        print("The --db-disk-size needs to be at least 100 MiB.")
-        exit(1)
-    set_build_config(False,
-                     tuple(),
-                     False,
-                     False,
-                     output_directory,
-                     temporary_base_directory,
-                     None,
-                     None)
-    set_docker_repository_config(source_docker_password, source_docker_repository_name, source_docker_username,
-                                 source_docker_tag_prefix, "source")
-    set_docker_repository_config(target_docker_password, target_docker_repository_name, target_docker_username,
-                                 target_docker_tag_prefix, "target")
-    task_creator = lambda: generate_root_task(task_class=SpawnTestEnvironmentWithDockerDB,
-        environment_name=environment_name,
-        database_port_forward=str(database_port_forward) if database_port_forward is not None else None,
-        bucketfs_port_forward=str(bucketfs_port_forward) if bucketfs_port_forward is not None else None,
-        mem_size=db_mem_size,
-        disk_size=db_disk_size,
-        nameservers=nameserver,
-        docker_runtime=docker_runtime,
-        docker_db_image_version=docker_db_image_version,
-        docker_db_image_name=docker_db_image_name,
-        db_user="sys",
-        db_password="exasol",
-        bucketfs_write_password="write",
-        no_test_container_cleanup_after_success=True,
-        no_test_container_cleanup_after_failure=False,
-        no_database_cleanup_after_success=True,
-        no_database_cleanup_after_failure=False,
-        is_setup_database_activated=not deactivate_database_setup,
-        create_certificates=create_certificates
-    )
-    success, task = run_task(task_creator, workers, task_dependencies_dot_file)
+    success = False
+    try:
+        success = api.spawn_test_environment(environment_name,
+                                             database_port_forward,
+                                             bucketfs_port_forward,
+                                             db_mem_size,
+                                             db_disk_size,
+                                             nameserver,
+                                             deactivate_database_setup,
+                                             docker_runtime,
+                                             docker_db_image_version,
+                                             docker_db_image_name,
+                                             create_certificates,
+                                             source_docker_repository_name,
+                                             source_docker_tag_prefix,
+                                             source_docker_username,
+                                             source_docker_password,
+                                             target_docker_repository_name,
+                                             target_docker_tag_prefix,
+                                             target_docker_username,
+                                             target_docker_password,
+                                             output_directory,
+                                             temporary_base_directory,
+                                             workers,
+                                             task_dependencies_dot_file)
+    except ArgumentConstraintError as e:
+        handle_wrong_argument_error(*e.args)
     if not success:
         exit(1)
+
+
+def handle_wrong_argument_error(argument_name, message):
+    formatted = f"--{argument_name}".replace("_", "-")
+    print(f"{formatted}: {message}")
+    ctx = click.get_current_context()
+    click.echo(ctx.get_help())
+    exit(1)
diff --git a/exasol_integration_test_docker_environment/cli/options/docker_repository_options.py b/exasol_integration_test_docker_environment/cli/options/docker_repository_options.py
index 24bcbe1dc..8a73e9d0b 100644
--- a/exasol_integration_test_docker_environment/cli/options/docker_repository_options.py
+++ b/exasol_integration_test_docker_environment/cli/options/docker_repository_options.py
@@ -1,8 +1,10 @@
 import click
 
+DEFAULT_DOCKER_REPOSITORY_NAME = 'exasol/script-language-container'
+
 docker_repository_options = [
     click.option('--source-docker-repository-name', type=str,
-                 default="exasol/script-language-container",
+                 default=DEFAULT_DOCKER_REPOSITORY_NAME,
                  show_default=True,
                  help="Name of the docker repository for pulling cached stages. "
                       "The repository name may contain the URL of the docker registry, "
@@ -19,7 +21,7 @@
                  help="Password for the docker registry from where the system pulls cached stages. "
                       "Without password option the system prompts for the password."),
     click.option('--target-docker-repository-name', type=str,
-                 default="exasol/script-language-container",
+                 default=DEFAULT_DOCKER_REPOSITORY_NAME,
                  show_default=True,
                  help="Name of the docker repository for naming and pushing images of stages. "
                       "The repository name may contain the URL of the docker registry, "
@@ -39,7 +41,7 @@
 
 simple_docker_repository_options = [
     click.option('--docker-repository-name', type=str,
-                 default="exasol/script-language-container",
+                 default=DEFAULT_DOCKER_REPOSITORY_NAME,
                  show_default=True,
                  help="Name of the docker repository for naming images. "
                       "The repository name may contain the URL of the docker registry, "
diff --git a/exasol_integration_test_docker_environment/cli/options/system_options.py b/exasol_integration_test_docker_environment/cli/options/system_options.py
index 9b94ac752..3ddb7b01e 100644
--- a/exasol_integration_test_docker_environment/cli/options/system_options.py
+++ b/exasol_integration_test_docker_environment/cli/options/system_options.py
@@ -1,7 +1,9 @@
 import click
 
+DEFAULT_OUTPUT_DIRECTORY = ".build_output"
+
 output_directory_option = click.option('--output-directory', type=click.Path(file_okay=False, dir_okay=True),
-                                       default=".build_output",
+                                       default=DEFAULT_OUTPUT_DIRECTORY,
                                        show_default=True,
                                        help="Output directory where the system stores all output and log files.")
 
diff --git a/exasol_integration_test_docker_environment/lib/api/__init__.py b/exasol_integration_test_docker_environment/lib/api/__init__.py
new file mode 100644
index 000000000..84fab474e
--- /dev/null
+++ b/exasol_integration_test_docker_environment/lib/api/__init__.py
@@ -0,0 +1,5 @@
+
+from .health import health
+from .build_test_container import build_test_container
+from .push_test_container import push_test_container
+from .spawn_test_environment import spawn_test_environment
diff --git a/exasol_integration_test_docker_environment/lib/api/api_errors.py b/exasol_integration_test_docker_environment/lib/api/api_errors.py
new file mode 100644
index 000000000..79d4469ec
--- /dev/null
+++ b/exasol_integration_test_docker_environment/lib/api/api_errors.py
@@ -0,0 +1,7 @@
+
+class ArgumentConstraintError(ValueError):
+    pass
+
+
+class HealthProblem(RuntimeError):
+    pass
diff --git a/exasol_integration_test_docker_environment/lib/api/build_test_container.py b/exasol_integration_test_docker_environment/lib/api/build_test_container.py
new file mode 100644
index 000000000..7dfafef71
--- /dev/null
+++ b/exasol_integration_test_docker_environment/lib/api/build_test_container.py
@@ -0,0 +1,52 @@
+from typing import Tuple, Optional
+
+from exasol_integration_test_docker_environment.cli.common import set_build_config, \
+    set_docker_repository_config, run_task, generate_root_task
+from exasol_integration_test_docker_environment.cli.options.docker_repository_options import DEFAULT_DOCKER_REPOSITORY_NAME
+from exasol_integration_test_docker_environment.cli.options.system_options import DEFAULT_OUTPUT_DIRECTORY
+from exasol_integration_test_docker_environment.lib.test_environment.analyze_test_container import AnalyzeTestContainer, \
+    DockerTestContainerBuild
+
+
+def build_test_container(
+        force_rebuild: bool = False,
+        force_rebuild_from: Tuple[str, ...] = tuple(),
+        force_pull: bool = False,
+        output_directory: str = DEFAULT_OUTPUT_DIRECTORY,
+        temporary_base_directory: str = "/tmp",
+        log_build_context_content: bool = False,
+        cache_directory: Optional[str] = None,
+        build_name: Optional[str] = None,
+        source_docker_repository_name: str = DEFAULT_DOCKER_REPOSITORY_NAME,
+        source_docker_tag_prefix: str = '',
+        source_docker_username: Optional[str] = None,
+        source_docker_password: Optional[str] = None,
+        target_docker_repository_name: str = DEFAULT_DOCKER_REPOSITORY_NAME,
+        target_docker_tag_prefix: str = '',
+        target_docker_username: Optional[str] = None,
+        target_docker_password: Optional[str] = None,
+        workers: int = 5,
+        task_dependencies_dot_file: Optional[str] = None) -> bool:
+    """
+    This command builds all stages of the test container for the test environment.
+    If stages are cached in a docker registry, they command is going to pull them,
+    instead of building them.
+    """
+    set_build_config(force_rebuild,
+                     force_rebuild_from,
+                     force_pull,
+                     log_build_context_content,
+                     output_directory,
+                     temporary_base_directory,
+                     cache_directory,
+                     build_name)
+    # Use AnalyzeTestContainer to ensure that all luigi processes got it loaded
+    analyze_task = AnalyzeTestContainer.__class__.__name__
+
+    set_docker_repository_config(source_docker_password, source_docker_repository_name, source_docker_username,
+                                 source_docker_tag_prefix, "source")
+    set_docker_repository_config(target_docker_password, target_docker_repository_name, target_docker_username,
+                                 target_docker_tag_prefix, "target")
+    task_creator = lambda: generate_root_task(task_class=DockerTestContainerBuild)
+    success, task = run_task(task_creator, workers, task_dependencies_dot_file)
+    return success
diff --git a/exasol_integration_test_docker_environment/lib/api/health.py b/exasol_integration_test_docker_environment/lib/api/health.py
new file mode 100644
index 000000000..8e4cf699e
--- /dev/null
+++ b/exasol_integration_test_docker_environment/lib/api/health.py
@@ -0,0 +1,46 @@
+from inspect import cleandoc
+from typing import Optional
+
+from exasol_integration_test_docker_environment.doctor import health_checkup, recommend_mitigation
+from exasol_integration_test_docker_environment.lib.api.api_errors import HealthProblem
+
+
+def health():
+    """
+    Check the health of the execution environment.
+
+    If no issues have been found, using the library or executing the test should work just fine.
+    For all found issues there will be a proposed fix/solution.
+
+    If the environment was found to be healthy it will return None, otherwise a description of the found issues.
+    :raises HealthProblem
+    """
+
+    problems = set(health_checkup())
+    if not problems:
+        return
+
+    suggestion_template = cleandoc(
+        """
+        * {problem}
+          Fix: {suggestion}
+        """
+    )
+    message = cleandoc(
+        """
+        {count} problem(s) have been identified.
+
+        {problems}
+        """
+    ).format(
+        count=len(problems),
+        problems="\n".join(
+            (
+                suggestion_template.format(
+                    problem=icd.value, suggestion=recommend_mitigation(icd)
+                )
+                for icd in problems
+            )
+        ),
+    )
+    raise HealthProblem(message)
diff --git a/exasol_integration_test_docker_environment/lib/api/push_test_container.py b/exasol_integration_test_docker_environment/lib/api/push_test_container.py
new file mode 100644
index 000000000..394f4da5f
--- /dev/null
+++ b/exasol_integration_test_docker_environment/lib/api/push_test_container.py
@@ -0,0 +1,55 @@
+from typing import Tuple, Optional
+
+from exasol_integration_test_docker_environment.cli.common import set_docker_repository_config, \
+    run_task, set_build_config, generate_root_task
+from exasol_integration_test_docker_environment.cli.options.docker_repository_options import DEFAULT_DOCKER_REPOSITORY_NAME
+from exasol_integration_test_docker_environment.cli.options.system_options import DEFAULT_OUTPUT_DIRECTORY
+from exasol_integration_test_docker_environment.lib.test_environment.analyze_test_container import \
+    AnalyzeTestContainer, DockerTestContainerPush
+
+
+def push_test_container(
+        force_push: bool = False,
+        push_all: bool = False,
+        force_rebuild: bool = False,
+        force_rebuild_from: Tuple[str, ...] = tuple(),
+        force_pull: bool = False,
+        output_directory: str = DEFAULT_OUTPUT_DIRECTORY,
+        temporary_base_directory: str = "/tmp",
+        log_build_context_content: bool = False,
+        cache_directory: Optional[str] = None,
+        build_name: Optional[str] = None,
+        source_docker_repository_name: str = DEFAULT_DOCKER_REPOSITORY_NAME,
+        source_docker_tag_prefix: str = '',
+        source_docker_username: Optional[str] = None,
+        source_docker_password: Optional[str] = None,
+        target_docker_repository_name: str = DEFAULT_DOCKER_REPOSITORY_NAME,
+        target_docker_tag_prefix: str = '',
+        target_docker_username: Optional[str] = None,
+        target_docker_password: Optional[str] = None,
+        workers: int = 5,
+        task_dependencies_dot_file: Optional[str] = None) -> bool:
+    """
+    This command pushs all stages of the test container for the test environment.
+    If the stages do not exists locally, the system will build or pull them before the push.
+    """
+    set_build_config(force_rebuild,
+                     force_rebuild_from,
+                     force_pull,
+                     log_build_context_content,
+                     output_directory,
+                     temporary_base_directory,
+                     cache_directory,
+                     build_name)
+    # Use AnalyzeTestContainer to ensure that all luigi processes got it loaded
+    analyze_task = AnalyzeTestContainer.__class__.__name__
+
+    set_docker_repository_config(source_docker_password, source_docker_repository_name, source_docker_username,
+                                 source_docker_tag_prefix, "source")
+    set_docker_repository_config(target_docker_password, target_docker_repository_name, target_docker_username,
+                                 target_docker_tag_prefix, "target")
+    task_creator = lambda: generate_root_task(task_class=DockerTestContainerPush,
+                                              force_push=force_push,
+                                              push_all=push_all)
+    success, task = run_task(task_creator, workers, task_dependencies_dot_file)
+    return success
diff --git a/exasol_integration_test_docker_environment/lib/api/spawn_test_environment.py b/exasol_integration_test_docker_environment/lib/api/spawn_test_environment.py
new file mode 100644
index 000000000..e1104d4fe
--- /dev/null
+++ b/exasol_integration_test_docker_environment/lib/api/spawn_test_environment.py
@@ -0,0 +1,83 @@
+from typing import Tuple, Optional
+import humanfriendly
+
+from exasol_integration_test_docker_environment.cli.common import set_build_config, set_docker_repository_config, \
+    run_task, generate_root_task
+from exasol_integration_test_docker_environment.cli.options.docker_repository_options import DEFAULT_DOCKER_REPOSITORY_NAME
+from exasol_integration_test_docker_environment.cli.options.system_options import DEFAULT_OUTPUT_DIRECTORY
+from exasol_integration_test_docker_environment.cli.options.test_environment_options import LATEST_DB_VERSION
+from exasol_integration_test_docker_environment.lib.api.api_errors import ArgumentConstraintError
+from exasol_integration_test_docker_environment.lib.test_environment.spawn_test_environment_with_docker_db import \
+    SpawnTestEnvironmentWithDockerDB
+
+
+def spawn_test_environment(
+        environment_name: str,
+        database_port_forward: Optional[int] = None,
+        bucketfs_port_forward: Optional[int] = None,
+        db_mem_size: str = "2 GiB",
+        db_disk_size: str = "2 GiB",
+        nameserver: Tuple[str,...] = tuple(),
+        deactivate_database_setup: bool = False,
+        docker_runtime: Optional[str] = None,
+        docker_db_image_version: str = LATEST_DB_VERSION,
+        docker_db_image_name: str = "exasol/docker-db",
+        create_certificates: bool = False,
+        source_docker_repository_name: str = DEFAULT_DOCKER_REPOSITORY_NAME,
+        source_docker_tag_prefix: str = '',
+        source_docker_username: Optional[str] = None,
+        source_docker_password: Optional[str] = None,
+        target_docker_repository_name: str = DEFAULT_DOCKER_REPOSITORY_NAME,
+        target_docker_tag_prefix: str = '',
+        target_docker_username: Optional[str] = None,
+        target_docker_password: Optional[str] = None,
+        output_directory: str = DEFAULT_OUTPUT_DIRECTORY,
+        temporary_base_directory: str = "/tmp",
+        workers: int = 5,
+        task_dependencies_dot_file: Optional[str] = None) -> bool:
+    """
+    This command spawn a test environment with a docker-db container and a connected test-container.
+    The test-container is reachable by the database for output redirects of UDFs.
+    """
+    parsed_db_mem_size = humanfriendly.parse_size(db_mem_size)
+    if parsed_db_mem_size < humanfriendly.parse_size("1 GiB"):
+        raise ArgumentConstraintError("db_mem_size", "needs to be at least 1 GiB")
+    parsed_db_disk_size = humanfriendly.parse_size(db_disk_size)
+    if parsed_db_disk_size < humanfriendly.parse_size("100 MiB"):
+        raise ArgumentConstraintError("db_disk_size", "needs to be at least 100 MiB")
+    set_build_config(False,
+                     tuple(),
+                     False,
+                     False,
+                     output_directory,
+                     temporary_base_directory,
+                     None,
+                     None)
+    set_docker_repository_config(source_docker_password, source_docker_repository_name, source_docker_username,
+                                 source_docker_tag_prefix, "source")
+    set_docker_repository_config(target_docker_password, target_docker_repository_name, target_docker_username,
+                                 target_docker_tag_prefix, "target")
+    task_creator = lambda: generate_root_task(task_class=SpawnTestEnvironmentWithDockerDB,
+                                              environment_name=environment_name,
+                                              database_port_forward=str(
+                                                  database_port_forward) if database_port_forward is not None else None,
+                                              bucketfs_port_forward=str(
+                                                  bucketfs_port_forward) if bucketfs_port_forward is not None else None,
+                                              mem_size=db_mem_size,
+                                              disk_size=db_disk_size,
+                                              nameservers=nameserver,
+                                              docker_runtime=docker_runtime,
+                                              docker_db_image_version=docker_db_image_version,
+                                              docker_db_image_name=docker_db_image_name,
+                                              db_user="sys",
+                                              db_password="exasol",
+                                              bucketfs_write_password="write",
+                                              no_test_container_cleanup_after_success=True,
+                                              no_test_container_cleanup_after_failure=False,
+                                              no_database_cleanup_after_success=True,
+                                              no_database_cleanup_after_failure=False,
+                                              is_setup_database_activated=not deactivate_database_setup,
+                                              create_certificates=create_certificates
+                                              )
+    success, task = run_task(task_creator, workers, task_dependencies_dot_file)
+    return success
diff --git a/exasol_integration_test_docker_environment/lib/config/build_config.py b/exasol_integration_test_docker_environment/lib/config/build_config.py
index 879c5b477..832edcbae 100644
--- a/exasol_integration_test_docker_environment/lib/config/build_config.py
+++ b/exasol_integration_test_docker_environment/lib/config/build_config.py
@@ -1,6 +1,7 @@
 import luigi
 
-DEFAULT_OUTPUT_DIRECTORY = ".build_output"
+from exasol_integration_test_docker_environment.cli.options.system_options import DEFAULT_OUTPUT_DIRECTORY
+
 
 class build_config(luigi.Config):
     force_pull = luigi.BoolParameter(False)
diff --git a/exasol_integration_test_docker_environment/lib/config/docker_config.py b/exasol_integration_test_docker_environment/lib/config/docker_config.py
index 3c4fd517c..99f5221a3 100644
--- a/exasol_integration_test_docker_environment/lib/config/docker_config.py
+++ b/exasol_integration_test_docker_environment/lib/config/docker_config.py
@@ -1,16 +1,18 @@
 import luigi
 from luigi.parameter import ParameterVisibility
 
+from exasol_integration_test_docker_environment.cli.options.docker_repository_options import DEFAULT_DOCKER_REPOSITORY_NAME
+
 
 class source_docker_repository_config(luigi.Config):
-    repository_name = luigi.Parameter("exasol/script-language-container")
+    repository_name = luigi.Parameter(DEFAULT_DOCKER_REPOSITORY_NAME)
     tag_prefix = luigi.Parameter("")
     username = luigi.OptionalParameter(None, significant=False, visibility=ParameterVisibility.PRIVATE)
     password = luigi.OptionalParameter(None, significant=False, visibility=ParameterVisibility.PRIVATE)
 
 
 class target_docker_repository_config(luigi.Config):
-    repository_name = luigi.Parameter("exasol/script-language-container")
+    repository_name = luigi.Parameter(DEFAULT_DOCKER_REPOSITORY_NAME)
     tag_prefix = luigi.Parameter("")
     username = luigi.OptionalParameter(None, significant=False, visibility=ParameterVisibility.PRIVATE)
     password = luigi.OptionalParameter(None, significant=False, visibility=ParameterVisibility.PRIVATE)
diff --git a/exasol_integration_test_docker_environment/main.py b/exasol_integration_test_docker_environment/main.py
index 0e71c2916..23664041f 100755
--- a/exasol_integration_test_docker_environment/main.py
+++ b/exasol_integration_test_docker_environment/main.py
@@ -1,13 +1,7 @@
 #! /usr/bin/env python3
 #
 from exasol_integration_test_docker_environment.cli.cli import cli
-# noinspection PyUnresolvedReferences
-from exasol_integration_test_docker_environment.cli.commands import (
-    health,
-    spawn_test_environment,
-    push_test_container,
-    build_test_container
-)
+import exasol_integration_test_docker_environment.cli.commands
 
 if __name__ == '__main__':
     cli()
diff --git a/exasol_integration_test_docker_environment/test/test_click_api_consistency.py b/exasol_integration_test_docker_environment/test/test_click_api_consistency.py
new file mode 100644
index 000000000..4068aaea5
--- /dev/null
+++ b/exasol_integration_test_docker_environment/test/test_click_api_consistency.py
@@ -0,0 +1,66 @@
+import inspect
+import unittest
+
+from exasol_integration_test_docker_environment.cli import commands
+from exasol_integration_test_docker_environment.lib import api
+
+from exasol_integration_test_docker_environment.testing.api_consistency_utils import \
+    param_names_of_click_call, get_click_and_api_functions, defaults_of_click_call, get_click_and_api_function_names
+from exasol_integration_test_docker_environment.testing.utils import multiassert
+
+
+class ClickApiConsistencyTest(unittest.TestCase):
+
+    def test_api_arguments(self):
+        """
+        Validate that the argument lists for all commands match!
+        """
+        self.maxDiff = None
+
+        click_commands, api_functions = get_click_and_api_functions(commands, api)
+        # Now iterate over the list and compare consistency
+        for cli_call, api_call in zip(click_commands, api_functions):
+            cli_spec = inspect.getfullargspec(cli_call.callback)
+            api_spec = inspect.getfullargspec(api_call)
+
+            # We don't compare the annotation for the return type as this is allowed to be different between CLI and API
+            if "return" in api_spec.annotations:
+                del api_spec.annotations["return"]
+
+            multiassert([lambda: self.assertEqual(api_spec.args, cli_spec.args),
+                         lambda: self.assertEqual(api_spec.annotations, cli_spec.annotations),
+                         lambda: self.assertEqual(api_spec.args, param_names_of_click_call(cli_call))], self)
+
+    def test_api_default_values(self):
+        """
+        Validate that the default values for all commands match!
+        """
+
+        click_commands, api_functions = get_click_and_api_functions(commands, api)
+
+        # Now iterate over the list and compare consistency
+        for cli_call, api_call in zip(click_commands, api_functions):
+            api_spec_defaults = inspect.getfullargspec(api_call).defaults or tuple()
+            cli_defaults = defaults_of_click_call(cli_call)
+
+            self.assertEqual(len(cli_defaults), len(api_spec_defaults))
+            for api_default_value, cli_default in zip(api_spec_defaults, cli_defaults):
+                cli_param_name, cli_default_value = cli_default
+                if api_default_value != cli_default_value:
+                    self.fail(f"Default value for parameter '{cli_param_name}' "
+                              f"for method '{api_call.__name__}' does not match. "
+                              f"API method has default value '{api_default_value}' "
+                              f"while CLI method has default value '{cli_default_value}'")
+
+    def test_same_functions(self):
+        """
+        Validate that Click commands and API functions match!
+        For that we use inspect to get all classes of type click.Command in module 'commands',
+        and on the other hand get all functions in module 'api'. The list of names from both most be identical.
+        """
+        click_command_names, api_function_names = get_click_and_api_function_names(commands, api)
+        self.assertEqual(click_command_names, api_function_names)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/exasol_integration_test_docker_environment/test/test_common_run_task_subprocess.py b/exasol_integration_test_docker_environment/test/test_common_run_task_subprocess.py
index 9e54042c5..34492cb91 100644
--- a/exasol_integration_test_docker_environment/test/test_common_run_task_subprocess.py
+++ b/exasol_integration_test_docker_environment/test/test_common_run_task_subprocess.py
@@ -6,6 +6,7 @@
 import luigi
 
 from exasol_integration_test_docker_environment.cli.common import generate_root_task, run_task
+from exasol_integration_test_docker_environment.cli.options.system_options import DEFAULT_OUTPUT_DIRECTORY
 from exasol_integration_test_docker_environment.lib.base.dependency_logger_base_task import DependencyLoggerBaseTask
 
 from exasol_integration_test_docker_environment.lib.base.luigi_log_config import LOG_ENV_VARIABLE_NAME
@@ -43,7 +44,7 @@ def run_test_same_logging_file() -> None:
     """
     with tempfile.TemporaryDirectory() as temp_dir:
         os.chdir(temp_dir)
-        log_path = Path(temp_dir) / ".build_output" / "jobs" / "logs" / "main.log"
+        log_path = Path(temp_dir) / DEFAULT_OUTPUT_DIRECTORY / "jobs" / "logs" / "main.log"
         run_simple_tasks(log_path=log_path)
 
 
diff --git a/exasol_integration_test_docker_environment/testing/api_consistency_utils.py b/exasol_integration_test_docker_environment/testing/api_consistency_utils.py
new file mode 100644
index 000000000..67208e0ec
--- /dev/null
+++ b/exasol_integration_test_docker_environment/testing/api_consistency_utils.py
@@ -0,0 +1,62 @@
+import inspect
+import unittest
+from typing import Any, List, Tuple
+
+import click
+
+from exasol_integration_test_docker_environment.testing.utils import multiassert
+
+
+def adjust_default_value_for_multiple(x: Any):
+    """
+    Click stores default values as list if 'Multiple'=true. However, for plain Python methods we
+    need to use (immutable) tuples for declaring default value. Hence, we need to convert lists with tuples
+    for comparison.
+    Also, default values for none-required parameters of with multiple=true are marked as "None"
+    in the command object, but in reality click invokes the command with "tuple()" for that parameter.
+    (Note that we use here internal structures of click and don't have guarantee of the behavior)
+    """
+    def_value = x.default
+    if x.multiple:
+        if type(def_value) == list:
+            return tuple(def_value)
+        elif def_value is None:
+            return tuple()
+    return def_value
+
+
+def is_click_command(obj: Any) -> bool:
+    """
+    Returns True if parameter obj is a click command, False otherwise.
+    """
+    return isinstance(obj, click.Command)
+
+
+def defaults_of_click_call(click_call: click.Command) -> List[Tuple[str, Any]]:
+    """
+    Returns the default values of all None-required parameters of a click-command.
+    """
+    return [(o.name, adjust_default_value_for_multiple(o)) for o in click_call.params if not o.required]
+
+
+def param_names_of_click_call(click_call: click.Command) -> List[str]:
+    """
+    Returns names of all parameters of a click call
+    """
+    return [o.name for o in click_call.params]
+
+
+def get_click_and_api_functions(click_module, api_module) -> Tuple[List[Any], List[Any]]:
+    # Get all click commands in module 'click_module'
+    click_commands = [c[1] for c in inspect.getmembers(click_module, is_click_command)]
+    # Get all functions in module 'api_module'
+    api_functions = [f[1] for f in inspect.getmembers(api_module, inspect.isfunction)]
+    return click_commands, api_functions
+
+
+def get_click_and_api_function_names(click_module, api_module) -> Tuple[List[Any], List[Any]]:
+    # Get all click command names in module 'click_module'
+    click_command_names = [c[0] for c in inspect.getmembers(click_module, is_click_command)]
+    # Get all function names in module 'api_module'
+    api_function_names = [f[0] for f in inspect.getmembers(api_module, inspect.isfunction)]
+    return click_command_names, api_function_names
diff --git a/exasol_integration_test_docker_environment/testing/utils.py b/exasol_integration_test_docker_environment/testing/utils.py
index 91af53fce..a69d8dd02 100644
--- a/exasol_integration_test_docker_environment/testing/utils.py
+++ b/exasol_integration_test_docker_environment/testing/utils.py
@@ -1,8 +1,9 @@
 import json
 import os
 import socket
+import unittest
 from contextlib import ExitStack
-from typing import Optional, List
+from typing import Optional, List, Callable
 
 import requests
 
@@ -76,3 +77,16 @@ def check_db_version_from_env() -> Optional[str]:
     if "EXASOL_VERSION" in os.environ and os.environ["EXASOL_VERSION"] != "default":
         retval = os.environ["EXASOL_VERSION"]
     return retval
+
+
+def multiassert(assert_list: List[Callable], unit_test: unittest.TestCase):
+    failure_log: List[str] = []
+    for assert_fn in assert_list:
+        try:
+            assert_fn()
+        except AssertionError as e:
+            failure_log.append(f"\nFailure {len(failure_log)}: {str(e)}")
+
+    if len(failure_log) != 0:
+        res_failure_log = '\n'.join(failure_log)
+        unit_test.fail(f"{len(failure_log)} failures within test.\n {res_failure_log}")