From bded09ea12c3b7b6daccd4907706add0ed2218fd Mon Sep 17 00:00:00 2001 From: Aliaksandr Kuzmik <98702584+alexkuzmik@users.noreply.github.com> Date: Sun, 8 Sep 2024 14:18:54 +0200 Subject: [PATCH] [OPIK-50] Add cli.py (#182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add cli.py * make opik-installer dynamically install ansible-playbill, and have opik cli invoke the click group directly * Fix lint errors --------- Co-authored-by: Diego Fernando CarriĆ³n --- .../installer/opik_installer/__init__.py | 77 +++++++++++++++++-- .../opik_installer/opik_constants.py | 4 + deployment/installer/pyproject.toml | 3 +- sdks/python/setup.py | 5 +- sdks/python/src/opik/cli.py | 26 +++++++ 5 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 sdks/python/src/opik/cli.py diff --git a/deployment/installer/opik_installer/__init__.py b/deployment/installer/opik_installer/__init__.py index 40eb514912..838ca24138 100644 --- a/deployment/installer/opik_installer/__init__.py +++ b/deployment/installer/opik_installer/__init__.py @@ -8,12 +8,13 @@ import time import traceback -from typing import Callable, Tuple, cast +from functools import update_wrapper +from typing import Callable, Tuple, cast, Dict, Optional from importlib import metadata import click -from ansible_playbill import AnsibleRunner, PlaybookConfig +from semver import VersionInfo as semver import opik_installer.opik_constants as c from .version import __version__ @@ -22,15 +23,21 @@ __called_with_debug: bool = False +AnsibleRunner = None # pylint: disable=invalid-name +PlaybookConfig = None # pylint: disable=invalid-name + def debug_set() -> bool: """Return the debug flag. Must be called within a Click command. """ - debug: bool = click.get_current_context().find_root().params.get( - "debug", False, - ) + ctx = click.get_current_context() + while ctx.params.get("debug") is None and ( + ctx_parent := ctx.parent + ) is not None: + ctx = ctx_parent + debug: bool = ctx.params.get("debug", False) return debug @@ -81,6 +88,61 @@ def run_external_subroutine( time.sleep(1/12) +def require_playbill( + func: Callable[..., None] +) -> Callable[..., None]: # noqa: D202, D301 + """require_playbill decorator. + + If a Click command requires anisble-playbill, this decorator will + ensure that it is available. + \f + Args: + f (Callable[..., None]): Function to be decorated. + + Returns: + Callable[..., None]: Transparently decorated function. + """ + + # Documented technique for adding a decorator to a click command: + # https://web.archive.org/web/20230302175114/https://click.palletsprojects.com/en/8.1.x/commands/#decorating-commands + @click.pass_context + def decorator(ctx: click.Context, *args: Tuple, **kwargs: Dict) -> None: + global AnsibleRunner, PlaybookConfig # pylint: disable=global-statement,invalid-name # noqa: E501 + try: + from ansible_playbill import __package__ as playbill_pkg # noqa: F401,E501 # pylint: disable=unused-import,import-outside-toplevel + playbill_version = metadata.version(playbill_pkg) + try: + if debug_set(): + click.echo(f"Playbill Version: {playbill_version}") + playbill_semver = semver.parse(playbill_version) + if playbill_semver < c.MINIMUM_PLAYBILL_VERSION: + if not str(playbill_semver.build).startswith("dev"): + raise ImportError + except ValueError: + pass + except ImportError: + subprocess.run( + [ + sys.executable, + "-m", "pip", + "install", + "--upgrade", + "ansible-playbill", + ], + check=True, + ) + finally: + from ansible_playbill import AnsibleRunner as ar # noqa: F401,E501 # pylint: disable=unused-import,import-outside-toplevel + from ansible_playbill import PlaybookConfig as pc # noqa: F401,E501 # pylint: disable=unused-import,import-outside-toplevel + AnsibleRunner = ar + PlaybookConfig = pc + + closure: None = ctx.invoke(func, *args, **kwargs) + return closure + + return update_wrapper(decorator, func) + + @click.group(invoke_without_command=True, context_settings=CONTEXT_SETTINGS) @click.version_option(__version__, *("--version", "-v")) @click.option( @@ -170,6 +232,7 @@ def opik_server(ctx: click.Context, debug: bool) -> None: # noqa: D301 is_flag=True, help="Skip installation of dependencies", ) +@require_playbill def install( # noqa: C901 helm_repo_name: str, helm_repo_url: str, @@ -322,8 +385,10 @@ def run_all(): debug=debug, ansible_bin_path=ansible_path, ).run_all() - except Exception: # pylint: disable=broad-except + except Exception as ex: # pylint: disable=broad-except failure_sentinel.set() + if debug: + raise ex click.echo() run_external_subroutine( diff --git a/deployment/installer/opik_installer/opik_constants.py b/deployment/installer/opik_installer/opik_constants.py index bf60a67331..2d6f43f61c 100644 --- a/deployment/installer/opik_installer/opik_constants.py +++ b/deployment/installer/opik_installer/opik_constants.py @@ -2,8 +2,12 @@ from typing import Final, Dict, Any, Union, List +from semver import VersionInfo as semver + from .version import __version__ +MINIMUM_PLAYBILL_VERSION: Final[semver] = semver.parse("0.4.0") + ANSI_GREEN: Final[str] = "\033[32m" ANSI_YELLOW: Final[str] = "\033[33m" ANSI_RESET: Final[str] = "\033[0m" diff --git a/deployment/installer/pyproject.toml b/deployment/installer/pyproject.toml index 3294ddcfeb..d2699927cf 100644 --- a/deployment/installer/pyproject.toml +++ b/deployment/installer/pyproject.toml @@ -39,8 +39,9 @@ keywords = [ ] requires-python = ">=3.8.1,<4.0" dependencies = [ - "ansible-playbill>=0.4.0", + # "ansible-playbill>=0.4.0", "click>=8.1.3", + "semver>=2.8.1", ] [project.urls] diff --git a/sdks/python/setup.py b/sdks/python/setup.py index 589afa7d38..5da28ada4c 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -34,11 +34,14 @@ "tqdm", "uuid7<1.0.0", "rich", + "click", + "opik-installer", ], entry_points={ "pytest11": [ "opik = opik.plugins.pytest.hooks", - ] + ], + "console_scripts": ["opik = opik.cli:cli"], }, include_package_data=True, keywords="opik", diff --git a/sdks/python/src/opik/cli.py b/sdks/python/src/opik/cli.py new file mode 100644 index 0000000000..0a1a9e62e1 --- /dev/null +++ b/sdks/python/src/opik/cli.py @@ -0,0 +1,26 @@ +"""CLI tool for Opik.""" + +from importlib import metadata + +import click + +from opik_installer import opik_server + +__version__: str = "0.0.0+dev" +if __package__: + __version__ = metadata.version(__package__) + +CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]} + + +@click.group(invoke_without_command=True, context_settings=CONTEXT_SETTINGS) +@click.version_option(__version__, *("--version", "-v")) +def cli() -> None: + """CLI tool for Opik.""" + + +cli.add_command(opik_server, name="server") + + +if __name__ == "__main__": + cli()