DynaCLI (Dynamic CLI) is a cloud-friendly, open source library for converting pure Python functions into Linux Shell commands. This article explains how makes writing Command Line Interfaces in Python easy and efficient, using as an example a function to generate a QR code that records a person’s vaccination status. DynaCLI This is a continuation of the article , which describes how to use different Python libraries like , , , , and to build CLI applications. To understand the motivations and use cases for DynaCLI, read the . To learn the differences between DynaCLI and alternatives, refer to . How to Write User-friendly Command Line Interfaces in Python argparse Click Typer docopt Fire Medium interview DynaCLI vs. Alternatives Motivation The basic idea behind DynaCLI is to accelerate and automate the process of building CLI applications as much as possible by focusing solely on Python code. Function arguments are converted to CLI commands, and DynaCLI generates help messages from the Python function docstrings. We’ll demonstrate this approach by generating a QR code that indicates a person’s vaccination status. Sounds interesting? Let’s start exploring… A conventional CLI building process Building CLIs, in general, is a two-step process: first, write the core code and, second, develop the CLI predefined as a set of arguments, as shown below. Let’s start by looking at the code from the original, reference ). article In this snippet, they are first writing methods to implement the core code. from dataclasses import dataclass, field, asdict import requests import shutil from datetime import datetime class ValidationException(Exception): pass @dataclass class Vaccination: manufacturer: str date: str def __post_init__(self): try: date = datetime.strptime(self.date, "%Y-%m-%d") except Exception: raise ValidationException("Vaccination date should be in format YYYY-MM-DD.") if date > datetime.today(): raise ValidationException("Vaccination date should not be a future date.") if self.manufacturer.lower() not in ["pfizer","moderna","astrazeneca","janssen","sinovac"]: raise ValidationException("Your vaccine manufacturer is not approved.") @dataclass class QRCode: name: str birth: str vaccine: list[Vaccination] = field(default_factory=list) def generate_qr_code(qr_code): code = asdict(qr_code) res = requests.get(f"http://api.qrserver.com/v1/create-qr-code/?data={code}", stream=True) if res.status_code == 200: with open("qr_code.png", "wb") as f: res.raw.decode_content = True shutil.copyfileobj(res.raw, f) return "QR code has been generated." else: raise Exception("QR code cannot be generated by QR Code Generator API.") Following that is the code needed to build the CLI. This example uses the argparse library (code snippet from the referenced article): def main(): parser = argparse.ArgumentParser(description="Generate your vaccination QR code.") parser.add_argument("-n", "--name", type=str, help="Your name", required=True) parser.add_argument("-b", "--birth", type=str, help="Your birthday in YYYY-MM-DD format", required=True) parser.add_argument("-m", "--manufacturer", type=str, nargs="+", help="The vaccine manufacturer", required=True, choices=[ "pfizer","moderna","astrazeneca","janssen","sinovac"]) parser.add_argument("-d", "--date", type=str, nargs="+", help="The date of vaccination", required=True) args = parser.parse_args() if len(args.manufacturer) != len(args.date): logging.error( "The number of vaccine manufacturer doesn't match with the number of vaccine dates." ) exit(1) qr_code = QRCode(name=args.name, birth=args.birth, vaccine=[Vaccination(args.manufacturer[i], args.date[i]) for i in range(len(args.date))]) generate_qr_code(qr_code) if __name__ == "__main__": try: main() except ValidationException as e: logging.error(e) except Exception as e: logging.exception(e) CLI Designing with DynaCLI With DynaCLI, we can skip the second part by designing our functions to be CLI-friendly. Functionally, the core logic is the same. To demonstrate the differences, we will update the original code as shown below. Before that, just quickly install DynaCLI to get ready: pip3 install dynacli Restructuring First of all, we would like to restructure the code. Thinking about the CLI design, there should be then feature-set (the actual Python package), which is for storing all commands, followed by to output the actual QR codes: ./qr-code green-badge generate $ tree green_badge -I __pycache__ green_badge ├── generate.py └── __init__.py From the original code, we know that the vaccine manufacturers are a limited set of companies; this kind of information is a good fit for type Enum. from enum import Enum class Manufacturer(Enum): pfizer = 1 moderna = 2 astrazeneca = 3 janssen = 4 sinovac = 5 Alternatively, we can use . I am going to use Enum here instead of Literal. By choosing this approach, we add some defensive control on input data as Enum will automatically validate the data without any redundant custom validator. Literal Therefore, we do not need a manufacturer validation inside anymore and can remove it. Going further, the whole purpose of here is validation, so we can replace this with . __post_init__ @dataclass TypedDict Our updated code file looks like this: generate.py class Vaccination(TypedDict): date: date manufacturer: Manufacturer class QRCode(TypedDict): name: str birth: str vaccinations: list[Vaccination] CLI Building The second big step is to design the actual function in a way that it is going to accept all the necessary information as arguments: generate def generate(first_name: str, last_name: str, birth_date: str, **vaccinations: Manufacturer) -> None: """ Generate your vaccination QR code. Args: first_name (str): name of the vaccinated person last_name (str): surname of the vaccindate person birth_date (str): birthday of the vaccinated person in YYYY-MM-DD format **vaccinations (Manufacturer): vaccination information as date=manufacturer Return: None """ # TODO: handle duplicate or wrong dates. qr_code = QRCode( name=f"{first_name} {last_name}", birth=birth_date, vaccinations=[ Vaccination(manufacturer=manufacturer, date=datetime.strptime(date, "%Y-%m-%d")) for date, manufacturer in vaccinations.items() ], ) res = requests.get( f"http://api.qrserver.com/v1/create-qr-code/?data={qr_code}", stream=True ) if res.status_code != 200: raise Exception("QR code cannot be generated by QR Code Generator API.") with open("qr_code.png", "wb") as f: res.raw.decode_content = True shutil.copyfileobj(res.raw, f) print("QR code has been generated.") Ideally, it should print a nice error message if one of the vaccination pairs contains wrong or duplicate date — but for the sake of simplicity, we just skip this step. The major change is that we are going to accept the date and the manufacturer name as key-value pairs. This is quite intuitive, isn’t it? The CLI call will look something like this: ./qr-code green-badge generate John Doe 1989-10-24 2021-01-01=pfizer 2021-06-01=pfizer The next important difference is that DynaCLI populates help messages from the docstrings in the methods containing the explanation of the arguments. …You are right, this is quite Pythonic; if the explanations are already added once, why not use the same information to build the help messages in the CLI. The rest of the code is the same — functionally the code logic is unchanged. CLI Entrypoint Now as the last step, we create the CLI entry point with DynaCLI. We have already provided the bootstrapper script, all you need is to provide the path for command: dynacli $ dynacli init qr-code path=. Successfully created CLI entrypoint qr-code at /home/ssm-user/OSS/medium-articles/how_to_convert_python_functions/code It will fill the qr-code script with some boilerplate, starter code, you need just change commented portions, in order to have a customized version of your CLI: #!/usr/bin/env python3 """ DynaCLI bootstrap script # Change me """ import os import sys from typing import Final from dynacli import main cwd = os.path.dirname(os.path.realpath(__file__)) __version__: Final[str] = "0.0.0" # Change me to define your own version search_path = [cwd] # Change me if you have different path; you can add multiple search pathes sys.path.extend(search_path) # root_packages = ['cli.dev', 'cli.admin'] # Change me if you have predefined root package name # main(search_path, root_packages) # Uncomment if you have root_packages defined main(search_path) Change the commented sections, remove redundant comments and you are ready to go: #!/usr/bin/env python3 """ Sample QR Generator """ import os import sys from dynacli import main cwd = os.path.dirname(os.path.realpath(__file__)) __version__ = '1.0' search_path = [cwd] sys.path.extend(search_path) main(search_path) As you must have already noticed, there is no CLI pre-processing, adding arguments, version callback, etc. Everything is dead simple Python. DynaCLI grabs the version from and the CLI name from the `qr_code` docstring. version That’s it — the changes are done and we are ready to run the CLI. Getting help output: $ ./qr-code -h usage: qr-code [-h] [-v] {green-badge} ... Sample QR Generator positional arguments: {green-badge} green-badge Generate Green Badge optional arguments: -h, --help show this help message and exit -v, --version show program's version number and exit The help message comes from the package file: green-badge __init__.py $ cat green_badge/__init__.py """ Generate Green Badge """ __version__ = "2.0" Getting the version of the CLI itself: $ ./qr-code --version qr-code - v1.0 Now, let’s think about a different situation when you are porting an already developed package to be exposed by the CLI, and it has its own version set to v2.0. Should it mess with the CLI version? It is not ideal to have a single version for the package and the CLI. With DynaCLI, you can version your packages and even modules. Again, it is quite Pythonic: just add to package and to the module itself. __version__ __init__.py generate.py $ ./qr-code green-badge --version qr-code green-badge - v2.0 $ ./qr-code green-badge generate --version qr-code green-badge generate - v3.0 DynaCLI detects and the function in it. Only public names are exposed by the CLI and the function docstring is used to register the help message. generate.py generate Here, we would like to stress that, with DynaCLI, there is no need to begin writing things from scratch and redo the whole code. All you need is to import already existing functionality to an intermediate representation as we did in and register it in the CLI. generate.py This effectively conforms to the , where your original code is closed to modification but is open to being extended via CLI. Open/Closed Principle Our final version of looks like: generate.py import shutil import requests from datetime import date, datetime from typing import Literal, TypedDict from enum import Enum __version__ = "3.0" class Manufacturer(Enum): pfizer = 1 moderna = 2 astrazeneca = 3 janssen = 4 sinovac = 5 class Vaccination(TypedDict): date: date manufacturer: Manufacturer class QRCode(TypedDict): name: str birth: str vaccinations: list[Vaccination] def generate(first_name: str, last_name: str, birth_date: str, **vaccinations: Manufacturer) -> None: """ Generate your vaccination QR code. Args: first_name (str): name of the vaccinated person last_name (str): surname of the vaccindate person birth_date (str): birthday of the vaccinated person in YYYY-MM-DD format **vaccinations (Manufacturer): vaccination information as date=manufacturer Return: None """ # TODO: handle duplicate or wrong dates. qr_code = QRCode( name=f"{first_name} {last_name}", birth=birth_date, vaccinations=[ Vaccination(manufacturer=manufacturer, date=datetime.strptime(date, "%Y-%m-%d")) for date, manufacturer in vaccinations.items() ], ) res = requests.get( f"http://api.qrserver.com/v1/create-qr-code/?data={qr_code}", stream=True ) if res.status_code != 200: raise Exception("QR code cannot be generated by QR Code Generator API.") with open("qr_code.png", "wb") as f: res.raw.decode_content = True shutil.copyfileobj(res.raw, f) print("QR code has been generated.") You can get help about the command(it is our Python function in fact) using: $ ./qr-code green-badge generate -h usage: qr-code green-badge generate [-h] [-v] first_name last_name birth_date [vaccinations <name>=<value> ...] positional arguments: first_name name of the vaccinated person last_name surname of the vaccindate person birth_date birthday of the vaccinated person in YYYY-MM-DD format vaccinations <name>=<value> vaccination information as date=manufacturer ['pfizer', 'moderna', 'astrazeneca', 'janssen', 'sinovac'] optional arguments: -h, --help show this help message and exit -v, --version show program's version number and exit In summary, the in this sample included: highlights of using DynaCLI We did not add or register any help messages — they were grabbed from the function docstrings. DynaCLI detects and registers them as pair. **kwargs <name>=<value> No special CLI pre-processing, adding arguments, or version callbacks were required. Now, it is time to run the command and generate a QR code with the arguments provided: $ ./qr-code green-badge generate Shako Rzayev 1989-10-24 2021-01-01=pfizer 2021-06-01=pfizer QR code has been generated. That’s it. We have focused only on the core functionality, simplified basic features of a CLI application, and reshaped it to be more user-friendly. That’s how CLIs should be! The source code: how_to_convert_python_functions/code DynaCLI is an open source offering from BST LABS. Our goal is to make it easier for organizations to realize the full potential of cloud computing through a range of open source and commercial offerings. We are best known for , the Cloud AI Operating System, a development platform featuring Infrastructure-from-Code technology. BST LABS is a software engineering unit of . CAIOS BlackSwan Technologies