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 1 commit
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
7 changes: 7 additions & 0 deletions .vscode/settings.json
Copy link
Owner

Choose a reason for hiding this comment

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

Remove IDE configuration files from the commit.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
24 changes: 24 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Variables
PYTHON := pdm run python
PYTEST := pdm run pytest
BLACK := pdm run black
FIND := find

# Directories
SRC_DIR := src
TEST_DIR := tests

# Targets
.PHONY: clean test run


#Format the files
format:
$(BLACK) $(SRC_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.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ distribution = true
[tool.pdm.dev-dependencies]
dev = [
"litestar[standard,cryptography,jinja]>=2.9.1",
"black>=24.8.0",
Copy link
Owner

Choose a reason for hiding this comment

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

The project uses ruff for linting. Remove black dependency and lint the PR using ruff.

]
lint = [
"pyright>=1.1.376",
Expand Down
45 changes: 37 additions & 8 deletions src/litestar_manage/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@
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 cli():
pass
Copy link
Owner

Choose a reason for hiding this comment

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

This is meant to be an extension of the LItestar's CLI, so it doesn't really make sense for the group to be named CLI. Rename this group back to project.


@project_group.command(name="init", help="Initialize a new Litestar project.")

@cli.command(name="new", help="Initialize a new Litestar project.")
@option(
"--app-name",
type=str,
Expand All @@ -31,14 +36,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 = RenderingContext(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", builder, packages_to_install)


@cli.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 = RenderingContext(app_name=resource_name)

render_template(template_dir, output_dir, ctx, run_ruff=True)
2 changes: 1 addition & 1 deletion src/litestar_manage/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,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 {{app_name.capitalize()}}Controller(Controller):
"""{{app_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.{{app_name.lower()}}.models import {{app_name.capitalize()}}


class {{app_name.capitalize()}}DTO(SQLAlchemyDTO[{{app_name.capitalize()}}]):
"""
{{app_name.capitalize()}} DTO
"""
config = SQLAlchemyDTOConfig()
7 changes: 7 additions & 0 deletions src/litestar_manage/templates/resource/models.py.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from litestar.contrib.sqlalchemy.base import BigIntAuditBase
from sqlalchemy.orm import Mapped, mapped_column, relationship


class {{app_name.capitalize()}}(BigIntAuditBase):
"""{{app_name.capitalize()}} in DB model"""
pass
7 changes: 7 additions & 0 deletions src/litestar_manage/templates/resource/repository.py.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from litestar.contrib.sqlalchemy.repository import SQLAlchemyAsyncRepository


class {{app_name.capitalize()}}Repository(SQLAlchemyAsyncRepository[{{app_name.capitalize()}}]): # pylint: disable=duplicate-bases
"""{{app_name.capitalize()}} SQLAlchemy Repository."""

model_type = {{app_name.capitalize()}}
13 changes: 13 additions & 0 deletions src/litestar_manage/templates/resource/service.py.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService
from src.{{app_name.lower()}}.repository import {{app_name.capitalize()}}Repository
from src.{{app_name.lower()}}.models import {{app_name.capitalize()}}
from typing import Any

class {{app_name.capitalize()}}Service(SQLAlchemyAsyncRepositoryService[{{app_name.capitalize()}}]):
""" {{app_name.capitalize()}} Service"""

repository_type = {{app_name.capitalize()}}Repository

def __init__(self, **repo_kwargs: Any) -> None:
self.repository: {{app_name.capitalize()}}Repository = self.repository_type(**repo_kwargs) #type ignore
self.model_type = self.repository.model_type
3 changes: 2 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path

TEMPLATE_DIR = Path(__file__).parent.parent / "src" / "litestar_manage" / "template"
TEMPLATE_DIR = Path(__file__).parent.parent / "src" / "litestar_manage" / "templates" / "app"
RESOURCE_DIR = Path(__file__).parent.parent / "src" / "litestar_manage" / "templates" / "resource"
17 changes: 14 additions & 3 deletions tests/test_renderer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
from click.testing import CliRunner
from testfixtures import TempDirectory

from litestar_manage.cli import cli
from litestar_manage.renderer import RenderingContext, _render_jinja_dir
from tests import TEMPLATE_DIR

Expand All @@ -10,10 +12,19 @@ def rendering_context() -> RenderingContext:
return RenderingContext(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: RenderingContext, runner: CliRunner) -> None:
with TempDirectory() as t:
temp_path = t.as_path()
_render_jinja_dir(TEMPLATE_DIR, temp_path, rendering_context)

assert (temp_path / "app").exists()
assert (temp_path / "app" / "app.py").exists()
assert (temp_path / "src").exists()
assert (temp_path / "tests").exists()
assert (temp_path / "src" / "main.py").exists()

result = runner.invoke(cli, ["new", "--app-name", "TestApp"])
assert result.exit_code == 0
Loading
Loading