Skip to content

Commit 1fbfd9f

Browse files
committed
Tighten structured metadata schemas
1 parent b9cd3a1 commit 1fbfd9f

6 files changed

Lines changed: 118 additions & 35 deletions

File tree

docs/commands/output_schema.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@ This section is auto-generated from the help text for the planemo command
1313
**Help**
1414

1515
Export JSON Schemas for Planemo machine-readable outputs.
16+
17+
Available schemas:
18+
19+
cli-command-metadata: output from `planemo cli_metadata --command NAME`.
20+
cli-metadata: output from `planemo cli_metadata`.
21+
invocation-download-manifest: output from `planemo invocation_download --output_json PATH`.
22+
run-outputs: output from `planemo run --output_json PATH`.
23+
test-report: output from test report JSON writers such as `planemo test --test_output_json PATH`.
1624
**Options**::
1725

1826

19-
--format [json] [default: json]
20-
--schema [invocation-download-manifest|run-outputs|test-report]
27+
--schema [cli-command-metadata|cli-metadata|invocation-download-manifest|run-outputs|test-report]
2128
Only export one schema.
2229
--help Show this message and exit.
2330

planemo/cli_metadata.py

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
from typing import Any
77

88
import click
9+
from pydantic import (
10+
BaseModel,
11+
ConfigDict,
12+
Field,
13+
)
914

1015
from planemo import __version__
1116
from planemo.cli import (
@@ -18,6 +23,57 @@
1823
SCHEMA_VERSION = "0.1"
1924

2025

26+
class PlanemoClickTypeMetadata(BaseModel):
27+
model_config = ConfigDict(extra="allow")
28+
29+
name: str
30+
31+
32+
class PlanemoCliParamMetadata(BaseModel):
33+
model_config = ConfigDict(extra="allow")
34+
35+
kind: str
36+
name: str | None
37+
opts: list[str] = Field(default_factory=list)
38+
secondary_opts: list[str] = Field(default_factory=list)
39+
help: str | None = None
40+
required: bool
41+
human_readable_name: str
42+
multiple: bool
43+
nargs: int
44+
type: PlanemoClickTypeMetadata
45+
default: Any = None
46+
is_flag: bool = False
47+
flag_value: Any = None
48+
envvar: Any = None
49+
hidden: bool = False
50+
prompt: str | bool | None = None
51+
planemo_config: dict[str, Any] = Field(default_factory=dict)
52+
53+
54+
class PlanemoCommandMetadata(BaseModel):
55+
model_config = ConfigDict(extra="allow")
56+
57+
name: str
58+
module: str
59+
help: str | None = None
60+
short_help: str | None = None
61+
usage: str
62+
internal: bool
63+
hidden: bool = False
64+
params: list[PlanemoCliParamMetadata] = Field(default_factory=list)
65+
66+
67+
class PlanemoCliMetadata(BaseModel):
68+
model_config = ConfigDict(extra="allow")
69+
70+
schema_version: str = SCHEMA_VERSION
71+
program: str = "planemo"
72+
planemo_version: str
73+
commands: list[PlanemoCommandMetadata] = Field(default_factory=list)
74+
aliases: dict[str, str] = Field(default_factory=dict)
75+
76+
2177
def iter_command_names(include_internal: bool = False) -> list[str]:
2278
"""Return command names included in public CLI metadata."""
2379
commands = list_cmds()
@@ -26,38 +82,36 @@ def iter_command_names(include_internal: bool = False) -> list[str]:
2682
return commands
2783

2884

29-
def load_command_metadata(command_name: str) -> dict[str, Any]:
85+
def load_command_metadata(command_name: str) -> PlanemoCommandMetadata:
3086
"""Load metadata for one Planemo command."""
3187
return serialize_click_command(command_name, name_to_command(command_name))
3288

3389

34-
def load_planemo_metadata(include_internal: bool = False) -> dict[str, Any]:
90+
def load_planemo_metadata(include_internal: bool = False) -> PlanemoCliMetadata:
3591
"""Load metadata for the Planemo command line interface."""
36-
return {
37-
"schema_version": SCHEMA_VERSION,
38-
"program": "planemo",
39-
"planemo_version": __version__,
40-
"commands": [load_command_metadata(command_name) for command_name in iter_command_names(include_internal)],
41-
"aliases": dict(sorted(COMMAND_ALIASES.items())),
42-
}
92+
return PlanemoCliMetadata(
93+
planemo_version=__version__,
94+
commands=[load_command_metadata(command_name) for command_name in iter_command_names(include_internal)],
95+
aliases=dict(sorted(COMMAND_ALIASES.items())),
96+
)
4397

4498

45-
def serialize_click_command(command_name: str, command: click.Command) -> dict[str, Any]:
99+
def serialize_click_command(command_name: str, command: click.Command) -> PlanemoCommandMetadata:
46100
"""Serialize one Click command into JSON-compatible metadata."""
47101
context = click.Context(command, info_name=command_name)
48-
return {
49-
"name": command_name,
50-
"module": f"planemo.commands.cmd_{command_name}",
51-
"help": command.help,
52-
"short_help": command.short_help,
53-
"usage": command.get_usage(context).removeprefix("Usage: "),
54-
"internal": command_name in INTERNAL_COMMANDS,
55-
"hidden": getattr(command, "hidden", False),
56-
"params": [serialize_click_param(param) for param in command.params],
57-
}
58-
59-
60-
def serialize_click_param(param: click.Parameter) -> dict[str, Any]:
102+
return PlanemoCommandMetadata(
103+
name=command_name,
104+
module=f"planemo.commands.cmd_{command_name}",
105+
help=command.help,
106+
short_help=command.short_help,
107+
usage=command.get_usage(context).removeprefix("Usage: "),
108+
internal=command_name in INTERNAL_COMMANDS,
109+
hidden=getattr(command, "hidden", False),
110+
params=[serialize_click_param(param) for param in command.params],
111+
)
112+
113+
114+
def serialize_click_param(param: click.Parameter) -> PlanemoCliParamMetadata:
61115
"""Serialize one Click parameter into JSON-compatible metadata."""
62116
planemo_config = getattr(param, "planemo_config", {})
63117
metadata = {
@@ -81,10 +135,10 @@ def serialize_click_param(param: click.Parameter) -> dict[str, Any]:
81135
}
82136
if isinstance(param, click.Option):
83137
metadata["is_bool_flag"] = param.is_bool_flag
84-
return metadata
138+
return PlanemoCliParamMetadata.model_validate(metadata)
85139

86140

87-
def serialize_click_type(param_type: click.ParamType) -> dict[str, Any]:
141+
def serialize_click_type(param_type: click.ParamType) -> PlanemoClickTypeMetadata:
88142
"""Serialize Click parameter type details where Click exposes them."""
89143
metadata: dict[str, Any] = {"name": param_type.name}
90144
if isinstance(param_type, click.Choice):
@@ -112,7 +166,7 @@ def serialize_click_type(param_type: click.ParamType) -> dict[str, Any]:
112166
for attr in ("exists", "file_okay", "dir_okay", "writable", "readable", "resolve_path", "allow_dash"):
113167
if hasattr(param_type, attr):
114168
metadata[attr] = getattr(param_type, attr)
115-
return _json_value(metadata)
169+
return PlanemoClickTypeMetadata.model_validate(_json_value(metadata))
116170

117171

118172
def _json_value(value: Any) -> Any:

planemo/commands/cmd_cli_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ def cli(output_format, command_name, include_internal):
2020
metadata = load_command_metadata(command_name)
2121
else:
2222
metadata = load_planemo_metadata(include_internal=include_internal)
23-
click.echo(json.dumps(metadata, indent=2, sort_keys=True))
23+
click.echo(json.dumps(metadata.model_dump(mode="json"), indent=2, sort_keys=True))

planemo/commands/cmd_output_schema.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,17 @@
1111

1212

1313
@click.command("output_schema")
14-
@click.option("--format", "output_format", default="json", type=click.Choice(["json"]), show_default=True)
1514
@click.option("--schema", "schema_name", type=click.Choice(sorted(SCHEMA_MODELS)), help="Only export one schema.")
16-
def cli(output_format, schema_name):
17-
"""Export JSON Schemas for Planemo machine-readable outputs."""
15+
def cli(schema_name):
16+
"""Export JSON Schemas for Planemo machine-readable outputs.
17+
18+
Available schemas:
19+
20+
\b
21+
cli-command-metadata: output from `planemo cli_metadata --command NAME`.
22+
cli-metadata: output from `planemo cli_metadata`.
23+
invocation-download-manifest: output from `planemo invocation_download --output_json PATH`.
24+
run-outputs: output from `planemo run --output_json PATH`.
25+
test-report: output from test report JSON writers such as `planemo test --test_output_json PATH`.
26+
"""
1827
click.echo(json.dumps(load_output_schemas(schema_name), indent=2, sort_keys=True))

planemo/output_schemas.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
from typing import Any
66

77
from planemo import __version__
8+
from planemo.cli_metadata import (
9+
PlanemoCliMetadata,
10+
PlanemoCommandMetadata,
11+
)
812
from planemo.output_models import (
913
PlanemoInvocationDownloadManifest,
1014
PlanemoRunOutputs,
@@ -14,6 +18,8 @@
1418
SCHEMA_VERSION = "0.1"
1519

1620
SCHEMA_MODELS = {
21+
"cli-command-metadata": PlanemoCommandMetadata,
22+
"cli-metadata": PlanemoCliMetadata,
1723
"invocation-download-manifest": PlanemoInvocationDownloadManifest,
1824
"run-outputs": PlanemoRunOutputs,
1925
"test-report": PlanemoTestReport,

tests/test_output_schema.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,23 @@ def vlog(self, message, exception=False):
2121

2222
class TestOutputSchema(CliTestCase):
2323
def test_output_schema_exports_test_report_schema(self):
24-
result = self._check_exit_code(["output_schema", "--format", "json", "--schema", "test-report"])
24+
result = self._check_exit_code(["output_schema", "--schema", "test-report"])
2525
metadata = json.loads(result.output)
2626

2727
assert metadata["schema_version"] == "0.1"
2828
assert list(metadata["schemas"]) == ["test-report"]
2929
assert metadata["schemas"]["test-report"]["$schema"] == "https://json-schema.org/draft/2020-12/schema"
3030

3131
def test_output_schema_exports_run_outputs_schema(self):
32-
result = self._check_exit_code(["output_schema", "--format", "json", "--schema", "run-outputs"])
32+
result = self._check_exit_code(["output_schema", "--schema", "run-outputs"])
3333
metadata = json.loads(result.output)
3434

3535
assert list(metadata["schemas"]) == ["run-outputs"]
3636
assert metadata["schemas"]["run-outputs"]["$schema"] == "https://json-schema.org/draft/2020-12/schema"
3737

3838
def test_output_schema_exports_invocation_download_manifest_schema(self):
3939
result = self._check_exit_code(
40-
["output_schema", "--format", "json", "--schema", "invocation-download-manifest"]
40+
["output_schema", "--schema", "invocation-download-manifest"]
4141
)
4242
metadata = json.loads(result.output)
4343

@@ -47,6 +47,13 @@ def test_output_schema_exports_invocation_download_manifest_schema(self):
4747
== "https://json-schema.org/draft/2020-12/schema"
4848
)
4949

50+
def test_output_schema_exports_cli_metadata_schema(self):
51+
result = self._check_exit_code(["output_schema", "--schema", "cli-metadata"])
52+
metadata = json.loads(result.output)
53+
54+
assert list(metadata["schemas"]) == ["cli-metadata"]
55+
assert metadata["schemas"]["cli-metadata"]["$schema"] == "https://json-schema.org/draft/2020-12/schema"
56+
5057
def test_invocation_download_manifest_relative_paths(self):
5158
output_directory = os.path.join(os.getcwd(), "outputs")
5259
output_path = os.path.join(output_directory, "answer.txt")

0 commit comments

Comments
 (0)