rpr-cli 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rpr/__init__.py +1 -0
- rpr/agent/__init__.py +29 -0
- rpr/agent/approval.py +19 -0
- rpr/agent/bootstrap.py +60 -0
- rpr/agent/client.py +115 -0
- rpr/agent/control.py +16 -0
- rpr/agent/mock.py +158 -0
- rpr/agent/runtime.py +291 -0
- rpr/agent/session.py +91 -0
- rpr/agent/tools/__init__.py +10 -0
- rpr/agent/tools/base.py +62 -0
- rpr/agent/tools/mutating.py +47 -0
- rpr/agent/tools/readonly.py +147 -0
- rpr/agent/tools/registry.py +30 -0
- rpr/application/__init__.py +1 -0
- rpr/application/catalog.py +427 -0
- rpr/application/chat_service.py +413 -0
- rpr/application/checks.py +49 -0
- rpr/application/cli_adapter.py +36 -0
- rpr/application/completer.py +163 -0
- rpr/application/conversation_service.py +92 -0
- rpr/application/prompt_service.py +85 -0
- rpr/application/selector.py +240 -0
- rpr/application/shell.py +543 -0
- rpr/checks/__init__.py +0 -0
- rpr/checks/base.py +13 -0
- rpr/checks/instructions.py +89 -0
- rpr/checks/packages.py +619 -0
- rpr/checks/workspace.py +170 -0
- rpr/cli.py +78 -0
- rpr/commands/__init__.py +0 -0
- rpr/commands/add.py +166 -0
- rpr/commands/chat.py +48 -0
- rpr/commands/check.py +53 -0
- rpr/commands/generate/__init__.py +0 -0
- rpr/commands/generate/api.py +228 -0
- rpr/commands/generate/domain.py +383 -0
- rpr/commands/generate/engine.py +148 -0
- rpr/commands/generate/storybook.py +442 -0
- rpr/commands/generate/ui.py +414 -0
- rpr/commands/init.py +822 -0
- rpr/commands/map.py +113 -0
- rpr/commands/settings.py +102 -0
- rpr/commands/sync.py +97 -0
- rpr/context.py +203 -0
- rpr/generators/__init__.py +0 -0
- rpr/generators/base.py +110 -0
- rpr/generators/claude.py +33 -0
- rpr/generators/copilot.py +36 -0
- rpr/generators/cursor.py +38 -0
- rpr/generators/gemini.py +33 -0
- rpr/map/__init__.py +0 -0
- rpr/map/architecture.py +495 -0
- rpr/map/chains.py +317 -0
- rpr/map/classifier.py +170 -0
- rpr/map/coverage.py +200 -0
- rpr/map/dependencies.py +243 -0
- rpr/map/extractor.py +223 -0
- rpr/map/graph.py +318 -0
- rpr/map/output.py +1030 -0
- rpr/map/responsibility.py +345 -0
- rpr/map/topology.py +327 -0
- rpr/map/walker.py +151 -0
- rpr/scaffolds/domain/base_entity.md +30 -0
- rpr/scaffolds/domain/base_repo.md +48 -0
- rpr/scaffolds/domain/container.md +76 -0
- rpr/scaffolds/domain/settings.md +57 -0
- rpr/scaffolds/instructions/all.instructions.md +50 -0
- rpr/scaffolds/instructions/api.instructions.md +42 -0
- rpr/scaffolds/instructions/domain.instructions.md +93 -0
- rpr/scaffolds/instructions/frontend.instructions.md +97 -0
- rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
- rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
- rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
- rpr/scaffolds/instructions/tooling.instructions.md +42 -0
- rpr/scaffolds/js_special_files/fetch.service.md +222 -0
- rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
- rpr/scaffolds/special_files/domain_container.md +76 -0
- rpr/scaffolds/special_files/domain_settings.md +57 -0
- rpr/scaffolds/special_files/dto_util.md +62 -0
- rpr/scaffolds/special_files/encrypted_column.md +98 -0
- rpr/scaffolds/special_files/mapper_util.md +159 -0
- rpr/scaffolds/special_files/partial_update.md +61 -0
- rpr/templates/__init__.py +0 -0
- rpr/templates/registry.py +81 -0
- rpr/ui/__init__.py +0 -0
- rpr/ui/console.py +32 -0
- rpr/ui/markdown.py +59 -0
- rpr/ui/prompt_session.py +430 -0
- rpr/ui/renderers.py +167 -0
- rpr/ui/theme.py +286 -0
- rpr/workspace.py +131 -0
- rpr_cli-0.1.1.dist-info/METADATA +201 -0
- rpr_cli-0.1.1.dist-info/RECORD +97 -0
- rpr_cli-0.1.1.dist-info/WHEEL +4 -0
- rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
- rpr_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from rpr.cli import generate_app
|
|
11
|
+
from rpr.context import RunContext
|
|
12
|
+
from rpr.workspace import add_workspace_member
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _api_runtime_dependencies(name: str) -> list[str]:
|
|
18
|
+
return [f"{name}-domain", "fastapi>=0.115", "uvicorn[standard]>=0.34"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@generate_app.command()
|
|
22
|
+
def api(
|
|
23
|
+
name: Annotated[
|
|
24
|
+
str | None, typer.Argument(help="App name (used as package prefix).")
|
|
25
|
+
] = None,
|
|
26
|
+
dry_run: Annotated[
|
|
27
|
+
bool, typer.Option("--dry-run", help="Preview changes without writing.")
|
|
28
|
+
] = False,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Scaffold a FastAPI application."""
|
|
31
|
+
if not name:
|
|
32
|
+
name = typer.prompt("App name")
|
|
33
|
+
|
|
34
|
+
ctx = RunContext(dry_run=dry_run, project_dir=Path.cwd())
|
|
35
|
+
app_name = f"{name}-api"
|
|
36
|
+
app_name_underscore = app_name.replace("-", "_")
|
|
37
|
+
domain_underscore = f"{name}_domain".replace("-", "_")
|
|
38
|
+
api_root = ctx.project_dir / "apps" / app_name
|
|
39
|
+
src_root = api_root / "src" / app_name_underscore
|
|
40
|
+
tests_root = api_root / "tests"
|
|
41
|
+
|
|
42
|
+
console.print(f"\n[bold]Generating FastAPI app:[/] {app_name}\n")
|
|
43
|
+
|
|
44
|
+
ctx.ensure_dir(src_root / "routes")
|
|
45
|
+
ctx.ensure_dir(tests_root)
|
|
46
|
+
|
|
47
|
+
_write_api_pyproject(ctx, api_root, name, app_name, app_name_underscore)
|
|
48
|
+
_write_api_project_json(ctx, api_root, app_name, app_name_underscore)
|
|
49
|
+
_write_main(ctx, src_root / "main.py", name, domain_underscore)
|
|
50
|
+
_write_init_file(ctx, src_root / "__init__.py")
|
|
51
|
+
_write_init_file(ctx, src_root / "routes" / "__init__.py")
|
|
52
|
+
_write_health_route(ctx, src_root / "routes" / "health.py")
|
|
53
|
+
_write_health_test(ctx, tests_root / "test_health.py", app_name_underscore)
|
|
54
|
+
|
|
55
|
+
add_workspace_member(ctx, f"apps/{app_name}")
|
|
56
|
+
ctx.preview_dependencies(
|
|
57
|
+
manager="uv",
|
|
58
|
+
target=f"apps/{app_name}",
|
|
59
|
+
runtime=_api_runtime_dependencies(name),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if not dry_run:
|
|
63
|
+
ctx.run_command(["uv", "sync"], cwd=ctx.project_dir)
|
|
64
|
+
|
|
65
|
+
console.print(f"\n[bold green]Done![/] FastAPI app created at {api_root}")
|
|
66
|
+
console.print(f" Run with: npx nx serve {app_name}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _write_init_file(ctx: RunContext, path: Path) -> None:
|
|
70
|
+
ctx.write_file(path, "")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _write_api_pyproject(
|
|
74
|
+
ctx: RunContext, api_root: Path, name: str, app_name: str, app_name_underscore: str
|
|
75
|
+
) -> None:
|
|
76
|
+
content = f"""[project]
|
|
77
|
+
name = "{app_name}"
|
|
78
|
+
version = "0.1.0"
|
|
79
|
+
description = "{name} API"
|
|
80
|
+
readme = "README.md"
|
|
81
|
+
requires-python = ">=3.12"
|
|
82
|
+
dependencies = [
|
|
83
|
+
"{_api_runtime_dependencies(name)[0]}",
|
|
84
|
+
"{_api_runtime_dependencies(name)[1]}",
|
|
85
|
+
"{_api_runtime_dependencies(name)[2]}",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
[build-system]
|
|
89
|
+
requires = ["hatchling"]
|
|
90
|
+
build-backend = "hatchling.build"
|
|
91
|
+
|
|
92
|
+
[tool.hatch.build.targets.wheel]
|
|
93
|
+
packages = ["src/{app_name_underscore}"]
|
|
94
|
+
|
|
95
|
+
[tool.uv.sources]
|
|
96
|
+
{name}-domain = {{ workspace = true }}
|
|
97
|
+
|
|
98
|
+
[tool.pytest.ini_options]
|
|
99
|
+
testpaths = ["tests"]
|
|
100
|
+
"""
|
|
101
|
+
ctx.write_file(api_root / "pyproject.toml", content)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _write_api_project_json(
|
|
105
|
+
ctx: RunContext, api_root: Path, app_name: str, app_name_underscore: str
|
|
106
|
+
) -> None:
|
|
107
|
+
project = {
|
|
108
|
+
"name": app_name,
|
|
109
|
+
"root": f"apps/{app_name}",
|
|
110
|
+
"targets": {
|
|
111
|
+
"serve": {
|
|
112
|
+
"executor": "nx:run-commands",
|
|
113
|
+
"options": {
|
|
114
|
+
"command": f"uv run uvicorn {app_name_underscore}.main:app --reload --host 0.0.0.0 --port 8000",
|
|
115
|
+
"cwd": f"apps/{app_name}",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
"build": {
|
|
119
|
+
"executor": "nx:run-commands",
|
|
120
|
+
"options": {
|
|
121
|
+
"command": "uv build",
|
|
122
|
+
"cwd": f"apps/{app_name}",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
"test": {
|
|
126
|
+
"executor": "nx:run-commands",
|
|
127
|
+
"options": {
|
|
128
|
+
"command": "uv run pytest",
|
|
129
|
+
"cwd": f"apps/{app_name}",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
"lint": {
|
|
133
|
+
"executor": "nx:run-commands",
|
|
134
|
+
"options": {
|
|
135
|
+
"command": "uv run ruff check .",
|
|
136
|
+
"cwd": f"apps/{app_name}",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
ctx.write_file(api_root / "project.json", json.dumps(project, indent=2) + "\n")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _write_main(ctx: RunContext, path: Path, name: str, domain_underscore: str) -> None:
|
|
145
|
+
content = f'''from contextlib import asynccontextmanager
|
|
146
|
+
|
|
147
|
+
from fastapi import FastAPI
|
|
148
|
+
from fastapi.exceptions import RequestValidationError
|
|
149
|
+
from starlette.requests import Request
|
|
150
|
+
from starlette.responses import JSONResponse
|
|
151
|
+
|
|
152
|
+
from {domain_underscore}.config.container import Container
|
|
153
|
+
|
|
154
|
+
_container = Container()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@asynccontextmanager
|
|
158
|
+
async def lifespan(app: FastAPI):
|
|
159
|
+
container = _container
|
|
160
|
+
container.wire(modules=[__name__])
|
|
161
|
+
app.state.container = container
|
|
162
|
+
try:
|
|
163
|
+
yield
|
|
164
|
+
finally:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
169
|
+
return JSONResponse(status_code=500, content={{"message": "Internal server error"}})
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
173
|
+
return JSONResponse(status_code=422, content={{"message": "Validation error", "details": exc.errors()}})
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def create_app() -> FastAPI:
|
|
177
|
+
app = FastAPI(
|
|
178
|
+
title="{name.replace("-", " ").title()}",
|
|
179
|
+
description="{name} API",
|
|
180
|
+
version="1.0.0",
|
|
181
|
+
lifespan=lifespan,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
app.add_exception_handler(Exception, generic_exception_handler)
|
|
185
|
+
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
|
186
|
+
from .routes.health import router as health_router
|
|
187
|
+
|
|
188
|
+
app.include_router(health_router, prefix="/api/health", tags=["Health"])
|
|
189
|
+
|
|
190
|
+
return app
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
app = create_app()
|
|
194
|
+
'''
|
|
195
|
+
ctx.write_file(path, content)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _write_health_route(ctx: RunContext, path: Path) -> None:
|
|
199
|
+
content = """from fastapi import APIRouter
|
|
200
|
+
from pydantic import BaseModel
|
|
201
|
+
|
|
202
|
+
router = APIRouter()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class HealthResponse(BaseModel):
|
|
206
|
+
status: str
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@router.get("", response_model=HealthResponse)
|
|
210
|
+
async def health_check() -> HealthResponse:
|
|
211
|
+
return HealthResponse(status="ok")
|
|
212
|
+
"""
|
|
213
|
+
ctx.write_file(path, content)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _write_health_test(ctx: RunContext, path: Path, app_name_underscore: str) -> None:
|
|
217
|
+
content = f"""import pytest
|
|
218
|
+
|
|
219
|
+
from {app_name_underscore}.routes.health import health_check
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@pytest.mark.asyncio
|
|
223
|
+
async def test_health_check() -> None:
|
|
224
|
+
response = await health_check()
|
|
225
|
+
|
|
226
|
+
assert response.status == "ok"
|
|
227
|
+
"""
|
|
228
|
+
ctx.write_file(path, content)
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from rpr.cli import generate_app
|
|
12
|
+
from rpr.context import RunContext
|
|
13
|
+
from rpr.workspace import add_workspace_member
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
_SCAFFOLD_DIR = Path(__file__).parent.parent.parent / "scaffolds" / "domain"
|
|
18
|
+
|
|
19
|
+
_UTILITY_TEMPLATES = ["mapper_util", "dto_util", "partial_update"]
|
|
20
|
+
_DOMAIN_RUNTIME_DEPS = [
|
|
21
|
+
"sqlmodel",
|
|
22
|
+
"httpx",
|
|
23
|
+
"dependency-injector",
|
|
24
|
+
"pydantic",
|
|
25
|
+
"pydantic-settings",
|
|
26
|
+
"asyncpg",
|
|
27
|
+
]
|
|
28
|
+
_DOMAIN_MIGRATION_DEPS = ["alembic"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@generate_app.command()
|
|
32
|
+
def domain(
|
|
33
|
+
name: Annotated[
|
|
34
|
+
str | None, typer.Argument(help="App name (used as package prefix).")
|
|
35
|
+
] = None,
|
|
36
|
+
migrations: Annotated[
|
|
37
|
+
bool,
|
|
38
|
+
typer.Option("--migrations", help="Scaffold Alembic migrations for the domain package."),
|
|
39
|
+
] = False,
|
|
40
|
+
dry_run: Annotated[
|
|
41
|
+
bool, typer.Option("--dry-run", help="Preview changes without writing.")
|
|
42
|
+
] = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Scaffold a DDD Python domain package."""
|
|
45
|
+
if not name:
|
|
46
|
+
name = typer.prompt("App name")
|
|
47
|
+
|
|
48
|
+
ctx = RunContext(dry_run=dry_run, project_dir=Path.cwd())
|
|
49
|
+
|
|
50
|
+
name_underscore = name.replace("-", "_")
|
|
51
|
+
pkg_name = f"{name}-domain"
|
|
52
|
+
pkg_name_underscore = f"{name_underscore}_domain"
|
|
53
|
+
|
|
54
|
+
domain_root = ctx.project_dir / "packages" / "python" / "domain"
|
|
55
|
+
src_root = domain_root / "src" / pkg_name_underscore
|
|
56
|
+
tests_root = domain_root / "tests"
|
|
57
|
+
migrations_root = domain_root / "migrations"
|
|
58
|
+
|
|
59
|
+
console.print(f"\n[bold]Generating domain package:[/] {pkg_name}\n")
|
|
60
|
+
|
|
61
|
+
# --- directories -------------------------------------------------------
|
|
62
|
+
dirs = ["entities", "dtos", "repos", "services", "workflows", "config", "utils"]
|
|
63
|
+
for d in dirs:
|
|
64
|
+
ctx.ensure_dir(src_root / d)
|
|
65
|
+
ctx.ensure_dir(tests_root)
|
|
66
|
+
if migrations:
|
|
67
|
+
ctx.ensure_dir(migrations_root / "versions")
|
|
68
|
+
|
|
69
|
+
# --- project metadata --------------------------------------------------
|
|
70
|
+
_write_domain_pyproject(ctx, domain_root, name, pkg_name_underscore)
|
|
71
|
+
_write_project_json(ctx, domain_root, include_migrations=migrations)
|
|
72
|
+
|
|
73
|
+
# --- __init__.py files -------------------------------------------------
|
|
74
|
+
for d in ["", *dirs]:
|
|
75
|
+
ctx.write_file(src_root / d / "__init__.py", "")
|
|
76
|
+
|
|
77
|
+
# --- config stubs ------------------------------------------------------
|
|
78
|
+
_write_settings(ctx, src_root / "config" / "settings.py")
|
|
79
|
+
_write_container(ctx, src_root / "config" / "container.py", pkg_name_underscore)
|
|
80
|
+
|
|
81
|
+
# --- scaffold files ----------------------------------------------------
|
|
82
|
+
_write_base_entity(ctx, src_root / "entities" / "base.py")
|
|
83
|
+
_write_base_repo(ctx, src_root / "repos" / "base.py", pkg_name_underscore)
|
|
84
|
+
_write_domain_test(ctx, tests_root / "test_package.py", pkg_name_underscore)
|
|
85
|
+
if migrations:
|
|
86
|
+
_write_alembic_ini(ctx, domain_root / "alembic.ini")
|
|
87
|
+
_write_migration_env(ctx, migrations_root / "env.py", pkg_name_underscore)
|
|
88
|
+
|
|
89
|
+
# --- workspace + deps --------------------------------------------------
|
|
90
|
+
add_workspace_member(ctx, "packages/python/domain")
|
|
91
|
+
ctx.preview_dependencies(
|
|
92
|
+
manager="uv",
|
|
93
|
+
target="packages/python/domain",
|
|
94
|
+
runtime=_domain_dependencies(include_migrations=migrations),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if not dry_run:
|
|
98
|
+
ctx.run_command(["uv", "sync"], cwd=ctx.project_dir)
|
|
99
|
+
_install_domain_deps(ctx, domain_root, include_migrations=migrations)
|
|
100
|
+
ctx.run_command(["uv", "sync"], cwd=ctx.project_dir)
|
|
101
|
+
|
|
102
|
+
console.print(f"\n[bold green]Done![/] Domain package created at {domain_root}")
|
|
103
|
+
|
|
104
|
+
# --- post-scaffold utility prompt --------------------------------------
|
|
105
|
+
if not dry_run:
|
|
106
|
+
_offer_utility_templates(ctx, name)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Dependencies
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _domain_dependencies(*, include_migrations: bool) -> list[str]:
|
|
115
|
+
deps = list(_DOMAIN_RUNTIME_DEPS)
|
|
116
|
+
if include_migrations:
|
|
117
|
+
deps.extend(_DOMAIN_MIGRATION_DEPS)
|
|
118
|
+
return deps
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _install_domain_deps(
|
|
122
|
+
ctx: RunContext, domain_root: Path, *, include_migrations: bool
|
|
123
|
+
) -> None:
|
|
124
|
+
ctx.run_command(
|
|
125
|
+
["uv", "add", *_domain_dependencies(include_migrations=include_migrations)],
|
|
126
|
+
cwd=domain_root,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Project metadata
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _write_domain_pyproject(
|
|
136
|
+
ctx: RunContext, domain_root: Path, name: str, pkg_name_underscore: str
|
|
137
|
+
) -> None:
|
|
138
|
+
content = f"""\
|
|
139
|
+
[project]
|
|
140
|
+
name = "{name}-domain"
|
|
141
|
+
version = "0.1.0"
|
|
142
|
+
description = "{name} domain layer"
|
|
143
|
+
readme = "README.md"
|
|
144
|
+
requires-python = ">=3.12"
|
|
145
|
+
dependencies = []
|
|
146
|
+
|
|
147
|
+
[build-system]
|
|
148
|
+
requires = ["hatchling"]
|
|
149
|
+
build-backend = "hatchling.build"
|
|
150
|
+
|
|
151
|
+
[tool.hatch.build.targets.wheel]
|
|
152
|
+
packages = ["src/{pkg_name_underscore}"]
|
|
153
|
+
|
|
154
|
+
[tool.pytest.ini_options]
|
|
155
|
+
testpaths = ["tests"]
|
|
156
|
+
"""
|
|
157
|
+
ctx.write_file(domain_root / "pyproject.toml", content)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _write_project_json(
|
|
161
|
+
ctx: RunContext, domain_root: Path, *, include_migrations: bool
|
|
162
|
+
) -> None:
|
|
163
|
+
project = {
|
|
164
|
+
"name": "domain",
|
|
165
|
+
"root": "packages/python/domain",
|
|
166
|
+
"targets": {
|
|
167
|
+
"build": {
|
|
168
|
+
"executor": "nx:run-commands",
|
|
169
|
+
"options": {
|
|
170
|
+
"command": "uv build",
|
|
171
|
+
"cwd": "packages/python/domain",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
"test": {
|
|
175
|
+
"executor": "nx:run-commands",
|
|
176
|
+
"options": {
|
|
177
|
+
"command": "uv run pytest packages/python/domain",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
"lint": {
|
|
181
|
+
"executor": "nx:run-commands",
|
|
182
|
+
"options": {
|
|
183
|
+
"command": "uv run ruff check packages/python/domain/src",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
if include_migrations:
|
|
189
|
+
project["targets"]["migrate"] = {
|
|
190
|
+
"executor": "nx:run-commands",
|
|
191
|
+
"options": {
|
|
192
|
+
"command": "uv run alembic -c packages/python/domain/alembic.ini",
|
|
193
|
+
"cwd": ".",
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
ctx.write_file(domain_root / "project.json", json.dumps(project, indent=2) + "\n")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Config stubs
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _write_settings(ctx: RunContext, path: Path) -> None:
|
|
205
|
+
code = _extract_first_code_block(_SCAFFOLD_DIR / "settings.md")
|
|
206
|
+
ctx.write_file(path, code)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _write_container(ctx: RunContext, path: Path, pkg_name_underscore: str) -> None:
|
|
210
|
+
code = _extract_first_code_block(_SCAFFOLD_DIR / "container.md")
|
|
211
|
+
code = code.replace("your_app_domain", pkg_name_underscore)
|
|
212
|
+
ctx.write_file(path, code)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# Scaffold helpers
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _extract_first_code_block(md_path: Path) -> str:
|
|
221
|
+
"""Return the content of the first fenced code block in a markdown file."""
|
|
222
|
+
text = md_path.read_text()
|
|
223
|
+
match = re.search(r"```(?:\w*)\n(.*?)```", text, re.DOTALL)
|
|
224
|
+
if not match:
|
|
225
|
+
raise ValueError(f"No fenced code block found in {md_path}")
|
|
226
|
+
return match.group(1)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _write_base_entity(ctx: RunContext, path: Path) -> None:
|
|
230
|
+
code = _extract_first_code_block(_SCAFFOLD_DIR / "base_entity.md")
|
|
231
|
+
ctx.write_file(path, code)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _write_base_repo(ctx: RunContext, path: Path, pkg_name_underscore: str) -> None:
|
|
235
|
+
code = _extract_first_code_block(_SCAFFOLD_DIR / "base_repo.md")
|
|
236
|
+
code = code.replace("your_app_domain", pkg_name_underscore)
|
|
237
|
+
ctx.write_file(path, code)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _write_domain_test(ctx: RunContext, path: Path, pkg_name_underscore: str) -> None:
|
|
241
|
+
content = f"""from {pkg_name_underscore}.config.settings import Settings
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_settings_database_url_uses_defaults() -> None:
|
|
245
|
+
settings = Settings()
|
|
246
|
+
|
|
247
|
+
assert settings.database_url.startswith("postgresql+")
|
|
248
|
+
"""
|
|
249
|
+
ctx.write_file(path, content)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _write_alembic_ini(ctx: RunContext, path: Path) -> None:
|
|
253
|
+
content = """[alembic]
|
|
254
|
+
script_location = %(here)s/migrations
|
|
255
|
+
|
|
256
|
+
[loggers]
|
|
257
|
+
keys = root,sqlalchemy,alembic
|
|
258
|
+
|
|
259
|
+
[handlers]
|
|
260
|
+
keys = console
|
|
261
|
+
|
|
262
|
+
[formatters]
|
|
263
|
+
keys = generic
|
|
264
|
+
|
|
265
|
+
[logger_root]
|
|
266
|
+
level = WARN
|
|
267
|
+
handlers = console
|
|
268
|
+
|
|
269
|
+
[logger_sqlalchemy]
|
|
270
|
+
level = WARN
|
|
271
|
+
handlers =
|
|
272
|
+
qualname = sqlalchemy.engine
|
|
273
|
+
|
|
274
|
+
[logger_alembic]
|
|
275
|
+
level = INFO
|
|
276
|
+
handlers =
|
|
277
|
+
qualname = alembic
|
|
278
|
+
|
|
279
|
+
[handler_console]
|
|
280
|
+
class = StreamHandler
|
|
281
|
+
args = (sys.stderr,)
|
|
282
|
+
level = NOTSET
|
|
283
|
+
formatter = generic
|
|
284
|
+
|
|
285
|
+
[formatter_generic]
|
|
286
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
287
|
+
"""
|
|
288
|
+
ctx.write_file(path, content)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _write_migration_env(
|
|
292
|
+
ctx: RunContext, path: Path, pkg_name_underscore: str
|
|
293
|
+
) -> None:
|
|
294
|
+
content = f"""from __future__ import annotations
|
|
295
|
+
|
|
296
|
+
import asyncio
|
|
297
|
+
from logging.config import fileConfig
|
|
298
|
+
|
|
299
|
+
from alembic import context
|
|
300
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
301
|
+
from sqlmodel import SQLModel
|
|
302
|
+
|
|
303
|
+
from {pkg_name_underscore}.config.settings import Settings
|
|
304
|
+
import {pkg_name_underscore}.entities # noqa: F401
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
settings = Settings()
|
|
308
|
+
config = context.config
|
|
309
|
+
|
|
310
|
+
if config.config_file_name is not None:
|
|
311
|
+
fileConfig(config.config_file_name)
|
|
312
|
+
|
|
313
|
+
target_metadata = SQLModel.metadata
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def include_object(object, name, type_, reflected, compare_to):
|
|
317
|
+
if type_ == "table":
|
|
318
|
+
entity_table_names = set(target_metadata.tables.keys())
|
|
319
|
+
return name in entity_table_names
|
|
320
|
+
|
|
321
|
+
if type_ in ("index", "unique_constraint", "foreign_key_constraint", "check_constraint"):
|
|
322
|
+
if hasattr(object, "table"):
|
|
323
|
+
entity_table_names = set(target_metadata.tables.keys())
|
|
324
|
+
return object.table.name in entity_table_names
|
|
325
|
+
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def do_run_migrations(connection) -> None:
|
|
330
|
+
context.configure(
|
|
331
|
+
connection=connection,
|
|
332
|
+
target_metadata=target_metadata,
|
|
333
|
+
compare_type=True,
|
|
334
|
+
include_object=include_object,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
with context.begin_transaction():
|
|
338
|
+
context.run_migrations()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def run_migrations_offline() -> None:
|
|
342
|
+
context.configure(
|
|
343
|
+
url=settings.database_url,
|
|
344
|
+
target_metadata=target_metadata,
|
|
345
|
+
literal_binds=True,
|
|
346
|
+
dialect_opts={{"paramstyle": "named"}},
|
|
347
|
+
include_object=include_object,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
with context.begin_transaction():
|
|
351
|
+
context.run_migrations()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
async def run_migrations_online() -> None:
|
|
355
|
+
connectable = create_async_engine(settings.database_url, pool_pre_ping=True)
|
|
356
|
+
|
|
357
|
+
async with connectable.connect() as connection:
|
|
358
|
+
await connection.run_sync(do_run_migrations)
|
|
359
|
+
|
|
360
|
+
await connectable.dispose()
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
if context.is_offline_mode():
|
|
364
|
+
run_migrations_offline()
|
|
365
|
+
else:
|
|
366
|
+
asyncio.run(run_migrations_online())
|
|
367
|
+
"""
|
|
368
|
+
ctx.write_file(path, content)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
# Post-scaffold utility prompt
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _offer_utility_templates(ctx: RunContext, name: str) -> None:
|
|
377
|
+
console.print("\n[bold]Tip:[/] Add common utilities with:")
|
|
378
|
+
for t in _UTILITY_TEMPLATES:
|
|
379
|
+
console.print(f" [dim]rpr add template {t} --name {name}[/]")
|
|
380
|
+
|
|
381
|
+
if typer.confirm("\nWould you like to add these utilities now?", default=False):
|
|
382
|
+
for t in _UTILITY_TEMPLATES:
|
|
383
|
+
ctx.run_command(["rpr", "add", "template", t, "--name", name])
|