Skip to content

Commit 495eff4

Browse files
committed
actual fixes this time
1 parent 657de63 commit 495eff4

8 files changed

Lines changed: 140 additions & 85 deletions

File tree

.github/workflows/type-checking.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
SECRET_KEY: test-secret-key-for-testing-only
2727

2828
- name: Mypy (cli)
29-
run: cd cli && uv run --no-sync mypy --no-namespace-packages src/cli
29+
run: cd cli && uv run --no-sync mypy -p cli
3030
env:
3131
ENVIRONMENT: local
3232
SECRET_KEY: test-secret-key-for-testing-only

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ dist/
1414
downloads/
1515
eggs/
1616
.eggs/
17-
lib/
18-
lib64/
17+
/lib/
18+
/lib64/
1919
parts/
2020
sdist/
2121
var/

backend/pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ dev = [
4242
"pytest-asyncio>=0.25.3",
4343
"pytest-mock>=3.14.0",
4444
"types-python-jose>=3.4.0.20250224",
45-
"testcontainers>=4.10.0",
46-
"testcontainers-postgres>=0.0.1rc1",
45+
"testcontainers[postgres]>=4.10.0",
4746
"pytest-xdist[psutil]>=3.8.0",
4847
]
4948

cli/src/cli/lib/__init__.py

Whitespace-only changes.

cli/src/cli/lib/project.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Locate the user's project on disk and read context from it.
2+
3+
The CLI is shipped inside ``backend/src/cli`` of the boilerplate, but
4+
when invoked it operates on whichever directory the user is in. These
5+
helpers resolve the repo root, the backend directory, and read values
6+
from the project's ``.env`` files without importing the application.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from dataclasses import dataclass
12+
from pathlib import Path
13+
14+
15+
@dataclass(frozen=True)
16+
class ProjectContext:
17+
"""Resolved paths the CLI needs to operate on the user's repo."""
18+
19+
repo_root: Path
20+
backend_dir: Path
21+
22+
@property
23+
def env_file(self) -> Path:
24+
return self.backend_dir / ".env"
25+
26+
@property
27+
def env_example(self) -> Path:
28+
return self.backend_dir / ".env.example"
29+
30+
@property
31+
def compose_file(self) -> Path:
32+
return self.repo_root / "docker-compose.yml"
33+
34+
35+
def discover_project(start: Path | None = None) -> ProjectContext:
36+
"""Walk up from ``start`` looking for a ``backend/pyproject.toml`` marker.
37+
38+
Falls back to the current working directory if no marker is found —
39+
the caller is responsible for deciding whether that's acceptable.
40+
"""
41+
current = (start or Path.cwd()).resolve()
42+
for candidate in [current, *current.parents]:
43+
if (candidate / "backend" / "pyproject.toml").is_file():
44+
return ProjectContext(repo_root=candidate, backend_dir=candidate / "backend")
45+
if (candidate / "pyproject.toml").is_file() and candidate.name == "backend":
46+
return ProjectContext(repo_root=candidate.parent, backend_dir=candidate)
47+
return ProjectContext(repo_root=current, backend_dir=current / "backend")
48+
49+
50+
def read_env_value(env_path: Path, key: str) -> str | None:
51+
"""Read a single value from a ``.env``-style file.
52+
53+
Returns ``None`` if the file is missing or the key isn't set. Quotes
54+
around the value (single or double) are stripped. Lines beginning
55+
with ``#`` are skipped.
56+
"""
57+
if not env_path.is_file():
58+
return None
59+
for raw in env_path.read_text(encoding="utf-8").splitlines():
60+
line = raw.strip()
61+
if not line or line.startswith("#") or "=" not in line:
62+
continue
63+
name, _, value = line.partition("=")
64+
if name.strip() != key:
65+
continue
66+
value = value.strip()
67+
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
68+
value = value[1:-1]
69+
return value
70+
return None

cli/src/cli/lib/prompts.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Consistent prompt helpers used across the CLI.
2+
3+
Centralizes ``--yes`` (assume-yes) and ``--quiet`` handling so individual
4+
commands don't each invent their own.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import typer
10+
11+
12+
def confirm_overwrite(path: str, *, assume_yes: bool) -> bool:
13+
"""Confirm overwriting an existing file. Returns True to proceed."""
14+
if assume_yes:
15+
return True
16+
return typer.confirm(f"{path} already exists. Overwrite?", default=False)
17+
18+
19+
def info(message: str) -> None:
20+
typer.echo(message)
21+
22+
23+
def success(message: str) -> None:
24+
typer.secho(message, fg=typer.colors.GREEN)
25+
26+
27+
def warn(message: str) -> None:
28+
typer.secho(message, fg=typer.colors.YELLOW)
29+
30+
31+
def error(message: str) -> None:
32+
typer.secho(message, fg=typer.colors.RED, err=True)

cli/src/cli/lib/render.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Jinja-based template rendering for in-tree features and plugins.
2+
3+
A feature points at a directory containing Jinja templates. ``Renderer``
4+
loads them with a sandboxed environment, applies the supplied context,
5+
and writes the result to disk.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from pathlib import Path
11+
from typing import Any
12+
13+
from jinja2 import Environment, FileSystemLoader, StrictUndefined
14+
15+
16+
class Renderer:
17+
"""Render Jinja templates from a feature's templates directory."""
18+
19+
def __init__(self, templates_root: Path) -> None:
20+
if not templates_root.is_dir():
21+
raise FileNotFoundError(f"Templates directory does not exist: {templates_root}")
22+
self.templates_root = templates_root
23+
self.env = Environment(
24+
loader=FileSystemLoader(str(templates_root)),
25+
keep_trailing_newline=True,
26+
undefined=StrictUndefined,
27+
autoescape=False,
28+
)
29+
30+
def render(self, template_path: str, context: dict[str, Any]) -> str:
31+
"""Render ``template_path`` (relative to ``templates_root``) with ``context``."""
32+
template = self.env.get_template(template_path)
33+
return template.render(**context)

0 commit comments

Comments
 (0)