sirius-cli 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.
- sirius_cli/__init__.py +1 -0
- sirius_cli/cli.py +335 -0
- sirius_cli/generator.py +125 -0
- sirius_cli/parser.py +322 -0
- sirius_cli/templates/backend/Dockerfile.jinja2 +17 -0
- sirius_cli/templates/backend/alembic/env.py.jinja2 +70 -0
- sirius_cli/templates/backend/alembic/script.py.mako.jinja2 +26 -0
- sirius_cli/templates/backend/database.py.jinja2 +39 -0
- sirius_cli/templates/backend/main.py.jinja2 +178 -0
- sirius_cli/templates/backend/models.py.jinja2 +31 -0
- sirius_cli/templates/backend/requirements.txt.jinja2 +13 -0
- sirius_cli/templates/backend/schemas.py.jinja2 +80 -0
- sirius_cli/templates/docker-compose.yml.jinja2 +27 -0
- sirius_cli/templates/frontend/.env.jinja2 +3 -0
- sirius_cli/templates/frontend/Dockerfile.jinja2 +12 -0
- sirius_cli/templates/frontend/index.html.jinja2 +16 -0
- sirius_cli/templates/frontend/package.json.jinja2 +28 -0
- sirius_cli/templates/frontend/postcss.config.js.jinja2 +6 -0
- sirius_cli/templates/frontend/src/App.tsx.jinja2 +89 -0
- sirius_cli/templates/frontend/src/Dashboard.tsx.jinja2 +210 -0
- sirius_cli/templates/frontend/src/TableCrud.tsx.jinja2 +567 -0
- sirius_cli/templates/frontend/src/index.css.jinja2 +25 -0
- sirius_cli/templates/frontend/src/main.tsx.jinja2 +10 -0
- sirius_cli/templates/frontend/tailwind.config.js.jinja2 +21 -0
- sirius_cli/templates/frontend/tsconfig.json.jinja2 +25 -0
- sirius_cli/templates/frontend/vite.config.ts.jinja2 +10 -0
- sirius_cli-0.1.0.dist-info/METADATA +165 -0
- sirius_cli-0.1.0.dist-info/RECORD +32 -0
- sirius_cli-0.1.0.dist-info/WHEEL +5 -0
- sirius_cli-0.1.0.dist-info/entry_points.txt +2 -0
- sirius_cli-0.1.0.dist-info/licenses/LICENSE +619 -0
- sirius_cli-0.1.0.dist-info/top_level.txt +1 -0
sirius_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# sirius_cli package
|
sirius_cli/cli.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import csv
|
|
3
|
+
import sqlite3
|
|
4
|
+
import subprocess
|
|
5
|
+
import typer
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
from sirius_cli.parser import parse_csv_files, parse_sqlite_db, parse_config_file, parse_excel_files, sanitize_table_name, sanitize_column_name
|
|
9
|
+
from sirius_cli.generator import generate_project, render_alembic_files
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Sirius-CLI: A rapid prototyping backend and frontend code generator.")
|
|
12
|
+
|
|
13
|
+
def seed_database_from_csvs(project_path: str, csv_paths: list):
|
|
14
|
+
"""Seeds the generated SQLite database with the row entries from the source CSVs."""
|
|
15
|
+
db_path = os.path.join(project_path, "backend", "app.db")
|
|
16
|
+
if not os.path.exists(db_path):
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
conn = sqlite3.connect(db_path)
|
|
20
|
+
cursor = conn.cursor()
|
|
21
|
+
|
|
22
|
+
for path in csv_paths:
|
|
23
|
+
if not os.path.exists(path):
|
|
24
|
+
continue
|
|
25
|
+
|
|
26
|
+
# Normalise xlsx → in-memory CSV rows via pandas
|
|
27
|
+
ext = Path(path).suffix.lower()
|
|
28
|
+
if ext in (".xlsx", ".xls"):
|
|
29
|
+
try:
|
|
30
|
+
import pandas as pd
|
|
31
|
+
df = pd.read_excel(path)
|
|
32
|
+
all_rows = df.to_dict(orient="records")
|
|
33
|
+
fieldnames = list(df.columns)
|
|
34
|
+
except Exception:
|
|
35
|
+
continue
|
|
36
|
+
else:
|
|
37
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
38
|
+
reader = csv.DictReader(f)
|
|
39
|
+
if not reader.fieldnames:
|
|
40
|
+
continue
|
|
41
|
+
fieldnames = list(reader.fieldnames)
|
|
42
|
+
all_rows = list(reader)
|
|
43
|
+
|
|
44
|
+
table_name = sanitize_table_name(path)
|
|
45
|
+
cols = [sanitize_column_name(c) for c in fieldnames]
|
|
46
|
+
|
|
47
|
+
rows = []
|
|
48
|
+
for row in all_rows:
|
|
49
|
+
mapped_row = {}
|
|
50
|
+
for k, v in row.items():
|
|
51
|
+
sanitized_k = sanitize_column_name(str(k))
|
|
52
|
+
if v == "" or v is None:
|
|
53
|
+
mapped_row[sanitized_k] = None
|
|
54
|
+
elif str(v).lower() == "true":
|
|
55
|
+
mapped_row[sanitized_k] = 1
|
|
56
|
+
elif str(v).lower() == "false":
|
|
57
|
+
mapped_row[sanitized_k] = 0
|
|
58
|
+
else:
|
|
59
|
+
mapped_row[sanitized_k] = v
|
|
60
|
+
rows.append(mapped_row)
|
|
61
|
+
|
|
62
|
+
if not rows:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# Verify columns exist in target table
|
|
66
|
+
cursor.execute(f"PRAGMA table_info({table_name});")
|
|
67
|
+
existing_cols = {info[1] for info in cursor.fetchall()}
|
|
68
|
+
|
|
69
|
+
valid_cols = [c for c in cols if c in existing_cols]
|
|
70
|
+
if not valid_cols:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
placeholders = ", ".join(["?"] * len(valid_cols))
|
|
74
|
+
col_names_str = ", ".join(valid_cols)
|
|
75
|
+
query = f"INSERT OR IGNORE INTO {table_name} ({col_names_str}) VALUES ({placeholders});"
|
|
76
|
+
|
|
77
|
+
data_to_insert = []
|
|
78
|
+
for row in rows:
|
|
79
|
+
row_tuple = tuple(row.get(c) for c in valid_cols)
|
|
80
|
+
data_to_insert.append(row_tuple)
|
|
81
|
+
|
|
82
|
+
cursor.executemany(query, data_to_insert)
|
|
83
|
+
|
|
84
|
+
conn.commit()
|
|
85
|
+
conn.close()
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
def init(
|
|
89
|
+
project_name: Optional[str] = typer.Argument(None, help="The name of the project directory to create"),
|
|
90
|
+
csv: Optional[List[str]] = typer.Option(None, "--csv", help="Paths to input CSV files (can declare multiple times)"),
|
|
91
|
+
excel: Optional[List[str]] = typer.Option(None, "--excel", help="Paths to input Excel (.xlsx/.xls) files (can declare multiple times)"),
|
|
92
|
+
db: Optional[str] = typer.Option(None, "--db", help="Path to input SQLite database file"),
|
|
93
|
+
config: Optional[str] = typer.Option(None, "--config", "-c", help="Path to JSON configuration file"),
|
|
94
|
+
theme: Optional[str] = typer.Option(None, "--theme", "-t", help="Color theme for the frontend (e.g. blue, indigo, emerald, amber, rose, sky, violet)"),
|
|
95
|
+
out: str = typer.Option(".", "--out", "-o", help="Target output directory"),
|
|
96
|
+
port: int = typer.Option(8000, "--port", "-p", help="Backend server port (used in docker-compose and .env)"),
|
|
97
|
+
api_url: Optional[str] = typer.Option(None, "--api-url", help="Override the frontend VITE_API_URL (default: http://localhost:<port>)"),
|
|
98
|
+
no_seed: bool = typer.Option(False, "--no-seed", help="Skip seeding the database from CSV/Excel files"),
|
|
99
|
+
pg: bool = typer.Option(False, "--pg", help="Use PostgreSQL instead of SQLite"),
|
|
100
|
+
mysql: bool = typer.Option(False, "--mysql", help="Use MySQL instead of SQLite")
|
|
101
|
+
):
|
|
102
|
+
"""Initializes a new FastAPI backend and React frontend stack from data files or configuration."""
|
|
103
|
+
# Ensure one and only one source parameter is provided
|
|
104
|
+
inputs = [bool(csv), bool(excel), bool(db), bool(config)]
|
|
105
|
+
if sum(inputs) != 1:
|
|
106
|
+
typer.secho("Error: You must provide exactly one input option: --csv, --excel, --db, or --config.", fg=typer.colors.RED, err=True)
|
|
107
|
+
raise typer.Exit(code=1)
|
|
108
|
+
|
|
109
|
+
# Resolve API URL
|
|
110
|
+
resolved_api_url = api_url or f"http://localhost:{port}"
|
|
111
|
+
|
|
112
|
+
db_type = "sqlite"
|
|
113
|
+
if pg:
|
|
114
|
+
db_type = "pg"
|
|
115
|
+
elif mysql:
|
|
116
|
+
db_type = "mysql"
|
|
117
|
+
|
|
118
|
+
typer.echo("Analyzing schema structure...")
|
|
119
|
+
try:
|
|
120
|
+
if csv:
|
|
121
|
+
schemas = parse_csv_files(csv)
|
|
122
|
+
resolved_theme = theme or "blue"
|
|
123
|
+
if not project_name:
|
|
124
|
+
typer.secho("Error: Project name argument is required when using --csv.", fg=typer.colors.RED, err=True)
|
|
125
|
+
raise typer.Exit(code=1)
|
|
126
|
+
elif excel:
|
|
127
|
+
schemas = parse_excel_files(excel)
|
|
128
|
+
resolved_theme = theme or "blue"
|
|
129
|
+
if not project_name:
|
|
130
|
+
typer.secho("Error: Project name argument is required when using --excel.", fg=typer.colors.RED, err=True)
|
|
131
|
+
raise typer.Exit(code=1)
|
|
132
|
+
elif db:
|
|
133
|
+
schemas = parse_sqlite_db(db)
|
|
134
|
+
resolved_theme = theme or "blue"
|
|
135
|
+
if not project_name:
|
|
136
|
+
typer.secho("Error: Project name argument is required when using --db.", fg=typer.colors.RED, err=True)
|
|
137
|
+
raise typer.Exit(code=1)
|
|
138
|
+
else: # config
|
|
139
|
+
schemas, cfg_project_name, cfg_theme = parse_config_file(config)
|
|
140
|
+
resolved_theme = theme or cfg_theme
|
|
141
|
+
project_name = project_name or cfg_project_name
|
|
142
|
+
if not project_name:
|
|
143
|
+
typer.secho("Error: Project name must be provided as an argument or defined in the configuration file.", fg=typer.colors.RED, err=True)
|
|
144
|
+
raise typer.Exit(code=1)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
typer.secho(f"Error parsing schemas: {e}", fg=typer.colors.RED, err=True)
|
|
147
|
+
raise typer.Exit(code=1)
|
|
148
|
+
|
|
149
|
+
dest_dir = os.path.abspath(os.path.join(out, project_name))
|
|
150
|
+
typer.echo(f"Scaffolding project in: {dest_dir} (Theme: {resolved_theme}, Port: {port}, DB: {db_type})")
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
generate_project(dest_dir, schemas, theme=resolved_theme, port=port, api_url=resolved_api_url, db_type=db_type)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
typer.secho(f"Error generating files: {e}", fg=typer.colors.RED, err=True)
|
|
156
|
+
raise typer.Exit(code=1)
|
|
157
|
+
|
|
158
|
+
backend_path = os.path.join(dest_dir, "backend")
|
|
159
|
+
|
|
160
|
+
typer.echo("Initializing Alembic migration system...")
|
|
161
|
+
try:
|
|
162
|
+
# Run alembic init
|
|
163
|
+
subprocess.run(
|
|
164
|
+
"alembic init alembic",
|
|
165
|
+
cwd=backend_path,
|
|
166
|
+
check=True,
|
|
167
|
+
shell=True,
|
|
168
|
+
stdout=subprocess.DEVNULL
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Write custom alembic migration runner templates
|
|
172
|
+
render_alembic_files(backend_path, schemas)
|
|
173
|
+
|
|
174
|
+
# Modify alembic.ini target database config
|
|
175
|
+
alembic_ini = os.path.join(backend_path, "alembic.ini")
|
|
176
|
+
if os.path.exists(alembic_ini):
|
|
177
|
+
with open(alembic_ini, "r") as f:
|
|
178
|
+
content = f.read()
|
|
179
|
+
if db_type == 'sqlite':
|
|
180
|
+
content = content.replace(
|
|
181
|
+
"sqlalchemy.url = driver://user:pass@localhost/dbname",
|
|
182
|
+
"sqlalchemy.url = sqlite:///./app.db"
|
|
183
|
+
)
|
|
184
|
+
elif db_type == 'pg':
|
|
185
|
+
content = content.replace(
|
|
186
|
+
"sqlalchemy.url = driver://user:pass@localhost/dbname",
|
|
187
|
+
"sqlalchemy.url = postgresql://postgres:postgres@localhost:5432/app"
|
|
188
|
+
)
|
|
189
|
+
elif db_type == 'mysql':
|
|
190
|
+
content = content.replace(
|
|
191
|
+
"sqlalchemy.url = driver://user:pass@localhost/dbname",
|
|
192
|
+
"sqlalchemy.url = mysql+pymysql://root:root@localhost:3306/app"
|
|
193
|
+
)
|
|
194
|
+
with open(alembic_ini, "w") as f:
|
|
195
|
+
f.write(content)
|
|
196
|
+
|
|
197
|
+
# Generate initial autogenerated migration script
|
|
198
|
+
typer.echo("Autogenerating migration scripts...")
|
|
199
|
+
env = os.environ.copy()
|
|
200
|
+
env["PYTHONPATH"] = dest_dir + os.pathsep + env.get("PYTHONPATH", "")
|
|
201
|
+
subprocess.run(
|
|
202
|
+
"alembic revision --autogenerate -m \"Initial migration\"",
|
|
203
|
+
cwd=backend_path,
|
|
204
|
+
check=True,
|
|
205
|
+
shell=True,
|
|
206
|
+
env=env,
|
|
207
|
+
stdout=subprocess.DEVNULL
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Apply initial migration structure to SQLite db
|
|
211
|
+
typer.echo("Running database migrations...")
|
|
212
|
+
subprocess.run(
|
|
213
|
+
"alembic upgrade head",
|
|
214
|
+
cwd=backend_path,
|
|
215
|
+
check=True,
|
|
216
|
+
shell=True,
|
|
217
|
+
env=env,
|
|
218
|
+
stdout=subprocess.DEVNULL
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
typer.secho("[OK] Alembic migration system initialized successfully!", fg=typer.colors.GREEN)
|
|
222
|
+
|
|
223
|
+
# Seed initial data if building from CSVs or Excel files
|
|
224
|
+
seed_paths = list(csv or []) + list(excel or [])
|
|
225
|
+
if seed_paths and not no_seed:
|
|
226
|
+
if db_type == "sqlite":
|
|
227
|
+
typer.echo("Seeding initial data from source files...")
|
|
228
|
+
try:
|
|
229
|
+
seed_database_from_csvs(dest_dir, seed_paths)
|
|
230
|
+
typer.secho("[OK] Database seeded successfully!", fg=typer.colors.GREEN)
|
|
231
|
+
except Exception as se:
|
|
232
|
+
typer.secho(f"[WARNING] Database seeding failed: {se}", fg=typer.colors.YELLOW)
|
|
233
|
+
else:
|
|
234
|
+
typer.secho(f"[SKIP] CSV/Excel seeding is only supported for SQLite at scaffold time. Skipping.", fg=typer.colors.YELLOW)
|
|
235
|
+
elif no_seed:
|
|
236
|
+
typer.secho("[SKIP] Database seeding skipped (--no-seed).", fg=typer.colors.YELLOW)
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
typer.secho(f"[WARNING] Autogenerated Alembic migration failed: {e}", fg=typer.colors.YELLOW)
|
|
240
|
+
typer.echo("You can configure database credentials and run migrations manually later.")
|
|
241
|
+
|
|
242
|
+
typer.secho(f"\n[SUCCESS] Project '{project_name}' has been created.", fg=typer.colors.GREEN, bold=True)
|
|
243
|
+
typer.echo("To run the stack with docker compose:")
|
|
244
|
+
typer.secho(f" cd {project_name} && docker compose up --build", fg=typer.colors.CYAN)
|
|
245
|
+
typer.echo("To start individual servers locally:")
|
|
246
|
+
typer.secho(f" Backend: cd {project_name}/backend && pip install -r requirements.txt && uvicorn backend.main:app --reload --port {port}", fg=typer.colors.CYAN)
|
|
247
|
+
typer.secho(f" Frontend: cd {project_name}/frontend && npm install && npm run dev", fg=typer.colors.CYAN)
|
|
248
|
+
typer.secho(f" API URL: {resolved_api_url}", fg=typer.colors.CYAN)
|
|
249
|
+
|
|
250
|
+
@app.command()
|
|
251
|
+
def update(
|
|
252
|
+
project_path: str = typer.Argument(..., help="Path to existing project directory"),
|
|
253
|
+
csv: Optional[List[str]] = typer.Option(None, "--csv", help="Paths to input CSV files"),
|
|
254
|
+
excel: Optional[List[str]] = typer.Option(None, "--excel", help="Paths to input Excel files"),
|
|
255
|
+
db: Optional[str] = typer.Option(None, "--db", help="Path to input SQLite db"),
|
|
256
|
+
config: Optional[str] = typer.Option(None, "--config", "-c", help="Path to JSON config"),
|
|
257
|
+
message: str = typer.Option("Auto-update schema", "-m", help="Alembic migration message"),
|
|
258
|
+
theme: Optional[str] = typer.Option("blue", "--theme", "-t", help="Color theme for the frontend (defaults to blue)"),
|
|
259
|
+
port: int = typer.Option(8000, "--port", "-p", help="Backend server port (used in docker-compose and .env)"),
|
|
260
|
+
api_url: Optional[str] = typer.Option(None, "--api-url", help="Override the frontend VITE_API_URL (default: http://localhost:<port>)"),
|
|
261
|
+
pg: bool = typer.Option(False, "--pg", help="Use PostgreSQL instead of SQLite"),
|
|
262
|
+
mysql: bool = typer.Option(False, "--mysql", help="Use MySQL instead of SQLite")
|
|
263
|
+
):
|
|
264
|
+
"""Updates an existing project with new columns/tables."""
|
|
265
|
+
if not os.path.exists(project_path):
|
|
266
|
+
typer.secho(f"Error: Project path '{project_path}' does not exist.", fg=typer.colors.RED, err=True)
|
|
267
|
+
raise typer.Exit(code=1)
|
|
268
|
+
|
|
269
|
+
inputs = [bool(csv), bool(excel), bool(db), bool(config)]
|
|
270
|
+
if sum(inputs) != 1:
|
|
271
|
+
typer.secho("Error: You must provide exactly one input option: --csv, --excel, --db, or --config.", fg=typer.colors.RED, err=True)
|
|
272
|
+
raise typer.Exit(code=1)
|
|
273
|
+
|
|
274
|
+
resolved_api_url = api_url or f"http://localhost:{port}"
|
|
275
|
+
|
|
276
|
+
db_type = "sqlite"
|
|
277
|
+
if pg:
|
|
278
|
+
db_type = "pg"
|
|
279
|
+
elif mysql:
|
|
280
|
+
db_type = "mysql"
|
|
281
|
+
|
|
282
|
+
typer.echo("Analyzing new schema structure...")
|
|
283
|
+
try:
|
|
284
|
+
if csv:
|
|
285
|
+
schemas = parse_csv_files(csv)
|
|
286
|
+
elif excel:
|
|
287
|
+
schemas = parse_excel_files(excel)
|
|
288
|
+
elif db:
|
|
289
|
+
schemas = parse_sqlite_db(db)
|
|
290
|
+
else: # config
|
|
291
|
+
schemas, _, _ = parse_config_file(config)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
typer.secho(f"Error parsing schemas: {e}", fg=typer.colors.RED, err=True)
|
|
294
|
+
raise typer.Exit(code=1)
|
|
295
|
+
|
|
296
|
+
typer.echo(f"Updating project in: {project_path}")
|
|
297
|
+
try:
|
|
298
|
+
generate_project(project_path, schemas, theme=theme, port=port, api_url=resolved_api_url, db_type=db_type)
|
|
299
|
+
except Exception as e:
|
|
300
|
+
typer.secho(f"Error generating files: {e}", fg=typer.colors.RED, err=True)
|
|
301
|
+
raise typer.Exit(code=1)
|
|
302
|
+
|
|
303
|
+
backend_path = os.path.join(project_path, "backend")
|
|
304
|
+
|
|
305
|
+
typer.echo("Generating Alembic migration...")
|
|
306
|
+
env = os.environ.copy()
|
|
307
|
+
env["PYTHONPATH"] = project_path + os.pathsep + env.get("PYTHONPATH", "")
|
|
308
|
+
try:
|
|
309
|
+
subprocess.run(
|
|
310
|
+
f"alembic revision --autogenerate -m \"{message}\"",
|
|
311
|
+
cwd=backend_path,
|
|
312
|
+
check=True,
|
|
313
|
+
shell=True,
|
|
314
|
+
env=env,
|
|
315
|
+
stdout=subprocess.DEVNULL
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
typer.echo("Running database migrations...")
|
|
319
|
+
subprocess.run(
|
|
320
|
+
"alembic upgrade head",
|
|
321
|
+
cwd=backend_path,
|
|
322
|
+
check=True,
|
|
323
|
+
shell=True,
|
|
324
|
+
env=env,
|
|
325
|
+
stdout=subprocess.DEVNULL
|
|
326
|
+
)
|
|
327
|
+
typer.secho("[OK] Database schema updated successfully!", fg=typer.colors.GREEN)
|
|
328
|
+
except Exception as e:
|
|
329
|
+
typer.secho(f"[WARNING] Autogenerated Alembic migration failed: {e}", fg=typer.colors.YELLOW)
|
|
330
|
+
typer.echo("You can configure database credentials and run migrations manually later.")
|
|
331
|
+
|
|
332
|
+
typer.secho(f"\n[SUCCESS] Project '{project_path}' has been updated.", fg=typer.colors.GREEN, bold=True)
|
|
333
|
+
|
|
334
|
+
if __name__ == "__main__":
|
|
335
|
+
app()
|
sirius_cli/generator.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from jinja2 import Environment, FileSystemLoader
|
|
3
|
+
|
|
4
|
+
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
|
|
5
|
+
|
|
6
|
+
def get_env():
|
|
7
|
+
return Environment(loader=FileSystemLoader(TEMPLATE_DIR))
|
|
8
|
+
|
|
9
|
+
def render_template(env, template_name, dest_path, **kwargs):
|
|
10
|
+
template = env.get_template(template_name)
|
|
11
|
+
content = template.render(**kwargs)
|
|
12
|
+
|
|
13
|
+
# Ensure destination directory exists
|
|
14
|
+
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
|
15
|
+
|
|
16
|
+
with open(dest_path, "w", encoding="utf-8") as f:
|
|
17
|
+
f.write(content)
|
|
18
|
+
|
|
19
|
+
def generate_project(project_path: str, schemas: dict, theme: str = "blue", port: int = 8000, api_url: str = "http://localhost:8000", db_type: str = "sqlite"):
|
|
20
|
+
"""Generates complete FastAPI and React frontend files structure based on inferred schemas and theme."""
|
|
21
|
+
env = get_env()
|
|
22
|
+
|
|
23
|
+
# 1. Root docker-compose configuration
|
|
24
|
+
render_template(
|
|
25
|
+
env,
|
|
26
|
+
"docker-compose.yml.jinja2",
|
|
27
|
+
os.path.join(project_path, "docker-compose.yml"),
|
|
28
|
+
schemas=schemas,
|
|
29
|
+
theme=theme,
|
|
30
|
+
port=port,
|
|
31
|
+
api_url=api_url,
|
|
32
|
+
db_type=db_type
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# 2. Backend FastAPI application files
|
|
36
|
+
backend_path = os.path.join(project_path, "backend")
|
|
37
|
+
|
|
38
|
+
backend_templates = {
|
|
39
|
+
"backend/database.py.jinja2": "database.py",
|
|
40
|
+
"backend/models.py.jinja2": "models.py",
|
|
41
|
+
"backend/schemas.py.jinja2": "schemas.py",
|
|
42
|
+
"backend/main.py.jinja2": "main.py",
|
|
43
|
+
"backend/requirements.txt.jinja2": "requirements.txt",
|
|
44
|
+
"backend/Dockerfile.jinja2": "Dockerfile"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for t_path, dest_name in backend_templates.items():
|
|
48
|
+
render_template(
|
|
49
|
+
env,
|
|
50
|
+
t_path,
|
|
51
|
+
os.path.join(backend_path, dest_name),
|
|
52
|
+
schemas=schemas,
|
|
53
|
+
theme=theme,
|
|
54
|
+
port=port,
|
|
55
|
+
api_url=api_url,
|
|
56
|
+
db_type=db_type
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Write init file to make backend a python package
|
|
60
|
+
with open(os.path.join(backend_path, "__init__.py"), "w") as f:
|
|
61
|
+
f.write("# backend package\n")
|
|
62
|
+
|
|
63
|
+
# 3. Frontend React configurator files
|
|
64
|
+
frontend_path = os.path.join(project_path, "frontend")
|
|
65
|
+
|
|
66
|
+
frontend_templates = {
|
|
67
|
+
"frontend/index.html.jinja2": "index.html",
|
|
68
|
+
"frontend/package.json.jinja2": "package.json",
|
|
69
|
+
"frontend/tsconfig.json.jinja2": "tsconfig.json",
|
|
70
|
+
"frontend/vite.config.ts.jinja2": "vite.config.ts",
|
|
71
|
+
"frontend/tailwind.config.js.jinja2": "tailwind.config.js",
|
|
72
|
+
"frontend/postcss.config.js.jinja2": "postcss.config.js",
|
|
73
|
+
"frontend/Dockerfile.jinja2": "Dockerfile",
|
|
74
|
+
"frontend/.env.jinja2": ".env",
|
|
75
|
+
"frontend/src/main.tsx.jinja2": "src/main.tsx",
|
|
76
|
+
"frontend/src/index.css.jinja2": "src/index.css",
|
|
77
|
+
"frontend/src/App.tsx.jinja2": "src/App.tsx",
|
|
78
|
+
"frontend/src/Dashboard.tsx.jinja2": "src/Dashboard.tsx"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for t_path, dest_name in frontend_templates.items():
|
|
82
|
+
render_template(
|
|
83
|
+
env,
|
|
84
|
+
t_path,
|
|
85
|
+
os.path.join(frontend_path, dest_name),
|
|
86
|
+
schemas=schemas,
|
|
87
|
+
theme=theme,
|
|
88
|
+
port=port,
|
|
89
|
+
api_url=api_url,
|
|
90
|
+
db_type=db_type
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# 4. Generate dynamic CRUD view pages for each table
|
|
94
|
+
for table_name, columns in schemas.items():
|
|
95
|
+
pascal_name = table_name.replace('_', ' ').title().replace(' ', '')
|
|
96
|
+
dest_crud_path = os.path.join(frontend_path, "src", "pages", f"{pascal_name}Crud.tsx")
|
|
97
|
+
render_template(
|
|
98
|
+
env,
|
|
99
|
+
"frontend/src/TableCrud.tsx.jinja2",
|
|
100
|
+
dest_crud_path,
|
|
101
|
+
table_name=table_name,
|
|
102
|
+
columns=columns,
|
|
103
|
+
theme=theme,
|
|
104
|
+
port=port,
|
|
105
|
+
api_url=api_url,
|
|
106
|
+
db_type=db_type
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def render_alembic_files(backend_path: str, schemas: dict):
|
|
110
|
+
"""Helper to render Alembic migration template files after init command is run."""
|
|
111
|
+
env = get_env()
|
|
112
|
+
# Render env.py config
|
|
113
|
+
render_template(
|
|
114
|
+
env,
|
|
115
|
+
"backend/alembic/env.py.jinja2",
|
|
116
|
+
os.path.join(backend_path, "alembic", "env.py"),
|
|
117
|
+
schemas=schemas
|
|
118
|
+
)
|
|
119
|
+
# Render script.py.mako template
|
|
120
|
+
render_template(
|
|
121
|
+
env,
|
|
122
|
+
"backend/alembic/script.py.mako.jinja2",
|
|
123
|
+
os.path.join(backend_path, "alembic", "script.py.mako"),
|
|
124
|
+
schemas=schemas
|
|
125
|
+
)
|