Skip to content

Commit 5ab49ad

Browse files
authored
Merge pull request #64 from python-ellar/non_pyproject_cli
Non-PyProject Ellar CLI Support
2 parents e535f65 + 09170e5 commit 5ab49ad

23 files changed

+568
-229
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
[![PyPI version](https://img.shields.io/pypi/pyversions/ellar-cli.svg)](https://pypi.python.org/pypi/ellar-cli)
1212

1313
# Introduction
14-
Ellar-CLI is an abstracted tool for ellar python web framework that helps in the standard project scaffolding and managing typer and click commands.
14+
Ellar-CLI is an abstracted tool for the Ellar web framework
15+
that helps in the standard project scaffolding and other application commands.
1516

1617
Ellar CLI is built on [Click](https://click.palletsprojects.com/en/8.1.x/)
1718

@@ -22,7 +23,7 @@ pip install ellar-cli
2223
```
2324

2425
## Usage
25-
To verify ellar-cli is working, run the command belove
26+
To verify ellar-cli is working, run the command below
2627
```shell
2728
ellar --help
2829
```
@@ -45,7 +46,6 @@ Commands:
4546
new - Runs a complete Ellar project scaffold and creates...
4647
runserver - Starts Uvicorn Server -
4748
say-hi
48-
4949
```
5050

5151
Full Documentation: [Here](https://python-ellar.github.io/ellar/cli/introduction/)

ellar_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Ellar CLI Tool for Scaffolding Ellar Projects, Modules and also running Ellar Commands"""
22

3-
__version__ = "0.3.2"
3+
__version__ = "0.3.3"

ellar_cli/click/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from .argument import Argument
4747
from .command import Command
4848
from .group import AppContextGroup, EllarCommandGroup
49-
from .util import with_app_context
49+
from .util import run_as_async, with_app_context
5050

5151

5252
def argument(
@@ -89,6 +89,8 @@ def group(
8989

9090
__all__ = [
9191
"argument",
92+
"run_as_async",
93+
"command",
9294
"Argument",
9395
"Option",
9496
"option",

ellar_cli/click/group.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import click
44
from ellar.app import AppFactory
55
from ellar.common.constants import MODULE_METADATA
6-
from ellar.core import ModuleSetup, reflector
6+
from ellar.core import ModuleBase, ModuleSetup, reflector
77

88
from ellar_cli.constants import ELLAR_META
9-
from ellar_cli.service import EllarCLIService
9+
from ellar_cli.service import EllarCLIService, EllarCLIServiceWithPyProject
1010

1111
from .command import Command
1212
from .util import with_app_context
@@ -45,29 +45,56 @@ def group(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
4545
class EllarCommandGroup(AppContextGroup):
4646
"""Overrides AppContextGroup to add loading of Ellar Application registered commands in the modules"""
4747

48+
def __init__(
49+
self, app_import_string: t.Optional[str] = None, **kwargs: t.Any
50+
) -> None:
51+
super().__init__(**kwargs)
52+
meta = None
53+
54+
if app_import_string:
55+
meta = EllarCLIServiceWithPyProject(app_import_string=app_import_string)
56+
57+
self._cli_meta: t.Optional[EllarCLIServiceWithPyProject] = meta
58+
4859
def _load_application_commands(self, ctx: click.Context) -> None:
4960
# get project option from cli
50-
app_name = ctx.params.get("project")
61+
module_configs: t.Any
62+
if self._cli_meta:
63+
application = self._cli_meta.import_application()
5164

52-
# loads project metadata from pyproject.toml
53-
meta_: t.Optional[EllarCLIService] = EllarCLIService.import_project_meta(
54-
app_name
55-
)
56-
ctx.meta[ELLAR_META] = meta_
65+
module_configs = (i for i in application.injector.get_modules().keys())
5766

58-
if meta_ and meta_.has_meta:
59-
module_configs = AppFactory.get_all_modules(
60-
ModuleSetup(meta_.import_root_module())
67+
ctx.meta[ELLAR_META] = self._cli_meta
68+
else:
69+
module_configs = ()
70+
app_name = ctx.params.get("project")
71+
72+
# loads project metadata from pyproject.toml
73+
meta_: t.Optional[EllarCLIService] = EllarCLIService.import_project_meta(
74+
app_name
6175
)
6276

63-
for module_config in module_configs:
64-
click_commands = (
65-
reflector.get(MODULE_METADATA.COMMANDS, module_config.module) or []
66-
)
77+
ctx.meta[ELLAR_META] = meta_
6778

68-
for click_command in click_commands:
69-
if isinstance(click_command, click.Command):
70-
self.add_command(click_command, click_command.name)
79+
if meta_ and meta_.has_meta:
80+
module_configs = (
81+
module_config.module
82+
for module_config in AppFactory.get_all_modules(
83+
ModuleSetup(meta_.import_root_module())
84+
)
85+
)
86+
self._find_commands_from_modules(module_configs)
87+
88+
def _find_commands_from_modules(
89+
self,
90+
modules: t.Union[t.Sequence[ModuleBase], t.Iterator[ModuleBase], t.Generator],
91+
) -> None:
92+
for module in modules:
93+
click_commands = reflector.get(MODULE_METADATA.COMMANDS, module) or []
94+
95+
for click_command in click_commands:
96+
if isinstance(click_command, click.Command):
97+
self.add_command(click_command, click_command.name)
7198

7299
def get_command(
73100
self, ctx: click.Context, cmd_name: str

ellar_cli/click/util.py

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,55 @@
11
import asyncio
2+
import functools
23
import typing as t
34
from functools import update_wrapper
45

56
import click
6-
from ellar.threading import execute_coroutine_with_sync_worker
7+
from ellar.threading.sync_worker import (
8+
execute_async_context_manager_with_sync_worker,
9+
execute_coroutine_with_sync_worker,
10+
)
711

812
from ellar_cli.constants import ELLAR_META
913
from ellar_cli.service import EllarCLIService
1014

1115

12-
def _async_run(future: t.Coroutine) -> t.Any:
13-
try:
14-
loop = asyncio.get_running_loop()
15-
except RuntimeError: # no event loop running:
16-
loop = asyncio.new_event_loop()
17-
18-
if not loop.is_running():
19-
try:
20-
res = loop.run_until_complete(loop.create_task(future))
21-
loop.run_until_complete(loop.shutdown_asyncgens())
22-
except Exception as e:
23-
raise e
24-
finally:
25-
loop.stop()
26-
loop.close()
27-
else:
28-
res = execute_coroutine_with_sync_worker(future)
29-
30-
return res
31-
32-
3316
def with_app_context(f: t.Callable) -> t.Any:
3417
"""
3518
Wraps a callback so that it's guaranteed to be executed with application context.
36-
Also, wrap commands can be a coroutine.
3719
"""
3820

39-
async def _run_with_application_context(
40-
meta_: t.Optional[EllarCLIService], *args: t.Any, **kwargs: t.Any
41-
) -> t.Any:
42-
if meta_ and meta_.has_meta:
43-
async with meta_.get_application_context():
44-
res = f(*args, **kwargs)
45-
if isinstance(res, t.Coroutine):
46-
return await res
47-
return res
48-
49-
res = f(*args, **kwargs)
50-
if isinstance(res, t.Coroutine):
51-
return await res
52-
5321
@click.pass_context
5422
def decorator(__ctx: click.Context, *args: t.Any, **kwargs: t.Any) -> t.Any:
55-
meta_ = __ctx.meta.get(ELLAR_META)
23+
meta_: t.Optional[EllarCLIService] = __ctx.meta.get(ELLAR_META)
5624

57-
def _get_command_args(*ar: t.Any, **kw: t.Any) -> t.Any:
58-
return _async_run(_run_with_application_context(meta_, *ar, **kw))
25+
if meta_ and meta_.has_meta:
26+
__ctx.with_resource(
27+
execute_async_context_manager_with_sync_worker(
28+
meta_.get_application_context()
29+
)
30+
)
5931

60-
return __ctx.invoke(update_wrapper(_get_command_args, f), *args, **kwargs)
32+
return __ctx.invoke(f, *args, **kwargs)
6133

6234
return update_wrapper(decorator, f)
35+
36+
37+
def run_as_async(f: t.Callable) -> t.Callable:
38+
"""
39+
Runs async click commands
40+
41+
eg:
42+
43+
@click.command()
44+
@click.argument('name')
45+
@click.run_as_async
46+
async def print_name(name: str):
47+
click.echo(f'Hello {name}, this is an async command.')
48+
"""
49+
assert asyncio.iscoroutinefunction(f), "Decorated function must be Coroutine"
50+
51+
@functools.wraps(f)
52+
def _decorator(*args: t.Any, **kw: t.Any) -> t.Any:
53+
return execute_coroutine_with_sync_worker(f(*args, **kw))
54+
55+
return _decorator

ellar_cli/main.py

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .click import EllarCommandGroup
1111
from .manage_commands import create_module, create_project, new_command, runserver
1212

13-
__all__ = ["app_cli"]
13+
__all__ = ["app_cli", "create_ellar_cli"]
1414

1515

1616
def version_callback(ctx: click.Context, _: t.Any, value: bool) -> None:
@@ -25,34 +25,40 @@ def version_callback(ctx: click.Context, _: t.Any, value: bool) -> None:
2525
raise click.Exit(0)
2626

2727

28-
@click.group(
29-
name="Ellar CLI Tool... ",
30-
cls=EllarCommandGroup,
31-
help="Ellar, ASGI Python Web framework",
32-
)
33-
@click.option(
34-
"--project",
35-
show_default=True,
36-
default="default",
37-
help="Run Specific Command on a specific project",
38-
)
39-
@click.option(
40-
"-v",
41-
"--version",
42-
callback=version_callback,
43-
help="Show the version and exit.",
44-
is_flag=True,
45-
expose_value=False,
46-
is_eager=True,
47-
)
48-
@click.pass_context
49-
def app_cli(ctx: click.Context, project: str) -> None:
50-
ctx.meta[ELLAR_PROJECT_NAME] = project
51-
52-
53-
app_cli.command(name="new")(new_command)
54-
app_cli.command(
55-
context_settings={"auto_envvar_prefix": "UVICORN"}, with_app_context=False
56-
)(runserver)
57-
app_cli.command(name="create-project")(create_project)
58-
app_cli.command(name="create-module")(create_module)
28+
def create_ellar_cli(app_import_string: t.Optional[str] = None) -> click.Group:
29+
@click.group(
30+
name="Ellar CLI Tool... ",
31+
cls=EllarCommandGroup,
32+
app_import_string=app_import_string,
33+
help="Ellar, ASGI Python Web framework",
34+
)
35+
@click.option(
36+
"--project",
37+
show_default=True,
38+
default="default",
39+
help="Run Specific Command on a specific project",
40+
)
41+
@click.option(
42+
"-v",
43+
"--version",
44+
callback=version_callback,
45+
help="Show the version and exit.",
46+
is_flag=True,
47+
expose_value=False,
48+
is_eager=True,
49+
)
50+
@click.pass_context
51+
def _app_cli(ctx: click.Context, **kwargs: t.Any) -> None:
52+
ctx.meta[ELLAR_PROJECT_NAME] = kwargs["project"]
53+
54+
if not app_import_string:
55+
_app_cli.command(name="new")(new_command)
56+
_app_cli.command(name="create-project")(create_project)
57+
58+
_app_cli.command(context_settings={"auto_envvar_prefix": "UVICORN"})(runserver)
59+
_app_cli.command(name="create-module")(create_module)
60+
61+
return _app_cli # type:ignore[no-any-return]
62+
63+
64+
app_cli = create_ellar_cli()

ellar_cli/manage_commands/runserver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from datetime import datetime
66

77
from ellar import __version__ as ellar_version
8-
from ellar.app import current_config
98
from ellar.common.utils.enums import create_enums_from_list
109
from uvicorn import config as uvicorn_config
1110
from uvicorn import run as uvicorn_run
@@ -382,6 +381,7 @@ def runserver(
382381
)
383382

384383
application_import_string = ellar_project_meta.project_meta.application
384+
current_config = ellar_project_meta.get_application_config()
385385

386386
log_config = current_config.LOGGING_CONFIG
387387
_log_level = current_config.LOG_LEVEL

ellar_cli/schema.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import typing as t
22

3+
from ellar.app import App
4+
from ellar.common.compatible import AttributeDict
35
from ellar.common.serializer import Serializer
6+
from ellar.core import ModuleBase
47
from ellar.pydantic import Field
58

69

10+
class MetadataStore(AttributeDict):
11+
app_instance: t.Optional[App]
12+
config_instance: t.Optional[App]
13+
is_app_reference_callable: bool
14+
root_module: t.Type[ModuleBase]
15+
16+
def __missing__(self, name) -> None:
17+
return None
18+
19+
720
class EllarPyProjectSerializer(Serializer):
821
project_name: str = Field(alias="project-name")
922
application: str = Field(alias="application")

ellar_cli/service/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from .cli import EllarCLIService, EllarCLIServiceWithPyProject
2+
from .exceptions import EllarCLIException
3+
from .pyproject import EllarPyProject
4+
5+
__all__ = [
6+
"EllarPyProject",
7+
"EllarCLIService",
8+
"EllarCLIServiceWithPyProject",
9+
"EllarCLIException",
10+
]

0 commit comments

Comments
 (0)