Skip to content

Commit b52af52

Browse files
committed
feat: add memory stores
1 parent cd0c6b0 commit b52af52

13 files changed

Lines changed: 345 additions & 11 deletions

AGENTS.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Agent Guidelines for litestar-start
2+
3+
This document contains instructions for AI agents operating in this repository.
4+
5+
## 1. Build, Lint, and Test Commands
6+
7+
This project uses `uv` for dependency management and `hatchling` for building.
8+
9+
### Dependency Management
10+
- **Install dependencies:** `uv sync`
11+
- **Update lockfile:** `uv lock` (or `uv lock --upgrade` to upgrade)
12+
13+
### Testing
14+
- **Run all tests:**
15+
```bash
16+
pytest
17+
```
18+
- **Run a single test file:**
19+
```bash
20+
pytest tests/test_generator.py
21+
```
22+
- **Run a specific test case:**
23+
```bash
24+
pytest tests/test_generator.py::test_function_name
25+
```
26+
- **Run with verbose output:**
27+
```bash
28+
pytest -v
29+
```
30+
31+
### Linting and Formatting
32+
- **Run all linters (Ruff, Type Checking, Pre-commit):**
33+
```bash
34+
make lint
35+
```
36+
*Note: This runs `ruff check --fix`, `ty check`, and `pre-commit run -a`.*
37+
38+
- **Run Ruff manually:**
39+
```bash
40+
ruff check .
41+
```
42+
43+
- **Run Type Checking manually:**
44+
```bash
45+
ty check
46+
```
47+
48+
## 2. Code Style Guidelines
49+
50+
Adhere strictly to the following conventions to match the existing codebase.
51+
52+
### General
53+
- **Python Version:** Target Python 3.13+.
54+
- **Line Length:** 120 characters (configured in `pyproject.toml`).
55+
- **File Operations:** Always use `pathlib.Path` instead of `os.path` strings.
56+
- **Data Models:** Use `msgspec.Struct` for defining data structures/configurations, not `dataclasses` or `pydantic`.
57+
- **Enums:** Use `enum.StrEnum` for string-based enumerations.
58+
59+
### Imports
60+
- Group imports:
61+
1. Standard library (e.g., `pathlib`, `re`)
62+
2. Third-party libraries (e.g., `jinja2`, `msgspec`)
63+
3. Local application imports (e.g., `src.utils`)
64+
- Imports are sorted automatically by Ruff; attempt to group them logically.
65+
66+
### Typing
67+
- **Strict Typing:** All functions and methods must have type annotations for arguments and return values.
68+
- Use built-in types (`list`, `dict`, `set`) instead of `typing.List`, etc.
69+
- Use `|` for unions (e.g., `str | None`) instead of `Optional` or `Union`.
70+
71+
### Naming Conventions
72+
- **Variables/Functions:** `snake_case`
73+
- **Classes:** `PascalCase`
74+
- **Constants:** `SCREAMING_SNAKE_CASE`
75+
- **Private Members:** Prefix with `_` (though `SLF001` is ignored in Ruff, still prefer public interfaces).
76+
77+
### Docstrings
78+
- Use **Google-style** docstrings for all public modules, functions, classes, and methods.
79+
- Format:
80+
```python
81+
def function(arg: int) -> str:
82+
"""Short summary of the function.
83+
84+
Args:
85+
arg: Description of the argument.
86+
87+
Returns:
88+
Description of the return value.
89+
90+
"""
91+
```
92+
93+
### Error Handling
94+
- Validate inputs early (fail fast).
95+
- Use specific exceptions where possible.
96+
- Avoid bare `except:` blocks.
97+
98+
### Directory Structure
99+
- `src/`: Source code.
100+
- `tests/`: Test files (mirrors source structure or flat list).
101+
- `tools/`: Build and maintenance scripts.

src/Litestar/App/app.py.jinja

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ from .controllers import UserController
1414
{% if litestar_vite %}
1515
from litestar_vite import ViteConfig, VitePlugin
1616
{% endif %}
17+
{% if has_store %}
18+
from .config import redis_store
19+
{% endif %}
1720

1821
@get("/health")
1922
async def health() -> dict[str, str]:
@@ -40,6 +43,9 @@ app = Litestar(
4043
},
4144
log_exceptions="always",
4245
),
46+
{% if has_store %}
47+
stores={"redis": redis_store},
48+
{% endif %}
4349
{% if advanced_alchemy %}
4450
exception_handlers={
4551
Exception: exception_handler,

src/Litestar/App/config.py.jinja

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ from advanced_alchemy.extensions.litestar import (
99
from pathlib import Path
1010
from litestar_vite import ViteConfig, PathConfig
1111
{% endif %}
12+
{% if has_store %}
13+
from litestar.stores.redis import RedisStore
14+
{% endif %}
1215

1316
from .settings import get_settings
1417

@@ -37,3 +40,11 @@ vite_config = ViteConfig(
3740
),
3841
)
3942
{% endif %}
43+
44+
{% if has_store %}
45+
redis_store = RedisStore.with_client(
46+
url=settings.REDIS_URL,
47+
port=6379,
48+
db=0,
49+
)
50+
{% endif %}

src/Litestar/App/settings.py.jinja

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ class Settings:
4747
{% endif %}
4848
{% endif %}
4949

50+
{% if has_store %}
51+
# Memory Store
52+
REDIS_URL: str = field(
53+
default_factory=lambda: os.getenv("REDIS_URL", "{{ store_config.default_url }}")
54+
)
55+
{% endif %}
56+
5057
@classmethod
5158
def from_env(cls, dotenv_filename: str) -> "Settings":
5259
env_file = (

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@ DATABASE_URL=sqlite+aiosqlite:///./app.db
1818
DATABASE_URL=mysql+asyncmy://myuser:mypassword@localhost:3306/mydb
1919
{% endif %}
2020
{% endif %}
21+
22+
{% if has_store %}
23+
# Memory Store
24+
REDIS_URL={{ store_config.default_url }}
25+
{% endif %}

src/Litestar/Config/pyproject.toml.jinja

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ dependencies = [
2020
{% if litestar_vite %}
2121
"litestar-vite>=0.18.1",
2222
{% endif %}
23+
{% if has_store %}
24+
"redis>=5.2.1",
25+
{% endif %}
2326
]
2427

2528
[tool.ruff]

src/Litestar/Containers/docker-compose.infra.yml.jinja

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,28 @@ volumes:
4343
volumes:
4444
{{ project_slug }}_mysql_dev_db: {}
4545
{% endif %}
46+
{% if memory_store.value == "Redis" %}
47+
redis:
48+
image: {{ store_config.docker_image }}
49+
container_name: {{ project_slug }}_redis_dev
50+
ports:
51+
- "{{ store_config.port }}:6379"
52+
healthcheck:
53+
test: ["CMD", "redis-cli", "ping"]
54+
interval: 10s
55+
timeout: 5s
56+
retries: 5
57+
restart: unless-stopped
58+
{% elif memory_store.value == "Valkey" %}
59+
valkey:
60+
image: {{ store_config.docker_image }}
61+
container_name: {{ project_slug }}_valkey_dev
62+
ports:
63+
- "{{ store_config.port }}:6379"
64+
healthcheck:
65+
test: ["CMD", "valkey-cli", "ping"]
66+
interval: 10s
67+
timeout: 5s
68+
retries: 5
69+
restart: unless-stopped
70+
{% endif %}

src/Litestar/Containers/docker-compose.yml.jinja

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,27 @@ services:
1818
- DATABASE_URL=sqlite+aiosqlite:///./app.db
1919
{% endif %}
2020
{% endif %}
21+
{% if has_store %}
22+
- REDIS_URL=redis://{{ project_slug }}_{{ memory_store.value|lower }}:6379/0
23+
{% endif %}
2124
env_file:
2225
- .env
23-
{% if database.value == "PostgreSQL" %}
26+
{% if (database.value == "PostgreSQL" or database.value == "MySQL") or has_store %}
2427
depends_on:
28+
{% if database.value == "PostgreSQL" %}
2529
postgres:
2630
condition: service_healthy
27-
{% elif database.value == "MySQL" %}
28-
depends_on:
31+
{% elif database.value == "MySQL" %}
2932
mysql:
3033
condition: service_healthy
34+
{% endif %}
35+
{% if memory_store.value == "Redis" %}
36+
redis:
37+
condition: service_healthy
38+
{% elif memory_store.value == "Valkey" %}
39+
valkey:
40+
condition: service_healthy
41+
{% endif %}
3142
{% endif %}
3243
restart: unless-stopped
3344

@@ -70,6 +81,32 @@ services:
7081
restart: unless-stopped
7182
{% endif %}
7283

84+
{% if memory_store.value == "Redis" %}
85+
redis:
86+
image: {{ store_config.docker_image }}
87+
container_name: {{ project_slug }}_redis
88+
ports:
89+
- "{{ store_config.port }}:6379"
90+
healthcheck:
91+
test: ["CMD", "redis-cli", "ping"]
92+
interval: 10s
93+
timeout: 5s
94+
retries: 5
95+
restart: unless-stopped
96+
{% elif memory_store.value == "Valkey" %}
97+
valkey:
98+
image: {{ store_config.docker_image }}
99+
container_name: {{ project_slug }}_valkey
100+
ports:
101+
- "{{ store_config.port }}:6379"
102+
healthcheck:
103+
test: ["CMD", "valkey-cli", "ping"]
104+
interval: 10s
105+
timeout: 5s
106+
retries: 5
107+
restart: unless-stopped
108+
{% endif %}
109+
73110
{% if database.value == "PostgreSQL" %}
74111
volumes:
75112
{{ project_slug }}_postgres_db: {}

src/Litestar/generator.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from jinja2 import Environment
66

7-
from src.models import DatabaseConfig, ProjectConfig
7+
from src.models import DatabaseConfig, MemoryStoreConfig, ProjectConfig
88
from src.plugin import discover_plugins
99
from src.utils import get_package_dir, get_template_env, write_file
1010

@@ -33,6 +33,7 @@ def _get_template_context(self) -> dict:
3333
3434
"""
3535
db_config = DatabaseConfig.for_database(self.config.database)
36+
store_config = MemoryStoreConfig.for_store(self.config.memory_store)
3637

3738
context = {
3839
"project": self.config,
@@ -41,6 +42,9 @@ def _get_template_context(self) -> dict:
4142
"database": self.config.database,
4243
"db_config": db_config,
4344
"has_database": self.config.database.value != "None",
45+
"memory_store": self.config.memory_store,
46+
"store_config": store_config,
47+
"has_store": self.config.memory_store.value != "None",
4448
"docker": self.config.docker,
4549
"docker_infra": self.config.docker_infra,
4650
}

src/cli.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from rich.text import Text
1111

1212
from src.generator import ProjectGenerator
13-
from src.models import Database, Framework, ProjectConfig
13+
from src.models import Database, Framework, MemoryStore, ProjectConfig
1414
from src.plugin import Plugin, discover_plugins
1515
from src.utils import validate_project_name
1616

@@ -107,6 +107,34 @@ def ask_database() -> Database:
107107
return result
108108

109109

110+
def ask_memory_store() -> MemoryStore:
111+
"""Ask for the memory store choice.
112+
113+
Returns:
114+
The selected memory store.
115+
116+
Raises:
117+
SystemExit: If the user cancels the operation.
118+
119+
"""
120+
choices = [
121+
questionary.Choice(title="Redis", value=MemoryStore.REDIS),
122+
questionary.Choice(title="Valkey", value=MemoryStore.VALKEY),
123+
questionary.Choice(title="None", value=MemoryStore.NONE),
124+
]
125+
126+
result = questionary.select(
127+
"Select memory store:",
128+
choices=choices,
129+
).ask()
130+
131+
if result is None:
132+
console.print("\n[yellow]Cancelled.[/yellow]")
133+
raise SystemExit(0)
134+
135+
return result
136+
137+
110138
def ask_plugins(config: ProjectConfig, discovered_plugins: list[Plugin]) -> list[str]:
111139
"""Ask for plugins to install.
112140
@@ -251,6 +279,7 @@ def main() -> None:
251279
name = ask_project_name()
252280
framework = ask_framework()
253281
database = ask_database()
282+
memory_store = ask_memory_store()
254283

255284
# Discover plugins early to pass to ask_plugins
256285
discovered_plugins = discover_plugins(framework.value)
@@ -260,6 +289,7 @@ def main() -> None:
260289
name=name,
261290
framework=framework,
262291
database=database,
292+
memory_store=memory_store,
263293
plugins=[], # Will be populated next
264294
docker=False, # Placeholder
265295
docker_infra=False, # Placeholder
@@ -279,6 +309,7 @@ def main() -> None:
279309
f"[bold]Project:[/bold] {config.name}\n"
280310
f"[bold]Framework:[/bold] {config.framework.value}\n"
281311
f"[bold]Database:[/bold] {config.database.value}\n"
312+
f"[bold]Memory Store:[/bold] {config.memory_store.value}\n"
282313
f"[bold]Plugins:[/bold] {', '.join(config.plugins) or 'None'}\n"
283314
f"[bold]Docker:[/bold] {'Yes' if config.docker else 'No'}\n"
284315
f"[bold]Docker Infra:[/bold] {'Yes' if config.docker_infra else 'No'}",

0 commit comments

Comments
 (0)