Skip to content

Commit 3e5d9c1

Browse files
committed
feat: add AA
1 parent bbf6a41 commit 3e5d9c1

31 files changed

Lines changed: 199 additions & 304 deletions

CONTRIBUTING.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This document explains the architecture and design of `litestar-start`, a CLI to
44

55
## Overview
66

7-
`litestar-start` is an interactive CLI tool that helps developers quickly scaffold new Litestar projects with optional plugins like SQLAlchemy and JWT authentication.
7+
`litestar-start` is an interactive CLI tool that helps developers quickly scaffold new Litestar projects with optional plugins like AdvancedAlchemy ORM.
88

99
## Project Structure
1010

@@ -98,8 +98,7 @@ All templates use **Jinja2** with `.jinja` extension. Template context includes:
9898
- `project_name` - Human-readable name
9999
- `project_slug` - Python-safe name
100100
- `database` - Selected database enum
101-
- `has_sqlalchemy` - Boolean flag
102-
- `has_jwt` - Boolean flag
101+
- `has_advanced_alchemy` - Boolean flag
103102
- `docker` - Boolean flag
104103

105104
## Adding New Features
@@ -118,8 +117,9 @@ All templates use **Jinja2** with `.jinja` extension. Template context includes:
118117
2. Add to `Plugin` enum in `models.py`:
119118
```python
120119
class Plugin(StrEnum):
121-
SQLALCHEMY = "SQLAlchemy"
122-
JWT = "JWT"
120+
ADVANCED_ALCHEMY = "AdvancedAlchemy"
121+
LITESTAR_SAQ = "LitestarSAQ"
122+
LITESTAR_VITE = "LitestarVite"
123123
NEW_PLUGIN = "NewPlugin" # Add this
124124
```
125125

@@ -180,8 +180,7 @@ Templates are rendered using Jinja2 with these settings:
180180
| `project_slug` | `str` | Python-safe name |
181181
| `database` | `Database` | Selected database enum |
182182
| `db_config` | `DatabaseConfig` | Database configuration |
183-
| `has_sqlalchemy` | `bool` | SQLAlchemy plugin enabled |
184-
| `has_jwt` | `bool` | JWT plugin enabled |
183+
| `has_advanced_alchemy` | `bool` | AdvancedAlchemy plugin enabled |
185184
| `has_database` | `bool` | Any database selected |
186185
| `docker` | `bool` | Dockerfile requested |
187186
| `docker_infra` | `bool` | Infra compose requested |
@@ -197,7 +196,7 @@ my_project/
197196
│ ├── __main__.py
198197
│ ├── config.py
199198
│ └── main.py
200-
├── db/ # If SQLAlchemy selected
199+
├── models/ # If AdvancedAlchemy selected
201200
│ ├── __init__.py
202201
│ ├── config.py
203202
│ └── models.py

src/Litestar/App/config.py.jinja

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from app.settings import get_settings
2+
3+
settings = get_settings()
4+
5+
{% if has_advanced_alchemy %}
6+
from advanced_alchemy.extensions.litestar import (
7+
AsyncSessionConfig,
8+
SQLAlchemyAsyncConfig,
9+
SQLAlchemyPlugin,
10+
)
11+
12+
session_config = AsyncSessionConfig(expire_on_commit=False)
13+
alchemy_config = SQLAlchemyAsyncConfig(
14+
connection_string=settings.DATABASE_URL,
15+
before_send_handler="autocommit",
16+
session_config=session_config,
17+
create_all=True,
18+
)
19+
alchemy = SQLAlchemyPlugin(config=alchemy_config)
20+
{% endif %}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Application library utilities."""
2+
3+
{% if has_advanced_alchemy %}
4+
from app.lib.utils import exception_handler
5+
6+
__all__ = ["exception_handler"]
7+
{% else %}
8+
__all__ = []
9+
{% endif %}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{% if has_advanced_alchemy %}
2+
from typing import Any
3+
4+
from advanced_alchemy.exceptions import IntegrityError, NotFoundError, RepositoryError
5+
from advanced_alchemy.extensions.litestar.exception_handler import (
6+
ConflictError,
7+
)
8+
from litestar import Request, Response
9+
from litestar.exceptions import (
10+
HTTPException,
11+
NotFoundException,
12+
)
13+
from litestar.exceptions.responses import create_exception_response
14+
from litestar.status_codes import (
15+
HTTP_409_CONFLICT,
16+
HTTP_500_INTERNAL_SERVER_ERROR,
17+
)
18+
19+
20+
21+
class _HTTPConflictException(HTTPException):
22+
"""Request conflict with the current state of the target resource."""
23+
24+
status_code = HTTP_409_CONFLICT
25+
26+
27+
def exception_handler(
28+
request: Request[Any, Any, Any],
29+
exc: Exception,
30+
) -> Response:
31+
"""Handle exceptions and convert them to HTTP exceptions."""
32+
http_exc: type[HTTPException]
33+
34+
if isinstance(exc, NotFoundError):
35+
http_exc = NotFoundException
36+
elif isinstance(exc, ConflictError | RepositoryError | IntegrityError):
37+
http_exc = _HTTPConflictException
38+
else:
39+
return create_exception_response(
40+
request,
41+
HTTPException(
42+
status_code=getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR),
43+
detail=str(exc),
44+
),
45+
)
46+
47+
return create_exception_response(request, http_exc(detail=str(exc.detail)))
48+
{% endif %}
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ from litestar import Litestar, get
44
from litestar.openapi import OpenAPIConfig
55

66
from app.config import settings
7-
{% if has_sqlalchemy %}
8-
from db.config import db_plugin
9-
{% endif %}
10-
{% if has_jwt %}
11-
from auth.guards import jwt_auth
7+
{% if has_advanced_alchemy %}
8+
from advanced_alchemy.exceptions import RepositoryError
9+
from app.config import alchemy
10+
from app.lib import exception_handler
1211
{% endif %}
1312

1413

@@ -32,10 +31,11 @@ app = Litestar(
3231
path="/api",
3332
),
3433
debug=settings.DEBUG,
35-
{% if has_sqlalchemy %}
36-
plugins=[db_plugin],
37-
{% endif %}
38-
{% if has_jwt %}
39-
on_app_init=[jwt_auth.on_app_init],
34+
{% if has_advanced_alchemy %}
35+
exception_handlers={
36+
Exception: exception_handler,
37+
RepositoryError: exception_handler,
38+
},
39+
plugins=[alchemy],
4040
{% endif %}
4141
)
Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,10 @@
22

33
import os
44
from dataclasses import dataclass, field
5+
from functools import lru_cache
56
from pathlib import Path
67

7-
from dotenv import load_dotenv
8-
9-
# Load environment variables
10-
load_dotenv()
11-
12-
13-
def _get_bool(key: str, default: bool = False) -> bool:
14-
"""Get a boolean from environment."""
15-
value = os.getenv(key, str(default)).lower()
16-
return value in ("true", "1", "yes", "on")
17-
18-
19-
def _get_int(key: str, default: int) -> int:
20-
"""Get an integer from environment."""
21-
return int(os.getenv(key, str(default)))
22-
8+
TRUTHY = ("true", "1", "yes")
239

2410
@dataclass
2511
class Settings:
@@ -28,11 +14,11 @@ class Settings:
2814
# Application
2915
APP_NAME: str = field(default_factory=lambda: os.getenv("APP_NAME", "{{ project_name }}"))
3016
APP_ENV: str = field(default_factory=lambda: os.getenv("APP_ENV", "development"))
31-
DEBUG: bool = field(default_factory=lambda: _get_bool("DEBUG", True))
17+
DEBUG: bool = field(default_factory=lambda: os.getenv("DEBUG", "true").lower() in TRUTHY)
3218

3319
# Server
3420
HOST: str = field(default_factory=lambda: os.getenv("HOST", "0.0.0.0"))
35-
PORT: int = field(default_factory=lambda: _get_int("PORT", 8000))
21+
PORT: int = field(default_factory=lambda: int(os.getenv("PORT", "8000")))
3622

3723
{% if has_database %}
3824
# Database
@@ -60,19 +46,24 @@ class Settings:
6046
{% endif %}
6147
{% endif %}
6248

63-
{% if has_jwt %}
64-
# JWT
65-
JWT_SECRET: str = field(
66-
default_factory=lambda: os.getenv("JWT_SECRET", "your-super-secret-key-change-in-production")
67-
)
68-
JWT_ALGORITHM: str = field(default_factory=lambda: os.getenv("JWT_ALGORITHM", "HS256"))
69-
JWT_EXPIRATION_MINUTES: int = field(default_factory=lambda: _get_int("JWT_EXPIRATION_MINUTES", 60))
70-
{% endif %}
49+
@classmethod
50+
def from_env(cls, dotenv_filename: str) -> "Settings":
51+
env_file = (
52+
Path(dotenv_filename) if Path(dotenv_filename).is_absolute() else Path(f"{os.curdir}/{dotenv_filename}")
53+
)
54+
55+
if env_file.is_file():
56+
from dotenv import load_dotenv
57+
58+
console.print(
59+
f"[yellow]Loading environment configuration from {dotenv_filename}[/]",
60+
markup=True,
61+
)
7162

72-
@property
73-
def is_production(self) -> bool:
74-
"""Check if running in production."""
75-
return self.APP_ENV == "production"
63+
load_dotenv(env_file, override=True)
64+
return Settings()
7665

7766

78-
settings = Settings()
67+
@lru_cache(maxsize=1, typed=True)
68+
def get_settings() -> Settings:
69+
return Settings.from_env(dotenv_filename=".env")

src/Litestar/Config/.env.example.jinja

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,3 @@ DATABASE_URL=sqlite+aiosqlite:///./app.db
1717
DATABASE_URL=mysql+asyncmy://myuser:mypassword@localhost:3306/mydb
1818
{% endif %}
1919
{% endif %}
20-
21-
{% if has_jwt %}
22-
# JWT
23-
JWT_SECRET=your-super-secret-key-change-in-production
24-
JWT_ALGORITHM=HS256
25-
JWT_EXPIRATION_MINUTES=60
26-
{% endif %}

src/Litestar/Config/.gitignore.jinja

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ dist/
1414
downloads/
1515
eggs/
1616
.eggs/
17-
lib/
1817
lib64/
1918
parts/
2019
sdist/

0 commit comments

Comments
 (0)