{"id":23512736,"url":"https://github.com/mpkocher/pydantic-cli","last_synced_at":"2025-04-05T11:13:19.352Z","repository":{"id":35031419,"uuid":"197854268","full_name":"mpkocher/pydantic-cli","owner":"mpkocher","description":"Turn Pydantic defined Data Models into CLI Tools","archived":false,"fork":false,"pushed_at":"2024-09-20T03:39:59.000Z","size":212,"stargazers_count":150,"open_issues_count":7,"forks_count":12,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-29T10:08:40.110Z","etag":null,"topics":["argparse","argparse-alternative","cli","commandline","commandline-interface","config-management","pydantic","python3","schema","schemas"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mpkocher.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-07-19T23:22:18.000Z","updated_at":"2025-03-23T22:21:55.000Z","dependencies_parsed_at":"2024-06-19T02:51:09.055Z","dependency_job_id":"3808fed8-14d3-4500-8810-8a3a7f96268d","html_url":"https://github.com/mpkocher/pydantic-cli","commit_stats":{"total_commits":143,"total_committers":3,"mean_commits":"47.666666666666664","dds":0.07692307692307687,"last_synced_commit":"d67ab8b265fd9ed045d27ecd904a04b8185481fa"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpkocher%2Fpydantic-cli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpkocher%2Fpydantic-cli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpkocher%2Fpydantic-cli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpkocher%2Fpydantic-cli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mpkocher","download_url":"https://codeload.github.com/mpkocher/pydantic-cli/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247325696,"owners_count":20920714,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["argparse","argparse-alternative","cli","commandline","commandline-interface","config-management","pydantic","python3","schema","schemas"],"created_at":"2024-12-25T13:19:14.839Z","updated_at":"2025-04-05T11:13:19.312Z","avatar_url":"https://github.com/mpkocher.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"# Pydantic Commandline Tool Interface\n\nTurn Pydantic defined Data Models into CLI Tools and enable loading values from JSON files\n\n**Requires Pydantic** `\u003e=2.8.2`. \n\n[![Downloads](https://pepy.tech/badge/pydantic-cli)](https://pepy.tech/project/pydantic-cli)\n\n[![Downloads](https://pepy.tech/badge/pydantic-cli/month)](https://pepy.tech/project/pydantic-cli)\n\n## Installation\n\n```bash\npip install pydantic-cli\n```\n\n## Features and Requirements\n\n1. Thin Schema driven interfaces constructed from [Pydantic](https://github.com/samuelcolvin/pydantic) defined data models\n1. Validation is performed in a single location as defined by Pydantic's validation model and defined types\n1. The CLI parsing level is only structurally validating the args or optional arguments/flags are provided\n1. Enable loading config defined in JSON to override or set specific values (e.g. `mytool -i in.csv --json-conf config.json`)\n1. Clear interface between the CLI and your application code\n1. Leverage the static analyzing tool [**mypy**](http://mypy.readthedocs.io) to catch type errors in your commandline tool   \n1. Easy to test (due to reasons defined above)\n\n### Motivating Use cases\n\n- Quick scrapy commandline tools for local development (e.g., webscraper CLI tool, or CLI application that runs a training algo)\n- Internal tools driven by a Pydantic data model/schema\n- Configuration heavy tools that are driven by either partial (i.e, \"presets\") or complete configuration files defined using JSON\n\nNote: Newer version of `Pydantic-settings` has support for commandline functionality. It allows mixing of \"sources\", such as ENV, YAML, JSON and might satisfy your requirements.  \n\nhttps://docs.pydantic.dev/2.8/concepts/pydantic_settings/#settings-management\n\n`Pydantic-cli` predates the CLI component of `pydantic-settings` and has a few different requirements and design approach. \n\n## Quick Start\n\n\nTo create a commandline tool that takes an input file and max number of records to process as arguments:\n\n```bash\nmy-tool --input_file /path/to/file.txt --max_records 1234\n```\n\nThis requires two components.\n\n- Create Pydantic Data Model of type `T` \n- write a function that takes an instance of `T` and returns the exit code (e.g., 0 for success, non-zero for failure).\n- pass the `T` into to the `to_runner` function, or the `run_and_exit`\n\nExplicit example show below.  \n\n```python\nimport sys\nfrom pydantic_cli import run_and_exit, to_runner, Cmd\n\n\nclass MinOptions(Cmd):\n    input_file: str\n    max_records: int\n\n    def run(self) -\u003e None:\n        print(f\"Mock example running with {self}\")\n\n\nif __name__ == '__main__':\n    # to_runner will return a function that takes the args list to run and \n    # will return an integer exit code\n    sys.exit(to_runner(MinOptions, version='0.1.0')(sys.argv[1:]))\n\n```\n\nOr to implicitly use `sys.argv[1:]`, leverage `run_and_exit` (`to_runner` is also useful for testing).\n\n```python\nif __name__ == '__main__':\n    run_and_exit(MinOptions, description=\"My Tool Description\", version='0.1.0')\n\n```\n\n## Customizing Description and Commandline Flags\n\nIf the Pydantic data model fields are reasonable well named (e.g., 'min_score', or 'max_records'), this can yield a good enough description when `--help` is called. \n\nCustomizing the commandline flags or the description can be done by leveraging  `description` keyword argument in `Field` from `pydantic`. See [`Field` model in Pydantic](https://pydantic-docs.helpmanual.io/usage/schema/) more details. \n\nCustom 'short' or 'long' forms of the commandline args can be provided by using a `Tuple[str]` or `Tuple2[str, str]`. For example, `cli=('-m', '--max-records')` or `cli=('--max-records',)`.\n\n**Note**, Pydantic interprets `...` as a \"required\" value when used in `Field`.\n\nhttps://docs.pydantic.dev/latest/concepts/models/#required-fields\n\n```python\nfrom pydantic import Field\nfrom pydantic_cli import run_and_exit, Cmd\n\n\nclass MinOptions(Cmd):\n    input_file: str = Field(..., description=\"Path to Input H5 file\", cli=('-i', '--input-file'))\n    max_records: int = Field(..., description=\"Max records to process\", cli=('-m', '--max-records'))\n    debug: bool = Field(False, description=\"Enable debugging mode\", cli= ('-d', '--debug'))\n\n    def run(self) -\u003e None:\n        print(f\"Mock example running with options {self}\")\n\n\nif __name__ == '__main__':\n    run_and_exit(MinOptions, description=\"My Tool Description\", version='0.1.0')\n```\n\nRunning\n\n```bash\n$\u003e mytool -i input.hdf5 --max-records 100 --debug y\nMock example running with options MinOptions(input_file=\"input.hdf5\", max_records=100, debug=True)\n```\n\n\nLeveraging `Field` is also useful for validating inputs using the standard Pydantic for validation.  \n\n```python\nfrom pydantic import Field\nfrom pydantic_cli import Cmd\n\n\nclass MinOptions(Cmd):\n    input_file: str = Field(..., description=\"Path to Input H5 file\", cli=('-i', '--input-file'))\n    max_records: int = Field(..., gt=0, lte=1000, description=\"Max records to process\", cli=('-m', '--max-records'))\n\n    def run(self) -\u003e None:\n        print(f\"Mock example running with options {self}\")\n```\n\nSee [Pydantic docs](https://docs.pydantic.dev/latest/concepts/validators/) for more details.\n\n## Loading Configuration using JSON\n\nUser created commandline tools using `pydantic-cli` can also load entire models or **partially** defined Pydantic data models from JSON files.\n\n\nFor example, given the following Pydantic data model with the `cli_json_enable = True` in `CliConfig`. \n\nThe `cli_json_key` will define the commandline argument (e.g., `config` will translate to `--config`). The default value is `json-config` (`--json-config`).\n\n```python\nfrom pydantic_cli import CliConfig, run_and_exit, Cmd\n\nclass Opts(Cmd):\n    model_config = CliConfig(\n        frozen=True, cli_json_key=\"json-training\", cli_json_enable=True\n    )\n\n    hdf_file: str\n    max_records: int = 10\n    min_filter_score: float\n    alpha: float\n    beta: float\n    \n    def run(self) -\u003e None:\n        print(f\"Running with opts:{self}\")\n\nif __name__ == '__main__':\n    run_and_exit(Opts, description=\"My Tool Description\", version='0.1.0')\n\n```\n\nCan be run with a JSON file that defines all the (required) values. \n\n```json\n{\"hdf_file\": \"/path/to/file.hdf5\", \"max_records\": 5, \"min_filter_score\": 1.5, \"alpha\": 1.0, \"beta\": 1.0}\n```\n\nThe tool can be executed as shown below. Note, options required at the commandline as defined in the `Opts` model (e.g., 'hdf_file', 'min_filter_score', 'alpha' and 'beta') are **NO longer required** values supplied to the commandline tool.\n```bash\nmy-tool --json-training /path/to/file.json\n```\n\nTo override values in the JSON config file, or provide the missing required values, simply provide the values at the commandline.\n\nThese values **will override** values defined in the JSON config file. The provides a general mechanism of using configuration \"preset\" files. \n\n```bash\nmy-tool --json-training /path/to/file.json --alpha -1.8 --max_records 100 \n```\n\nSimilarly, a partially described data model can be used combined with explict values provided at the commandline.\n\nIn this example, `hdf_file` and `min_filter_score` are still required values that need to be provided to the commandline tool.\n\n```json\n{\"max_records\":10, \"alpha\":1.234, \"beta\":9.876}\n``` \n\n```bash\nmy-tool --json-training /path/to/file.json --hdf_file /path/to/file.hdf5 --min_filter_score -12.34\n```\n\n**Note:** The mixing and matching of a config/preset JSON file and commandline args is the fundamental design requirement of `pydantic-cli`. \n\n## Catching Type Errors with mypy\n\nIf you've used `argparse`, you've probably been bitten by an `AttributeError` exception raised on the Namespace instance returned from parsing the raw args.\n\nFor example,\n\n```python\nimport sys\nfrom argparse import ArgumentParser\n\n\ndef to_parser() -\u003e ArgumentParser:\n    p = ArgumentParser(description=\"Example\")\n    f = p.add_argument\n\n    f('hdf5_file', type=str, help=\"Path to HDF5 records\")\n    f(\"--num_records\", required=True, type=int, help=\"Number of records to filter over\")\n    f('-f', '-filter-score', required=True, type=float, default=1.234, help=\"Min filter score\")\n    f('-g', '--enable-gamma-filter', action=\"store_true\", help=\"Enable gamma filtering\")\n    return p\n\n\ndef my_library_code(path: str, num_records: float, min_filter_score, enable_gamma=True) -\u003e int:\n    print(\"Mock running of code\")\n    return 0\n\n\ndef main(argv) -\u003e int:\n    p = to_parser()\n    pargs = p.parse_args(argv)\n    return my_library_code(pargs.hdf5_file, pargs.num_record, pargs.min_filter_score, pargs.enable_gamma_filter)\n\n\nif __name__ == '__main__':\n    sys.exit(main(sys.argv[1:]))\n\n```\n\nThe first error found at runtime is show below. \n\n```bash\nTraceback (most recent call last):\n  File \"junk.py\", line 35, in \u003cmodule\u003e\n    sys.exit(main(sys.argv[1:]))\n  File \"junk.py\", line 31, in main\n    return my_library_code(pargs.hdf5_file, pargs.num_record, pargs.min_filter_score, pargs.enable_gamma_filter)\nAttributeError: 'Namespace' object has no attribute 'num_record'\n```\n\nThe errors in `pargs.num_records` and `pargs.filter_score` are inconsistent with what is defined in `to_parser` method. Each error will have to be manually hunted down.\n\nWith `pydantic-cli`, it's possible to catch these errors by running `mypy`. This also enables you to refactor your code with more confidence.\n\nFor example,\n\n```python\nfrom pydantic_cli import run_and_exit, Cmd\n\n\nclass Options(Cmd):\n    input_file: str\n    max_records: int\n\n    def run(self) -\u003e None:\n        print(f\"Mock example running with {self.max_score}\")\n\n\nif __name__ == \"__main__\":\n    run_and_exit(Options, version=\"0.1.0\")\n```\n\nWith `mypy`, it's possible to proactively catch these types of errors. \n\n\n## Using Boolean Flags\n\nThere's an ergonomic tradeoff to lean on Pydantic and avoid some friction points at CLI level. This yields an explicit model, but slight added verboseness.\n\nSummary:\n\n- `xs:bool` can be set from commandline as `--xs true` or `--xs false`. Or [using Pydantic's casting](https://docs.pydantic.dev/2.8/api/standard_library_types/#booleans), `--xs yes` or `--xs y`. \n- `xs:Optional[bool]` can be set from commandline as `--xs true`, `--xs false`, or `--xs none`\n\nFor the `None` case, you can configure your Pydantic model to handle the casting/coercing/validation. Similarly, the bool casting should be configured in Pydantic.\n\nConsider a basic model:\n\n```python\nfrom typing import Optional\nfrom pydantic import Field\nfrom pydantic_cli import run_and_exit, Cmd\n\nclass Options(Cmd):\n    input_file: str\n    max_records: int = Field(100, cli=('-m', '--max-records'))\n    dry_run: bool = Field(default=False, description=\"Enable dry run mode\", cli=('-d', '--dry-run'))\n    filtering: Optional[bool]\n    \n    def run(self) -\u003e None:\n        print(f\"Mock example running with {self}\")\n    \n\nif __name__ == \"__main__\":\n    run_and_exit(Options, description=__doc__, version=\"0.1.0\")\n```\n\nIn this case, \n\n- `dry_run` is an optional value with a default and can be set as `--dry-run yes` or `--dry-run no`\n- `filtering` is a required value and can be set `--filtering true`, `--filtering False`, and `--filtering None`   \n\nSee the Pydantic docs for more details on boolean casting.\n\nhttps://docs.pydantic.dev/2.8/api/standard_library_types/#booleans\n\n\n## Customization and Hooks\n\n\n## Hooks into the CLI Execution\n\nThere are three core hooks into the customization of CLI execution. \n\n- exception handler (log or write to stderr and map specific exception classes to integer exit codes)\n- prologue handler (pre-execution hook)\n- epilogue handler (post-execution hook)\n\nBoth of these cases can be customized by passing in a function to the running/execution method. \n\n\nThe exception handler should handle any logging or writing to stderr as well as mapping the specific exception to non-zero integer exit code. \n\nFor example: \n\n```python\nimport sys\n\nfrom pydantic import Field\nfrom pydantic_cli import run_and_exit, Cmd\n\n\nclass MinOptions(Cmd):\n    input_file: str = Field(..., cli=('-i',))\n    max_records: int = Field(10, cli=('-m', '--max-records'))\n\n    def run(self) -\u003e None:\n        # example/mock error raised. Will be mapped to exit code 3 \n        raise ValueError(f\"No records found in input file {self.input_file}\")\n\n\ndef custom_exception_handler(ex: Exception) -\u003e int:\n    exception_map = dict(ValueError=3, IOError=7)\n    sys.stderr.write(str(ex))\n    exit_code = exception_map.get(ex.__class__, 1)\n    return exit_code\n\n\nif __name__ == '__main__':\n    run_and_exit(MinOptions, exception_handler=custom_exception_handler)\n```\n\nA general pre-execution hook can be called using the `prologue_handler`. This function is `Callable[[T], None]`, where `T` is an instance of your Pydantic data model.\n\nThis setup hook will be called before the execution of your main function (e.g., `example_runner`).\n\n\n```python\nimport sys\nimport logging\n\ndef custom_prologue_handler(opts) -\u003e None:\n    logging.basicConfig(level=\"DEBUG\", stream=sys.stdout)\n\nif __name__ == '__main__':\n    run_and_exit(MinOptions, prolgue_handler=custom_prologue_handler)\n```\n\n\nSimilarly, the post execution hook can be called. This function is `Callable[[int, float], None]` that is the `exit code` and `program runtime` in sec as input.\n\n\n```python\nfrom pydantic_cli import run_and_exit\n\n\ndef custom_epilogue_handler(exit_code: int, run_time_sec:float) -\u003e None:\n    m = \"Success\" if exit_code else \"Failed\"\n    msg = f\"Completed running ({m}) in {run_time_sec:.2f} sec\"\n    print(msg)\n\n\nif __name__ == '__main__':\n    run_and_exit(MinOptions, epilogue_handler=custom_epilogue_handler)\n\n```\n\n## SubParsers\n\nDefining a subcommand to your commandline tool is enabled by creating a container of `dict[str, Cmd]` (with `str` is the subcommand name) into `run_and_exit` (or `to_runner`). \n\n\n```python\n\"\"\"Example Subcommand Tool\"\"\"\nfrom pydantic import AnyUrl, Field\nfrom pydantic_cli import run_and_exit, Cmd\n\n\nclass AlphaOptions(Cmd):\n    input_file: str = Field(..., cli=('-i',))\n    max_records: int = Field(10, cli=('-m', '--max-records'))\n    \n    def run(self) -\u003e None:\n        print(f\"Running alpha with {self}\")\n\n\nclass BetaOptions(Cmd):\n    \"\"\"Beta command for testing. Description of tool\"\"\"\n    url: AnyUrl = Field(..., cli=('-u', '--url'))\n    num_retries: int = Field(3, cli=('-n', '--num-retries'))\n    \n    def run(self) -\u003e None:\n        print(f\"Running beta with {self}\")\n\n\nif __name__ == \"__main__\":\n    run_and_exit({\"alpha\": AlphaOptions, \"beta\": BetaOptions}, description=__doc__, version='0.1.0')\n\n```\n# Configuration Details and Advanced Features\n\nPydantic-cli attempts to stylistically follow Pydantic's approach using a class style configuration. See `DefaultConfig in ``pydantic_cli' for more details.\n\n```python\nimport typing as T\nfrom pydantic import ConfigDict\n\n\nclass CliConfig(ConfigDict, total=False):\n    # value used to generate the CLI format --{key}\n    cli_json_key: str\n    # Enable JSON config loading\n    cli_json_enable: bool\n\n    # Set the default ENV var for defining the JSON config path\n    cli_json_config_env_var: str\n    # Set the default Path for JSON config file\n    cli_json_config_path: T.Optional[str]\n    # If a default path is provided or provided from the commandline\n    cli_json_validate_path: bool\n\n    # Add a flag that will emit the shell completion\n    # this requires 'shtab'\n    # https://github.com/iterative/shtab\n    cli_shell_completion_enable: bool\n    cli_shell_completion_flag: str\n```\n\n## AutoComplete leveraging shtab\n\nThere is support for `zsh` and `bash` autocomplete generation using [shtab](https://github.com/iterative/shtab)\n\nThe **optional** dependency can be installed as follows.\n```bash\npip install \"pydantic-cli[shtab]\"\n```\n\nTo enable the emitting of bash/zsh autocomplete files from shtab, set `CliConfig(cli_shell_completion_enable=True)` in your data model config.\n\nThen use your executable (or `.py` file) emit the autocomplete file to the necessary output directory. \n\nFor example, using `zsh` and a script call `my-tool.py`, `my-tool.py --emit-completion zsh \u003e ~/.zsh/completions/_my-tool.py`. By convention/default, the executable name must be prefixed with an underscore.  \n\nWhen using autocomplete it should look similar to this. \n\n\n```bash\n\u003e ./my-tool.py --emit-completion zsh \u003e ~/.zsh/completions/_my-tool.py\nCompleted writing zsh shell output to stdout\n\u003e ./my-tool.py --max\n -- option --\n--max_filter_score  --  (type:int default:1.0)\n--max_length        --  (type:int default:12)\n--max_records       --  (type:int default:123455)\n--max_size          --  (type:int default:13)\n```\n\nSee [shtab](https://github.com/iterative/shtab) for more details.\n\n\nNote, that due to the (typically) global zsh completions directory, this can create some friction points with different virtual (or conda) ENVS with the same executable name.\n\n# General Suggested Testing Model\n\nAt a high level, `pydantic_cli` is (hopefully) a thin bridge between your `Options` defined as a Pydantic model and your \nmain `Cmd.run() -\u003e None` method that has hooks into the startup, shutdown and error handling of the command line tool. \nIt also supports loading config files defined as JSON. By design, `pydantic_cli` explicitly **does not expose, or leak the argparse instance** or implementation details. \nArgparse is a bit thorny and was written in a different era of Python. Exposing these implementation details would add too much surface area and would enable users' to start mucking with the argparse instance in all kinds of unexpected ways. \n\nTesting can be done by leveraging the `to_runner` interface.  \n\n\n1. It's recommend trying to do the majority of testing via unit tests (independent of `pydantic_cli`) with your main function and different instances of your pydantic data model.\n2. Once this test coverage is reasonable, it can be useful to add a few smoke tests at the integration level leveraging `to_runner` to make sure the tool is functional. Any bugs at this level are probably at the `pydantic_cli` level, not your library code.\n\nNote, that `to_runner(Opts)` returns a `Callable[[List[str]], int]` that can be used with `sys.argv[1:]` to return an integer exit code of your program. The `to_runner` layer will also catch any exceptions. \n\n```python\nimport unittest\n\nfrom pydantic_cli import to_runner, Cmd\n\n\nclass Options(Cmd):\n    alpha: int\n    \n    def run(self) -\u003e None:\n        if self.alpha \u003c 0:\n            raise Exception(f\"Got options {self}. Forced raise for testing.\")\n\n\n\nclass TestExample(unittest.TestCase):\n\n    def test_core(self):\n        # Note, this has nothing to do with pydantic_cli\n        # If possible, this is where the bulk of the testing should be\n        # You code should raise exceptions here or return None on success\n        self.assertTrue(Options(alpha=1).run() is None)\n\n    def test_example(self):\n        # This is intended to mimic end-to-end testing \n        # from argv[1:]. The exception handler will map exceptions to int exit codes.   \n        f = to_runner(Options)\n        self.assertEqual(1, f([\"--alpha\", \"100\"]))\n\n    def test_expected_error(self):\n        f = to_runner(Options)\n        self.assertEqual(1, f([\"--alpha\", \"-10\"]))\n```\n\n\n\nFor more scrappy, interactive local development, it can be useful to add `ipdb` or `pdb` and create a custom `exception_handler`.\n\n```python\nfrom pydantic_cli import default_exception_handler, run_and_exit, Cmd\n\n\nclass Options(Cmd):\n    alpha: int\n    \n    def run(self) -\u003e None:\n        if self.alpha \u003c 0:\n            raise Exception(f\"Got options {self}. Forced raise for testing.\")\n\ndef exception_handler(ex: BaseException) -\u003e int:\n    exit_code = default_exception_handler(ex)\n    import ipdb; ipdb.set_trace()\n    return exit_code\n\n\nif __name__ == \"__main__\":\n    run_and_exit(Options, exception_handler=exception_handler)\n```\n\n\nThe core design choice in `pydantic_cli` is leveraging composable functions `f(g(x))` style providing a straight-forward mechanism to plug into.\n\n# More Examples\n\n[More examples are provided here](https://github.com/mpkocher/pydantic-cli/tree/master/pydantic_cli/examples) and [Testing Examples can be seen here](https://github.com/mpkocher/pydantic-cli/tree/master/pydantic_cli/tests). \n\nThe [TestHarness](https://github.com/mpkocher/pydantic-cli/blob/master/pydantic_cli/tests/__init__.py) might provide examples of how to test your CLI tool(s)\n\n# Limitations\n\n- **Positional Arguments are not supported** (See more info in the next subsection)\n- Using Pydantic BaseSettings to set values from `dotenv` or ENV variables is **not supported**. Loading `dotenv` or similar in Pydantic overlapped and competed too much with the \"preset\" JSON loading model in `pydantic-cli`.\n- Currently **only support \"simple\" types** (e.g., floats, ints, strings, boolean) and limited support for fields defined as `List[T]`, `Set[T]` and simple `Enum`s. There is **no support** for nested models. Pydantic-settings might be a better fit for these cases.\n- Leverages [argparse](https://docs.python.org/3/library/argparse.html#module-argparse) underneath the hood (argparse is a bit thorny of an API to build on top of).\n\n## Why are Positional Arguments not supported?\n\nThe core features of pydantic-cli are:\n\n- Define and validate models using Pydantic and use these schemas as an interface to the command line\n- Leverage `mypy` (or similar static analyzer) to enable validating/checking typesafe-ness prior to runtime\n- Load partial or complete models using JSON (these are essentially, partial or complete config or \"preset\" files)\n\nPositional arguments create friction points when combined with loading model values from a JSON file. More specifically, (required) positional values of the model could be supplied in the JSON and are no longer required at the command line. This can fundamentally change the commandline interface and create ambiguities/bugs.\n\nFor example:\n\n```python\nfrom pydantic_cli import CliConfig, Cmd\n\n\nclass MinOptions(Cmd):\n    model_config = CliConfig(cli_json_enable=True)\n    \n    input_file: str\n    input_hdf: str\n    max_records: int = 100\n\n    def run(self) -\u003e None:\n        print(f\"Running with mock {self}\")\n```\n\nAnd the vanilla case running from the command line works as expected.\n\n```bash\nmy-tool /path/to/file.txt /path/to/file.h5 --max_records 200\n```\n\nHowever, when using the JSON \"preset\" feature, there are potential problems where the positional arguments of the tool are shifting around depending on what fields have been defined in the JSON preset.\n\nFor example, running with this `preset.json`, the `input_file` positional argument is no longer required. \n\n```json\n{\"input_file\": \"/system/config.txt\", \"max_records\": 12345}\n```\n\nVanilla case works as expected.\n\n```bash\nmy-tool  file.txt /path/to/file.h5 --json-config ./preset.json\n```\n\nHowever, this also works as well.\n\n```bash\nmy-tool  /path/to/file.h5 --json-config ./preset.json\n```\n\nIn my experience, **the changing of the semantic meaning of the command line tool's positional arguments depending on the contents of the `preset.json` created issues and bugs**.\n\nThe simplest fix is to remove the positional arguments in favor of `-i` or similar which removed the issue.\n\n```python\nfrom pydantic import Field\nfrom pydantic_cli import CliConfig, Cmd\n\nclass MinOptions(Cmd):\n    model_config = CliConfig(cli_json_enable=True)\n    \n    input_file: str = Field(..., cli=('-i', ))\n    input_hdf: str = Field(..., cli=('-d', '--hdf'))\n    max_records: int = Field(100, cli=('-m', '--max-records'))\n\n    def run(self) -\u003e None:\n        print(f\"Running {self}\")\n```\n\nRunning with the `preset.json` defined above, works as expected.\n\n```bash\nmy-tool --hdf /path/to/file.h5 --json-config ./preset.json\n```\n\nAs well as overriding the `-i`. \n\n```bash\nmy-tool -i file.txt --hdf /path/to/file.h5 --json-config ./preset.json\n```\n\nOr \n\n```bash\nmy-tool --hdf /path/to/file.h5 -i file.txt --json-config ./preset.json\n```\n\nThis consistency was the motivation for removing positional argument support in earlier versions of `pydantic-cli`. \n\n# Other Related Tools\n\nOther tools that leverage type annotations to create CLI tools. \n\n- [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#command-line-support) Pydantic \u003e= 2.8.2 supports CLI as a settings \"source\". \n- [cyto](https://github.com/sbtinstruments/cyto) Pydantic based model leveraging Pydantic's settings sources. Supports nested values. Optional TOML support. (Leverages: click, pydantic)\n- [typer](https://github.com/tiangolo/typer) Typer is a library for building CLI applications that users will love using and developers will love creating. Based on Python 3.6+ type hints. (Leverages: click)\n- [glacier](https://github.com/relastle/glacier) Building Python CLI using docstrings and typehints (Leverages: click)\n- [Typed-Settings](https://gitlab.com/sscherfke/typed-settings) Manage typed settings with attrs classes – for server processes as well as click applications (Leverages: attrs, click)\n- [cliche](https://github.com/kootenpv/cliche) Build a simple command-line interface from your functions. (Leverages: argparse and type annotations/hints)\n- [SimpleParsing](https://github.com/lebrice/SimpleParsing) Simple, Elegant, Typed Argument Parsing with argparse. (Leverages: dataclasses, argparse)\n- [recline](https://github.com/NetApp/recline) This library helps you quickly implement an interactive command-based application in Python. (Leverages: argparse + type annotations/hints)\n- [clippy](https://github.com/gowithfloat/clippy) Clippy crawls the abstract syntax tree (AST) of a Python file and generates a simple command-line interface. \n- [clize](https://github.com/epsy/clize) Turn Python functions into command-line interfaces (Leverages: attrs)\n- [plac](https://github.com/micheles/plac)  Parsing the Command Line the Easy Way.\n- [typedparse](https://github.com/khud/typedparse) Parser for command-line options based on type hints (Leverages: argparse and type annotations/hints)\n- [paiargparse](https://github.com/Planet-AI-GmbH/paiargparse) Extension to the python argparser allowing to automatically generate a hierarchical argument list based on dataclasses. (Leverages: argparse + dataclasses)\n- [piou](https://github.com/Andarius/piou) A CLI tool to build beautiful command-line interfaces with type validation.\n- [pyrallis](https://github.com/eladrich/pyrallis) A framework for simple dataclass-based configurations.\n- [ConfigArgParse](https://github.com/bw2/ConfigArgParse) A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables.\n- [spock](https://github.com/fidelity/spock) spock is a framework that helps manage complex parameter configurations during research and development of Python applications. (Leverages: argparse, attrs, and type annotations/hints)\n- [oneFace](https://github.com/Nanguage/oneFace) Generating interfaces(CLI, Qt GUI, Dash web app) from a Python function.\n- [configpile](https://github.com/denisrosset/configpile) Overlay for argparse that takes additional parameters from environment variables and configuration files\n\n# Stats\n\n- [Github Star Growth of pydantic-cli](https://star-history.t9t.io/#mpkocher/pydantic-cli)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmpkocher%2Fpydantic-cli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmpkocher%2Fpydantic-cli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmpkocher%2Fpydantic-cli/lists"}