Skip to content

Commit

Permalink
refactor: Use inheritance for plugins' CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarrmondragon committed Sep 1, 2022
1 parent 16751ee commit a4d449a
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 307 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
.. currentmodule:: singer_sdk.authenticators

.. autoclass:: APIAuthenticatorBase
:members:
:members:
:special-members: __init__
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 --format json
$ poetry run sdk-tap-countries-sample --about=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 --format=json` | `meltano invoke my-tap --about --format=json` |
| Other CLI options | `my-tap --about=json` | `meltano invoke my-tap --about=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: 13 additions & 5 deletions docs/implementation/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ This page describes how SDK-based taps and targets can be invoked via the comman
- [`--help`](#--help)
- [`--version`](#--version)
- [`--about`](#--about)
- [`--format`](#--format)
- [`--about=plain`](#--about-plain)
- [`--about=json`](#--about-json)
- [`--about=markdown`](#--about-markdown)
- [`--config`](#--config)
- [`--config=ENV`](#--config-env)
- [Tap-Specific CLI Options](#tap-specific-cli-options)
Expand Down Expand Up @@ -43,13 +45,19 @@ 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 invoke `--about` in combination with the `--format` option described below to have the output printed in different formats._
_Note: By default, the format of `--about` is plain text. You can pass a value to `--about` from one of the options described below._

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

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

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.
#### `--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.

### `--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 --format=markdown
poetry run tap-mysource --about=markdown
```

This text will automatically document all settings, including setting descriptions. Optionally, paste this into your existing `README.md` file.
1 change: 0 additions & 1 deletion singer_sdk/cli/__init__.py

This file was deleted.

37 changes: 0 additions & 37 deletions singer_sdk/cli/common_options.py

This file was deleted.

102 changes: 40 additions & 62 deletions singer_sdk/mapper_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@

import abc
from io import FileIO
from typing import Callable, Iterable, List, Tuple
from typing import Iterable, List, Tuple, Type

import click
import singer

from singer_sdk.cli import common_options
from singer_sdk.configuration._dict_config import merge_config_sources
from singer_sdk.helpers._classproperty import classproperty
from singer_sdk.helpers.capabilities import CapabilitiesEnum, PluginCapabilities
from singer_sdk.io_base import SingerReader
Expand Down Expand Up @@ -89,68 +87,48 @@ def map_activate_version_message(
"""
...

# CLI handler

@classmethod
def invoke(
cls: Type["InlineMapper"],
config: Tuple[str, ...] = (),
file_input: FileIO = None,
) -> None:
"""Invoke the mapper.
Args:
config: Configuration file location or 'ENV' to use environment
variables. Accepts multiple inputs as a tuple.
file_input: Optional file to read input from.
"""
cls.print_version(print_fn=cls.logger.info)
config_files, parse_env_config = cls.config_from_cli_args(*config)

mapper = cls(
config=config_files,
validate_config=True,
parse_env_config=parse_env_config,
)
mapper.listen(file_input)

@classproperty
def cli(cls) -> Callable:
def cli(cls) -> click.Command:
"""Execute standard CLI handler for inline mappers.
Returns:
A callable CLI object.
A click.Command object.
"""

@common_options.PLUGIN_VERSION
@common_options.PLUGIN_ABOUT
@common_options.PLUGIN_ABOUT_FORMAT
@common_options.PLUGIN_CONFIG
@common_options.PLUGIN_FILE_INPUT
@click.command(
help="Execute the Singer mapper.",
context_settings={"help_option_names": ["--help"]},
command = super().cli
command.help = "Execute the Singer mapper."
command.params.extend(
[
click.Option(
["--input", "file_input"],
help="A path to read messages from instead of from standard in.",
type=click.File("r"),
),
],
)
def cli(
version: bool = False,
about: bool = False,
config: Tuple[str, ...] = (),
format: str = None,
file_input: FileIO = None,
) -> None:
"""Handle command line execution.
Args:
version: Display the package version.
about: Display package metadata and settings.
format: Specify output style for `--about`.
config: Configuration file location or 'ENV' to use environment
variables. Accepts multiple inputs as a tuple.
file_input: Specify a path to an input file to read messages from.
Defaults to standard in if unspecified.
"""
if version:
cls.print_version()
return

if not about:
cls.print_version(print_fn=cls.logger.info)

validate_config: bool = True
if about:
validate_config = False

cls.print_version(print_fn=cls.logger.info)

config_dict = merge_config_sources(
config,
cls.config_jsonschema,
cls._env_prefix,
)

mapper = cls( # type: ignore # Ignore 'type not callable'
config=config_dict,
validate_config=validate_config,
)

if about:
mapper.print_about(format)
else:
mapper.listen(file_input)

return cli

return command
131 changes: 123 additions & 8 deletions singer_sdk/plugin_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
import os
from collections import OrderedDict
from pathlib import PurePath
from pathlib import Path, PurePath
from types import MappingProxyType
from typing import (
Any,
Expand Down Expand Up @@ -397,16 +397,131 @@ def print_about(cls: Type["PluginBase"], format: Optional[str] = None) -> None:
formatted = "\n".join([f"{k.title()}: {v}" for k, v in info.items()])
print(formatted)

@staticmethod
def config_from_cli_args(*args: str) -> Tuple[List[str], bool]:
"""Parse CLI arguments into a config dictionary.
Args:
args: CLI arguments.
Raises:
FileNotFoundError: If the config file does not exist.
Returns:
A tuple containing the config dictionary and a boolean indicating whether
the config file was found.
"""
config_files = []
parse_env_config = False

for config_path in args:
if config_path == "ENV":
# Allow parse from env vars:
parse_env_config = True
continue

# Validate config file paths before adding to list
if not Path(config_path).is_file():
raise FileNotFoundError(
f"Could not locate config file at '{config_path}'."
"Please check that the file exists."
)

config_files.append(Path(config_path))

return config_files, parse_env_config

@abc.abstractclassmethod
def invoke(cls: Type["PluginBase"], *args: Any, **kwargs: Any) -> None:
"""Invoke the plugin.
Args:
args: Plugin arguments.
kwargs: Plugin keyword arguments.
"""
...

@classmethod
def cb_version(
cls: Type["PluginBase"],
ctx: click.Context,
param: click.Option,
value: bool,
) -> None:
"""CLI callback to print the plugin version and exit.
Args:
ctx: Click context.
param: Click parameter.
value: Boolean indicating whether to print the version.
"""
if not value:
return
cls.print_version(print_fn=click.echo)
ctx.exit()

@classmethod
def cb_about(
cls: Type["PluginBase"],
ctx: click.Context,
param: click.Option,
value: str,
) -> None:
"""CLI callback to print the plugin information and exit.
Args:
ctx: Click context.
param: Click parameter.
value: String indicating the format of the information to print.
"""
if not value:
return
cls.print_about(format=value)
ctx.exit()

@classproperty
def cli(cls) -> Callable:
def cli(cls) -> click.Command:
"""Handle command line execution.
Returns:
A callable CLI object.
"""

@click.command()
def cli() -> None:
pass

return cli
return click.Command(
name=cls.name,
callback=cls.invoke,
context_settings={"help_option_names": ["--help"]},
params=[
click.Option(
["--version"],
is_flag=True,
help="Display the package version.",
is_eager=True,
expose_value=False,
callback=cls.cb_version,
),
click.Option(
["--about"],
type=click.Choice(
["plain", "json", "markdown"],
case_sensitive=False,
),
help="Display package metadata and settings.",
is_flag=False,
is_eager=True,
expose_value=False,
callback=cls.cb_about,
flag_value="plain",
),
click.Option(
["--config"],
multiple=True,
help=(
"Configuration file location or 'ENV' to use environment "
+ "variables."
),
type=click.STRING,
default=(),
is_eager=True,
),
],
)
Loading

0 comments on commit a4d449a

Please sign in to comment.