|
1 | 1 | # **Custom Commands** |
2 | | -In this section, we are going to go over how to create a custom command and throw more light on how Ella CLI works. |
| 2 | +In this section, we will guide you through the process of creating a command and explain why you |
| 3 | +should utilize `ellar_cli.click` as your primary Click package when working with Ellar. |
| 4 | + |
| 5 | + |
| 6 | +## **Command Options Arguments** |
| 7 | +The `ellar_cli.click` package offers comprehensive help messages and documentation for command options and arguments |
| 8 | +without compromising the fundamental functionality of the command. |
| 9 | +For instance: |
| 10 | + |
| 11 | +```python |
| 12 | +import ellar_cli.click as click |
| 13 | + |
| 14 | +@click.command() |
| 15 | +@click.argument("arg1", required=True, help="Arg1 description") |
| 16 | +@click.argument("arg2", required=False, help="Arg2 description") |
| 17 | +@click.option("-op1", required=False, help="option1 description") |
| 18 | +@click.option("-op2", required=False, help="option2 description") |
| 19 | +def my_command(arg1, arg2, op1, op2): |
| 20 | + print(f"ARG1={arg1} ARG2={arg2}; op1={op1} op2={op2}") |
| 21 | +``` |
| 22 | + |
| 23 | +When you register `my_command` to a `@Module` class, you can then execute it in your terminal with the following command: |
| 24 | +```shell |
| 25 | +ellar my-command --help |
| 26 | + |
| 27 | +## OUTPUT |
| 28 | + |
| 29 | +Usage: ellar my-command <arg1> [<arg2>] [OPTIONS] |
| 30 | + |
| 31 | +ARGUMENTS: |
| 32 | + arg1: <arg1> Arg1 description [required] |
| 33 | + arg2: [<arg2>] Arg2 description |
| 34 | + |
| 35 | +[OPTIONS]: |
| 36 | + -op1 TEXT option1 description |
| 37 | + -op2 TEXT option2 description |
| 38 | + --help Show this message and exit. |
| 39 | +``` |
| 40 | + |
| 41 | +## **With App Context Decorator** |
| 42 | +The `ellar_cli.click` module includes a command decorator function called `with_app_context`. |
| 43 | +This decorator ensures that a click command is executed within the application context, |
| 44 | +allowing `current_app`, `current_injector`, and `current_config` to have values. |
| 45 | + |
| 46 | +For instance: |
| 47 | + |
| 48 | +```python |
| 49 | +import ellar_cli.click as click |
| 50 | +from ellar.core import Config |
| 51 | +from ellar.app import current_injector |
| 52 | + |
| 53 | +@click.command() |
| 54 | +@click.argument("arg1", required=True, help="Arg1 description") |
| 55 | +@click.with_app_context |
| 56 | +def command_context(arg1): |
| 57 | + config = current_injector.get(Config) |
| 58 | + print("ALLOWED_HOSTS:", config.ALLOWED_HOSTS, ";ELLAR_CONFIG_MODULE:", config.config_module) |
| 59 | + |
| 60 | +@click.command() |
| 61 | +@click.argument("arg1", required=True, help="Arg1 description") |
| 62 | +def command_wc(arg1): |
| 63 | + config = current_injector.get(Config) |
| 64 | + print("ALLOWED_HOSTS:", config.ALLOWED_HOSTS, ";ELLAR_CONFIG_MODULE:", config.config_module) |
| 65 | +``` |
| 66 | + |
| 67 | +In this example, `command_context` is wrapped with `with_app_context`, while `command_wc` is not. |
| 68 | +When executing both commands, `command_context` will run successfully, and `command_wc` will raise a RuntimeError |
| 69 | +because it attempts to access a value outside the context. |
| 70 | + |
| 71 | +## **AppContextGroup** |
| 72 | +`AppContextGroup` extended from `click.Group` to wrap all its commands with `with_app_context` decorator. |
3 | 73 |
|
4 | | -## **Create Custom Command** |
5 | | -Let's create a file called `commands.py` at the root level of the project. |
6 | 74 |
|
7 | 75 | ```python |
8 | | -# project_name/commands.py |
| 76 | +import ellar_cli.click as click |
| 77 | +from ellar.core import Config |
| 78 | +from ellar.app import current_injector |
| 79 | + |
| 80 | +cm = click.AppContextGroup(name='cm') |
| 81 | + |
| 82 | +@cm.command() |
| 83 | +@click.argument("arg1", required=True, help="Arg1 description") |
| 84 | +def command_context(arg1): |
| 85 | + config = current_injector.get(Config) |
| 86 | + print("ALLOWED_HOSTS:", config.ALLOWED_HOSTS, ";ELLAR_CONFIG_MODULE:", config.config_module) |
9 | 87 |
|
10 | | -from ellar.common import command |
11 | 88 |
|
12 | | -@command |
13 | | -def my_new_command(): |
14 | | - """my_new_command cli description """ |
| 89 | +@cm.command() |
| 90 | +@click.argument("arg1", required=True, help="Arg1 description") |
| 91 | +def command_wc(arg1): |
| 92 | + config = current_injector.get(Config) |
| 93 | + print("ALLOWED_HOSTS:", config.ALLOWED_HOSTS, ";ELLAR_CONFIG_MODULE:", config.config_module) |
15 | 94 | ``` |
| 95 | +All commands registered under `cm` will be executed under within the context of the application. |
16 | 96 |
|
17 | | -## **Custom Command with Context** |
| 97 | +### **Disabling `with_app_context` in AppContextGroup** |
| 98 | +There are some cases where you may want to execute a command under `AppContextGroup` outside application context. |
| 99 | +This can be done by setting `with_app_context=False` as command parameter. |
18 | 100 |
|
19 | | -Ellar CLI tools is a wrapper round [typer](https://typer.tiangolo.com/). |
20 | | -So, therefore, we can easily get the command context by adding a parameter with the annotation of `typer.Context` |
| 101 | +```python |
| 102 | +import ellar_cli.click as click |
| 103 | + |
| 104 | +cm = click.AppContextGroup(name='cm') |
21 | 105 |
|
22 | | -Ellar CLI adds some meta-data CLI context that provides an interface for interaction with the Ellar project. |
| 106 | +@cm.command(with_app_context=False) |
| 107 | +@click.argument("arg1", required=True, help="Arg1 description") |
| 108 | +def command_wc(arg1): |
| 109 | + # config = current_injector.get(Config) |
| 110 | + print("ALLOWED_HOSTS:Unavailable;ELLAR_CONFIG_MODULE:Unavailable") |
| 111 | +``` |
23 | 112 |
|
24 | | -For example: |
| 113 | +## Async Command |
| 114 | +The `ellar_cli.click` package provides a utility decorator function, `run_as_async`, |
| 115 | +specifically designed to execute coroutine commands. |
| 116 | +This is useful when you want to define asynchronous commands using the `click` package. |
| 117 | +Here's an example: |
25 | 118 |
|
26 | 119 | ```python |
27 | | -import typing as t |
28 | | -import typer |
29 | | -from ellar.common import command |
30 | | -from ellar_cli.service import EllarCLIService |
31 | | -from ellar_cli.constants import ELLAR_META |
32 | | - |
33 | | -@command |
34 | | -def my_new_command(ctx:typer.Context): |
35 | | - """my_new_command CLI Description """ |
36 | | - ellar_cli_service = t.cast(t.Optional[EllarCLIService], ctx.meta.get(ELLAR_META)) |
37 | | - app = ellar_cli_service.import_application() |
| 120 | +import ellar_cli.click as click |
| 121 | +from ellar.core import Config |
| 122 | +from ellar.app import current_injector |
| 123 | + |
| 124 | +@click.command() |
| 125 | +@click.argument("arg1", required=True, help="Arg1 description") |
| 126 | +@click.with_app_context |
| 127 | +@click.run_as_async |
| 128 | +async def command_context(arg1): |
| 129 | + config = current_injector.get(Config) |
| 130 | + print("ALLOWED_HOSTS:", config.ALLOWED_HOSTS, ";ELLAR_CONFIG_MODULE:", config.config_module) |
38 | 131 | ``` |
39 | | -`EllarCLIService` is an Ellar CLI meta-data for interacting with Ellar project. |
40 | 132 |
|
41 | | -Some important method that may be of interest: |
| 133 | +In this example, the `run_as_async` decorator enables the `command_context` coroutine |
| 134 | +command to be executed appropriately during execution. |
| 135 | + |
| 136 | +## **Custom Command With Ellar** |
| 137 | +Let's create a command group `db` which contains sub-commands such as `makemigrations`, `migrate`, `reset-db`, and so on. |
| 138 | + |
| 139 | +To implement this scenario, let's create a file `commands.py` at the root level of the project and add the code below. |
| 140 | +```python |
| 141 | +from ellar_cli.click import AppContextGroup |
| 142 | + |
| 143 | +db = AppContextGroup(name="db") |
42 | 144 |
|
43 | | -- `import_application`: returns application instance. |
44 | | -- `get_application_config`: gets current application config. |
45 | 145 |
|
46 | | -## **Register a Custom Command** |
| 146 | +@db.command(name="make-migrations") |
| 147 | +def makemigrations(): |
| 148 | + """Create DB Migration """ |
47 | 149 |
|
48 | | -Lets, make the `my_new_command` visible on the CLI. |
49 | | -In other for Ellar CLI to identify custom command, its has to be registered to a `@Module` class. |
| 150 | +@db.command() |
| 151 | +def migrate(): |
| 152 | + """Applies Migrations""" |
| 153 | +``` |
| 154 | + |
| 155 | +### **Registering Command** |
50 | 156 |
|
51 | | -For example: |
| 157 | +To make the `db` command visible on the CLI, it **must** be registered within a `@Module` class. |
| 158 | +This ensures that the Ellar CLI can recognize and identify custom commands. |
52 | 159 |
|
53 | 160 | ```python |
54 | | -# project_name/root_module.py |
55 | 161 | from ellar.common import Module |
56 | 162 | from ellar.core import ModuleBase |
57 | | -from .commands import my_new_command |
| 163 | +from .commands import db |
58 | 164 |
|
59 | | -@Module(commands=[my_new_command]) |
| 165 | +@Module(commands=[db]) |
60 | 166 | class ApplicationModule(ModuleBase): |
61 | 167 | pass |
62 | 168 | ``` |
63 | | - |
64 | | -open your terminal and navigate to project directory and run the command below |
| 169 | +Open your terminal and navigate to the project directory and run the command below |
65 | 170 | ```shell |
66 | | -ellar --help |
| 171 | +ellar db --help |
67 | 172 | ``` |
68 | 173 |
|
69 | 174 | command output |
70 | 175 | ```shell |
71 | | -Usage: Ellar, Python Web framework [OPTIONS] COMMAND [ARGS]... |
| 176 | +Usage: Ellar, Python Web framework db [OPTIONS] COMMAND [ARGS]... |
72 | 177 |
|
73 | 178 | Options: |
74 | | - -p, --project TEXT Run Specific Command on a specific project |
75 | | - --install-completion [bash|zsh|fish|powershell|pwsh] |
76 | | - Install completion for the specified shell. |
77 | | - --show-completion [bash|zsh|fish|powershell|pwsh] |
78 | | - Show completion for the specified shell, to |
79 | | - copy it or customize the installation. |
80 | | - --help Show this message and exit. |
| 179 | + --help Show this message and exit. |
81 | 180 |
|
82 | 181 | Commands: |
83 | | - create-module - Scaffolds Ellar Application Module - |
84 | | - create-project - Scaffolds Ellar Application - |
85 | | - my-new-command - my_new_command cli description |
86 | | - new - Runs a complete Ellar project scaffold and creates... |
87 | | - runserver - Starts Uvicorn Server - |
88 | | - say-hi |
89 | | -``` |
90 | | - |
91 | | -## **Using Click Commands** |
92 | | -If prefer click commands, Ellar-CLI supports that too. Simply create a click command and register it to any module registered in |
93 | | -the `ApplicationModule`. For example |
94 | | - |
95 | | -```python |
96 | | -import click |
97 | | -from ellar.common import JSONResponse, Module, Response, exception_handler |
98 | | -from ellar.core import ModuleBase |
99 | | -from ellar.core.connection import Request |
| 182 | + make-migrations Create DB Migration |
| 183 | + migrate Applies Migrations |
100 | 184 |
|
101 | | -@click.command() |
102 | | -def say_hello(): |
103 | | - click.echo("Hello from ellar.") |
| 185 | +``` |
104 | 186 |
|
| 187 | +Having explored various methods for crafting commands and understanding the roles of `wrap_app_context` and `run_as_async` decorators, |
| 188 | +you now possess the knowledge to create diverse commands for your Ellar application. |
105 | 189 |
|
106 | | -@Module(commands=[say_hello]) |
107 | | -class ApplicationModule(ModuleBase): |
108 | | - @exception_handler(404) |
109 | | - def exception_404_handler(cls, request: Request, exc: Exception) -> Response: |
110 | | - return JSONResponse({"detail": "Resource not found."}) |
111 | | -``` |
| 190 | +It's crucial to keep in mind that any custom command you develop needs to be registered within a `@Module` class, which, |
| 191 | +in turn, should be registered with the `ApplicationModule`. |
| 192 | +This ensures that your commands are recognized and integrated into the Ellar application's command-line interface. |
0 commit comments