Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: improve cli #3

Merged
merged 14 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
25 changes: 25 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason behind this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks cleaner in my opinion, I can remove it if you want

app/
├─ templates/
│ ├─ index.html
Expand Down
61 changes: 56 additions & 5 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ lint.ignore = [
"PT",
"TD",
"PERF203", # ignore for now; investigate
"COM812",
"ISC001"
]
lint.select = ["ALL"]
# Allow unused variables when underscore-prefixed.
Expand Down
4 changes: 2 additions & 2 deletions src/litestar_manage/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()


Expand Down
48 changes: 39 additions & 9 deletions src/litestar_manage/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to make assumptions about what the virtual environment name is, nor do we want to hard code that assumption. Factor out the virtual environment name into a constant and possibly make a list of possible venv names to check against.

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)
1 change: 1 addition & 0 deletions src/litestar_manage/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VENV_NAME = ".venv"
16 changes: 12 additions & 4 deletions src/litestar_manage/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
import sysconfig
from dataclasses import dataclass
from pathlib import Path
from typing import Union

from jinja2 import Template


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.
Expand All @@ -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.
Expand Down Expand Up @@ -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"
Expand Down
12 changes: 0 additions & 12 deletions src/litestar_manage/template/app/controllers/web.py

This file was deleted.

18 changes: 18 additions & 0 deletions src/litestar_manage/templates/app/src/app_controller.py
Original file line number Diff line number Diff line change
@@ -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()
16 changes: 16 additions & 0 deletions src/litestar_manage/templates/app/src/app_service.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
Empty file.
6 changes: 6 additions & 0 deletions src/litestar_manage/templates/resource/controller.py.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from litestar.controller import Controller


class {{resource_name.capitalize()}}Controller(Controller):
"""{{resource_name.capitalize()}} Controller"""
pass
10 changes: 10 additions & 0 deletions src/litestar_manage/templates/resource/dto.py.jinja2
Original file line number Diff line number Diff line change
@@ -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()
Loading