diff --git a/.gitignore b/.gitignore index 7b6caf3..2f5d844 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ +.vscode/ +.ruff_cache/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a3884d9 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +# Variables +PYTHON := pdm run python +PYTEST := pdm run pytest +RUFF := pdm run ruff format +FIND := find + +# Directories +SRC_DIR := src +TEST_DIR := tests + +# Targets +.PHONY: clean test run + + +#Format the files +format: + $(RUFF) $(SRC_DIR) + $(RUFF) $(TEST_DIR) + +# Clean target to delete __pycache__ directories +clean: + $(FIND) . -type d -name "__pycache__" -exec rm -rf {} + + +test: + $(PYTEST) $(TEST_DIR) -vv -s --showlocals diff --git a/README.md b/README.md index 767f9a9..200fca9 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ project using the Litestar CLI. When the litestar-manage module is installed, it will extend the Litestar native CLI. To create a starter project, run the following command: -``` +```bash litestar project init --app-name MyProject ``` This command is used to initialize a Litestar project named MyProject in the current working directory. MyProject will have the following tree structure: -``` +```bash app/ ├─ templates/ │ ├─ index.html diff --git a/pdm.lock b/pdm.lock index d77c1f8..a980d39 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "lint", "test"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:16f390e458577185da2cbaeb42b42d1bb6dc2fd61506151b69574a9dda65b192" +content_hash = "sha256:d6036997a864485ccc3341e183c4f0bd8f5568e10e00e6943969ede35bd4e6d5" [[metadata.targets]] requires_python = ">=3.8" @@ -27,6 +27,46 @@ files = [ {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, ] +[[package]] +name = "black" +version = "24.8.0" +requires_python = ">=3.8" +summary = "The uncompromising code formatter." +groups = ["dev"] +dependencies = [ + "click>=8.0.0", + "mypy-extensions>=0.4.3", + "packaging>=22.0", + "pathspec>=0.9.0", + "platformdirs>=2", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions>=4.0.1; python_version < \"3.11\"", +] +files = [ + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] + [[package]] name = "certifi" version = "2024.7.4" @@ -942,7 +982,7 @@ name = "mypy-extensions" version = "1.0.0" requires_python = ">=3.5" summary = "Type system extensions for programs checked with the mypy type checker." -groups = ["lint"] +groups = ["dev", "lint"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -964,18 +1004,29 @@ name = "packaging" version = "24.1" requires_python = ">=3.8" summary = "Core utilities for Python packages" -groups = ["test"] +groups = ["dev", "test"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +requires_python = ">=3.8" +summary = "Utility library for gitignore style pattern matching of file paths." +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "platformdirs" version = "4.2.2" requires_python = ">=3.8" summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -groups = ["lint"] +groups = ["dev", "lint"] files = [ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, @@ -1268,7 +1319,7 @@ name = "tomli" version = "2.0.1" requires_python = ">=3.7" summary = "A lil' TOML parser" -groups = ["lint", "test"] +groups = ["dev", "lint", "test"] marker = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, diff --git a/pyproject.toml b/pyproject.toml index 914316c..60c7476 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,6 +150,8 @@ lint.ignore = [ "PT", "TD", "PERF203", # ignore for now; investigate + "COM812", + "ISC001" ] lint.select = ["ALL"] # Allow unused variables when underscore-prefixed. diff --git a/src/litestar_manage/__main__.py b/src/litestar_manage/__main__.py index bf259b9..4ab8040 100644 --- a/src/litestar_manage/__main__.py +++ b/src/litestar_manage/__main__.py @@ -12,7 +12,7 @@ def run_cli() -> None: """Application Entrypoint.""" import sys - from litestar_manage.cli import project_group + from litestar_manage.cli import project try: from litestar.__main__ import run_cli as run_litestar_cli @@ -26,7 +26,7 @@ def run_cli() -> None: print(exc) # noqa: T201 sys.exit(1) else: - litestar_group.add_command(cast(RichCommand, project_group)) + litestar_group.add_command(cast(RichCommand, project)) run_litestar_cli() diff --git a/src/litestar_manage/cli.py b/src/litestar_manage/cli.py index 36d8847..07e73e9 100644 --- a/src/litestar_manage/cli.py +++ b/src/litestar_manage/cli.py @@ -6,16 +6,22 @@ from click import group, option from litestar.cli._utils import LitestarGroup -from litestar_manage.renderer import RenderingContext, render_template +from litestar_manage.constants import VENV_NAME +from litestar_manage.renderer import AppRenderingContext, ResourceRenderingContext, render_template from litestar_manage.venv_builder import PipVenvBuilder, init_venv -@group(cls=LitestarGroup, name="project") -def project_group() -> None: - """Manage Scaffolding Tasks.""" +def is_project_initialized() -> bool: + output_dir = Path.cwd() + return (output_dir / "src").exists() or (output_dir / "venv").exists() + +@click.group(cls=LitestarGroup) +def project(): + pass -@project_group.command(name="init", help="Initialize a new Litestar project.") + +@project.command(name="new", help="Initialize a new Litestar project.") @option( "--app-name", type=str, @@ -31,14 +37,38 @@ def project_group() -> None: def init_project(app_name: str, venv: str | None) -> None: """CLI command to initialize a Litestar project""" - template_dir = Path(__file__).parent / "template" + template_dir = Path(__file__).parent / "templates" / "app" output_dir = Path.cwd() - ctx = RenderingContext(app_name=app_name) + if is_project_initialized(): + click.echo("Project already initialized.") + return + + ctx = AppRenderingContext(app_name=app_name) render_template(template_dir, output_dir, ctx, run_ruff=True) packages_to_install = ["litestar"] - venv_name = "venv" if venv == "pip": builder = PipVenvBuilder() - init_venv(output_dir / venv_name, builder, packages_to_install) + init_venv(output_dir / VENV_NAME, builder, packages_to_install) + + +@project.command(name="resource") +@option( + "--resource-name", + "-n", + type=str, + required=True, +) +def generate_resource(resource_name: str) -> None: + """CLI command to generate a new resource (controller, service, dto, models, repository)""" + + if not is_project_initialized(): + click.echo("Project not initialized. Please initialize the project first.") + return + + template_dir = Path(__file__).parent / "templates" / "resource" + output_dir = Path.cwd() / "src" / f"{resource_name.lower()}" + ctx = ResourceRenderingContext(resource_name=resource_name) + + render_template(template_dir, output_dir, ctx, run_ruff=True) diff --git a/src/litestar_manage/constants.py b/src/litestar_manage/constants.py new file mode 100644 index 0000000..143c33b --- /dev/null +++ b/src/litestar_manage/constants.py @@ -0,0 +1 @@ +VENV_NAME = ".venv" diff --git a/src/litestar_manage/renderer.py b/src/litestar_manage/renderer.py index b3bf8e1..e99a6d0 100644 --- a/src/litestar_manage/renderer.py +++ b/src/litestar_manage/renderer.py @@ -7,6 +7,7 @@ import sysconfig from dataclasses import dataclass from pathlib import Path +from typing import Union from jinja2 import Template @@ -14,7 +15,7 @@ def render_template( template_dir: Path, output_dir: Path, - ctx: RenderingContext, + ctx: Union[AppRenderingContext, ResourceRenderingContext], run_ruff: bool = True, ) -> None: """Renders a template from template_dir to output_dir using the provided context. @@ -36,16 +37,23 @@ def render_template( @dataclass -class RenderingContext: +class AppRenderingContext: """Context for rendering an application template.""" app_name: str +@dataclass +class ResourceRenderingContext: + """Context for rendering resource (controller, service, dto, model and repository).""" + + resource_name: str + + def _render_jinja_dir( input_directory: Path, output_directory: Path, - ctx: RenderingContext, + ctx: Union[AppRenderingContext, ResourceRenderingContext], ) -> list[Path]: """Recursively renders all files in the input directory to the output directory, while preserving the file-tree structure. Returns the list of paths to the created files. @@ -101,7 +109,7 @@ def find_ruff_bin() -> Path: if scripts_path.is_file(): return scripts_path - if sys.version_info >= (3, 10): # noqa: UP036 + if sys.version_info >= (3, 10): # noqa: UP036 user_scheme = sysconfig.get_preferred_scheme("user") elif os.name == "nt": user_scheme = "nt_user" diff --git a/src/litestar_manage/template/app/controllers/web.py b/src/litestar_manage/template/app/controllers/web.py deleted file mode 100644 index 6c0321b..0000000 --- a/src/litestar_manage/template/app/controllers/web.py +++ /dev/null @@ -1,12 +0,0 @@ -from litestar import Controller, get -from litestar.response import Template -from litestar.status_codes import HTTP_200_OK - - -class WebController(Controller): - """Web controller.""" - - @get(path="/", status_code=HTTP_200_OK) - async def index(self) -> Template: - """Serve site root.""" - return Template(template_name="index.html") diff --git a/src/litestar_manage/template/.gitignore b/src/litestar_manage/templates/.gitignore similarity index 100% rename from src/litestar_manage/template/.gitignore rename to src/litestar_manage/templates/.gitignore diff --git a/src/litestar_manage/template/README.md.jinja2 b/src/litestar_manage/templates/README.md.jinja2 similarity index 100% rename from src/litestar_manage/template/README.md.jinja2 rename to src/litestar_manage/templates/README.md.jinja2 diff --git a/src/litestar_manage/template/app/__init__.py b/src/litestar_manage/templates/app/__init__.py similarity index 100% rename from src/litestar_manage/template/app/__init__.py rename to src/litestar_manage/templates/app/__init__.py diff --git a/src/litestar_manage/template/app/controllers/__init__.py b/src/litestar_manage/templates/app/src/__init__.py similarity index 100% rename from src/litestar_manage/template/app/controllers/__init__.py rename to src/litestar_manage/templates/app/src/__init__.py diff --git a/src/litestar_manage/template/app/__main__.py b/src/litestar_manage/templates/app/src/__main__.py similarity index 100% rename from src/litestar_manage/template/app/__main__.py rename to src/litestar_manage/templates/app/src/__main__.py diff --git a/src/litestar_manage/templates/app/src/app_controller.py b/src/litestar_manage/templates/app/src/app_controller.py new file mode 100644 index 0000000..0b5681f --- /dev/null +++ b/src/litestar_manage/templates/app/src/app_controller.py @@ -0,0 +1,18 @@ +from typing import Dict + +from litestar import Controller, get +from litestar.di import Provide +from litestar.enums import MediaType +from litestar.status_codes import HTTP_200_OK +from src.app_service import AppService, provide_app_service + + +class AppController(Controller): + """App controller.""" + + dependencies = {"app_service": Provide(provide_app_service)} + + @get(path="/", status_code=HTTP_200_OK, media_type=MediaType.JSON) + async def index(self, app_service: AppService) -> Dict: + """App index""" + return await app_service.app_info() diff --git a/src/litestar_manage/templates/app/src/app_service.py b/src/litestar_manage/templates/app/src/app_service.py new file mode 100644 index 0000000..0282b8f --- /dev/null +++ b/src/litestar_manage/templates/app/src/app_service.py @@ -0,0 +1,16 @@ +from typing import Dict + + +class AppService: + """App Service""" + + app_name = "" + app_version = "0.1.0" + + async def app_info(self) -> Dict[str, str]: + """Return info about the app""" + return {"app_name": self.app_name, "verion": self.app_version} + + +def provide_app_service(): + return AppService() diff --git a/src/litestar_manage/template/app/assets/.gitkeep b/src/litestar_manage/templates/app/src/assets/.gitkeep similarity index 100% rename from src/litestar_manage/template/app/assets/.gitkeep rename to src/litestar_manage/templates/app/src/assets/.gitkeep diff --git a/src/litestar_manage/template/app/config.py b/src/litestar_manage/templates/app/src/config.py similarity index 100% rename from src/litestar_manage/template/app/config.py rename to src/litestar_manage/templates/app/src/config.py diff --git a/src/litestar_manage/template/app/app.py b/src/litestar_manage/templates/app/src/main.py similarity index 89% rename from src/litestar_manage/template/app/app.py rename to src/litestar_manage/templates/app/src/main.py index 46bc5b5..d1b0aa6 100644 --- a/src/litestar_manage/template/app/app.py +++ b/src/litestar_manage/templates/app/src/main.py @@ -18,15 +18,15 @@ def create_app() -> Litestar: from litestar.static_files import create_static_files_router from litestar.template.config import TemplateConfig - from app.config import assets_path, templates_path - from app.controllers.web import WebController + from .app_controller import AppController + from .config import assets_path, templates_path logging_middleware_config = LoggingMiddlewareConfig() session_config = CookieBackendConfig(secret=urandom(16)) return Litestar( route_handlers=[ - WebController, + AppController, create_static_files_router(path="/static", directories=[assets_path]), ], middleware=[session_config.middleware, logging_middleware_config.middleware], diff --git a/src/litestar_manage/template/app/templates/index.html.jinja2 b/src/litestar_manage/templates/app/src/templates/index.html.jinja2 similarity index 100% rename from src/litestar_manage/template/app/templates/index.html.jinja2 rename to src/litestar_manage/templates/app/src/templates/index.html.jinja2 diff --git a/src/litestar_manage/template/app/tests/__init__.py b/src/litestar_manage/templates/app/tests/__init__.py similarity index 100% rename from src/litestar_manage/template/app/tests/__init__.py rename to src/litestar_manage/templates/app/tests/__init__.py diff --git a/src/litestar_manage/templates/resource/__init__.py b/src/litestar_manage/templates/resource/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/litestar_manage/templates/resource/controller.py.jinja2 b/src/litestar_manage/templates/resource/controller.py.jinja2 new file mode 100644 index 0000000..a34276b --- /dev/null +++ b/src/litestar_manage/templates/resource/controller.py.jinja2 @@ -0,0 +1,6 @@ +from litestar.controller import Controller + + +class {{resource_name.capitalize()}}Controller(Controller): + """{{resource_name.capitalize()}} Controller""" + pass \ No newline at end of file diff --git a/src/litestar_manage/templates/resource/dto.py.jinja2 b/src/litestar_manage/templates/resource/dto.py.jinja2 new file mode 100644 index 0000000..32099fc --- /dev/null +++ b/src/litestar_manage/templates/resource/dto.py.jinja2 @@ -0,0 +1,10 @@ +from advanced_alchemy.extensions.litestar.dto import SQLAlchemyDTO, SQLAlchemyDTOConfig + +from src.{{resource_name.lower()}}.models import {{resource_name.capitalize()}} + + +class {{resource_name.capitalize()}}DTO(SQLAlchemyDTO[{{resource_name.capitalize()}}]): + """ + {{resource_name.capitalize()}} DTO + """ + config = SQLAlchemyDTOConfig() diff --git a/src/litestar_manage/templates/resource/models.py.jinja2 b/src/litestar_manage/templates/resource/models.py.jinja2 new file mode 100644 index 0000000..f17ce3e --- /dev/null +++ b/src/litestar_manage/templates/resource/models.py.jinja2 @@ -0,0 +1,7 @@ +from litestar.contrib.sqlalchemy.base import BigIntAuditBase +from sqlalchemy.orm import Mapped, mapped_column, relationship + + +class {{resource_name.capitalize()}}(BigIntAuditBase): + """{{resource_name.capitalize()}} in DB model""" + pass \ No newline at end of file diff --git a/src/litestar_manage/templates/resource/repository.py.jinja2 b/src/litestar_manage/templates/resource/repository.py.jinja2 new file mode 100644 index 0000000..7d65188 --- /dev/null +++ b/src/litestar_manage/templates/resource/repository.py.jinja2 @@ -0,0 +1,7 @@ +from litestar.contrib.sqlalchemy.repository import SQLAlchemyAsyncRepository + + +class {{resource_name.capitalize()}}Repository(SQLAlchemyAsyncRepository[{{resource_name.capitalize()}}]): # pylint: disable=duplicate-bases + """{{resource_name.capitalize()}} SQLAlchemy Repository.""" + + model_type = {{resource_name.capitalize()}} \ No newline at end of file diff --git a/src/litestar_manage/templates/resource/service.py.jinja2 b/src/litestar_manage/templates/resource/service.py.jinja2 new file mode 100644 index 0000000..3078d77 --- /dev/null +++ b/src/litestar_manage/templates/resource/service.py.jinja2 @@ -0,0 +1,13 @@ +from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService +from src.{{resource_name.lower()}}.repository import {{resource_name.capitalize()}}Repository +from src.{{resource_name.lower()}}.models import {{resource_name.capitalize()}} +from typing import Any + +class {{resource_name.capitalize()}}Service(SQLAlchemyAsyncRepositoryService[{{resource_name.capitalize()}}]): + """ {{resource_name.capitalize()}} Service""" + + repository_type = {{resource_name.capitalize()}}Repository + + def __init__(self, **repo_kwargs: Any) -> None: + self.repository: {{resource_name.capitalize()}}Repository = self.repository_type(**repo_kwargs) #type ignore + self.model_type = self.repository.model_type \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index e4de290..4003eaf 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,4 @@ from pathlib import Path -TEMPLATE_DIR = Path(__file__).parent.parent / "src" / "litestar_manage" / "template" +APP_TEMPLATE_DIR = Path(__file__).parent.parent / "src" / "litestar_manage" / "templates" / "app" +RESOURCE_TEMPLATE_DIR = Path(__file__).parent.parent / "src" / "litestar_manage" / "templates" / "resource" diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 9394e40..7bf218f 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -1,19 +1,30 @@ import pytest +from click.testing import CliRunner from testfixtures import TempDirectory -from litestar_manage.renderer import RenderingContext, _render_jinja_dir -from tests import TEMPLATE_DIR +from litestar_manage.cli import project +from litestar_manage.renderer import AppRenderingContext, _render_jinja_dir +from tests import APP_TEMPLATE_DIR @pytest.fixture -def rendering_context() -> RenderingContext: - return RenderingContext(app_name="TestApp") +def rendering_context() -> AppRenderingContext: + return AppRenderingContext(app_name="TestApp") -def test_render_jinja_dir(rendering_context: RenderingContext) -> None: +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +def test_render_jinja_dir(rendering_context: AppRenderingContext, runner: CliRunner) -> None: with TempDirectory() as t: temp_path = t.as_path() - _render_jinja_dir(TEMPLATE_DIR, temp_path, rendering_context) + _render_jinja_dir(APP_TEMPLATE_DIR, temp_path, rendering_context) + + assert (temp_path / "src").exists() + assert (temp_path / "tests").exists() + assert (temp_path / "src" / "main.py").exists() - assert (temp_path / "app").exists() - assert (temp_path / "app" / "app.py").exists() + result = runner.invoke(project, ["new", "--app-name", "TestApp"]) + assert result.exit_code == 0 diff --git a/tests/test_resource.py b/tests/test_resource.py new file mode 100644 index 0000000..4a9c4bd --- /dev/null +++ b/tests/test_resource.py @@ -0,0 +1,74 @@ +from pathlib import Path + +import pytest +from click.testing import CliRunner +from testfixtures import TempDirectory + +from litestar_manage.cli import project +from litestar_manage.renderer import AppRenderingContext, ResourceRenderingContext, _render_jinja_dir +from tests import RESOURCE_TEMPLATE_DIR, APP_TEMPLATE_DIR + + +@pytest.fixture +def rendering_context() -> ResourceRenderingContext: + return ResourceRenderingContext(resource_name="User") + + +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +def create_mock_project_structure(temp_path: Path) -> Path: + ctx = AppRenderingContext(app_name="app_name") + _render_jinja_dir(APP_TEMPLATE_DIR, temp_path, ctx) + + return temp_path / "src" + + +def test_render_resource_dir(rendering_context: ResourceRenderingContext) -> None: + with TempDirectory() as t: + temp_path = t.as_path() + + src_path = create_mock_project_structure(temp_path) + + resource_path = src_path / "user" + + assert src_path.exists() + + _render_jinja_dir(RESOURCE_TEMPLATE_DIR, temp_path / resource_path, rendering_context) + + assert (temp_path / "src").exists() + assert (temp_path / "tests").exists() + assert (temp_path / "src" / "main.py").exists() + assert (temp_path / "src" / "app_controller.py").exists() + assert (temp_path / "src" / "app_service.py").exists() + assert (temp_path / "src" / "user").exists() + + assert (temp_path / "src" / "user" / "__init__.py").exists() + assert (temp_path / "src" / "user" / "controller.py").exists() + assert (temp_path / "src" / "user" / "service.py").exists() + assert (temp_path / "src" / "user" / "repository.py").exists() + assert (temp_path / "src" / "user" / "dto.py").exists() + assert (temp_path / "src" / "user" / "models.py").exists() + + +def test_render_resource_dir_no_app(rendering_context: ResourceRenderingContext, runner: CliRunner) -> None: + with TempDirectory() as t: + temp_path = t.as_path() + + _render_jinja_dir(RESOURCE_TEMPLATE_DIR, temp_path, rendering_context) + + assert not (temp_path / "src").exists() + assert not (temp_path / "src" / "user" / "__init__.py").exists() + assert not (temp_path / "src" / "user" / "controller.py").exists() + assert not (temp_path / "src" / "user" / "service.py").exists() + assert not (temp_path / "src" / "user" / "repository.py").exists() + assert not (temp_path / "src" / "user" / "dto.py").exists() + assert not (temp_path / "src" / "user" / "models.py").exists() + + expected = "Project already initialized." + result = runner.invoke(project, ["new", "--app-name", "TestApp"]) + + assert result.exit_code == 0 + assert expected in result.output diff --git a/tests/test_venv_builder.py b/tests/test_venv_builder.py index 18c0256..0b1d27b 100644 --- a/tests/test_venv_builder.py +++ b/tests/test_venv_builder.py @@ -9,7 +9,7 @@ def test_pip_venv_builder() -> None: with TempDirectory() as t: temp_path = t.as_path() builder = PipVenvBuilder(nopip=True) - builder.install_packages = MagicMock() # type: ignore[method-assign] + builder.install_packages = MagicMock() # type: ignore[method-assign] init_venv(temp_path, builder, ["litestar"]) assert builder.venv_path is not None