Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
89603e0
ci: add GitHub Actions workflow for running pytest
Harshal6927 Apr 12, 2026
8818d11
chore: cleanup
Harshal6927 Apr 12, 2026
52ad07c
refactor: remove unused create_directory function
Harshal6927 Apr 12, 2026
7735e31
refactor: remove unused render_template function
Harshal6927 Apr 12, 2026
7865d2d
fix: unify slug generation by delegating ProjectConfig.slug to slugify()
Harshal6927 Apr 12, 2026
1df897b
refactor: remove redundant MIN_PROJECT_NAME_LENGTH check in validate_…
Harshal6927 Apr 12, 2026
e5bd3d6
chore: cleanup
Harshal6927 Apr 12, 2026
9450da2
docs: refresh docs and make public interfaces explicit
Harshal6927 Apr 12, 2026
b22f17b
chore: cleanup
Harshal6927 Apr 12, 2026
fea4bf0
test: add conftest.py with shared ProjectConfig factory fixture
Harshal6927 Apr 12, 2026
ef34190
test: add boundary tests for slugify and validate_project_name
Harshal6927 Apr 12, 2026
7433729
test: fix weak OR-assertions in test_generator.py
Harshal6927 Apr 12, 2026
8765511
test: add spec= to Mock objects for type safety
Harshal6927 Apr 12, 2026
e003c7a
test: add coverage for NotImplementedError on unsupported framework
Harshal6927 Apr 12, 2026
d866d00
test: add coverage for discover_plugins error handling branches
Harshal6927 Apr 12, 2026
055e35a
test: add direct tests for LitestarGenerator.post_generate()
Harshal6927 Apr 12, 2026
232b7ce
test: add dedicated test file for LitestarGranianPlugin
Harshal6927 Apr 12, 2026
3008beb
test: add coverage for .env.example and .dockerignore copy behavior
Harshal6927 Apr 12, 2026
56b6d47
test: add coverage for subprocess failure paths in post-generation setup
Harshal6927 Apr 12, 2026
1717e96
test: add coverage for main() CLI entry point
Harshal6927 Apr 12, 2026
0ed878c
refactor: rename test_generator.py to test_litestar_generator.py for …
Harshal6927 Apr 12, 2026
d00d443
chore: cleanup
Harshal6927 Apr 12, 2026
698d735
fix: lint
Harshal6927 Jun 12, 2026
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
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Test

on:
push:
branches:
- main
pull_request:

concurrency: test-${{ github.sha }}

jobs:
test:
runs-on: ubuntu-latest

env:
PYTHON_VERSION: "3.13"

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install dependencies
run: uv sync

- name: Run tests
run: uv run pytest -v
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ wheels/
docs/prd
.geminiignore
.agent/
.opencode/
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ repos:
hooks:
- id: codespell
exclude: "uv.lock"
args: ["--ignore-words-list=ure"]
args: ["--ignore-words-list=ure,caf"]
additional_dependencies:
- tomli
98 changes: 52 additions & 46 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,25 @@ src/
│ ├── gitignore.jinja
│ ├── env.example.jinja
│ └── readme.md.jinja
├── App/ # Core application templates
│ └── *.jinja
├── Containers/ # Docker templates
│ ├── Dockerfile.jinja
│ ├── docker-compose.yml.jinja
│ └── docker-compose.infra.yml.jinja
├── app/ # Core application templates
│ ├── __init__.py.jinja
│ ├── config.py.jinja
│ └── main.py.jinja
└── Plugins/ # Optional plugin templates
├── __init__.py
├── SQLAlchemy/
├── AdvancedAlchemy/
│ ├── __init__.py
│ └── Templates/
│ └── db/
│ ├── __init__.py.jinja
│ ├── config.py.jinja
│ └── models.py.jinja
└── JWT/
├── __init__.py
└── Templates/
└── auth/
├── __init__.py.jinja
├── guards.py.jinja
└── schemas.py.jinja
├── LitestarSAQ/
│ ├── __init__.py
│ └── Templates/
├── LitestarVite/
│ ├── __init__.py
│ └── Templates/
└── LitestarGranian/
└── __init__.py
```

## Core Components
Expand All @@ -73,7 +68,7 @@ Uses **msgspec** for efficient data serialization. Key models:

- **Framework** - Enum of supported frameworks (Litestar, future: FastAPI)
- **Database** - Enum of database options (PostgreSQL, SQLite, MySQL, None)
- **Plugin** - Enum of available plugins (SQLAlchemy, JWT)
- **MemoryStore** - Enum of memory store options (Redis, Valkey, None)
- **ProjectConfig** - Main configuration struct containing all user choices
- **DatabaseConfig** - Database-specific configuration (driver, port, URL)

Expand Down Expand Up @@ -104,27 +99,41 @@ All templates use **Jinja2** with `.jinja` extension. Template context includes:

### Adding a New Plugin

1. Create plugin directory:
1. Create plugin directory under the framework's `Plugins/` directory:
```
Litestar/Plugins/NewPlugin/
src/Litestar/Plugins/NewPlugin/
├── __init__.py
└── Templates/
└── new_module/
└── *.jinja
└── *.jinja
```

2. Add to `Plugin` enum in `models.py`:
2. In `__init__.py`, create a class that extends `BasePlugin`:
```python
class Plugin(StrEnum):
ADVANCED_ALCHEMY = "AdvancedAlchemy"
LITESTAR_SAQ = "LitestarSAQ"
LITESTAR_VITE = "LitestarVite"
NEW_PLUGIN = "NewPlugin" # Add this
from src.models import ProjectConfig
from src.plugin import BasePlugin


class NewPlugin(BasePlugin):
"""Description of the plugin."""

@property
def name(self) -> str:
"""Get the plugin display name."""
return "New Plugin"

@property
def description(self) -> str:
"""Get the plugin description."""
return "Description for the CLI"

def is_applicable(self, config: ProjectConfig) -> bool:
"""Check if this plugin is applicable."""
return True # or check config fields
```

3. Update CLI in `cli.py` to include the new option
3. The plugin will be automatically discovered by `discover_plugins()`. No enum or CLI changes are needed.

4. Update base templates if the plugin requires imports/configuration changes
4. Add Jinja2 templates in the `Templates/` subdirectory. They will be rendered into `src/backend/` of the generated project.

### Adding a New Framework

Expand Down Expand Up @@ -190,18 +199,17 @@ A typical generated project looks like:

```
my_project/
├── app/
│ ├── __init__.py
│ ├── config.py
│ └── main.py
├── models/ # If AdvancedAlchemy selected
│ ├── __init__.py
│ ├── config.py
│ └── models.py
├── auth/ # If JWT selected
│ ├── __init__.py
│ ├── guards.py
│ └── schemas.py
├── src/
│ └── backend/
│ ├── __init__.py
│ ├── app.py
│ ├── config.py
│ ├── models/ # If AdvancedAlchemy selected
│ │ └── users.py
│ └── lib/ # If plugins are selected
│ ├── dependencies.py
│ ├── services.py
│ └── tasks.py # If SAQ selected
├── .env.example
├── .gitignore
├── pyproject.toml
Expand All @@ -221,8 +229,6 @@ my_project/
## Future Improvements

- [ ] Add FastAPI framework support
- [ ] Add more plugins (Structlog, Redis, CORS)
- [ ] Add Alembic migrations setup
- [ ] Add test scaffolding
- [ ] Add GitHub Actions workflows
- [ ] Add pre-commit configuration
- [ ] Add more plugins (Structlog, CORS)
- [ ] Add test scaffolding for generated projects
- [ ] Add CI workflow for generated projects
69 changes: 68 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,76 @@
# Litestar Start

Interactive CLI to scaffold fullstack projects with modular choices
Interactive CLI to scaffold fullstack [Litestar](https://litestar.dev) projects with modular choices.

## Features

- **Interactive prompts** — guided setup with [questionary](https://questionary.readthedocs.io/)
- **Database support** — PostgreSQL, MySQL, or SQLite via AdvancedAlchemy
- **Memory stores** — Redis or Valkey for caching / background tasks
- **Plugin system** — modular plugins that add functionality:
- **AdvancedAlchemy** — SQLAlchemy ORM integration with models, services, and dependencies
- **Litestar SAQ** — background task queue powered by SAQ (requires a memory store)
- **Litestar Vite** — frontend asset bundling with Vite
- **Litestar Granian** — high-performance Granian ASGI server
- **Docker** — optional Dockerfile and `docker-compose.infra.yml` for local development
- **Post-generation setup** — automatic `git init`, `uv sync`, Docker infrastructure startup, and import sorting

## Requirements

- Python 3.13+
- [uv](https://docs.astral.sh/uv/) (used for dependency management in generated projects)

## Installation

```bash
uvx litestar-start
```

Or install globally:

```bash
uv tool install litestar-start
```

## Usage

Run the CLI and follow the interactive prompts:

```bash
litestar-start
```

You will be asked to:

1. Enter a project name
2. Select a database (PostgreSQL, SQLite, MySQL, or None)
3. Select a memory store (Redis, Valkey, or None)
4. Choose plugins (based on your database/store choices)
5. Configure Docker options
6. Confirm and generate

The generated project includes a working Litestar application with your selected options pre-configured.

## Development

```bash
git clone https://github.com/Harshal6927/litestar-start.git
cd litestar-start
uv sync
```

Run tests:

```bash
pytest
```

Run linters:

```bash
make lint
```

## License

MIT
27 changes: 24 additions & 3 deletions src/Litestar/Plugins/AdvancedAlchemy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,33 @@ class AdvancedAlchemyPlugin(BasePlugin):
"""Litestar plugin providing AdvancedAlchemy integration."""

@property
def name(self) -> str: # noqa: D102
def name(self) -> str:
"""Get the plugin display name.

Returns:
The display name shown in the CLI.

"""
return "AdvancedAlchemy (ORM)"

@property
def description(self) -> str: # noqa: D102
def description(self) -> str:
"""Get the plugin description.

Returns:
A short description of the plugin.

"""
return "SQLAlchemy integration with Litestar"

def is_applicable(self, config: ProjectConfig) -> bool: # noqa: D102, PLR6301
def is_applicable(self, config: ProjectConfig) -> bool: # noqa: PLR6301
"""Check if this plugin is applicable for the given configuration.

Args:
config: The project configuration.

Returns:
True if a database (other than None) is selected.

"""
return config.database != Database.NONE
27 changes: 24 additions & 3 deletions src/Litestar/Plugins/LitestarSAQ/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,33 @@ class LitestarSAQPlugin(BasePlugin):
"""SAQ integration plugin for Litestar background tasks."""

@property
def name(self) -> str: # noqa: D102
def name(self) -> str:
"""Get the plugin display name.

Returns:
The display name shown in the CLI.

"""
return "Litestar SAQ (Background Tasks)"

@property
def description(self) -> str: # noqa: D102
def description(self) -> str:
"""Get the plugin description.

Returns:
A short description of the plugin.

"""
return "SAQ integration for background tasks in Litestar"

def is_applicable(self, config: ProjectConfig) -> bool: # noqa: D102, PLR6301
def is_applicable(self, config: ProjectConfig) -> bool: # noqa: PLR6301
"""Check if this plugin is applicable for the given configuration.

Args:
config: The project configuration.

Returns:
True if a memory store (other than None) is selected.

"""
return config.memory_store != MemoryStore.NONE
16 changes: 14 additions & 2 deletions src/Litestar/Plugins/LitestarVite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,23 @@ class LitestarVitePlugin(BasePlugin):
"""Plugin providing Vite integration for Litestar frontend assets."""

@property
def name(self) -> str: # noqa: D102
def name(self) -> str:
"""Get the plugin display name.

Returns:
The display name shown in the CLI.

"""
return "Litestar Vite (Frontend Integration)"

@property
def description(self) -> str: # noqa: D102
def description(self) -> str:
"""Get the plugin description.

Returns:
A short description of the plugin.

"""
return "Vite integration for frontend assets in Litestar"

def post_generate(self, config: ProjectConfig, output_dir: Path) -> None: # noqa: ARG002
Expand Down
17 changes: 16 additions & 1 deletion src/Litestar/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,22 @@ def _render_templates(
context: dict,
root_template_dir: Path | None = None,
) -> None:
"""Recursively render templates from a directory."""
"""Recursively render Jinja2 templates from a directory into the output.

Walks `template_dir`, rendering every `*.jinja` file with the given
context and writing the result (sans `.jinja` suffix) into `output_subdir`.
Subdirectories are traversed recursively.

Args:
template_dir: Directory containing `.jinja` template files.
output_subdir: Target directory where rendered files are written.
env: The Jinja2 environment used for template loading.
context: Template variables passed to each render call.
root_template_dir: The root of the template tree, used to compute
template names relative to the Jinja2 loader. Defaults to
`template_dir` on first call and is preserved during recursion.

"""
if root_template_dir is None:
root_template_dir = template_dir

Expand Down
Loading
Loading