moltpy 0.1.0__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.
- moltpy/__init__.py +4 -0
- moltpy/builder.py +341 -0
- moltpy/cli.py +446 -0
- moltpy/config.py +230 -0
- moltpy/external.py +176 -0
- moltpy/generator.py +551 -0
- moltpy/generator_templates/model.py.j2 +36 -0
- moltpy/generator_templates/router.py.j2 +56 -0
- moltpy/generator_templates/schema.py.j2 +38 -0
- moltpy/generator_templates/service.py.j2 +65 -0
- moltpy/generator_templates/test_api.py.j2 +86 -0
- moltpy/modules/admin/manifest.json +20 -0
- moltpy/modules/admin/templates/app/admin.py.j2 +29 -0
- moltpy/modules/api_versioning/manifest.json +9 -0
- moltpy/modules/api_versioning/templates/app/routers/v1/__init__.py +1 -0
- moltpy/modules/api_versioning/templates/app/routers/v1/api.py.j2 +13 -0
- moltpy/modules/arq/manifest.json +17 -0
- moltpy/modules/arq/templates/app/arq.py.j2 +228 -0
- moltpy/modules/arq/templates/app/routers/tasks.py.j2 +70 -0
- moltpy/modules/arq/templates/app/tasks.py.j2 +26 -0
- moltpy/modules/arq/templates/scripts/worker.py.j2 +79 -0
- moltpy/modules/auth/manifest.json +26 -0
- moltpy/modules/auth/templates/app/routers/auth.py.j2 +295 -0
- moltpy/modules/auth/templates/tests/test_auth.py +52 -0
- moltpy/modules/auth_social/manifest.json +30 -0
- moltpy/modules/auth_social/templates/app/models/social_account.py.j2 +38 -0
- moltpy/modules/auth_social/templates/app/routers/oauth.py.j2 +76 -0
- moltpy/modules/auth_social/templates/app/schemas/oauth.py.j2 +44 -0
- moltpy/modules/auth_social/templates/app/services/oauth.py.j2 +260 -0
- moltpy/modules/base.py +51 -0
- moltpy/modules/celery/manifest.json +29 -0
- moltpy/modules/celery/templates/app/celery.py.j2 +43 -0
- moltpy/modules/celery/templates/app/celerybeat_schedule.py +14 -0
- moltpy/modules/celery/templates/app/tasks.py +64 -0
- moltpy/modules/cors/manifest.json +9 -0
- moltpy/modules/database/manifest.json +22 -0
- moltpy/modules/database/templates/alembic/env.py.j2 +82 -0
- moltpy/modules/database/templates/alembic.ini +53 -0
- moltpy/modules/database/templates/app/database.py.j2 +96 -0
- moltpy/modules/docker/manifest.json +16 -0
- moltpy/modules/docker/templates/.dockerignore +40 -0
- moltpy/modules/docker/templates/Dockerfile.j2 +61 -0
- moltpy/modules/docker/templates/docker-compose.yml.j2 +112 -0
- moltpy/modules/docs/manifest.json +9 -0
- moltpy/modules/docs/templates/docs/api.md.j2 +58 -0
- moltpy/modules/docs/templates/docs/development.md.j2 +53 -0
- moltpy/modules/docs/templates/docs/index.md.j2 +42 -0
- moltpy/modules/docs/templates/mkdocs.yml.j2 +50 -0
- moltpy/modules/email/manifest.json +20 -0
- moltpy/modules/email/templates/app/core/email.py.j2 +131 -0
- moltpy/modules/email/templates/app/templates/email/password_reset.html.j2 +54 -0
- moltpy/modules/email/templates/app/templates/email/password_reset.txt.j2 +17 -0
- moltpy/modules/email/templates/app/templates/email/receipt.html.j2 +71 -0
- moltpy/modules/email/templates/app/templates/email/receipt.txt.j2 +20 -0
- moltpy/modules/email/templates/app/templates/email/welcome.html.j2 +52 -0
- moltpy/modules/email/templates/app/templates/email/welcome.txt.j2 +15 -0
- moltpy/modules/events/manifest.json +9 -0
- moltpy/modules/events/templates/app/core/events.py.j2 +52 -0
- moltpy/modules/events/templates/app/events/__init__.py.j2 +3 -0
- moltpy/modules/events/templates/app/events/handlers.py.j2 +24 -0
- moltpy/modules/github_actions/manifest.json +9 -0
- moltpy/modules/github_actions/templates/.github/workflows/ci.yml.j2 +107 -0
- moltpy/modules/graphql/manifest.json +32 -0
- moltpy/modules/graphql/templates/app/graphql/__init__.py.j2 +3 -0
- moltpy/modules/graphql/templates/app/graphql/resolvers.py.j2 +3 -0
- moltpy/modules/graphql/templates/app/graphql/schema.py.j2 +160 -0
- moltpy/modules/grpc/manifest.json +15 -0
- moltpy/modules/grpc/templates/app/grpc/__init__.py.j2 +3 -0
- moltpy/modules/grpc/templates/app/grpc/server.py.j2 +43 -0
- moltpy/modules/grpc/templates/protos/service.proto +19 -0
- moltpy/modules/health/manifest.json +9 -0
- moltpy/modules/kubernetes/manifest.json +31 -0
- moltpy/modules/kubernetes/templates/k8s/00-namespace.yml.j2 +5 -0
- moltpy/modules/kubernetes/templates/k8s/01-secrets.yml.j2 +41 -0
- moltpy/modules/kubernetes/templates/k8s/02-configmap.yml.j2 +34 -0
- moltpy/modules/kubernetes/templates/k8s/03-deployment.yml.j2 +74 -0
- moltpy/modules/kubernetes/templates/k8s/04-service.yml.j2 +17 -0
- moltpy/modules/kubernetes/templates/k8s/05-ingress.yml.j2 +58 -0
- moltpy/modules/kubernetes/templates/k8s/06-postgres.yml.j2 +81 -0
- moltpy/modules/kubernetes/templates/k8s/07-redis.yml.j2 +59 -0
- moltpy/modules/kubernetes/templates/k8s/README.md.j2 +104 -0
- moltpy/modules/logging/manifest.json +9 -0
- moltpy/modules/makefile/manifest.json +9 -0
- moltpy/modules/makefile/templates/Makefile.j2 +86 -0
- moltpy/modules/notifications/manifest.json +21 -0
- moltpy/modules/notifications/snippets/app/arq.py.j2 +19 -0
- moltpy/modules/notifications/snippets/app/tasks.py.j2 +24 -0
- moltpy/modules/notifications/templates/app/api/v1/endpoints/notifications.py.j2 +63 -0
- moltpy/modules/notifications/templates/app/core/notifications.py.j2 +58 -0
- moltpy/modules/notifications/templates/app/schemas/notification.py.j2 +21 -0
- moltpy/modules/notifications/templates/app/services/notification_service.py.j2 +31 -0
- moltpy/modules/observability/manifest.json +20 -0
- moltpy/modules/observability/templates/app/middleware/telemetry.py.j2 +72 -0
- moltpy/modules/observability/templates/app/routers/metrics.py.j2 +50 -0
- moltpy/modules/observability/templates/grafana/dashboards/fastapi-overview.json +258 -0
- moltpy/modules/observability/templates/grafana/provisioning/dashboards/dashboards.yml +12 -0
- moltpy/modules/observability/templates/grafana/provisioning/datasources/prometheus.yml +9 -0
- moltpy/modules/openapi/manifest.json +16 -0
- moltpy/modules/openapi/templates/app/core/openapi.py +39 -0
- moltpy/modules/pagination/manifest.json +9 -0
- moltpy/modules/payments/manifest.json +21 -0
- moltpy/modules/payments/templates/app/api/v1/endpoints/payments.py.j2 +91 -0
- moltpy/modules/payments/templates/app/core/payments.py.j2 +39 -0
- moltpy/modules/payments/templates/app/schemas/payment.py.j2 +27 -0
- moltpy/modules/pre_commit/manifest.json +9 -0
- moltpy/modules/pre_commit/templates/.pre-commit-config.yaml +13 -0
- moltpy/modules/rate_limit/manifest.json +16 -0
- moltpy/modules/rate_limit/templates/app/middleware/rate_limit.py.j2 +31 -0
- moltpy/modules/redis/manifest.json +9 -0
- moltpy/modules/redis/templates/app/redis.py +55 -0
- moltpy/modules/scheduler/manifest.json +21 -0
- moltpy/modules/scheduler/templates/app/core/scheduler.py.j2 +15 -0
- moltpy/modules/scheduler/templates/app/jobs/__init__.py.j2 +3 -0
- moltpy/modules/scheduler/templates/app/jobs/example_job.py.j2 +68 -0
- moltpy/modules/search/manifest.json +15 -0
- moltpy/modules/search/snippets/docker-compose.yml.j2 +13 -0
- moltpy/modules/search/templates/app/core/search.py.j2 +15 -0
- moltpy/modules/search/templates/app/routers/search.py.j2 +18 -0
- moltpy/modules/search/templates/app/services/search_service.py.j2 +37 -0
- moltpy/modules/seeding/manifest.json +15 -0
- moltpy/modules/seeding/templates/scripts/seed.py.j2 +153 -0
- moltpy/modules/sentry/manifest.json +9 -0
- moltpy/modules/sentry/templates/app/core/sentry.py +30 -0
- moltpy/modules/storage/manifest.json +16 -0
- moltpy/modules/storage/templates/app/routers/storage.py.j2 +205 -0
- moltpy/modules/storage/templates/app/schemas/storage.py.j2 +70 -0
- moltpy/modules/storage/templates/app/services/storage.py.j2 +270 -0
- moltpy/modules/terraform/manifest.json +16 -0
- moltpy/modules/terraform/templates/terraform/main.tf.j2 +527 -0
- moltpy/modules/terraform/templates/terraform/outputs.tf.j2 +55 -0
- moltpy/modules/terraform/templates/terraform/variables.tf.j2 +52 -0
- moltpy/modules/testing/manifest.json +9 -0
- moltpy/modules/users/manifest.json +9 -0
- moltpy/modules/users/templates/app/models/__init__.py +3 -0
- moltpy/modules/users/templates/app/models/user.py +33 -0
- moltpy/modules/users/templates/app/routers/users.py +54 -0
- moltpy/modules/users/templates/app/schemas/__init__.py +3 -0
- moltpy/modules/users/templates/app/schemas/user.py +36 -0
- moltpy/modules/users/templates/app/services/__init__.py +3 -0
- moltpy/modules/users/templates/app/services/user.py +64 -0
- moltpy/modules/websockets/manifest.json +16 -0
- moltpy/modules/websockets/templates/app/routers/websockets.py.j2 +168 -0
- moltpy/modules/websockets/templates/app/websockets.py.j2 +307 -0
- moltpy/presets.py +168 -0
- moltpy/registry.py +241 -0
- moltpy/renderer.py +421 -0
- moltpy/templates/.env.example.j2 +130 -0
- moltpy/templates/.gitignore +73 -0
- moltpy/templates/CONTRIBUTING.md.j2 +126 -0
- moltpy/templates/README.md.j2 +154 -0
- moltpy/templates/__init__.py +1 -0
- moltpy/templates/app/__init__.py.j2 +3 -0
- moltpy/templates/app/config.py.j2 +154 -0
- moltpy/templates/app/dependencies.py.j2 +37 -0
- moltpy/templates/app/main.py.j2 +223 -0
- moltpy/templates/app/routers/__init__.py +1 -0
- moltpy/templates/app/routers/example.py +48 -0
- moltpy/templates/app/routers/health.py +34 -0
- moltpy/templates/app/utils/__init__.py +1 -0
- moltpy/templates/app/utils/logging.py +43 -0
- moltpy/templates/app/utils/pagination.py +38 -0
- moltpy/templates/docs/modules.md.j2 +40 -0
- moltpy/templates/pyproject.toml.j2 +159 -0
- moltpy/templates/tests/__init__.py +1 -0
- moltpy/templates/tests/conftest.py.j2 +67 -0
- moltpy/templates/tests/test_example.py +37 -0
- moltpy/wizard.py +243 -0
- moltpy-0.1.0.dist-info/METADATA +322 -0
- moltpy-0.1.0.dist-info/RECORD +172 -0
- moltpy-0.1.0.dist-info/WHEEL +4 -0
- moltpy-0.1.0.dist-info/entry_points.txt +2 -0
- moltpy-0.1.0.dist-info/licenses/LICENSE +21 -0
moltpy/__init__.py
ADDED
moltpy/builder.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Project builder orchestrates the full generation flow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import py_compile
|
|
6
|
+
import re
|
|
7
|
+
import secrets
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from moltpy.config import ProjectConfig
|
|
16
|
+
from moltpy.registry import ModuleRegistry
|
|
17
|
+
from moltpy.renderer import RenderedFile, TemplateRenderer
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProjectBuilder:
|
|
23
|
+
"""Orchestrates project generation from configuration to rendered output."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
config: ProjectConfig,
|
|
28
|
+
registry: ModuleRegistry | None = None,
|
|
29
|
+
template_dir: Path | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize builder with project configuration."""
|
|
32
|
+
self.config = config
|
|
33
|
+
self.registry = registry or ModuleRegistry()
|
|
34
|
+
self.renderer = TemplateRenderer(config, self.registry, template_overrides_dir=template_dir)
|
|
35
|
+
|
|
36
|
+
def build(
|
|
37
|
+
self,
|
|
38
|
+
target_dir: Path | None = None,
|
|
39
|
+
force: bool = False,
|
|
40
|
+
dry_run: bool = False,
|
|
41
|
+
skip_post_generation: bool = False,
|
|
42
|
+
) -> Path:
|
|
43
|
+
"""Build the complete project.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
target_dir: Output directory. Defaults to ./<project_name>
|
|
47
|
+
force: Overwrite existing non-empty directory.
|
|
48
|
+
dry_run: Preview files without writing them.
|
|
49
|
+
skip_post_generation: Skip uv sync, git init, and pre-commit install.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Path to the generated project directory.
|
|
53
|
+
"""
|
|
54
|
+
if target_dir is None:
|
|
55
|
+
target_dir = Path.cwd() / self.config.name
|
|
56
|
+
|
|
57
|
+
# Check for overwrite
|
|
58
|
+
if target_dir.exists() and any(target_dir.iterdir()) and not force and not dry_run:
|
|
59
|
+
console.print(
|
|
60
|
+
f"[red]Error: Directory '{target_dir}' already exists and is not empty.[/red]"
|
|
61
|
+
)
|
|
62
|
+
console.print("[dim]Use --force to overwrite.[/dim]")
|
|
63
|
+
raise ValueError("Target directory already exists")
|
|
64
|
+
|
|
65
|
+
# Validate module selection
|
|
66
|
+
is_valid, conflicts = self.registry.validate_selection(self.config.modules)
|
|
67
|
+
if not is_valid:
|
|
68
|
+
for msg in conflicts:
|
|
69
|
+
console.print(f"[red]Error: {msg}[/red]")
|
|
70
|
+
raise ValueError("Module selection has conflicts")
|
|
71
|
+
|
|
72
|
+
# Resolve modules and run pre-render hooks
|
|
73
|
+
resolved_modules = self.registry.resolve_dependencies(self.config.modules)
|
|
74
|
+
context = self.renderer._get_context()
|
|
75
|
+
for name in resolved_modules:
|
|
76
|
+
instance = self.registry.get_instance(name)
|
|
77
|
+
if instance is not None:
|
|
78
|
+
try:
|
|
79
|
+
hook_result = instance.pre_render(context)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
console.print(
|
|
82
|
+
f"[red]Error: Module '{name}' pre_render hook failed: {exc}[/red]"
|
|
83
|
+
)
|
|
84
|
+
raise ValueError(f"Module '{name}' pre_render hook failed: {exc}") from exc
|
|
85
|
+
if hook_result is None:
|
|
86
|
+
raise ValueError(f"Module '{name}' pre_render returned None")
|
|
87
|
+
context = hook_result
|
|
88
|
+
|
|
89
|
+
if dry_run:
|
|
90
|
+
# Dry-run: collect files without writing
|
|
91
|
+
entries = self.renderer.render(target_dir, dry_run=True)
|
|
92
|
+
self._print_dry_run_table(entries, target_dir)
|
|
93
|
+
return target_dir
|
|
94
|
+
|
|
95
|
+
with Progress(
|
|
96
|
+
SpinnerColumn(),
|
|
97
|
+
TextColumn("[progress.description]{task.description}"),
|
|
98
|
+
console=console,
|
|
99
|
+
) as progress:
|
|
100
|
+
# Step 1: Render templates
|
|
101
|
+
task = progress.add_task("Rendering project templates...", total=None)
|
|
102
|
+
self.renderer.render(target_dir)
|
|
103
|
+
progress.update(task, completed=True)
|
|
104
|
+
|
|
105
|
+
# Step 2: Validate generated Python syntax
|
|
106
|
+
progress.update(task, description="Validating generated code...")
|
|
107
|
+
self._validate_generated_code(target_dir)
|
|
108
|
+
|
|
109
|
+
# Step 3: Generate .env file with secure secrets
|
|
110
|
+
progress.update(task, description="Generating secure .env file...")
|
|
111
|
+
self._generate_env_file(target_dir)
|
|
112
|
+
|
|
113
|
+
# Step 4: Post-generation hooks
|
|
114
|
+
if not skip_post_generation:
|
|
115
|
+
if self.config.post_generation.get("uv_sync", False):
|
|
116
|
+
progress.update(task, description="Running uv sync...")
|
|
117
|
+
self._run_uv_sync(target_dir)
|
|
118
|
+
|
|
119
|
+
if self.config.post_generation.get("git_init", False):
|
|
120
|
+
progress.update(task, description="Initializing git repository...")
|
|
121
|
+
self._run_git_init(target_dir)
|
|
122
|
+
|
|
123
|
+
if self.config.post_generation.get("pre_commit_install", False):
|
|
124
|
+
progress.update(task, description="Installing pre-commit hooks...")
|
|
125
|
+
self._run_pre_commit_install(target_dir)
|
|
126
|
+
|
|
127
|
+
# Step 5: Module post-render hooks
|
|
128
|
+
for name in resolved_modules:
|
|
129
|
+
instance = self.registry.get_instance(name)
|
|
130
|
+
if instance is not None:
|
|
131
|
+
try:
|
|
132
|
+
instance.post_render(target_dir, context)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
console.print(
|
|
135
|
+
f"[red]Error: Module '{name}' post_render hook failed: {exc}[/red]"
|
|
136
|
+
)
|
|
137
|
+
raise ValueError(f"Module '{name}' post_render hook failed: {exc}") from exc
|
|
138
|
+
|
|
139
|
+
# Summary
|
|
140
|
+
module_count = len(self.registry.resolve_dependencies(self.config.modules))
|
|
141
|
+
console.print()
|
|
142
|
+
console.print(
|
|
143
|
+
f"[green]✅[/green] Project [bold]{self.config.name}[/bold] created at {target_dir}"
|
|
144
|
+
)
|
|
145
|
+
console.print(f" [dim]{module_count} modules included[/dim]")
|
|
146
|
+
|
|
147
|
+
# Operational assets summary
|
|
148
|
+
assets: list[str] = []
|
|
149
|
+
if "email" in self.config.modules:
|
|
150
|
+
assets.append("📧 Email: Responsive HTML templates generated in app/templates/email/")
|
|
151
|
+
if "observability" in self.config.modules and "docker" in self.config.modules:
|
|
152
|
+
assets.append(
|
|
153
|
+
"📊 Metrics: Grafana dashboard configured. Access at http://localhost:3000"
|
|
154
|
+
" (admin/admin)"
|
|
155
|
+
)
|
|
156
|
+
if "openapi" in self.config.modules:
|
|
157
|
+
client_var = self.config.module_variables.get("openapi", {}).get(
|
|
158
|
+
"generate_frontend_client", False
|
|
159
|
+
)
|
|
160
|
+
if client_var:
|
|
161
|
+
assets.append(
|
|
162
|
+
"🔌 Frontend SDK: Run `make generate-client` to generate TypeScript client"
|
|
163
|
+
)
|
|
164
|
+
if "terraform" in self.config.modules:
|
|
165
|
+
assets.append("☁️ Terraform: Infrastructure configuration generated in terraform/")
|
|
166
|
+
|
|
167
|
+
if assets:
|
|
168
|
+
console.print()
|
|
169
|
+
console.print("[bold]Operational Assets Included:[/bold]")
|
|
170
|
+
for asset in assets:
|
|
171
|
+
console.print(f" {asset}")
|
|
172
|
+
|
|
173
|
+
console.print()
|
|
174
|
+
console.print("[bold]Next steps:[/bold]")
|
|
175
|
+
console.print(f" cd {self.config.name}")
|
|
176
|
+
if self.config.post_generation.get("uv_sync", False):
|
|
177
|
+
console.print(" make run-dev")
|
|
178
|
+
else:
|
|
179
|
+
console.print(" uv sync")
|
|
180
|
+
console.print(" make run-dev")
|
|
181
|
+
|
|
182
|
+
return target_dir
|
|
183
|
+
|
|
184
|
+
def _print_dry_run_table(self, entries: list[RenderedFile], target_dir: Path) -> None:
|
|
185
|
+
"""Print a preview table of files that would be generated."""
|
|
186
|
+
from moltpy.renderer import RenderedFile
|
|
187
|
+
|
|
188
|
+
table = Table(title=f"[bold]Dry Run: {self.config.name}[/bold]")
|
|
189
|
+
table.add_column("File", style="cyan")
|
|
190
|
+
table.add_column("Size", justify="right", style="dim")
|
|
191
|
+
table.add_column("Source", style="green")
|
|
192
|
+
|
|
193
|
+
total_size = 0
|
|
194
|
+
for entry in entries:
|
|
195
|
+
if isinstance(entry, RenderedFile):
|
|
196
|
+
rel_path = (
|
|
197
|
+
str(entry.path.relative_to(target_dir))
|
|
198
|
+
if entry.path.is_relative_to(target_dir)
|
|
199
|
+
else str(entry.path)
|
|
200
|
+
)
|
|
201
|
+
size_str = (
|
|
202
|
+
f"{entry.size:,} B" if entry.size < 1024 else f"{entry.size / 1024:.1f} KB"
|
|
203
|
+
)
|
|
204
|
+
table.add_row(rel_path, size_str, entry.source)
|
|
205
|
+
total_size += entry.size
|
|
206
|
+
|
|
207
|
+
console.print()
|
|
208
|
+
console.print(table)
|
|
209
|
+
console.print()
|
|
210
|
+
console.print(f"[dim]{len(entries)} files, {total_size:,} bytes total[/dim]")
|
|
211
|
+
console.print("[yellow]⚠️ Dry run — no files were written.[/yellow]")
|
|
212
|
+
|
|
213
|
+
def _validate_generated_code(self, target_dir: Path) -> None:
|
|
214
|
+
"""Validate syntax of all generated Python files using py_compile.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
ValueError: If any generated Python file has invalid syntax.
|
|
218
|
+
"""
|
|
219
|
+
errors: list[str] = []
|
|
220
|
+
for py_file in target_dir.rglob("*.py"):
|
|
221
|
+
try:
|
|
222
|
+
py_compile.compile(str(py_file), doraise=True)
|
|
223
|
+
except py_compile.PyCompileError as exc:
|
|
224
|
+
rel = py_file.relative_to(target_dir)
|
|
225
|
+
errors.append(f"{rel}: {exc}")
|
|
226
|
+
|
|
227
|
+
if errors:
|
|
228
|
+
for msg in errors:
|
|
229
|
+
console.print(f"[red]Syntax error: {msg}[/red]")
|
|
230
|
+
raise ValueError(f"Generated code has syntax errors in {len(errors)} file(s)")
|
|
231
|
+
|
|
232
|
+
def _run_uv_sync(self, target_dir: Path) -> None:
|
|
233
|
+
"""Run uv sync in the target directory."""
|
|
234
|
+
import os
|
|
235
|
+
|
|
236
|
+
env = os.environ.copy()
|
|
237
|
+
env.pop("VIRTUAL_ENV", None)
|
|
238
|
+
env.pop("PYTHONPATH", None)
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
subprocess.run(
|
|
242
|
+
["uv", "sync"],
|
|
243
|
+
cwd=target_dir,
|
|
244
|
+
check=True,
|
|
245
|
+
capture_output=True,
|
|
246
|
+
env=env,
|
|
247
|
+
)
|
|
248
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
249
|
+
console.print("[yellow]Warning: uv sync failed. Run it manually.[/yellow]")
|
|
250
|
+
|
|
251
|
+
def _run_git_init(self, target_dir: Path) -> None:
|
|
252
|
+
"""Initialize git repository in the target directory."""
|
|
253
|
+
try:
|
|
254
|
+
subprocess.run(
|
|
255
|
+
["git", "init"],
|
|
256
|
+
cwd=target_dir,
|
|
257
|
+
check=True,
|
|
258
|
+
capture_output=True,
|
|
259
|
+
)
|
|
260
|
+
# Create initial commit
|
|
261
|
+
subprocess.run(
|
|
262
|
+
["git", "add", "."],
|
|
263
|
+
cwd=target_dir,
|
|
264
|
+
check=True,
|
|
265
|
+
capture_output=True,
|
|
266
|
+
)
|
|
267
|
+
subprocess.run(
|
|
268
|
+
["git", "commit", "-m", "Initial commit from MoltPy"],
|
|
269
|
+
cwd=target_dir,
|
|
270
|
+
check=True,
|
|
271
|
+
capture_output=True,
|
|
272
|
+
)
|
|
273
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
274
|
+
console.print("[yellow]Warning: git init failed. Run it manually.[/yellow]")
|
|
275
|
+
|
|
276
|
+
def _run_pre_commit_install(self, target_dir: Path) -> None:
|
|
277
|
+
"""Install pre-commit hooks in the target directory."""
|
|
278
|
+
import os
|
|
279
|
+
|
|
280
|
+
env = os.environ.copy()
|
|
281
|
+
env.pop("VIRTUAL_ENV", None)
|
|
282
|
+
env.pop("PYTHONPATH", None)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
subprocess.run(
|
|
286
|
+
["uv", "run", "pre-commit", "install"],
|
|
287
|
+
cwd=target_dir,
|
|
288
|
+
check=True,
|
|
289
|
+
capture_output=True,
|
|
290
|
+
env=env,
|
|
291
|
+
)
|
|
292
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
293
|
+
console.print("[yellow]Warning: pre-commit install failed. Run it manually.[/yellow]")
|
|
294
|
+
|
|
295
|
+
def _generate_env_file(self, target_dir: Path) -> None:
|
|
296
|
+
"""Generate .env file from .env.example with secure random values.
|
|
297
|
+
|
|
298
|
+
Reads the rendered .env.example file, replaces placeholder values with
|
|
299
|
+
cryptographically secure random strings, and writes the result to .env.
|
|
300
|
+
Never overwrites an existing .env file.
|
|
301
|
+
"""
|
|
302
|
+
env_path = target_dir / ".env"
|
|
303
|
+
env_example_path = target_dir / ".env.example"
|
|
304
|
+
|
|
305
|
+
# Don't overwrite existing .env
|
|
306
|
+
if env_path.exists():
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if not env_example_path.exists():
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
content = env_example_path.read_text()
|
|
313
|
+
|
|
314
|
+
# Generate secure values
|
|
315
|
+
secret_key = secrets.token_hex(64) # 128 hex characters
|
|
316
|
+
db_password = secrets.token_urlsafe(24) # ~32 characters, URL-safe
|
|
317
|
+
|
|
318
|
+
# Replace known placeholder patterns with generated values
|
|
319
|
+
# SECRET_KEY placeholder
|
|
320
|
+
content = re.sub(
|
|
321
|
+
r"SECRET_KEY=.*",
|
|
322
|
+
f"SECRET_KEY={secret_key}",
|
|
323
|
+
content,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Database password in connection URLs (matches :password@ pattern)
|
|
327
|
+
content = re.sub(
|
|
328
|
+
r"(postgresql\+asyncpg://[^:]+:)[^@]+(@)",
|
|
329
|
+
rf"\g<1>{db_password}\g<2>",
|
|
330
|
+
content,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Standalone POSTGRES_PASSWORD if present
|
|
334
|
+
content = re.sub(
|
|
335
|
+
r"POSTGRES_PASSWORD=.*",
|
|
336
|
+
f"POSTGRES_PASSWORD={db_password}",
|
|
337
|
+
content,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
env_path.write_text(content)
|
|
341
|
+
console.print(f"[green]✓[/green] Generated .env with secure secrets ({env_path})")
|