Skip to content
This repository has been archived by the owner on Oct 16, 2024. It is now read-only.

Semi-autogenerated cli reference #172

Merged
merged 17 commits into from
Oct 5, 2022
134 changes: 134 additions & 0 deletions content/docs/command-reference/_generate_cli_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import json
from typing import Dict, List, Optional

from click import Command, Context, Group, Option
from pydantic import BaseModel
from typer.main import get_group

from mlem import cli
from mlem.cli.main import get_cmd_name

use_group = ["deployment"]
skip = ["dev"]

abc_group = ["apply-remote", "build", "declare", "serve"]


class Opt(BaseModel):
decls: List[str]
secondary: List[str]
metavar: str
help: str
is_flag: bool


class Args(BaseModel):
args: List[Opt]
impls: Optional[List[str]]
impl_metavar: Optional[str]
subcommands: Optional[Dict[str, str]]


class Spec(BaseModel):
args: Args
options: List[Opt]
doc: str
name: str


def get_options(command: Command, ctx):
if command.name not in abc_group:
yield from command.get_params(ctx)
return

options = None
for subcommand in command.commands.values():
if options is None:
options = list(get_options(subcommand, ctx))
continue
new_options = {o.help for o in
get_options(subcommand, ctx)}
options = [o for o in options if o.help in new_options]
yield from options


def repr_option(option, ctx) -> Opt:
_, help_ = option.get_help_record(ctx)
help_ = help_.replace(" ", " ") # TODO: maybe fix in typer code?
return Opt(decls=sorted(option.opts, reverse=True),
secondary=sorted(option.secondary_opts, reverse=True),
metavar=option.make_metavar(),
help=help_,
is_flag=option.is_flag if isinstance(option, Option) else False)


def generate_options(command: Command, ctx):
res = []
for option in get_options(command, ctx):
if not isinstance(option, Option):
continue
res.append(repr_option(option, ctx))
return res


def generate_args(command, ctx):
args = []
for arg in command.get_params(ctx):
if isinstance(arg, Option):
continue
args.append(repr_option(arg, ctx))
impls = None
metavar = None
subcommands = None
if command.name in abc_group:
impls = list(
sorted([c for c in command.commands if not c.startswith("_")]))
metavar = command.subcommand_metavar
args.extend(generate_args(list(command.commands.values())[0], ctx).args)
if command.name in use_group:
subcommands = {c.name: c.get_short_help_str() for c in
command.commands.values()}
return Args(args=args, impls=impls, impl_metavar=metavar,
subcommands=subcommands)


def generate_usage(command: Command, ctx):
if command.name not in abc_group:
return command.get_usage(ctx)
subcommand = list(command.commands.values())[0]
subctx = Context(subcommand, parent=ctx, info_name=subcommand.name)
sub_usage = generate_usage(subcommand, subctx)
return sub_usage.replace(subcommand.name, command.subcommand_metavar)


def generate_cli_command(command: Command, ctx):
return Spec(args=generate_args(command, ctx),
options=generate_options(command, ctx),
doc=command.help.strip(),
name=get_cmd_name(ctx))


def main():
group = get_group(cli.app)
ctx = Context(group, info_name="mlem", help_option_names=["-h", "--help"])
spec = {}
for name, command in group.commands.items():
if name in skip:
continue
subctx = Context(command, ctx, info_name=name)
if isinstance(command, Group) and name in use_group:

spec[f"{name}/index"] = generate_cli_command(command, subctx)
for subname, subcommand in command.commands.items():
subsubctx = Context(subcommand, subctx, info_name=subname)
spec[f"{name}/{subname}"] = generate_cli_command(subcommand,
subsubctx)
continue
spec[name] = generate_cli_command(command, subctx)

with open("spec.json", "w", encoding="utf8") as f:
json.dump({k: v.dict() for k, v in spec.items()}, f, indent=2)


if __name__ == '__main__':
main()
152 changes: 152 additions & 0 deletions content/docs/command-reference/_generate_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import json
import os
import re
import textwrap
from typing import Dict, List

from pydantic import BaseModel, parse_obj_as

from _generate_cli_spec import Opt, Spec

DOC_AUTO_REPLACE = {
"MLEM Object": "[MLEM Object](/doc/user-guide/basic-concepts#mlem-objects)",
"MLEM project": "[MLEM project](/doc/user-guide/project-structure)"
}
LINE_WIDTH = 80


def replace_section(data: str, section_name: str, new_value: str,
section_prefix: str = "## ") -> str:
return re.sub(f"{section_prefix}{section_name}(.*?)^{section_prefix}",
f"{section_prefix}{section_name}{new_value}{section_prefix}",
data, flags=re.MULTILINE | re.DOTALL)


def repr_option(option: Opt):
decls = ", ".join(f"`{d} <{option.metavar.lower()}>`" for d in option.decls)
if option.is_flag:
decls = ", ".join(f"`{d}`" for d in option.decls)
if option.secondary:
decls += " / " + ", ".join(f"`{d}`" for d in option.secondary)
return textwrap.fill(f"- {decls} - {option.help}", width=LINE_WIDTH,
subsequent_indent=" ")


def repr_arg(option: Opt):
margin = 17
metavar = option.metavar.lower()
option_help = option.help
if option_help.endswith(" [required]"):
option_help = option_help[:-len(" [required]")]
return textwrap.fill(f" {metavar:{margin}}{option_help}", width=LINE_WIDTH,
subsequent_indent=" " * (margin + 2))


def generate_options(options: List[Opt]):
res = ["", ""]
for option in options:
res.append(repr_option(option))
return "\n".join(res + ["", ""])


def _gen_usage_string(spec: Spec):
usage = f"usage: mlem {spec.name} "
indent = len(usage)
options = []
for opt in spec.options:
decl = min(opt.decls, key=len)
metavar = opt.metavar.lower()
if metavar == "boolean":
options.append(f"[{decl}]")
else:
options.append(f"[{decl} <{metavar}>]")
max_opts_len = min(45, LINE_WIDTH - indent)
option_lines = [""]
for o in options:
line = f"{option_lines[-1]}{o} "
if len(line) > max_opts_len and option_lines[-1] != "":
option_lines[-1] = option_lines[-1].strip()
option_lines.append(o + " ")
else:
option_lines[-1] = line
option_lines[-1] = option_lines[-1].strip()
options = ("\n" + " " * indent).join(option_lines)
impl = ""
if spec.args.impl_metavar:
impl = f"[<{spec.args.impl_metavar}> [{spec.args.impl_metavar} options] | --load <declaration>]"
args = impl + " ".join(a.metavar for a in spec.args.args).lower()
if spec.args.subcommands:
args += "command"
res = f"{usage}{options}"
if args:
res += "\n" + " " * indent + f"{args}"
return res


def generate_usage(spec: Spec):
usage = _gen_usage_string(spec)
argspec = spec.args
if argspec.args:
args = '\n'.join(repr_arg(a) for a in argspec.args)
args = f"\n\narguments:\n{args}"
else:
args = ""
if argspec.impls:
impls = "\n".join(f"- {c}" for c in argspec.impls)
impls = f"\n\nBuiltin {argspec.impl_metavar}s:\n{impls}"
else:
impls = ""
if argspec.subcommands:
margin = 17
subcommands = "\n".join(
f" {k:{margin}}{v}" for k, v in argspec.subcommands.items())
subcommands = f"\n\nsubcommands:\n{subcommands}"
else:
subcommands = ""
usage = usage[0].lower() + usage[1:]
return f"\n{usage}{subcommands}{impls}{args}\n"


def generate_doc(doc):
for k, v in DOC_AUTO_REPLACE.items():
doc = doc.replace(k, v)
return f"\n\n{textwrap.fill(doc, width=LINE_WIDTH)}\n\n"


def generate_cli_command(name: str, spec: Spec):
with open(f"{name}.md", "r", encoding="utf8") as f:
data = f.read()

data = replace_section(data, "usage", generate_usage(spec),
section_prefix="```")
data = replace_section(data, "Options", generate_options(spec.options))

cmd_name = name.replace("/", " ")
if cmd_name.endswith(" index"):
cmd_name = cmd_name[:-len(" index")]
data = replace_section(data, " " + cmd_name, generate_doc(spec.doc),
section_prefix="#")
with open(f"{name}.md", "w", encoding="utf8") as f:
f.write(data)


class AllSpec(BaseModel):
__root__: Dict[str, Spec]


def main():
with open("spec.json", "r", encoding="utf8") as f:
spec = parse_obj_as(AllSpec, json.load(f))

# spec.__root__ = {"apply": spec.__root__["apply"]}
for k, s in spec.__root__.items():
generate_cli_command(k, s)

os.unlink("spec.json")


if __name__ == '__main__':
from _generate_cli_spec import main as spec_main

spec_main()
main()
36 changes: 20 additions & 16 deletions content/docs/command-reference/apply-remote.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ a MLEM object to `output` if provided. Otherwise, it will be printed to
## Synopsis

```usage
usage: mlem apply-remote [options] [subtype] data

arguments:
[SUBTYPE] Type of client. Choices: ['http', 'rmq']
DATA Path to dataset object [required]
usage: mlem apply-remote [-d <path>] [-p <path>] [--rev <commitish>]
[-o <path>] [--tp <path>] [-m <text>]
[--index] [--json] [-f <text>] [-h]
[<client> [client options] | --load <declaration>]

Builtin clients:
- http
- rmq
```

## Description
Expand All @@ -27,18 +30,19 @@ clients are `http` and `rmq` - which are used to launch requests against the

## Options

- `-p, --project TEXT`: Path to MLEM project [default: (none)]
- `--rev TEXT`: Repo revision to use [default: (none)]
- `-o, --output TEXT`: Where to store the outputs.
- `--target-project, --tp TEXT`: Project to save target to [default: (none)]
- `-m, --method TEXT`: Which model method is to be applied [default: predict]
- `--index / --no-index`: Whether to index output in .mlem directory
- `--json`: Output as json
- `-l, --load TEXT`: File to load client config from
- `-c, --conf TEXT`: Options for client in format `field.name=value`
- `-f, --file_conf TEXT`: File with options for client in format
- `-d <path>`, `--data <path>` - Path to MLEM data object [required]
- `-p <path>`, `--project <path>` - Path to MLEM project [default: (none)]
- `--rev <commitish>` - Repo revision to use [default: (none)]
- `-o <path>`, `--output <path>` - Where to save model outputs
- `--tp <path>`, `--target-project <path>` - Project to save target to [default:
(none)]
- `-m <text>`, `--method <text>` - Which model method is to be applied [default:
predict]
- `--index` / `--no-index` - Whether to index output in .mlem directory
- `--json` - Output as json
- `-f <text>`, `--file_conf <text>` - File with options for client in format
`field.name=path_to_config`
- `-h, --help`: Show this message and exit.
- `-h`, `--help` - Show this message and exit.

## Example: Apply a locally hosted model to a local dataset

Expand Down
40 changes: 23 additions & 17 deletions content/docs/command-reference/apply.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ provided. Otherwise, it will be printed to `stdout`.
## Synopsis

```usage
usage: mlem apply [options] model data
usage: mlem apply [-p <path>] [--rev <commitish>] [-o <path>]
[-m <text>] [--dr <path>]
[--data-rev <commitish>] [-i] [--it <text>]
[-b <integer>] [--index] [-e] [--json] [-h]
model data

arguments:
MODEL Path to model object [required]
DATA Path to dataset object [required]
model Path to model object
data Path to data object
```

## Description
Expand All @@ -29,20 +33,22 @@ datasets.

## Options

- `-p, --project TEXT`: Path to MLEM project [default: (none)]
- `--rev TEXT`: Repo revision to use [default: (none)]
- `-o, --output TEXT`: Where to store the outputs.
- `-m, --method TEXT`: Which model method is to be applied [default: predict]
- `--data-project, --dr TEXT`: Project with data
- `--data-rev TEXT`: Revision of data
- `-i, --import`: Try to import data on-the-fly
- `--import-type, --it TEXT`: Specify how to read data file for import.
Available types: ['pandas', 'pickle']
- `-b, --batch_size INTEGER`: Batch size for reading data in batches.
- `--index / --no-index`: Whether to index output in .mlem directory
- `-e, --external`: Save result not in .mlem, but directly in project
- `--json`: Output as json
- `-h, --help`: Show this message and exit.
- `-p <path>`, `--project <path>` - Path to MLEM project [default: (none)]
- `--rev <commitish>` - Repo revision to use [default: (none)]
- `-o <path>`, `--output <path>` - Where to save model outputs
- `-m <text>`, `--method <text>` - Which model method is to be applied [default:
predict]
- `--dr <path>`, `--data-project <path>` - Project with data
- `--data-rev <commitish>` - Revision of data
- `-i`, `--import` - Try to import data on-the-fly
- `--it <text>`, `--import-type <text>` - Specify how to read data file for
import. Available types: ['pandas', 'pickle', 'torch']
- `-b <integer>`, `--batch_size <integer>` - Batch size for reading data in
batches
- `--index` / `--no-index` - Whether to index output in .mlem directory
- `-e`, `--external` - Save result not in .mlem, but directly in project
- `--json` - Output as json
- `-h`, `--help` - Show this message and exit.

## Examples

Expand Down
Loading