Skip to content

Commit

Permalink
Restore --about --format ...
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarrmondragon committed Dec 21, 2022
1 parent d3c414f commit f6ed78e
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 55 deletions.
4 changes: 2 additions & 2 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Settings: {'type': 'object', 'properties': {}}
This information can also be printed in JSON format for consumption by other applications

```console
$ poetry run sdk-tap-countries-sample --about=json
$ poetry run sdk-tap-countries-sample --about --format json
{
"name": "sample-tap-countries",
"version": "[could not be detected]",
Expand Down Expand Up @@ -179,7 +179,7 @@ plugins:
| ------------------- | :-----------------------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| Configuration store | Config JSON file (`--config=path/to/config.json`) or environment variables (`--config=ENV`) | `meltano.yml`, `.env`, environment variables, or Meltano's system db |
| Simple invocation | `my-tap --config=...` | `meltano invoke my-tap` |
| Other CLI options | `my-tap --about=json` | `meltano invoke my-tap --about=json` |
| Other CLI options | `my-tap --about --format=json` | `meltano invoke my-tap --about --format=json` |
| ELT | `my-tap --config=... \| path/to/target-jsonl --config=...` | `meltano elt my-tap target-jsonl` |

[Meltano]: https://www.meltano.com
Expand Down
18 changes: 5 additions & 13 deletions docs/implementation/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ This page describes how SDK-based taps and targets can be invoked via the comman
- [`--help`](#--help)
- [`--version`](#--version)
- [`--about`](#--about)
- [`--about=plain`](#--about-plain)
- [`--about=json`](#--about-json)
- [`--about=markdown`](#--about-markdown)
- [`--format`](#--format)
- [`--config`](#--config)
- [`--config=ENV`](#--config-env)
- [Tap-Specific CLI Options](#tap-specific-cli-options)
Expand Down Expand Up @@ -45,19 +43,13 @@ Prints the version of the tap or target along with the SDK version and then exit

Prints important information about the tap or target, including the list of supported CLI commands, the `--version` metadata, and list of supported capabilities.

_Note: By default, the format of `--about` is plain text. You can pass a value to `--about` from one of the options described below._
_Note: By default, the format of `--about` is plain text. You can invoke `--about` in combination with the `--format` option described below to have the output printed in different formats._

#### `--about plain`
#### `--format`

Prints the plain text version of the `--about` output. This is the default.
When `--format=json` is specified, the `--about` information will be printed as `json` in order to easily process the metadata in automated workflows.

#### `--about json`

Information will be printed as `json` in order to easily process the metadata in automated workflows.

#### `--about markdown`

Information will be printed as Markdown, optimized for copy-pasting into the maintainer's `README.md` file. Among other helpful guidance, this automatically creates a markdown table of all settings, their descriptions, and their default values.
When `--format=markdown` is specified, the `--about` information will be printed as Markdown, optimized for copy-pasting into the maintainer's `README.md` file. Among other helpful guidance, this automatically creates a markdown table of all settings, their descriptions, and their default values.

### `--config`

Expand Down
2 changes: 1 addition & 1 deletion docs/porting.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ To handle the conversion operation, you'll override [`Tap.load_state()`](singer_
The SDK provides autogenerated markdown you can paste into your README:

```console
poetry run tap-mysource --about=markdown
poetry run tap-mysource --about --format=markdown
```

This text will automatically document all settings, including setting descriptions. Optionally, paste this into your existing `README.md` file.
7 changes: 7 additions & 0 deletions singer_sdk/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Helpers for the tap, target and mapper CLIs."""

from __future__ import annotations

from singer_sdk.cli.options import NestedOption

__all__ = ["NestedOption"]
69 changes: 69 additions & 0 deletions singer_sdk/cli/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Helpers for building the CLI for a Singer tap or target."""

from __future__ import annotations

from typing import Any, Sequence

import click


class NestedOption(click.Option):
"""A Click option that has suboptions."""

def __init__(
self,
*args: Any,
suboptions: Sequence[click.Option] | None = None,
**kwargs: Any,
) -> None:
"""Initialize the option.
Args:
*args: Positional arguments to pass to the parent class.
suboptions: A list of suboptions to be added to the context.
**kwargs: Keyword arguments to pass to the parent class.
"""
self.suboptions = suboptions or []
super().__init__(*args, **kwargs)

def handle_parse_result(
self,
ctx: click.Context,
opts: dict[str, Any],
args: Sequence[Any],
) -> tuple[Any, list[str]]:
"""Handle the parse result.
Args:
ctx: The Click context.
opts: The options.
args: The arguments.
Raises:
UsageError: If an option is used without the parent option.
Returns:
The parse result.
"""
ctx.ensure_object(dict)
ctx.obj[self.name] = {}

if self.name in opts:
for option in self.suboptions:
value = opts.get(option.name, option.get_default(ctx))
ctx.obj[self.name][option.name] = value
else:
for option in self.suboptions:
if option.name in opts:
errmsg = f"{option.opts[0]} is not allowed without {self.opts[0]}"
raise click.UsageError(errmsg)

return super().handle_parse_result(ctx, opts, args)

def as_params(self) -> list[click.Option]:
"""Return a list of options, including this one and its suboptions.
Returns:
List of options.
"""
return [self, *self.suboptions]
77 changes: 38 additions & 39 deletions singer_sdk/plugin_base.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
"""Shared parent class for Tap, Target (future), and Transform (future)."""

from __future__ import annotations

import abc
import json
import logging
import os
from collections import OrderedDict
from pathlib import Path
from types import MappingProxyType
from typing import (
Any,
Callable,
Dict,
List,
Mapping,
Optional,
Sequence,
Tuple,
Type,
Union,
cast,
)
from typing import Any, Callable, Dict, Mapping, Sequence, cast

import click
from jsonschema import Draft7Validator, SchemaError, ValidationError

from singer_sdk import metrics
from singer_sdk._python_types import _FilePath
from singer_sdk.cli import NestedOption
from singer_sdk.configuration._dict_config import parse_environment_config
from singer_sdk.exceptions import ConfigValidationError
from singer_sdk.helpers._classproperty import classproperty
Expand Down Expand Up @@ -80,7 +71,7 @@ def logger(cls) -> logging.Logger:

def __init__(
self,
config: Optional[Union[dict, _FilePath, Sequence[_FilePath]]] = None,
config: dict | _FilePath | Sequence[_FilePath] | None = None,
parse_env_config: bool = False,
validate_config: bool = True,
) -> None:
Expand Down Expand Up @@ -125,7 +116,7 @@ def __init__(
self.metrics_logger = metrics.get_metrics_logger()

@classproperty
def capabilities(self) -> List[CapabilitiesEnum]:
def capabilities(self) -> list[CapabilitiesEnum]:
"""Get capabilities.
Developers may override this property in oder to add or remove
Expand All @@ -140,7 +131,7 @@ def capabilities(self) -> List[CapabilitiesEnum]:
]

@classproperty
def _env_var_config(cls) -> Dict[str, Any]:
def _env_var_config(cls) -> dict[str, Any]:
"""Return any config specified in environment variables.
Variables must match the convention "<PLUGIN_NAME>_<SETTING_NAME>",
Expand Down Expand Up @@ -221,7 +212,7 @@ def _is_secret_config(config_key: str) -> bool:

def _validate_config(
self, raise_errors: bool = True, warnings_as_errors: bool = False
) -> Tuple[List[str], List[str]]:
) -> tuple[list[str], list[str]]:
"""Validate configuration input against the plugin configuration JSON schema.
Args:
Expand All @@ -234,8 +225,8 @@ def _validate_config(
Raises:
ConfigValidationError: If raise_errors is True and validation fails.
"""
warnings: List[str] = []
errors: List[str] = []
warnings: list[str] = []
errors: list[str] = []
log_fn = self.logger.info
config_jsonschema = self.config_jsonschema
if config_jsonschema:
Expand Down Expand Up @@ -270,7 +261,7 @@ def _validate_config(

@classmethod
def print_version(
cls: Type["PluginBase"],
cls: type[PluginBase],
print_fn: Callable[[Any], None] = print,
) -> None:
"""Print help text for the tap.
Expand All @@ -284,13 +275,13 @@ def print_version(
print_fn(f"{cls.name} v{cls.plugin_version}, Meltano SDK v{cls.sdk_version}")

@classmethod
def _get_about_info(cls: Type["PluginBase"]) -> Dict[str, Any]:
def _get_about_info(cls: type[PluginBase]) -> dict[str, Any]:
"""Returns capabilities and other tap metadata.
Returns:
A dictionary containing the relevant 'about' information.
"""
info: Dict[str, Any] = OrderedDict({})
info: dict[str, Any] = OrderedDict({})
info["name"] = cls.name
info["description"] = cls.__doc__
info["version"] = cls.plugin_version
Expand All @@ -303,7 +294,7 @@ def _get_about_info(cls: Type["PluginBase"]) -> Dict[str, Any]:
return info

@classmethod
def append_builtin_config(cls: Type["PluginBase"], config_jsonschema: dict) -> None:
def append_builtin_config(cls: type[PluginBase], config_jsonschema: dict) -> None:
"""Appends built-in config to `config_jsonschema` if not already set.
To customize or disable this behavior, developers may either override this class
Expand Down Expand Up @@ -333,7 +324,7 @@ def _merge_missing(source_jsonschema: dict, target_jsonschema: dict) -> None:
_merge_missing(FLATTENING_CONFIG, config_jsonschema)

@classmethod
def print_about(cls: Type["PluginBase"], format: Optional[str] = None) -> None:
def print_about(cls: type[PluginBase], format: str | None = None) -> None:
"""Print capabilities and other tap metadata.
Args:
Expand Down Expand Up @@ -403,7 +394,7 @@ def print_about(cls: Type["PluginBase"], format: Optional[str] = None) -> None:
print(formatted)

@staticmethod
def config_from_cli_args(*args: str) -> Tuple[List[Path], bool]:
def config_from_cli_args(*args: str) -> tuple[list[Path], bool]:
"""Parse CLI arguments into a config dictionary.
Args:
Expand Down Expand Up @@ -448,7 +439,7 @@ def invoke(cls, *args: Any, **kwargs: Any) -> None:

@classmethod
def cb_version(
cls: Type["PluginBase"],
cls: type[PluginBase],
ctx: click.Context,
param: click.Option,
value: bool,
Expand All @@ -467,7 +458,7 @@ def cb_version(

@classmethod
def cb_about(
cls: Type["PluginBase"],
cls: type[PluginBase],
ctx: click.Context,
param: click.Option,
value: str,
Expand All @@ -481,11 +472,11 @@ def cb_about(
"""
if not value:
return
cls.print_about(format=value)
cls.print_about(format=ctx.obj["about"]["about_format"])
ctx.exit()

@classmethod
def get_command(cls: Type["PluginBase"]) -> click.Command:
def get_command(cls: type[PluginBase]) -> click.Command:
"""Handle command line execution.
Returns:
Expand All @@ -504,19 +495,27 @@ def get_command(cls: Type["PluginBase"]) -> click.Command:
expose_value=False,
callback=cls.cb_version,
),
click.Option(
*NestedOption(
["--about"],
type=click.Choice(
["plain", "json", "markdown"],
case_sensitive=False,
),
help="Display package metadata and settings.",
is_flag=False,
is_eager=True,
is_flag=True,
expose_value=False,
callback=cls.cb_about,
flag_value="plain",
),
suboptions=[
click.Option(
["--format", "about_format"],
type=click.Choice(
["plain", "json", "markdown"],
case_sensitive=False,
),
help="Format for the --about option.",
is_flag=False,
is_eager=True,
expose_value=False,
flag_value="plain",
),
],
).as_params(),
click.Option(
["--config"],
multiple=True,
Expand All @@ -532,7 +531,7 @@ def get_command(cls: Type["PluginBase"]) -> click.Command:
)

@classmethod
def cli(cls: Type["PluginBase"]) -> Any: # noqa: ANN401
def cli(cls: type[PluginBase]) -> Any: # noqa: ANN401
"""Execute standard CLI handler for taps.
Returns:
Expand Down

0 comments on commit f6ed78e

Please sign in to comment.