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.
Files changed (97) hide show
  1. rpr/__init__.py +1 -0
  2. rpr/agent/__init__.py +29 -0
  3. rpr/agent/approval.py +19 -0
  4. rpr/agent/bootstrap.py +60 -0
  5. rpr/agent/client.py +115 -0
  6. rpr/agent/control.py +16 -0
  7. rpr/agent/mock.py +158 -0
  8. rpr/agent/runtime.py +291 -0
  9. rpr/agent/session.py +91 -0
  10. rpr/agent/tools/__init__.py +10 -0
  11. rpr/agent/tools/base.py +62 -0
  12. rpr/agent/tools/mutating.py +47 -0
  13. rpr/agent/tools/readonly.py +147 -0
  14. rpr/agent/tools/registry.py +30 -0
  15. rpr/application/__init__.py +1 -0
  16. rpr/application/catalog.py +427 -0
  17. rpr/application/chat_service.py +413 -0
  18. rpr/application/checks.py +49 -0
  19. rpr/application/cli_adapter.py +36 -0
  20. rpr/application/completer.py +163 -0
  21. rpr/application/conversation_service.py +92 -0
  22. rpr/application/prompt_service.py +85 -0
  23. rpr/application/selector.py +240 -0
  24. rpr/application/shell.py +543 -0
  25. rpr/checks/__init__.py +0 -0
  26. rpr/checks/base.py +13 -0
  27. rpr/checks/instructions.py +89 -0
  28. rpr/checks/packages.py +619 -0
  29. rpr/checks/workspace.py +170 -0
  30. rpr/cli.py +78 -0
  31. rpr/commands/__init__.py +0 -0
  32. rpr/commands/add.py +166 -0
  33. rpr/commands/chat.py +48 -0
  34. rpr/commands/check.py +53 -0
  35. rpr/commands/generate/__init__.py +0 -0
  36. rpr/commands/generate/api.py +228 -0
  37. rpr/commands/generate/domain.py +383 -0
  38. rpr/commands/generate/engine.py +148 -0
  39. rpr/commands/generate/storybook.py +442 -0
  40. rpr/commands/generate/ui.py +414 -0
  41. rpr/commands/init.py +822 -0
  42. rpr/commands/map.py +113 -0
  43. rpr/commands/settings.py +102 -0
  44. rpr/commands/sync.py +97 -0
  45. rpr/context.py +203 -0
  46. rpr/generators/__init__.py +0 -0
  47. rpr/generators/base.py +110 -0
  48. rpr/generators/claude.py +33 -0
  49. rpr/generators/copilot.py +36 -0
  50. rpr/generators/cursor.py +38 -0
  51. rpr/generators/gemini.py +33 -0
  52. rpr/map/__init__.py +0 -0
  53. rpr/map/architecture.py +495 -0
  54. rpr/map/chains.py +317 -0
  55. rpr/map/classifier.py +170 -0
  56. rpr/map/coverage.py +200 -0
  57. rpr/map/dependencies.py +243 -0
  58. rpr/map/extractor.py +223 -0
  59. rpr/map/graph.py +318 -0
  60. rpr/map/output.py +1030 -0
  61. rpr/map/responsibility.py +345 -0
  62. rpr/map/topology.py +327 -0
  63. rpr/map/walker.py +151 -0
  64. rpr/scaffolds/domain/base_entity.md +30 -0
  65. rpr/scaffolds/domain/base_repo.md +48 -0
  66. rpr/scaffolds/domain/container.md +76 -0
  67. rpr/scaffolds/domain/settings.md +57 -0
  68. rpr/scaffolds/instructions/all.instructions.md +50 -0
  69. rpr/scaffolds/instructions/api.instructions.md +42 -0
  70. rpr/scaffolds/instructions/domain.instructions.md +93 -0
  71. rpr/scaffolds/instructions/frontend.instructions.md +97 -0
  72. rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
  73. rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
  74. rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
  75. rpr/scaffolds/instructions/tooling.instructions.md +42 -0
  76. rpr/scaffolds/js_special_files/fetch.service.md +222 -0
  77. rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
  78. rpr/scaffolds/special_files/domain_container.md +76 -0
  79. rpr/scaffolds/special_files/domain_settings.md +57 -0
  80. rpr/scaffolds/special_files/dto_util.md +62 -0
  81. rpr/scaffolds/special_files/encrypted_column.md +98 -0
  82. rpr/scaffolds/special_files/mapper_util.md +159 -0
  83. rpr/scaffolds/special_files/partial_update.md +61 -0
  84. rpr/templates/__init__.py +0 -0
  85. rpr/templates/registry.py +81 -0
  86. rpr/ui/__init__.py +0 -0
  87. rpr/ui/console.py +32 -0
  88. rpr/ui/markdown.py +59 -0
  89. rpr/ui/prompt_session.py +430 -0
  90. rpr/ui/renderers.py +167 -0
  91. rpr/ui/theme.py +286 -0
  92. rpr/workspace.py +131 -0
  93. rpr_cli-0.1.1.dist-info/METADATA +201 -0
  94. rpr_cli-0.1.1.dist-info/RECORD +97 -0
  95. rpr_cli-0.1.1.dist-info/WHEEL +4 -0
  96. rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
  97. 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])