diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index aef7f70..506a8ae 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,6 +23,6 @@ jobs: - name: Build distribution run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.8.14 + uses: pypa/gh-action-pypi-publish@v1.9.0 with: password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 56a99cb..a11167f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,4 +22,4 @@ jobs: - name: Test run: make test-cov - name: Coverage - uses: codecov/codecov-action@v4.1.1 + uses: codecov/codecov-action@v4.5.0 diff --git a/ellar_cli/__init__.py b/ellar_cli/__init__.py index 5d9abde..f885a2a 100644 --- a/ellar_cli/__init__.py +++ b/ellar_cli/__init__.py @@ -1,3 +1,3 @@ """Ellar CLI Tool for Scaffolding Ellar Projects, Modules and also running Ellar Commands""" -__version__ = "0.4.1" +__version__ = "0.4.3" diff --git a/ellar_cli/click/__init__.py b/ellar_cli/click/__init__.py index 8ea5faa..c0f1326 100644 --- a/ellar_cli/click/__init__.py +++ b/ellar_cli/click/__init__.py @@ -42,12 +42,12 @@ from click.utils import get_binary_stream as get_binary_stream from click.utils import get_text_stream as get_text_stream from click.utils import open_file as open_file -from ellar.threading import run_as_async +from ellar.threading import run_as_sync from .argument import Argument from .command import Command from .group import AppContextGroup, EllarCommandGroup -from .util import with_app_context +from .util import with_injector_context def argument( @@ -90,14 +90,14 @@ def group( __all__ = [ "argument", - "run_as_async", + "run_as_sync", "command", "Argument", "Option", "option", "AppContextGroup", "EllarCommandGroup", - "with_app_context", + "with_injector_context", "Context", "Group", "Parameter", diff --git a/ellar_cli/click/group.py b/ellar_cli/click/group.py index f715754..e6ca6bf 100644 --- a/ellar_cli/click/group.py +++ b/ellar_cli/click/group.py @@ -1,45 +1,54 @@ +import inspect import typing as t import click from ellar.app import AppFactory from ellar.common.constants import MODULE_METADATA from ellar.core import ModuleBase, ModuleSetup, reflector +from ellar.threading import run_as_sync from ellar_cli.constants import ELLAR_META from ellar_cli.service import EllarCLIService, EllarCLIServiceWithPyProject from .command import Command -from .util import with_app_context +from .util import with_injector_context class AppContextGroup(click.Group): """This works similar to a regular click.Group, but it changes the behavior of the command decorator so that it - automatically wraps the functions in `pass_with_app_context`. + automatically wraps the functions in `with_injector_context`. """ command_class = Command - def command(self, *args: t.Any, **kwargs: t.Any) -> t.Callable: # type:ignore[override] + def command( # type:ignore[override] + self, *args: t.Any, **kwargs: t.Any + ) -> t.Union[t.Callable[[t.Callable[..., t.Any]], click.Command], click.Command]: """The same with click.Group command. - It only decorates the command function to be run under `applicationContext`. - It's disabled by passing `with_app_context=False`. + It only decorates the command function to be run under `ellar.core.with_injector_context`. + It's disabled by passing `with_injector_context=False`. """ - wrap_for_ctx = kwargs.pop("with_app_context", True) + wrap_for_ctx = kwargs.pop("with_injector_context", True) def decorator(f: t.Callable) -> t.Any: + if inspect.iscoroutinefunction(f): + f = run_as_sync(f) + if wrap_for_ctx: - f = with_app_context(f) - return click.Group.command(self, *args, **kwargs)(f) + f = with_injector_context(f) + return super(AppContextGroup, self).command(*args, **kwargs)(f) return decorator - def group(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + def group( # type:ignore[override] + self, *args: t.Any, **kwargs: t.Any + ) -> t.Union[t.Callable[[t.Callable[..., t.Any]], click.Group], click.Group]: """This works exactly like the method of the same name on a regular click.Group but it defaults the group class to `AppGroup`. """ - kwargs.setdefault("cls", AppContextGroup) - return click.Group.group(self, *args, **kwargs) + kwargs.setdefault("cls", self.__class__) + return super(AppContextGroup, self).group(*args, **kwargs) # type:ignore[no-any-return] class EllarCommandGroup(AppContextGroup): @@ -62,7 +71,9 @@ def _load_application_commands(self, ctx: click.Context) -> None: if self._cli_meta: application = self._cli_meta.import_application() - module_configs = (i for i in application.injector.get_modules().keys()) + module_configs = ( + i for i in application.injector.tree_manager.modules.keys() + ) ctx.meta[ELLAR_META] = self._cli_meta else: @@ -77,12 +88,10 @@ def _load_application_commands(self, ctx: click.Context) -> None: ctx.meta[ELLAR_META] = meta_ if meta_ and meta_.has_meta: - module_configs = ( - module_config.module - for module_config in AppFactory.get_all_modules( - ModuleSetup(meta_.import_root_module()) - ) + tree_manager = AppFactory.read_all_module( + ModuleSetup(meta_.import_root_module()) ) + module_configs = tree_manager.modules.keys() self._find_commands_from_modules(module_configs) def _find_commands_from_modules( diff --git a/ellar_cli/click/util.py b/ellar_cli/click/util.py index 4f94472..096d450 100644 --- a/ellar_cli/click/util.py +++ b/ellar_cli/click/util.py @@ -10,9 +10,9 @@ from ellar_cli.service import EllarCLIService -def with_app_context(f: t.Callable) -> t.Any: +def with_injector_context(f: t.Callable) -> t.Any: """ - Wraps a callback so that it's guaranteed to be executed with application context. + Wraps a callback so that it's guaranteed to be executed with `ellar.core.with_injector_context` contextmanager. """ @click.pass_context diff --git a/ellar_cli/main.py b/ellar_cli/main.py index 53964d0..31dc03c 100644 --- a/ellar_cli/main.py +++ b/ellar_cli/main.py @@ -25,7 +25,7 @@ def version_callback(ctx: click.Context, _: t.Any, value: bool) -> None: raise click.Exit(0) -def create_ellar_cli(app_import_string: t.Optional[str] = None) -> click.Group: +def create_ellar_cli(app_import_string: t.Optional[str] = None) -> EllarCommandGroup: @click.group( name="Ellar CLI Tool... ", cls=EllarCommandGroup, @@ -52,11 +52,11 @@ def _app_cli(ctx: click.Context, **kwargs: t.Any) -> None: ctx.meta[ELLAR_PROJECT_NAME] = kwargs["project"] if not app_import_string: - _app_cli.command(name="new")(new_command) - _app_cli.command(name="create-project")(create_project) + _app_cli.add_command(new_command) + _app_cli.add_command(create_project) - _app_cli.command(context_settings={"auto_envvar_prefix": "UVICORN"})(runserver) - _app_cli.command(name="create-module")(create_module) + _app_cli.add_command(runserver) + _app_cli.add_command(create_module) return _app_cli # type:ignore[no-any-return] diff --git a/ellar_cli/manage_commands/create_module.py b/ellar_cli/manage_commands/create_module.py index c17c913..9d27898 100644 --- a/ellar_cli/manage_commands/create_module.py +++ b/ellar_cli/manage_commands/create_module.py @@ -8,10 +8,9 @@ import ellar_cli.click as eClick from ellar_cli import scaffolding +from ellar_cli.file_scaffolding import FileTemplateScaffold from ellar_cli.schema import EllarScaffoldSchema - -from ..file_scaffolding import FileTemplateScaffold -from ..service import EllarCLIException +from ellar_cli.service import EllarCLIException __all__ = ["create_module"] @@ -72,12 +71,14 @@ def get_scaffolding_context(self, working_project_name: str) -> t.Dict: return template_context +@eClick.command(name="create-module") @eClick.argument("module_name") @eClick.argument( "directory", help="The name of a new directory to scaffold the module into.", required=False, ) +@eClick.with_injector_context def create_module(module_name: str, directory: t.Optional[str]): """- Scaffolds Ellar Application Module -""" diff --git a/ellar_cli/manage_commands/create_project.py b/ellar_cli/manage_commands/create_project.py index 27b3287..26ec5c0 100644 --- a/ellar_cli/manage_commands/create_project.py +++ b/ellar_cli/manage_commands/create_project.py @@ -106,6 +106,7 @@ def on_scaffold_completed(self) -> None: print("Happy coding!") +@eClick.command(name="create-project") @eClick.argument("project_name", help="Project Name") @eClick.argument( "directory", @@ -119,6 +120,7 @@ def on_scaffold_completed(self) -> None: help="Create a new without including `pyproject.toml`.", ) @eClick.pass_context +@eClick.with_injector_context def create_project( ctx: eClick.Context, project_name: str, directory: t.Optional[str], plain: bool ): diff --git a/ellar_cli/manage_commands/new.py b/ellar_cli/manage_commands/new.py index c23648c..044f1ed 100644 --- a/ellar_cli/manage_commands/new.py +++ b/ellar_cli/manage_commands/new.py @@ -130,6 +130,7 @@ def get_project_cwd(self) -> str: return os.path.join(self._working_directory, self._working_project_name) +@eClick.command(name="new") @eClick.argument( "project_name", help="Project Module Name. Defaults to `project-name` if not set", @@ -145,6 +146,7 @@ def get_project_cwd(self) -> str: default=False, help="Create a new without including `pyproject.toml`.", ) +@eClick.with_injector_context def new_command(project_name: str, directory: t.Optional[str], plain: bool): """- Runs a complete Ellar project scaffold and creates all files required for managing you application -""" root_scaffold_template_path = new_template_template_path diff --git a/ellar_cli/manage_commands/runserver.py b/ellar_cli/manage_commands/runserver.py index 71e0ef5..a1ce908 100644 --- a/ellar_cli/manage_commands/runserver.py +++ b/ellar_cli/manage_commands/runserver.py @@ -37,6 +37,7 @@ INTERFACE_CHOICES = eClick.Choice(INTERFACES) +@eClick.command(name="runserver", context_settings={"auto_envvar_prefix": "UVICORN"}) @eClick.option( "--host", type=str, diff --git a/ellar_cli/scaffolding/module_template/module_name/module.ellar b/ellar_cli/scaffolding/module_template/module_name/module.ellar index 0ce7c11..5da3cea 100644 --- a/ellar_cli/scaffolding/module_template/module_name/module.ellar +++ b/ellar_cli/scaffolding/module_template/module_name/module.ellar @@ -12,13 +12,13 @@ base_directory=`default is the `{{module_name}}` folder` ) class MyModule(ModuleBase): - def register_providers(self, container: Container) -> None: + def register_providers(self, moduleRef: ModuleRefBase) -> None: # for more complicated provider registrations pass """ from ellar.common import Module -from ellar.core import ModuleBase +from ellar.core.modules import ModuleBase, ModuleRefBase from ellar.di import Container from .controllers import {{module_name | capitalize}}Controller @@ -34,5 +34,5 @@ class {{module_name | capitalize}}Module(ModuleBase): {{module_name | capitalize}} Module """ - def register_providers(self, container: Container) -> None: + def register_providers(self, moduleRef: ModuleRefBase) -> None: """for more complicated provider registrations, use container.register_instance(...) """ diff --git a/ellar_cli/scaffolding/module_template/setup.json b/ellar_cli/scaffolding/module_template/setup.json index a91f0ec..6ff7712 100644 --- a/ellar_cli/scaffolding/module_template/setup.json +++ b/ellar_cli/scaffolding/module_template/setup.json @@ -14,9 +14,6 @@ { "name": "module.ellar" }, - { - "name": "routers.ellar" - }, { "name": "schemas.ellar" }, diff --git a/ellar_cli/scaffolding/project_template/project_name/config.ellar b/ellar_cli/scaffolding/project_template/project_name/config.ellar index 4bb2892..fea55c5 100644 --- a/ellar_cli/scaffolding/project_template/project_name/config.ellar +++ b/ellar_cli/scaffolding/project_template/project_name/config.ellar @@ -9,6 +9,7 @@ export ELLAR_CONFIG_MODULE={{project_name}}.config:DevelopmentConfig import typing as t from ellar.pydantic import ENCODERS_BY_TYPE as encoders_by_type +from starlette.requests import Request from starlette.middleware import Middleware from ellar.common import IExceptionHandler, JSONResponse from ellar.core import ConfigDefaultTypesMixin @@ -29,6 +30,15 @@ class BaseConfig(ConfigDefaultTypesMixin): # https://jinja.palletsprojects.com/en/3.0.x/api/#high-level-api JINJA_TEMPLATES_OPTIONS: t.Dict[str, t.Any] = {} + # Injects context to jinja templating context values + TEMPLATES_CONTEXT_PROCESSORS:t.List[ + t.Union[str, t.Callable[[t.Union[Request]], t.Dict[str, t.Any]]] + ] = [ + "ellar.core.templating.context_processors:request_context", + "ellar.core.templating.context_processors:user", + "ellar.core.templating.context_processors:request_state", + ] + # Application route versioning scheme VERSIONING_SCHEME: BaseAPIVersioning = DefaultAPIVersioning() @@ -51,14 +61,24 @@ class BaseConfig(ConfigDefaultTypesMixin): ALLOWED_HOSTS: t.List[str] = ["*"] # Application middlewares - MIDDLEWARE: t.Sequence[Middleware] = [] + MIDDLEWARE: t.Union[str, Middleware] = [ + "ellar.core.middleware.trusted_host:trusted_host_middleware", + "ellar.core.middleware.cors:cors_middleware", + "ellar.core.middleware.errors:server_error_middleware", + "ellar.core.middleware.versioning:versioning_middleware", + "ellar.auth.middleware.session:session_middleware", + "ellar.auth.middleware.auth:identity_middleware", + "ellar.core.middleware.exceptions:exception_middleware", + ] # A dictionary mapping either integer status codes, # or exception class types onto callables which handle the exceptions. # Exception handler callables should be of the form - # `handler(context:IExecutionContext, exc: Exception) -> response` + # `handler(context:IExecutionContext, exc: Exception) -> response` # and may be either standard functions, or async functions. - EXCEPTION_HANDLERS: t.List[IExceptionHandler] = [] + EXCEPTION_HANDLERS: t.Union[str, IExceptionHandler] = [ + "ellar.core.exceptions:error_404_handler" + ] # Object Serializer custom encoders SERIALIZER_CUSTOM_ENCODER: t.Dict[ diff --git a/ellar_cli/scaffolding/project_template/project_name/root_module.ellar b/ellar_cli/scaffolding/project_template/project_name/root_module.ellar index 78e7a4d..8d03f47 100644 --- a/ellar_cli/scaffolding/project_template/project_name/root_module.ellar +++ b/ellar_cli/scaffolding/project_template/project_name/root_module.ellar @@ -1,10 +1,8 @@ -from ellar.common import Module, exception_handler, IExecutionContext, JSONResponse, Response +from ellar.common import Module from ellar.core import ModuleBase from ellar.samples.modules import HomeModule @Module(modules=[HomeModule]) class ApplicationModule(ModuleBase): - @exception_handler(404) - def exception_404_handler(cls, ctx: IExecutionContext, exc: Exception) -> Response: - return JSONResponse({"detail": "Resource not found."}, status_code=404) + pass diff --git a/ellar_cli/scaffolding/project_template/project_name/server.ellar b/ellar_cli/scaffolding/project_template/project_name/server.ellar index 50c52b0..60eb6cd 100644 --- a/ellar_cli/scaffolding/project_template/project_name/server.ellar +++ b/ellar_cli/scaffolding/project_template/project_name/server.ellar @@ -25,7 +25,7 @@ def bootstrap() -> App: # .add_server('/', description='Development Server') # # document = document_builder.build_document(application) - # module = OpenAPIDocumentModule.setup( + # OpenAPIDocumentModule.setup( # app=application, # document=document, # docs_ui=SwaggerUI(), diff --git a/ellar_cli/scaffolding/project_template/setup.json b/ellar_cli/scaffolding/project_template/setup.json index 0a76eca..790dd98 100644 --- a/ellar_cli/scaffolding/project_template/setup.json +++ b/ellar_cli/scaffolding/project_template/setup.json @@ -5,24 +5,6 @@ "name": "project_name", "is_directory": "true", "files": [ - { - "name": "core", - "is_directory": "true", - "files": [ - { - "name": "__init__.ellar" - } - ] - }, - { - "name": "domain", - "is_directory": "true", - "files": [ - { - "name": "__init__.ellar" - } - ] - }, { "name": "__init__.ellar" }, diff --git a/ellar_cli/service/cli.py b/ellar_cli/service/cli.py index d56869d..5203c7a 100644 --- a/ellar_cli/service/cli.py +++ b/ellar_cli/service/cli.py @@ -5,9 +5,9 @@ import typing as t from ellar.app import App -from ellar.app.context import ApplicationContext from ellar.common.constants import ELLAR_CONFIG_MODULE -from ellar.core import Config, ModuleBase +from ellar.core import Config, ModuleBase, injector_context +from ellar.di import EllarInjector from ellar.utils.importer import import_from_string, module_import from tomlkit import dumps as tomlkit_dumps from tomlkit import parse as tomlkit_parse @@ -179,7 +179,7 @@ def _import_and_validate_application( ) if is_callable: - app = app() # type:ignore[call-arg] + app = app() if not isinstance(app, App): raise EllarCLIException( @@ -232,9 +232,9 @@ def import_root_module(self) -> t.Type["ModuleBase"]: return self._store.root_module @_export_ellar_config_module - def get_application_context(self) -> ApplicationContext: + def get_application_context(self) -> t.AsyncGenerator[EllarInjector, t.Any]: app = t.cast(App, self.import_application()) - return app.application_context() + return injector_context(app.injector) class EllarCLIServiceWithPyProject(EllarCLIService): diff --git a/pyproject.toml b/pyproject.toml index b7cbe80..f58a750 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,8 +42,8 @@ classifiers = [ dependencies = [ # exclude 0.11.2 and 0.11.3 due to https://github.com/sdispater/tomlkit/issues/225 "tomlkit >=0.11.1,<1.0.0,!=0.11.2,!=0.11.3", - "uvicorn[standard] == 0.29.0", - "ellar >= 0.7.4", + "ellar >= 0.8.1", + "uvicorn[standard] == 0.30.4", "click >= 8.1.7", ] diff --git a/requirements-tests.txt b/requirements-tests.txt index f99dd74..cf1b39c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,6 +1,6 @@ autoflake -mypy == 1.9.0 +mypy == 1.11.1 pytest >=7.1.3,<9.0.0 pytest-asyncio pytest-cov >=2.12.0,<6.0.0 -ruff ==0.3.4 +ruff ==0.6.1 diff --git a/tests/click/test_app_context_group.py b/tests/click/test_app_context_group.py index e05d9c8..5992e5a 100644 --- a/tests/click/test_app_context_group.py +++ b/tests/click/test_app_context_group.py @@ -1,5 +1,4 @@ -from ellar.app import current_injector -from ellar.core import Config +from ellar.core import Config, current_injector from ellar_cli.click.group import AppContextGroup @@ -11,7 +10,7 @@ def app_group(): pass -@app_group.command(with_app_context=False) +@app_group.command(with_injector_context=False) def invoke_without_app_context(): assert current_injector.get(Config) print("Application Context wont be initialized.") diff --git a/tests/sample_app/example_project/commands.py b/tests/sample_app/example_project/commands.py index 33a168c..1a07c0c 100644 --- a/tests/sample_app/example_project/commands.py +++ b/tests/sample_app/example_project/commands.py @@ -1,5 +1,4 @@ -from ellar.app import current_injector -from ellar.core import Config +from ellar.core import Config, current_injector import ellar_cli.click as click @@ -27,7 +26,7 @@ def say_hello(): @db.command(name="command-with-context") -@click.with_app_context +@click.with_injector_context def command_with_app_context(): print( f"Running a command with application context - {current_injector.get(Config).APPLICATION_NAME}" diff --git a/tests/sample_app/plain_project/plain_project/core/__init__.py b/tests/sample_app/plain_project/plain_project/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/sample_app/plain_project/plain_project/domain/__init__.py b/tests/sample_app/plain_project/plain_project/domain/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/sample_app/plain_project/plain_project/root_module.py b/tests/sample_app/plain_project/plain_project/root_module.py index 09a24e1..241af95 100644 --- a/tests/sample_app/plain_project/plain_project/root_module.py +++ b/tests/sample_app/plain_project/plain_project/root_module.py @@ -1,4 +1,3 @@ -from ellar.app import current_injector from ellar.common import ( IExecutionContext, IModuleSetup, @@ -7,7 +6,7 @@ Response, exception_handler, ) -from ellar.core import Config, DynamicModule, ModuleBase +from ellar.core import Config, DynamicModule, ModuleBase, current_injector from ellar.samples.modules import HomeModule import ellar_cli.click as click @@ -18,7 +17,7 @@ class DynamicCommandModule(ModuleBase, IModuleSetup): @classmethod def setup(cls) -> "DynamicModule": @click.command() - @click.with_app_context + @click.with_injector_context def plain_project(): """Project 2 Custom Command""" assert isinstance(current_injector.get(Config), Config) diff --git a/tests/test_ellar_commands/test_create_module_command.py b/tests/test_ellar_commands/test_create_module_command.py index eadb15d..7490c1e 100644 --- a/tests/test_ellar_commands/test_create_module_command.py +++ b/tests/test_ellar_commands/test_create_module_command.py @@ -21,13 +21,12 @@ def test_create_module_with_directory_case_1(process_runner, tmpdir): files = os.listdir(module_path) for file in [ - "module.py", - "tests", - "routers.py", "services.py", "controllers.py", - "schemas.py", + "tests", "__init__.py", + "schemas.py", + "module.py", ]: assert file in files @@ -42,13 +41,12 @@ def test_create_module_with_directory_case_2(process_runner, tmpdir): files = os.listdir(tmpdir / "test_new_module") for file in [ - "module.py", - "tests", - "routers.py", "services.py", "controllers.py", - "schemas.py", + "tests", "__init__.py", + "schemas.py", + "module.py", ]: assert file in files @@ -108,13 +106,12 @@ def test_create_module_works(tmpdir, process_runner, write_empty_py_project): files = os.listdir(module_path) for file in [ - "module.py", - "tests", - "routers.py", "services.py", "controllers.py", - "schemas.py", + "tests", "__init__.py", + "schemas.py", + "module.py", ]: assert file in files diff --git a/tests/test_without_context.py b/tests/test_without_context.py index a4de512..89d5ccc 100644 --- a/tests/test_without_context.py +++ b/tests/test_without_context.py @@ -10,14 +10,14 @@ def without_context_command(): @app_cli.command() -@click.run_as_async +@click.run_as_sync async def without_context_command_async(): print("Working outside context Async") @click.command() @click.argument("name") -@click.run_as_async +@click.run_as_sync async def print_name(name: str): click.echo(f"Hello {name}, this is an async command.") @@ -43,6 +43,6 @@ def test_print_name_works(cli_runner): def test_run_as_async_fails(): with pytest.raises(AssertionError): - @click.run_as_async + @click.run_as_sync def print_name_1(name: str): pass
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: