fastforge-cli 0.0.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.
- fastforge/__init__.py +1 -0
- fastforge/cli.py +693 -0
- fastforge/infra_template/cookiecutter.json +11 -0
- fastforge/infra_template/hooks/post_gen_project.py +77 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/README.md +41 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.app.yml +27 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.elasticsearch.yml +32 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.fluentbit.yml +14 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.kafka.yml +20 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.logstash.yml +14 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.mongodb.yml +17 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.postgres.yml +21 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.vault.yml +19 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.vector-agent.yml +13 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.vector-aggregator.yml +9 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.yml +31 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/fluentbit/fluent-bit.conf +24 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/fluentbit/parsers.conf +5 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/logstash/pipeline/logstash.conf +31 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/vault/config.hcl +10 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/vault/policies/app-policy.hcl +7 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/vector/vector-agent.toml +36 -0
- fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/vector/vector-aggregator.toml +29 -0
- fastforge/template/cookiecutter.json +34 -0
- fastforge/template/hooks/post_gen_project.py +71 -0
- fastforge/template/{{cookiecutter.project_slug}}/.codeclimate.yml +40 -0
- fastforge/template/{{cookiecutter.project_slug}}/.dockerignore +15 -0
- fastforge/template/{{cookiecutter.project_slug}}/.env.staging +86 -0
- fastforge/template/{{cookiecutter.project_slug}}/.gitignore +15 -0
- fastforge/template/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +16 -0
- fastforge/template/{{cookiecutter.project_slug}}/Dockerfile +30 -0
- fastforge/template/{{cookiecutter.project_slug}}/README.md +254 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/api/exception_handlers.py +78 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/api/models/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/api/models/{{cookiecutter.model_name}}.py +22 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/api/routes/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/api/routes/health.py +20 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/api/routes/{{cookiecutter.model_name_plural}}.py +70 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/cache.py +63 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/config.py +112 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/db/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/db/models/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/db/models/{{cookiecutter.model_name}}.py +34 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/db/mongodb.py +23 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/db/sqlalchemy.py +14 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/dependencies.py +45 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/logging_config.py +84 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/main.py +106 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/middleware/logging_middleware.py +45 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/middleware/security_headers.py +18 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/repositories/{{cookiecutter.model_name}}_repository.py +172 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/secrets.py +101 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/services/{{cookiecutter.model_name}}_service.py +63 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/streaming/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/streaming/consumer.py +187 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/streaming/handler.py +31 -0
- fastforge/template/{{cookiecutter.project_slug}}/app/streaming/producer.py +151 -0
- fastforge/template/{{cookiecutter.project_slug}}/docker-compose.debug.yml +15 -0
- fastforge/template/{{cookiecutter.project_slug}}/docker-compose.yml +141 -0
- fastforge/template/{{cookiecutter.project_slug}}/pyproject.toml +100 -0
- fastforge/template/{{cookiecutter.project_slug}}/qodana.yaml +22 -0
- fastforge/template/{{cookiecutter.project_slug}}/sonar-project.properties +20 -0
- fastforge/template/{{cookiecutter.project_slug}}/tests/__init__.py +0 -0
- fastforge/template/{{cookiecutter.project_slug}}/tests/conftest.py +11 -0
- fastforge/template/{{cookiecutter.project_slug}}/tests/test_api.py +51 -0
- fastforge_cli-0.0.1.dist-info/METADATA +163 -0
- fastforge_cli-0.0.1.dist-info/RECORD +76 -0
- fastforge_cli-0.0.1.dist-info/WHEEL +5 -0
- fastforge_cli-0.0.1.dist-info/entry_points.txt +12 -0
- fastforge_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
- fastforge_cli-0.0.1.dist-info/top_level.txt +1 -0
fastforge/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
fastforge/cli.py
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
"""FastForge CLI — Interactive project generator for production-grade FastAPI applications."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import questionary
|
|
8
|
+
from cookiecutter.main import cookiecutter
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.rule import Rule
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
TEMPLATE_DIR = str(Path(__file__).parent / "template")
|
|
18
|
+
INFRA_TEMPLATE_DIR = str(Path(__file__).parent / "infra_template")
|
|
19
|
+
|
|
20
|
+
BANNER = """
|
|
21
|
+
[bold blue] ___ _ ___[/]
|
|
22
|
+
[bold blue] | __| _ _ __| |_| __|__ _ _ __ _ ___[/]
|
|
23
|
+
[bold cyan] | _/ _` (_-< _| _/ _ \\ '_/ _` / -_)[/]
|
|
24
|
+
[bold cyan] |_|\\__,_/__/\\__|_|\\___/_| \\__, \\___|[/]
|
|
25
|
+
[bold cyan] |___/[/]
|
|
26
|
+
[bold white] Production-grade FastAPI Generator[/] [dim]v1.0.0[/]
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
STYLE_SECTION = "bold bright_cyan"
|
|
30
|
+
STYLE_HINT = "dim italic"
|
|
31
|
+
STYLE_SUCCESS = "bold green"
|
|
32
|
+
STYLE_WARN = "bold yellow"
|
|
33
|
+
|
|
34
|
+
# Questionary styling
|
|
35
|
+
CUSTOM_STYLE = questionary.Style(
|
|
36
|
+
[
|
|
37
|
+
("qmark", "fg:cyan bold"),
|
|
38
|
+
("question", "fg:white bold"),
|
|
39
|
+
("pointer", "fg:cyan bold"),
|
|
40
|
+
("highlighted", "fg:cyan bold"),
|
|
41
|
+
("selected", "fg:green"),
|
|
42
|
+
("answer", "fg:bright_green bold"),
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
48
|
+
# fastforge (main — developer app)
|
|
49
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _section(title: str) -> None:
|
|
53
|
+
"""Print a styled section header."""
|
|
54
|
+
console.print()
|
|
55
|
+
console.print(Rule(f"[{STYLE_SECTION}] {title} [/]", style="bright_cyan"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ask_basics() -> dict:
|
|
59
|
+
"""Project basics — always asked."""
|
|
60
|
+
_section("📦 Project Basics")
|
|
61
|
+
return {
|
|
62
|
+
"project_name": questionary.text(
|
|
63
|
+
"Project name:",
|
|
64
|
+
default="my-fastapi-service",
|
|
65
|
+
validate=lambda x: bool(x.strip()) or "Required",
|
|
66
|
+
style=CUSTOM_STYLE,
|
|
67
|
+
).ask(),
|
|
68
|
+
"description": questionary.text(
|
|
69
|
+
"Description:",
|
|
70
|
+
default="A production-grade FastAPI service",
|
|
71
|
+
style=CUSTOM_STYLE,
|
|
72
|
+
).ask(),
|
|
73
|
+
"author_name": questionary.text("Author name:", default="Your Name", style=CUSTOM_STYLE).ask(),
|
|
74
|
+
"author_email": questionary.text("Author email:", default="you@example.com", style=CUSTOM_STYLE).ask(),
|
|
75
|
+
"python_version": questionary.select(
|
|
76
|
+
"Python version:",
|
|
77
|
+
choices=["3.13", "3.12", "3.11"],
|
|
78
|
+
default="3.13",
|
|
79
|
+
style=CUSTOM_STYLE,
|
|
80
|
+
).ask(),
|
|
81
|
+
"port": questionary.text("HTTP port:", default="8000", style=CUSTOM_STYLE).ask(),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def ask_model() -> dict:
|
|
86
|
+
"""Model name — drives CRUD stack generation."""
|
|
87
|
+
_section("🧩 Domain Model")
|
|
88
|
+
console.print(f" [{STYLE_HINT}]Generates: route → service → repository → DB model (SOLID)[/]")
|
|
89
|
+
|
|
90
|
+
model_name = questionary.text(
|
|
91
|
+
"Model name (singular, lowercase):",
|
|
92
|
+
default="item",
|
|
93
|
+
validate=lambda x: (x.isidentifier() and x.islower()) or "Must be a valid lowercase Python identifier",
|
|
94
|
+
style=CUSTOM_STYLE,
|
|
95
|
+
).ask()
|
|
96
|
+
|
|
97
|
+
model_name_class = model_name.capitalize()
|
|
98
|
+
model_name_plural = model_name + "s"
|
|
99
|
+
|
|
100
|
+
plural = questionary.text(
|
|
101
|
+
"Plural form (for route /api/v1/...):",
|
|
102
|
+
default=model_name_plural,
|
|
103
|
+
style=CUSTOM_STYLE,
|
|
104
|
+
).ask()
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
"model_name": model_name,
|
|
108
|
+
"model_name_class": model_name_class,
|
|
109
|
+
"model_name_plural": plural,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def ask_logging_basic() -> dict:
|
|
114
|
+
"""Logging — basic mode (always structlog + json)."""
|
|
115
|
+
_section("📋 Logging")
|
|
116
|
+
log_output = questionary.select(
|
|
117
|
+
"Log output:",
|
|
118
|
+
choices=[
|
|
119
|
+
questionary.Choice("Stdout (containers / cloud-native)", value="stdout"),
|
|
120
|
+
questionary.Choice("Stdout + File (for log agent collection)", value="file"),
|
|
121
|
+
],
|
|
122
|
+
default="stdout",
|
|
123
|
+
style=CUSTOM_STYLE,
|
|
124
|
+
).ask()
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"logging": "structlog",
|
|
128
|
+
"log_format": "json",
|
|
129
|
+
"log_connector": log_output,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def ask_docker_basic() -> dict:
|
|
134
|
+
"""Docker — basic mode (always yes, just ask about debug)."""
|
|
135
|
+
_section("🐳 Docker")
|
|
136
|
+
debug = questionary.confirm(
|
|
137
|
+
"Include debug compose (debugpy on port 5678)?",
|
|
138
|
+
default=False,
|
|
139
|
+
style=CUSTOM_STYLE,
|
|
140
|
+
).ask()
|
|
141
|
+
|
|
142
|
+
return {"docker": "yes", "docker_debug": "yes" if debug else "no"}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ── Advanced-only questions ──────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
def ask_database() -> dict:
|
|
148
|
+
"""Database."""
|
|
149
|
+
_section("🗄️ Database")
|
|
150
|
+
db = questionary.select(
|
|
151
|
+
"Database:",
|
|
152
|
+
choices=[
|
|
153
|
+
questionary.Choice("None (in-memory store)", value="none"),
|
|
154
|
+
questionary.Choice("PostgreSQL (SQLAlchemy async + asyncpg)", value="postgres"),
|
|
155
|
+
questionary.Choice("MySQL (SQLAlchemy async + aiomysql)", value="mysql"),
|
|
156
|
+
questionary.Choice("SQLite (SQLAlchemy async + aiosqlite)", value="sqlite"),
|
|
157
|
+
questionary.Choice("MongoDB (Motor async)", value="mongodb"),
|
|
158
|
+
],
|
|
159
|
+
default="none",
|
|
160
|
+
style=CUSTOM_STYLE,
|
|
161
|
+
).ask()
|
|
162
|
+
|
|
163
|
+
return {"database": db}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def ask_cache() -> dict:
|
|
167
|
+
"""Cache backend."""
|
|
168
|
+
_section("⚡ Cache")
|
|
169
|
+
cache = questionary.select(
|
|
170
|
+
"Cache backend:",
|
|
171
|
+
choices=[
|
|
172
|
+
questionary.Choice("None", value="none"),
|
|
173
|
+
questionary.Choice("Redis", value="redis"),
|
|
174
|
+
questionary.Choice("Memcached", value="memcached"),
|
|
175
|
+
questionary.Choice("In-memory (cachetools TTLCache)", value="in_memory"),
|
|
176
|
+
],
|
|
177
|
+
default="none",
|
|
178
|
+
style=CUSTOM_STYLE,
|
|
179
|
+
).ask()
|
|
180
|
+
|
|
181
|
+
return {"cache": cache}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def ask_streaming() -> dict:
|
|
185
|
+
"""Event streaming / messaging."""
|
|
186
|
+
_section("📡 Streaming")
|
|
187
|
+
streaming = questionary.select(
|
|
188
|
+
"Event streaming:",
|
|
189
|
+
choices=[
|
|
190
|
+
questionary.Choice("None", value="none"),
|
|
191
|
+
questionary.Choice("Kafka (aiokafka)", value="kafka"),
|
|
192
|
+
questionary.Choice("RabbitMQ (aio-pika)", value="rabbitmq"),
|
|
193
|
+
questionary.Choice("Redis Pub/Sub", value="redis_pubsub"),
|
|
194
|
+
questionary.Choice("NATS", value="nats"),
|
|
195
|
+
],
|
|
196
|
+
default="none",
|
|
197
|
+
style=CUSTOM_STYLE,
|
|
198
|
+
).ask()
|
|
199
|
+
|
|
200
|
+
return {"streaming": streaming}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def ask_secrets() -> dict:
|
|
204
|
+
"""Secret management provider."""
|
|
205
|
+
_section("🔐 Secrets")
|
|
206
|
+
secrets = questionary.select(
|
|
207
|
+
"Secret management:",
|
|
208
|
+
choices=[
|
|
209
|
+
questionary.Choice("None (use .env / pydantic-settings)", value="none"),
|
|
210
|
+
questionary.Choice("HashiCorp Vault", value="vault"),
|
|
211
|
+
questionary.Choice("AWS Secrets Manager", value="aws_sm"),
|
|
212
|
+
questionary.Choice("Azure Key Vault", value="azure_kv"),
|
|
213
|
+
questionary.Choice("GCP Secret Manager", value="gcp_sm"),
|
|
214
|
+
],
|
|
215
|
+
default="none",
|
|
216
|
+
style=CUSTOM_STYLE,
|
|
217
|
+
).ask()
|
|
218
|
+
|
|
219
|
+
return {"secrets": secrets}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def ask_logging_advanced() -> dict:
|
|
223
|
+
"""Logging — advanced mode (choice of structlog or none, format, output)."""
|
|
224
|
+
_section("📋 Logging")
|
|
225
|
+
enabled = questionary.confirm("Enable structured logging (structlog)?", default=True, style=CUSTOM_STYLE).ask()
|
|
226
|
+
if not enabled:
|
|
227
|
+
return {"logging": "none", "log_format": "console", "log_connector": "stdout"}
|
|
228
|
+
|
|
229
|
+
log_format = questionary.select(
|
|
230
|
+
"Log format:",
|
|
231
|
+
choices=["json", "console"],
|
|
232
|
+
default="json",
|
|
233
|
+
style=CUSTOM_STYLE,
|
|
234
|
+
).ask()
|
|
235
|
+
|
|
236
|
+
log_connector = questionary.select(
|
|
237
|
+
"Log output:",
|
|
238
|
+
choices=[
|
|
239
|
+
questionary.Choice("Stdout (containers)", value="stdout"),
|
|
240
|
+
questionary.Choice("Stdout + File (for log agent collection)", value="file"),
|
|
241
|
+
],
|
|
242
|
+
default="stdout",
|
|
243
|
+
style=CUSTOM_STYLE,
|
|
244
|
+
).ask()
|
|
245
|
+
|
|
246
|
+
return {"logging": "structlog", "log_format": log_format, "log_connector": log_connector}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def ask_quality_gate() -> dict:
|
|
250
|
+
"""Quality gate tool."""
|
|
251
|
+
_section("🛡️ Quality Gate")
|
|
252
|
+
gate = questionary.select(
|
|
253
|
+
"Quality gate:",
|
|
254
|
+
choices=[
|
|
255
|
+
questionary.Choice("None", value="none"),
|
|
256
|
+
questionary.Choice("SonarQube (self-hosted)", value="sonarqube"),
|
|
257
|
+
questionary.Choice("SonarCloud (cloud)", value="sonarcloud"),
|
|
258
|
+
questionary.Choice("Qodana (JetBrains)", value="qodana"),
|
|
259
|
+
questionary.Choice("CodeClimate", value="codeclimate"),
|
|
260
|
+
],
|
|
261
|
+
default="none",
|
|
262
|
+
style=CUSTOM_STYLE,
|
|
263
|
+
).ask()
|
|
264
|
+
|
|
265
|
+
return {"quality_gate": gate}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def ask_containerization() -> dict:
|
|
269
|
+
"""Docker setup — advanced mode."""
|
|
270
|
+
_section("🐳 Docker")
|
|
271
|
+
enabled = questionary.confirm("Enable Docker?", default=True, style=CUSTOM_STYLE).ask()
|
|
272
|
+
if not enabled:
|
|
273
|
+
return {"docker": "no", "docker_debug": "no"}
|
|
274
|
+
|
|
275
|
+
debug = questionary.confirm(
|
|
276
|
+
"Include debug compose (debugpy on port 5678)?",
|
|
277
|
+
default=False,
|
|
278
|
+
style=CUSTOM_STYLE,
|
|
279
|
+
).ask()
|
|
280
|
+
|
|
281
|
+
return {"docker": "yes", "docker_debug": "yes" if debug else "no"}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def ask_precommit() -> dict:
|
|
285
|
+
"""Pre-commit hooks."""
|
|
286
|
+
_section("🪝 Pre-commit")
|
|
287
|
+
enabled = questionary.confirm("Enable pre-commit hooks (ruff, pytest)?", default=True, style=CUSTOM_STYLE).ask()
|
|
288
|
+
return {"precommit": "yes" if enabled else "no"}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ── Summary & Generation ─────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
def show_summary(ctx: dict, mode: str) -> None:
|
|
294
|
+
"""Display a rich summary of selected features."""
|
|
295
|
+
table = Table(
|
|
296
|
+
title=f"[bold]FastForge Configuration[/] [dim]({mode} mode)[/]",
|
|
297
|
+
show_header=True,
|
|
298
|
+
header_style="bold bright_white",
|
|
299
|
+
border_style="bright_cyan",
|
|
300
|
+
title_style="bold bright_cyan",
|
|
301
|
+
)
|
|
302
|
+
table.add_column("Feature", style="cyan", min_width=16)
|
|
303
|
+
table.add_column("Value", style="bright_green")
|
|
304
|
+
|
|
305
|
+
table.add_row("Project", f"[bold]{ctx['project_name']}[/] [dim](Python {ctx['python_version']})[/]")
|
|
306
|
+
table.add_row("Port", ctx["port"])
|
|
307
|
+
table.add_row("Model", f"[bold]{ctx['model_name_class']}[/] → /api/v1/{ctx['model_name_plural']}")
|
|
308
|
+
table.add_row("Database", ctx["database"] if ctx["database"] != "none" else "[dim]in-memory[/]")
|
|
309
|
+
|
|
310
|
+
if mode == "advanced":
|
|
311
|
+
table.add_row("Cache", ctx["cache"] if ctx["cache"] != "none" else "[dim]none[/]")
|
|
312
|
+
table.add_row("Streaming", ctx["streaming"] if ctx["streaming"] != "none" else "[dim]none[/]")
|
|
313
|
+
table.add_row("Secrets", ctx["secrets"] if ctx["secrets"] != "none" else "[dim]none[/]")
|
|
314
|
+
|
|
315
|
+
log_val = ctx["logging"]
|
|
316
|
+
if log_val != "none":
|
|
317
|
+
table.add_row("Logging", f"structlog [dim]({ctx['log_format']} → {ctx['log_connector']})[/]")
|
|
318
|
+
else:
|
|
319
|
+
table.add_row("Logging", "[dim]disabled[/]")
|
|
320
|
+
|
|
321
|
+
if mode == "advanced":
|
|
322
|
+
table.add_row("Quality gate", ctx["quality_gate"] if ctx["quality_gate"] != "none" else "[dim]none[/]")
|
|
323
|
+
|
|
324
|
+
docker_val = ctx["docker"]
|
|
325
|
+
if docker_val == "yes":
|
|
326
|
+
debug_tag = " [dim]+debug[/]" if ctx["docker_debug"] == "yes" else ""
|
|
327
|
+
table.add_row("Docker", f"yes{debug_tag}")
|
|
328
|
+
else:
|
|
329
|
+
table.add_row("Docker", "[dim]no[/]")
|
|
330
|
+
|
|
331
|
+
table.add_row("Pre-commit", ctx["precommit"])
|
|
332
|
+
|
|
333
|
+
console.print()
|
|
334
|
+
console.print(table)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _basic_defaults() -> dict:
|
|
338
|
+
"""Defaults applied in basic mode — no DB, no cache, no streaming, no secrets."""
|
|
339
|
+
return {
|
|
340
|
+
"database": "none",
|
|
341
|
+
"cache": "none",
|
|
342
|
+
"streaming": "none",
|
|
343
|
+
"secrets": "none",
|
|
344
|
+
"quality_gate": "none",
|
|
345
|
+
"precommit": "yes",
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def generate(ctx: dict) -> None:
|
|
350
|
+
"""Call cookiecutter with collected context."""
|
|
351
|
+
ctx["project_slug"] = ctx["project_name"].lower().replace(" ", "-").replace("_", "-")
|
|
352
|
+
ctx["package_name"] = ctx["project_slug"].replace("-", "_")
|
|
353
|
+
|
|
354
|
+
output_dir = os.getcwd()
|
|
355
|
+
|
|
356
|
+
with console.status("[bold cyan]Generating project...[/]", spinner="dots"):
|
|
357
|
+
cookiecutter(
|
|
358
|
+
TEMPLATE_DIR,
|
|
359
|
+
no_input=True,
|
|
360
|
+
extra_context=ctx,
|
|
361
|
+
output_dir=output_dir,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
project_dir = os.path.join(output_dir, ctx["project_slug"])
|
|
365
|
+
|
|
366
|
+
# Build next-steps
|
|
367
|
+
steps = [f" [bold]cd {ctx['project_slug']}[/]"]
|
|
368
|
+
if ctx["docker"] == "yes":
|
|
369
|
+
steps.append(" [green]docker compose up --build[/]")
|
|
370
|
+
else:
|
|
371
|
+
steps.append(' [green]pip install -e ".[dev]"[/]')
|
|
372
|
+
steps.append(" [green]uvicorn app.main:app --reload[/]")
|
|
373
|
+
steps.append(" [green]pytest[/]")
|
|
374
|
+
steps.append("")
|
|
375
|
+
steps.append(f" [dim]API docs → http://localhost:{ctx['port']}/docs[/]")
|
|
376
|
+
steps.append("")
|
|
377
|
+
steps.append("[bold bright_cyan]Extend your project:[/]")
|
|
378
|
+
steps.append(" [cyan]fastforge-infra[/] → Infrastructure stack")
|
|
379
|
+
steps.append(" [cyan]fastforge-cicd[/] → CI/CD pipeline")
|
|
380
|
+
steps.append(" [cyan]fastforge-secops[/] → Security tools")
|
|
381
|
+
steps.append(" [cyan]fastforge-helm[/] → Helm chart")
|
|
382
|
+
steps.append(" [cyan]fastforge-k8s[/] → Kubernetes manifests")
|
|
383
|
+
steps.append(" [cyan]fastforge-observability[/] → Tracing + Metrics")
|
|
384
|
+
|
|
385
|
+
console.print()
|
|
386
|
+
console.print(
|
|
387
|
+
Panel(
|
|
388
|
+
f"[{STYLE_SUCCESS}]✔ Project created:[/] [bold]{project_dir}[/]\n\n"
|
|
389
|
+
+ "\n".join(steps),
|
|
390
|
+
title="[bold bright_cyan]🚀 Next Steps[/]",
|
|
391
|
+
border_style="green",
|
|
392
|
+
padding=(1, 2),
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def main():
|
|
398
|
+
"""Entry point for `fastforge`."""
|
|
399
|
+
console.print(BANNER)
|
|
400
|
+
|
|
401
|
+
console.print(
|
|
402
|
+
Panel(
|
|
403
|
+
"[bold white]Basic mode[/] → SOLID app, JSON logging, Docker, async CRUD — [bold green]ready to run[/]\n"
|
|
404
|
+
"[bold white]Advanced[/] → + Database, Cache, Streaming, Secrets, Quality Gate",
|
|
405
|
+
title="[bold bright_cyan]Choose your path[/]",
|
|
406
|
+
border_style="bright_cyan",
|
|
407
|
+
padding=(0, 2),
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
ctx = {}
|
|
413
|
+
|
|
414
|
+
# ── Always asked ─────────────────────────────────────────────────
|
|
415
|
+
ctx.update(ask_basics())
|
|
416
|
+
ctx.update(ask_model())
|
|
417
|
+
|
|
418
|
+
# ── Basic vs Advanced ────────────────────────────────────────────
|
|
419
|
+
console.print()
|
|
420
|
+
advanced = questionary.confirm(
|
|
421
|
+
"Enable advanced configuration? (database, cache, streaming, secrets, quality gate)",
|
|
422
|
+
default=False,
|
|
423
|
+
style=CUSTOM_STYLE,
|
|
424
|
+
).ask()
|
|
425
|
+
|
|
426
|
+
if advanced:
|
|
427
|
+
mode = "advanced"
|
|
428
|
+
ctx.update(ask_database())
|
|
429
|
+
ctx.update(ask_cache())
|
|
430
|
+
ctx.update(ask_streaming())
|
|
431
|
+
ctx.update(ask_secrets())
|
|
432
|
+
ctx.update(ask_logging_advanced())
|
|
433
|
+
ctx.update(ask_quality_gate())
|
|
434
|
+
ctx.update(ask_containerization())
|
|
435
|
+
ctx.update(ask_precommit())
|
|
436
|
+
else:
|
|
437
|
+
mode = "basic"
|
|
438
|
+
ctx.update(_basic_defaults())
|
|
439
|
+
ctx.update(ask_logging_basic())
|
|
440
|
+
ctx.update(ask_docker_basic())
|
|
441
|
+
|
|
442
|
+
show_summary(ctx, mode)
|
|
443
|
+
|
|
444
|
+
console.print()
|
|
445
|
+
proceed = questionary.confirm("Generate project?", default=True, style=CUSTOM_STYLE).ask()
|
|
446
|
+
if not proceed:
|
|
447
|
+
console.print(f"[{STYLE_WARN}]Aborted.[/]")
|
|
448
|
+
sys.exit(0)
|
|
449
|
+
|
|
450
|
+
generate(ctx)
|
|
451
|
+
|
|
452
|
+
except KeyboardInterrupt:
|
|
453
|
+
console.print(f"\n[{STYLE_WARN}]Aborted.[/]")
|
|
454
|
+
sys.exit(1)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
458
|
+
# fastforge infra (infrastructure stack)
|
|
459
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
460
|
+
|
|
461
|
+
INFRA_BANNER = """
|
|
462
|
+
[bold blue] ___ _ ___[/]
|
|
463
|
+
[bold blue] | __| _ _ __| |_| __|__ _ _ __ _ ___[/]
|
|
464
|
+
[bold cyan] | _/ _` (_-< _| _/ _ \\ '_/ _` / -_)[/]
|
|
465
|
+
[bold cyan] |_|\\__,_/__/\\__|_|\\___/_| \\__, \\___|[/]
|
|
466
|
+
[bold cyan] |___/[/]
|
|
467
|
+
[bold white] Infrastructure Stack Generator[/]
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def ask_infra_basics() -> dict:
|
|
472
|
+
"""Infrastructure basics."""
|
|
473
|
+
_section("📦 Infrastructure Basics")
|
|
474
|
+
return {
|
|
475
|
+
"project_slug": questionary.text(
|
|
476
|
+
"Target project slug (used for container naming):",
|
|
477
|
+
default="my-fastapi-service",
|
|
478
|
+
validate=lambda x: bool(x.strip()) or "Required",
|
|
479
|
+
style=CUSTOM_STYLE,
|
|
480
|
+
).ask(),
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def ask_infra_log_pipeline() -> dict:
|
|
485
|
+
"""Log pipeline configuration for infrastructure."""
|
|
486
|
+
_section("📋 Log Pipeline")
|
|
487
|
+
enabled = questionary.confirm(
|
|
488
|
+
"Set up a log collection pipeline (agent → Kafka → aggregator → Elasticsearch)?",
|
|
489
|
+
default=False,
|
|
490
|
+
style=CUSTOM_STYLE,
|
|
491
|
+
).ask()
|
|
492
|
+
|
|
493
|
+
if not enabled:
|
|
494
|
+
return {"log_agent": "none", "log_aggregator": "none"}
|
|
495
|
+
|
|
496
|
+
agent = questionary.select(
|
|
497
|
+
"Log collection agent:",
|
|
498
|
+
choices=[
|
|
499
|
+
questionary.Choice("Vector (Rust, lightweight)", value="vector"),
|
|
500
|
+
questionary.Choice("Fluent Bit (C, CNCF)", value="fluentbit"),
|
|
501
|
+
],
|
|
502
|
+
default="vector",
|
|
503
|
+
style=CUSTOM_STYLE,
|
|
504
|
+
).ask()
|
|
505
|
+
|
|
506
|
+
aggregator = questionary.select(
|
|
507
|
+
"Log aggregator / pipeline:",
|
|
508
|
+
choices=[
|
|
509
|
+
questionary.Choice("Vector aggregator", value="vector"),
|
|
510
|
+
questionary.Choice("Logstash", value="logstash"),
|
|
511
|
+
],
|
|
512
|
+
default="vector",
|
|
513
|
+
style=CUSTOM_STYLE,
|
|
514
|
+
).ask()
|
|
515
|
+
|
|
516
|
+
return {"log_agent": agent, "log_aggregator": aggregator}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def ask_infra_services() -> dict:
|
|
520
|
+
"""Supporting services."""
|
|
521
|
+
_section("🔧 Services")
|
|
522
|
+
result = {"streaming": "none", "database": "none", "secrets": "none"}
|
|
523
|
+
|
|
524
|
+
if questionary.confirm("Include Kafka (message broker)?", default=False, style=CUSTOM_STYLE).ask():
|
|
525
|
+
result["streaming"] = "enabled"
|
|
526
|
+
|
|
527
|
+
db = questionary.select(
|
|
528
|
+
"Database:",
|
|
529
|
+
choices=[
|
|
530
|
+
questionary.Choice("None", value="none"),
|
|
531
|
+
questionary.Choice("PostgreSQL", value="postgres"),
|
|
532
|
+
questionary.Choice("MongoDB", value="mongodb"),
|
|
533
|
+
],
|
|
534
|
+
default="none",
|
|
535
|
+
style=CUSTOM_STYLE,
|
|
536
|
+
).ask()
|
|
537
|
+
result["database"] = db
|
|
538
|
+
|
|
539
|
+
if questionary.confirm("Include HashiCorp Vault?", default=False, style=CUSTOM_STYLE).ask():
|
|
540
|
+
result["secrets"] = "vault"
|
|
541
|
+
|
|
542
|
+
return result
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def show_infra_summary(ctx: dict) -> None:
|
|
546
|
+
"""Display infrastructure summary."""
|
|
547
|
+
table = Table(
|
|
548
|
+
title="[bold]Infrastructure Configuration[/]",
|
|
549
|
+
show_header=True,
|
|
550
|
+
header_style="bold bright_white",
|
|
551
|
+
border_style="bright_cyan",
|
|
552
|
+
)
|
|
553
|
+
table.add_column("Component", style="cyan", min_width=16)
|
|
554
|
+
table.add_column("Value", style="bright_green")
|
|
555
|
+
|
|
556
|
+
table.add_row("Project", ctx["project_slug"])
|
|
557
|
+
|
|
558
|
+
if ctx["log_agent"] != "none":
|
|
559
|
+
table.add_row("Log agent", ctx["log_agent"])
|
|
560
|
+
table.add_row("Log aggregator", ctx["log_aggregator"])
|
|
561
|
+
table.add_row("Elasticsearch", "yes")
|
|
562
|
+
table.add_row("Kafka", "yes (log transport)")
|
|
563
|
+
elif ctx["streaming"] != "none":
|
|
564
|
+
table.add_row("Kafka", "yes (streaming)")
|
|
565
|
+
else:
|
|
566
|
+
table.add_row("Kafka", "no")
|
|
567
|
+
|
|
568
|
+
table.add_row("Database", ctx["database"] if ctx["database"] != "none" else "none")
|
|
569
|
+
table.add_row("Vault", "yes" if ctx["secrets"] == "vault" else "no")
|
|
570
|
+
|
|
571
|
+
console.print()
|
|
572
|
+
console.print(table)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def generate_infra(ctx: dict) -> None:
|
|
576
|
+
"""Generate infrastructure stack."""
|
|
577
|
+
output_dir = os.getcwd()
|
|
578
|
+
|
|
579
|
+
with console.status("[bold cyan]Generating infrastructure...[/]", spinner="dots"):
|
|
580
|
+
cookiecutter(
|
|
581
|
+
INFRA_TEMPLATE_DIR,
|
|
582
|
+
no_input=True,
|
|
583
|
+
extra_context=ctx,
|
|
584
|
+
output_dir=output_dir,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
infra_dir = os.path.join(output_dir, f"{ctx['project_slug']}-infrastructure")
|
|
588
|
+
|
|
589
|
+
console.print()
|
|
590
|
+
console.print(
|
|
591
|
+
Panel(
|
|
592
|
+
f"[{STYLE_SUCCESS}]✔ Infrastructure created:[/] [bold]{infra_dir}[/]\n\n"
|
|
593
|
+
f" [bold]cd {ctx['project_slug']}-infrastructure[/]\n"
|
|
594
|
+
f" [green]docker compose up -d[/]",
|
|
595
|
+
title="[bold bright_cyan]🚀 Next Steps[/]",
|
|
596
|
+
border_style="green",
|
|
597
|
+
padding=(1, 2),
|
|
598
|
+
)
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def infra_main():
|
|
603
|
+
"""Entry point for `fastforge-infra`."""
|
|
604
|
+
console.print(INFRA_BANNER)
|
|
605
|
+
console.print(f" [{STYLE_HINT}]Generate Docker Compose infrastructure stack for your project.[/]\n")
|
|
606
|
+
|
|
607
|
+
try:
|
|
608
|
+
ctx = {}
|
|
609
|
+
ctx.update(ask_infra_basics())
|
|
610
|
+
ctx.update(ask_infra_log_pipeline())
|
|
611
|
+
ctx.update(ask_infra_services())
|
|
612
|
+
|
|
613
|
+
show_infra_summary(ctx)
|
|
614
|
+
|
|
615
|
+
proceed = questionary.confirm("\nGenerate infrastructure stack?", default=True, style=CUSTOM_STYLE).ask()
|
|
616
|
+
if not proceed:
|
|
617
|
+
console.print(f"[{STYLE_WARN}]Aborted.[/]")
|
|
618
|
+
sys.exit(0)
|
|
619
|
+
|
|
620
|
+
generate_infra(ctx)
|
|
621
|
+
|
|
622
|
+
except KeyboardInterrupt:
|
|
623
|
+
console.print(f"\n[{STYLE_WARN}]Aborted.[/]")
|
|
624
|
+
sys.exit(1)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
628
|
+
# Subcommand stubs (placeholder entry points)
|
|
629
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
630
|
+
|
|
631
|
+
def _stub_command(name: str, description: str) -> None:
|
|
632
|
+
"""Generic stub for not-yet-implemented subcommands."""
|
|
633
|
+
console.print(BANNER)
|
|
634
|
+
console.print(f" [{STYLE_WARN}]fastforge {name}[/] — {description}\n")
|
|
635
|
+
console.print(f" [{STYLE_HINT}]This command is not yet implemented. Coming soon![/]\n")
|
|
636
|
+
console.print(
|
|
637
|
+
Panel(
|
|
638
|
+
f"This will interactively generate {description.lower()} for your project.\n\n"
|
|
639
|
+
"[dim]Track progress: https://github.com/VibhuviOiO/fastforge-cli[/]",
|
|
640
|
+
title=f"[bold bright_cyan]fastforge {name}[/]",
|
|
641
|
+
border_style="yellow",
|
|
642
|
+
padding=(1, 2),
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def cicd_main():
|
|
648
|
+
"""Entry point for `fastforge-cicd`."""
|
|
649
|
+
_stub_command("cicd", "CI/CD pipeline (GitHub Actions, GitLab CI, Bitbucket Pipelines)")
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def secops_main():
|
|
653
|
+
"""Entry point for `fastforge-secops`."""
|
|
654
|
+
_stub_command("secops", "Security tools (Bandit, Gitleaks, Trivy, OWASP ZAP, pip-audit, detect-secrets)")
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def helm_main():
|
|
658
|
+
"""Entry point for `fastforge-helm`."""
|
|
659
|
+
_stub_command("helm", "Helm chart for Kubernetes deployment")
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def k8s_main():
|
|
663
|
+
"""Entry point for `fastforge-k8s`."""
|
|
664
|
+
_stub_command("k8s", "Kubernetes manifests (Deployment, Service, Ingress, HPA)")
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def swarm_main():
|
|
668
|
+
"""Entry point for `fastforge-swarm`."""
|
|
669
|
+
_stub_command("swarm", "Docker Swarm stack definition")
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def marathon_main():
|
|
673
|
+
"""Entry point for `fastforge-marathon`."""
|
|
674
|
+
_stub_command("marathon", "Marathon (Mesos) application definition")
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def observability_main():
|
|
678
|
+
"""Entry point for `fastforge-observability`."""
|
|
679
|
+
_stub_command("observability", "Observability stack (Tracing + Metrics — ELK APM, Jaeger, Prometheus)")
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def docs_main():
|
|
683
|
+
"""Entry point for `fastforge-docs`."""
|
|
684
|
+
_stub_command("docs", "API documentation setup")
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def model_main():
|
|
688
|
+
"""Entry point for `fastforge-model`."""
|
|
689
|
+
_stub_command("model", "Add a new domain model (CRUD route + service + repository + DB model)")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
if __name__ == "__main__":
|
|
693
|
+
main()
|