Skip to content

Commit

Permalink
Added commands with interactive prompts (#66)
Browse files Browse the repository at this point in the history
If your command needs some terminal interactivity, simply add [`prompts:
Prompts` argument](#basic-terminal-user-interface-tui-primitives) to
your command:

```python
from databricks.sdk import WorkspaceClient
from databricks.labs.blueprint.entrypoint import get_logger
from databricks.labs.blueprint.cli import App
from databricks.labs.blueprint.tui import Prompts

app = App(__file__)
logger = get_logger(__file__)

@app.command
def me(w: WorkspaceClient, prompts: Prompts):
    """Shows current username"""
    if prompts.confirm("Are you sure?"):
        logger.info(f"Hello, {w.current_user.me().user_name}!")

if "__main__" == __name__:
    app()
```

Fix #49
  • Loading branch information
nfx authored Mar 9, 2024
1 parent cfb2c32 commit aa5c0ee
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 23 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Baseline for Databricks Labs projects written in Python. Sources are validated w
* [Publishing Wheels to Databricks Workspace](#publishing-wheels-to-databricks-workspace)
* [Databricks CLI's `databricks labs ...` Router](#databricks-clis-databricks-labs--router)
* [Account-level Commands](#account-level-commands)
* [Commands with interactive prompts](#commands-with-interactive-prompts)
* [Integration with Databricks Connect](#integration-with-databricks-connect)
* [Starting New Projects](#starting-new-projects)
* [Notable Downstream Projects](#notable-downstream-projects)
Expand All @@ -74,6 +75,8 @@ Your command-line apps do need testable interactivity, which is provided by `fro

![ucx install](docs/ucx-install.gif)

It is also integrated with our [command router](#commands-with-interactive-prompts).

[[back to top](#databricks-labs-blueprint)]

### Simple Text Questions
Expand Down Expand Up @@ -1002,6 +1005,32 @@ def workspaces(a: AccountClient):

[[back to top](#databricks-labs-blueprint)]

### Commands with interactive prompts

If your command needs some terminal interactivity, simply add [`prompts: Prompts` argument](#basic-terminal-user-interface-tui-primitives) to your command:

```python
from databricks.sdk import WorkspaceClient
from databricks.labs.blueprint.entrypoint import get_logger
from databricks.labs.blueprint.cli import App
from databricks.labs.blueprint.tui import Prompts
app = App(__file__)
logger = get_logger(__file__)
@app.command
def me(w: WorkspaceClient, prompts: Prompts):
"""Shows current username"""
if prompts.confirm("Are you sure?"):
logger.info(f"Hello, {w.current_user.me().user_name}!")
if "__main__" == __name__:
app()
```

[[back to top](#databricks-labs-blueprint)]

### Integration with Databricks Connect

Invoking Sparksession using Databricks Connect
Expand Down
37 changes: 30 additions & 7 deletions src/databricks/labs/blueprint/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Baseline CLI for Databricks Labs projects."""

import functools
import inspect
import json
import logging
from collections.abc import Callable
Expand All @@ -9,6 +10,7 @@
from databricks.sdk import AccountClient, WorkspaceClient

from databricks.labs.blueprint.entrypoint import get_logger, run_main
from databricks.labs.blueprint.tui import Prompts
from databricks.labs.blueprint.wheels import ProductInfo


Expand All @@ -27,6 +29,13 @@ def needs_workspace_client(self):
return False
return True

def prompts_argument_name(self) -> str | None:
sig = inspect.signature(self.fn)
for param in sig.parameters.values():
if param.annotation is Prompts:
return param.name
return None


class App:
def __init__(self, __file: str):
Expand Down Expand Up @@ -70,19 +79,33 @@ def _route(self, raw):
databricks_logger.setLevel(log_level.upper())
kwargs = {k.replace("-", "_"): v for k, v in flags.items()}
try:
product_name = self._product_info.product_name()
product_version = self._product_info.version()
if self._mapping[command].needs_workspace_client():
kwargs["w"] = WorkspaceClient(product=product_name, product_version=product_version)
elif self._mapping[command].is_account:
kwargs["a"] = AccountClient(product=product_name, product_version=product_version)
self._mapping[command].fn(**kwargs)
cmd = self._mapping[command]
if cmd.needs_workspace_client():
kwargs["w"] = self._workspace_client()
elif cmd.is_account:
kwargs["a"] = self._account_client()
prompts_argument = cmd.prompts_argument_name()
if prompts_argument:
kwargs[prompts_argument] = Prompts()
cmd.fn(**kwargs)
except Exception as err: # pylint: disable=broad-exception-caught
logger = self._logger.getChild(command)
if log_level.lower() in {"debug", "trace"}:
logger.error(f"Failed to call {command}", exc_info=err)
else:
logger.error(f"{err.__class__.__name__}: {err}")

def _account_client(self):
return AccountClient(
product=self._product_info.product_name(),
product_version=self._product_info.version(),
)

def _workspace_client(self):
return WorkspaceClient(
product=self._product_info.product_name(),
product_version=self._product_info.version(),
)

def __call__(self):
run_main(self._route)
44 changes: 28 additions & 16 deletions tests/unit/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
from unittest import mock

from databricks.labs.blueprint.cli import App
from databricks.labs.blueprint.tui import Prompts

FOO_COMMAND = json.dumps(
{
"command": "foo",
"flags": {
"name": "y",
"log_level": "disabled",
},
}
)


def test_commands():
Expand All @@ -15,22 +26,23 @@ def foo(name: str):
"""Some comment"""
some(name)

with mock.patch.object(
sys,
"argv",
[
...,
json.dumps(
{
"command": "foo",
"flags": {
"name": "y",
"log_level": "disabled",
},
}
),
],
):
with mock.patch.object(sys, "argv", [..., FOO_COMMAND]):
app()

some.assert_called_with("y")


def test_injects_prompts():
some = mock.Mock()
app = App(inspect.getfile(App))

@app.command(is_unauthenticated=True)
def foo(name: str, prompts: Prompts):
"""Some comment"""
assert isinstance(prompts, Prompts)
some(name)

with mock.patch.object(sys, "argv", [..., FOO_COMMAND]):
app()

some.assert_called_with("y")

0 comments on commit aa5c0ee

Please sign in to comment.