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.
Files changed (32) hide show
  1. sirius_cli/__init__.py +1 -0
  2. sirius_cli/cli.py +335 -0
  3. sirius_cli/generator.py +125 -0
  4. sirius_cli/parser.py +322 -0
  5. sirius_cli/templates/backend/Dockerfile.jinja2 +17 -0
  6. sirius_cli/templates/backend/alembic/env.py.jinja2 +70 -0
  7. sirius_cli/templates/backend/alembic/script.py.mako.jinja2 +26 -0
  8. sirius_cli/templates/backend/database.py.jinja2 +39 -0
  9. sirius_cli/templates/backend/main.py.jinja2 +178 -0
  10. sirius_cli/templates/backend/models.py.jinja2 +31 -0
  11. sirius_cli/templates/backend/requirements.txt.jinja2 +13 -0
  12. sirius_cli/templates/backend/schemas.py.jinja2 +80 -0
  13. sirius_cli/templates/docker-compose.yml.jinja2 +27 -0
  14. sirius_cli/templates/frontend/.env.jinja2 +3 -0
  15. sirius_cli/templates/frontend/Dockerfile.jinja2 +12 -0
  16. sirius_cli/templates/frontend/index.html.jinja2 +16 -0
  17. sirius_cli/templates/frontend/package.json.jinja2 +28 -0
  18. sirius_cli/templates/frontend/postcss.config.js.jinja2 +6 -0
  19. sirius_cli/templates/frontend/src/App.tsx.jinja2 +89 -0
  20. sirius_cli/templates/frontend/src/Dashboard.tsx.jinja2 +210 -0
  21. sirius_cli/templates/frontend/src/TableCrud.tsx.jinja2 +567 -0
  22. sirius_cli/templates/frontend/src/index.css.jinja2 +25 -0
  23. sirius_cli/templates/frontend/src/main.tsx.jinja2 +10 -0
  24. sirius_cli/templates/frontend/tailwind.config.js.jinja2 +21 -0
  25. sirius_cli/templates/frontend/tsconfig.json.jinja2 +25 -0
  26. sirius_cli/templates/frontend/vite.config.ts.jinja2 +10 -0
  27. sirius_cli-0.1.0.dist-info/METADATA +165 -0
  28. sirius_cli-0.1.0.dist-info/RECORD +32 -0
  29. sirius_cli-0.1.0.dist-info/WHEEL +5 -0
  30. sirius_cli-0.1.0.dist-info/entry_points.txt +2 -0
  31. sirius_cli-0.1.0.dist-info/licenses/LICENSE +619 -0
  32. 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()
@@ -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
+ )