Files
nextjs-python-web-template/pkgs/clan-cli/clan_cli/config/__init__.py
2023-08-15 13:29:48 +02:00

131 lines
3.7 KiB
Python

# !/usr/bin/env python3
import argparse
import json
import sys
from pathlib import Path
from typing import Any, Optional, Type
from clan_cli.errors import ClanError
from . import parsing
script_dir = Path(__file__).parent
class Kwargs:
def __init__(self) -> None:
self.type: Optional[Type] = None
self.default: Any = None
self.required: bool = False
self.help: Optional[str] = None
self.action: Optional[str] = None
self.choices: Optional[list] = None
# A container inheriting from list, but overriding __contains__ to return True
# for all values.
# This is used to allow any value for the "choices" field of argparse
class AllContainer(list):
def __contains__(self, item: Any) -> bool:
return True
def process_args(args: argparse.Namespace, schema: dict) -> None:
option = args.option
value_arg = args.value
option_path = option.split(".")
# construct a nested dict from the option path and set the value
result: dict[str, Any] = {}
current = result
for part in option_path[:-1]:
current[part] = {}
current = current[part]
current[option_path[-1]] = value_arg
# validate the result against the schema and cast the value to the expected type
schema_type = parsing.type_from_schema_path(schema, option_path)
# we use nargs="+", so we need to unwrap non-list values
if isinstance(schema_type(), list):
subtype = schema_type.__args__[0]
casted = [subtype(x) for x in value_arg]
elif isinstance(schema_type(), dict):
subtype = schema_type.__args__[1]
raise ClanError("Dicts are not supported")
else:
casted = schema_type(value_arg[0])
current[option_path[-1]] = casted
# print the result as json
print(json.dumps(result, indent=2))
def register_parser(
parser: argparse.ArgumentParser,
file: Path = Path(f"{script_dir}/jsonschema/example-schema.json"),
) -> None:
if file.name.endswith(".nix"):
schema = parsing.schema_from_module_file(file)
else:
schema = json.loads(file.read_text())
return _register_parser(parser, schema)
# takes a (sub)parser and configures it
def _register_parser(
parser: Optional[argparse.ArgumentParser],
schema: dict[str, Any],
) -> None:
# check if schema is a .nix file and load it in that case
if "type" not in schema:
raise ClanError("Schema has no type")
if schema["type"] != "object":
raise ClanError("Schema is not an object")
if parser is None:
parser = argparse.ArgumentParser(description=schema.get("description"))
# get all possible options from the schema
options = parsing.options_types_from_schema(schema)
# inject callback function to process the input later
parser.set_defaults(func=lambda args: process_args(args, schema=schema))
# add single positional argument for the option (e.g. "foo.bar")
parser.add_argument(
"option",
# force this arg to be set
nargs="?",
help="Option to configure",
type=str,
choices=AllContainer(list(options.keys())),
)
# add a single optional argument for the value
parser.add_argument(
"value",
# force this arg to be set
nargs="+",
help="Value to set",
)
def main(argv: Optional[list[str]] = None) -> None:
if argv is None:
argv = sys.argv
parser = argparse.ArgumentParser()
parser.add_argument(
"schema",
help="The schema to use for the configuration",
type=Path,
)
args = parser.parse_args(argv[1:2])
register_parser(parser, args.schema)
parser.parse_args(argv[2:])
if __name__ == "__main__":
main()