sirius-cli 0.1.0__tar.gz → 0.2.1__tar.gz
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-0.1.0/sirius_cli.egg-info → sirius_cli-0.2.1}/PKG-INFO +5 -3
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/README.md +4 -2
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/pyproject.toml +1 -1
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/cli.py +16 -4
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/generator.py +27 -5
- sirius_cli-0.2.1/sirius_cli/templates/backend/auth.py.jinja2 +80 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/backend/main.py.jinja2 +20 -7
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/backend/models.py.jinja2 +9 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/backend/requirements.txt.jinja2 +5 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/src/App.tsx.jinja2 +31 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/src/Dashboard.tsx.jinja2 +6 -6
- sirius_cli-0.2.1/sirius_cli/templates/frontend/src/Login.tsx.jinja2 +106 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/src/TableCrud.tsx.jinja2 +30 -6
- {sirius_cli-0.1.0 → sirius_cli-0.2.1/sirius_cli.egg-info}/PKG-INFO +5 -3
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli.egg-info/SOURCES.txt +2 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/LICENSE +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/MANIFEST.in +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/setup.cfg +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/__init__.py +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/parser.py +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/backend/Dockerfile.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/backend/alembic/env.py.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/backend/alembic/script.py.mako.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/backend/database.py.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/backend/schemas.py.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/docker-compose.yml.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/.env.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/Dockerfile.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/index.html.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/package.json.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/postcss.config.js.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/src/index.css.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/src/main.tsx.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/tailwind.config.js.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/tsconfig.json.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/vite.config.ts.jinja2 +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli.egg-info/dependency_links.txt +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli.egg-info/entry_points.txt +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli.egg-info/requires.txt +0 -0
- {sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sirius-cli
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A rapid prototyping tool for generating containerized backend + frontend CRUD stacks from CSV/SQLite files.
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -36,11 +36,13 @@ A rapid prototyping CLI tool designed to take multiple CSV files, Excel spreadsh
|
|
|
36
36
|
|
|
37
37
|
## Installation
|
|
38
38
|
|
|
39
|
-
Install the package
|
|
39
|
+
Install the package directly from PyPI:
|
|
40
40
|
```bash
|
|
41
|
-
pip install -
|
|
41
|
+
pip install sirius-cli
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
*(For local development: clone the repository and run `pip install -e .`)*
|
|
45
|
+
|
|
44
46
|
---
|
|
45
47
|
|
|
46
48
|
## Usage: Creating a New Project (`sirius-init`)
|
|
@@ -20,11 +20,13 @@ A rapid prototyping CLI tool designed to take multiple CSV files, Excel spreadsh
|
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
|
-
Install the package
|
|
23
|
+
Install the package directly from PyPI:
|
|
24
24
|
```bash
|
|
25
|
-
pip install -
|
|
25
|
+
pip install sirius-cli
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
*(For local development: clone the repository and run `pip install -e .`)*
|
|
29
|
+
|
|
28
30
|
---
|
|
29
31
|
|
|
30
32
|
## Usage: Creating a New Project (`sirius-init`)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sirius-cli"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "A rapid prototyping tool for generating containerized backend + frontend CRUD stacks from CSV/SQLite files."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -97,7 +97,10 @@ def init(
|
|
|
97
97
|
api_url: Optional[str] = typer.Option(None, "--api-url", help="Override the frontend VITE_API_URL (default: http://localhost:<port>)"),
|
|
98
98
|
no_seed: bool = typer.Option(False, "--no-seed", help="Skip seeding the database from CSV/Excel files"),
|
|
99
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")
|
|
100
|
+
mysql: bool = typer.Option(False, "--mysql", help="Use MySQL instead of SQLite"),
|
|
101
|
+
auth: bool = typer.Option(False, "--auth", help="Generate JWT authentication logic"),
|
|
102
|
+
admin_user: str = typer.Option("admin", "--admin-user", help="Default admin username if --auth is used"),
|
|
103
|
+
admin_pass: str = typer.Option("admin", "--admin-pass", help="Default admin password if --auth is used")
|
|
101
104
|
):
|
|
102
105
|
"""Initializes a new FastAPI backend and React frontend stack from data files or configuration."""
|
|
103
106
|
# Ensure one and only one source parameter is provided
|
|
@@ -150,7 +153,10 @@ def init(
|
|
|
150
153
|
typer.echo(f"Scaffolding project in: {dest_dir} (Theme: {resolved_theme}, Port: {port}, DB: {db_type})")
|
|
151
154
|
|
|
152
155
|
try:
|
|
153
|
-
generate_project(
|
|
156
|
+
generate_project(
|
|
157
|
+
dest_dir, schemas, theme=resolved_theme, port=port, api_url=resolved_api_url,
|
|
158
|
+
db_type=db_type, auth=auth, admin_user=admin_user, admin_pass=admin_pass
|
|
159
|
+
)
|
|
154
160
|
except Exception as e:
|
|
155
161
|
typer.secho(f"Error generating files: {e}", fg=typer.colors.RED, err=True)
|
|
156
162
|
raise typer.Exit(code=1)
|
|
@@ -259,7 +265,10 @@ def update(
|
|
|
259
265
|
port: int = typer.Option(8000, "--port", "-p", help="Backend server port (used in docker-compose and .env)"),
|
|
260
266
|
api_url: Optional[str] = typer.Option(None, "--api-url", help="Override the frontend VITE_API_URL (default: http://localhost:<port>)"),
|
|
261
267
|
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")
|
|
268
|
+
mysql: bool = typer.Option(False, "--mysql", help="Use MySQL instead of SQLite"),
|
|
269
|
+
auth: bool = typer.Option(False, "--auth", help="Generate JWT authentication logic"),
|
|
270
|
+
admin_user: str = typer.Option("admin", "--admin-user", help="Default admin username if --auth is used"),
|
|
271
|
+
admin_pass: str = typer.Option("admin", "--admin-pass", help="Default admin password if --auth is used")
|
|
263
272
|
):
|
|
264
273
|
"""Updates an existing project with new columns/tables."""
|
|
265
274
|
if not os.path.exists(project_path):
|
|
@@ -295,7 +304,10 @@ def update(
|
|
|
295
304
|
|
|
296
305
|
typer.echo(f"Updating project in: {project_path}")
|
|
297
306
|
try:
|
|
298
|
-
generate_project(
|
|
307
|
+
generate_project(
|
|
308
|
+
project_path, schemas, theme=theme, port=port, api_url=resolved_api_url,
|
|
309
|
+
db_type=db_type, auth=auth, admin_user=admin_user, admin_pass=admin_pass
|
|
310
|
+
)
|
|
299
311
|
except Exception as e:
|
|
300
312
|
typer.secho(f"Error generating files: {e}", fg=typer.colors.RED, err=True)
|
|
301
313
|
raise typer.Exit(code=1)
|
|
@@ -16,7 +16,11 @@ def render_template(env, template_name, dest_path, **kwargs):
|
|
|
16
16
|
with open(dest_path, "w", encoding="utf-8") as f:
|
|
17
17
|
f.write(content)
|
|
18
18
|
|
|
19
|
-
def generate_project(
|
|
19
|
+
def generate_project(
|
|
20
|
+
project_path: str, schemas: dict, theme: str = "blue",
|
|
21
|
+
port: int = 8000, api_url: str = "http://localhost:8000", db_type: str = "sqlite",
|
|
22
|
+
auth: bool = False, admin_user: str = "admin", admin_pass: str = "admin"
|
|
23
|
+
):
|
|
20
24
|
"""Generates complete FastAPI and React frontend files structure based on inferred schemas and theme."""
|
|
21
25
|
env = get_env()
|
|
22
26
|
|
|
@@ -29,7 +33,10 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
|
|
|
29
33
|
theme=theme,
|
|
30
34
|
port=port,
|
|
31
35
|
api_url=api_url,
|
|
32
|
-
db_type=db_type
|
|
36
|
+
db_type=db_type,
|
|
37
|
+
auth=auth,
|
|
38
|
+
admin_user=admin_user,
|
|
39
|
+
admin_pass=admin_pass
|
|
33
40
|
)
|
|
34
41
|
|
|
35
42
|
# 2. Backend FastAPI application files
|
|
@@ -44,6 +51,9 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
|
|
|
44
51
|
"backend/Dockerfile.jinja2": "Dockerfile"
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
if auth:
|
|
55
|
+
backend_templates["backend/auth.py.jinja2"] = "auth.py"
|
|
56
|
+
|
|
47
57
|
for t_path, dest_name in backend_templates.items():
|
|
48
58
|
render_template(
|
|
49
59
|
env,
|
|
@@ -53,7 +63,10 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
|
|
|
53
63
|
theme=theme,
|
|
54
64
|
port=port,
|
|
55
65
|
api_url=api_url,
|
|
56
|
-
db_type=db_type
|
|
66
|
+
db_type=db_type,
|
|
67
|
+
auth=auth,
|
|
68
|
+
admin_user=admin_user,
|
|
69
|
+
admin_pass=admin_pass
|
|
57
70
|
)
|
|
58
71
|
|
|
59
72
|
# Write init file to make backend a python package
|
|
@@ -78,6 +91,9 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
|
|
|
78
91
|
"frontend/src/Dashboard.tsx.jinja2": "src/Dashboard.tsx"
|
|
79
92
|
}
|
|
80
93
|
|
|
94
|
+
if auth:
|
|
95
|
+
frontend_templates["frontend/src/Login.tsx.jinja2"] = "src/pages/Login.tsx"
|
|
96
|
+
|
|
81
97
|
for t_path, dest_name in frontend_templates.items():
|
|
82
98
|
render_template(
|
|
83
99
|
env,
|
|
@@ -87,7 +103,10 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
|
|
|
87
103
|
theme=theme,
|
|
88
104
|
port=port,
|
|
89
105
|
api_url=api_url,
|
|
90
|
-
db_type=db_type
|
|
106
|
+
db_type=db_type,
|
|
107
|
+
auth=auth,
|
|
108
|
+
admin_user=admin_user,
|
|
109
|
+
admin_pass=admin_pass
|
|
91
110
|
)
|
|
92
111
|
|
|
93
112
|
# 4. Generate dynamic CRUD view pages for each table
|
|
@@ -103,7 +122,10 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
|
|
|
103
122
|
theme=theme,
|
|
104
123
|
port=port,
|
|
105
124
|
api_url=api_url,
|
|
106
|
-
db_type=db_type
|
|
125
|
+
db_type=db_type,
|
|
126
|
+
auth=auth,
|
|
127
|
+
admin_user=admin_user,
|
|
128
|
+
admin_pass=admin_pass
|
|
107
129
|
)
|
|
108
130
|
|
|
109
131
|
def render_alembic_files(backend_path: str, schemas: dict):
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
4
|
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
5
|
+
from sqlalchemy.orm import Session
|
|
6
|
+
from passlib.context import CryptContext
|
|
7
|
+
from jose import JWTError, jwt
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from .database import get_db
|
|
10
|
+
from .models import AppUser
|
|
11
|
+
|
|
12
|
+
SECRET_KEY = "super-secret-key-change-me-in-production"
|
|
13
|
+
ALGORITHM = "HS256"
|
|
14
|
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
|
|
15
|
+
|
|
16
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
17
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/token")
|
|
18
|
+
|
|
19
|
+
auth_router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
20
|
+
|
|
21
|
+
class Token(BaseModel):
|
|
22
|
+
access_token: str
|
|
23
|
+
token_type: str
|
|
24
|
+
|
|
25
|
+
def verify_password(plain_password, hashed_password):
|
|
26
|
+
return pwd_context.verify(plain_password, hashed_password)
|
|
27
|
+
|
|
28
|
+
def get_password_hash(password):
|
|
29
|
+
return pwd_context.hash(password)
|
|
30
|
+
|
|
31
|
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
|
32
|
+
to_encode = data.copy()
|
|
33
|
+
if expires_delta:
|
|
34
|
+
expire = datetime.utcnow() + expires_delta
|
|
35
|
+
else:
|
|
36
|
+
expire = datetime.utcnow() + timedelta(minutes=15)
|
|
37
|
+
to_encode.update({"exp": expire})
|
|
38
|
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
39
|
+
return encoded_jwt
|
|
40
|
+
|
|
41
|
+
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
|
42
|
+
credentials_exception = HTTPException(
|
|
43
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
44
|
+
detail="Could not validate credentials",
|
|
45
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
46
|
+
)
|
|
47
|
+
try:
|
|
48
|
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
49
|
+
username: str = payload.get("sub")
|
|
50
|
+
if username is None:
|
|
51
|
+
raise credentials_exception
|
|
52
|
+
except JWTError:
|
|
53
|
+
raise credentials_exception
|
|
54
|
+
user = db.query(AppUser).filter(AppUser.username == username).first()
|
|
55
|
+
if user is None:
|
|
56
|
+
raise credentials_exception
|
|
57
|
+
return user
|
|
58
|
+
|
|
59
|
+
@auth_router.post("/token", response_model=Token)
|
|
60
|
+
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
|
61
|
+
user = db.query(AppUser).filter(AppUser.username == form_data.username).first()
|
|
62
|
+
if not user or not verify_password(form_data.password, user.hashed_password):
|
|
63
|
+
raise HTTPException(
|
|
64
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
65
|
+
detail="Incorrect username or password",
|
|
66
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
67
|
+
)
|
|
68
|
+
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
69
|
+
access_token = create_access_token(
|
|
70
|
+
data={"sub": user.username}, expires_delta=access_token_expires
|
|
71
|
+
)
|
|
72
|
+
return {"access_token": access_token, "token_type": "bearer"}
|
|
73
|
+
|
|
74
|
+
def seed_admin_user(db: Session):
|
|
75
|
+
admin_user = "{{ admin_user }}"
|
|
76
|
+
admin_pass = "{{ admin_pass }}"
|
|
77
|
+
if not db.query(AppUser).first():
|
|
78
|
+
db_user = AppUser(username=admin_user, hashed_password=get_password_hash(admin_pass))
|
|
79
|
+
db.add(db_user)
|
|
80
|
+
db.commit()
|
|
@@ -7,8 +7,11 @@ from sqlalchemy import or_, cast, String as SAString, desc as sa_desc, asc as sa
|
|
|
7
7
|
from sqlalchemy.orm import Session
|
|
8
8
|
from typing import List, Optional
|
|
9
9
|
|
|
10
|
-
from .database import engine, get_db
|
|
10
|
+
from .database import engine, get_db, SessionLocal
|
|
11
11
|
from . import models, schemas
|
|
12
|
+
{% if auth %}
|
|
13
|
+
from .auth import auth_router, get_current_user, seed_admin_user
|
|
14
|
+
{% endif %}
|
|
12
15
|
|
|
13
16
|
# Automatically create tables if they do not exist
|
|
14
17
|
models.Base.metadata.create_all(bind=engine)
|
|
@@ -23,6 +26,14 @@ app.add_middleware(
|
|
|
23
26
|
allow_headers=["*"],
|
|
24
27
|
)
|
|
25
28
|
|
|
29
|
+
{% if auth %}
|
|
30
|
+
# Seed initial admin user if table is empty
|
|
31
|
+
with SessionLocal() as db:
|
|
32
|
+
seed_admin_user(db)
|
|
33
|
+
|
|
34
|
+
app.include_router(auth_router)
|
|
35
|
+
{% endif %}
|
|
36
|
+
|
|
26
37
|
@app.get("/")
|
|
27
38
|
def read_root():
|
|
28
39
|
return {"message": "Welcome to the generated Proto-CLI API!"}
|
|
@@ -55,7 +66,8 @@ def list_{{ table_name }}(
|
|
|
55
66
|
search: Optional[str] = Query(None, description="Search across all text columns"),
|
|
56
67
|
order_by: Optional[str] = Query(None, description="Column name to sort by"),
|
|
57
68
|
dir: Optional[str] = Query("asc", description="Sort direction: asc or desc"),
|
|
58
|
-
db: Session = Depends(get_db)
|
|
69
|
+
db: Session = Depends(get_db){% if auth %},
|
|
70
|
+
current_user = Depends(get_current_user){% endif %}
|
|
59
71
|
):
|
|
60
72
|
query = db.query(models.{{ model_name }})
|
|
61
73
|
|
|
@@ -79,7 +91,8 @@ def list_{{ table_name }}(
|
|
|
79
91
|
@app.get("/api/{{ table_name }}/export")
|
|
80
92
|
def export_{{ table_name }}(
|
|
81
93
|
fmt: Optional[str] = Query("csv", description="Export format: csv or xlsx"),
|
|
82
|
-
db: Session = Depends(get_db)
|
|
94
|
+
db: Session = Depends(get_db){% if auth %},
|
|
95
|
+
current_user = Depends(get_current_user){% endif %}
|
|
83
96
|
):
|
|
84
97
|
records = db.query(models.{{ model_name }}).all()
|
|
85
98
|
col_names = [c.name for c in models.{{ model_name }}.__table__.columns]
|
|
@@ -137,14 +150,14 @@ def export_{{ table_name }}(
|
|
|
137
150
|
)
|
|
138
151
|
|
|
139
152
|
@app.get("/api/{{ table_name }}/{id}", response_model=schemas.{{ schema_response }})
|
|
140
|
-
def get_{{ table_name }}(id: int, db: Session = Depends(get_db)):
|
|
153
|
+
def get_{{ table_name }}(id: int, db: Session = Depends(get_db){% if auth %}, current_user = Depends(get_current_user){% endif %}):
|
|
141
154
|
record = db.query(models.{{ model_name }}).filter(models.{{ model_name }}.id == id).first()
|
|
142
155
|
if not record:
|
|
143
156
|
raise HTTPException(status_code=404, detail="Record not found in {{ table_name }}")
|
|
144
157
|
return record
|
|
145
158
|
|
|
146
159
|
@app.post("/api/{{ table_name }}", response_model=schemas.{{ schema_response }}, status_code=status.HTTP_201_CREATED)
|
|
147
|
-
def create_{{ table_name }}(payload: schemas.{{ schema_create }}, db: Session = Depends(get_db)):
|
|
160
|
+
def create_{{ table_name }}(payload: schemas.{{ schema_create }}, db: Session = Depends(get_db){% if auth %}, current_user = Depends(get_current_user){% endif %}):
|
|
148
161
|
db_record = models.{{ model_name }}(**payload.model_dump())
|
|
149
162
|
db.add(db_record)
|
|
150
163
|
db.commit()
|
|
@@ -152,7 +165,7 @@ def create_{{ table_name }}(payload: schemas.{{ schema_create }}, db: Session =
|
|
|
152
165
|
return db_record
|
|
153
166
|
|
|
154
167
|
@app.put("/api/{{ table_name }}/{id}", response_model=schemas.{{ schema_response }})
|
|
155
|
-
def update_{{ table_name }}(id: int, payload: schemas.{{ schema_update }}, db: Session = Depends(get_db)):
|
|
168
|
+
def update_{{ table_name }}(id: int, payload: schemas.{{ schema_update }}, db: Session = Depends(get_db){% if auth %}, current_user = Depends(get_current_user){% endif %}):
|
|
156
169
|
db_record = db.query(models.{{ model_name }}).filter(models.{{ model_name }}.id == id).first()
|
|
157
170
|
if not db_record:
|
|
158
171
|
raise HTTPException(status_code=404, detail="Record not found in {{ table_name }}")
|
|
@@ -166,7 +179,7 @@ def update_{{ table_name }}(id: int, payload: schemas.{{ schema_update }}, db: S
|
|
|
166
179
|
return db_record
|
|
167
180
|
|
|
168
181
|
@app.delete("/api/{{ table_name }}/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
169
|
-
def delete_{{ table_name }}(id: int, db: Session = Depends(get_db)):
|
|
182
|
+
def delete_{{ table_name }}(id: int, db: Session = Depends(get_db){% if auth %}, current_user = Depends(get_current_user){% endif %}):
|
|
170
183
|
db_record = db.query(models.{{ model_name }}).filter(models.{{ model_name }}.id == id).first()
|
|
171
184
|
if not db_record:
|
|
172
185
|
raise HTTPException(status_code=404, detail="Record not found in {{ table_name }}")
|
|
@@ -29,3 +29,12 @@ class {{ class_name }}(Base):
|
|
|
29
29
|
{% endfor %}
|
|
30
30
|
|
|
31
31
|
{% endfor %}
|
|
32
|
+
|
|
33
|
+
{% if auth %}
|
|
34
|
+
class AppUser(Base):
|
|
35
|
+
__tablename__ = "app_users"
|
|
36
|
+
|
|
37
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
38
|
+
username = Column(String, unique=True, index=True, nullable=False)
|
|
39
|
+
hashed_password = Column(String, nullable=False)
|
|
40
|
+
{% endif %}
|
|
@@ -6,6 +6,11 @@ pydantic>=2.0.0
|
|
|
6
6
|
pandas>=1.5.0
|
|
7
7
|
openpyxl>=3.0.0
|
|
8
8
|
email-validator>=2.0.0
|
|
9
|
+
{% if auth %}
|
|
10
|
+
passlib[bcrypt]>=1.7.4
|
|
11
|
+
python-jose[cryptography]>=3.3.0
|
|
12
|
+
python-multipart>=0.0.6
|
|
13
|
+
{% endif %}
|
|
9
14
|
{% if db_type == 'pg' %}
|
|
10
15
|
psycopg2-binary>=2.9.0
|
|
11
16
|
{% elif db_type == 'mysql' %}
|
|
@@ -2,6 +2,11 @@ import React from 'react'
|
|
|
2
2
|
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom'
|
|
3
3
|
import { Database, Table, LayoutDashboard, ChevronRight } from 'lucide-react'
|
|
4
4
|
import Dashboard from './Dashboard'
|
|
5
|
+
{% if auth %}
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
import Login from './pages/Login'
|
|
8
|
+
import { LogOut } from 'lucide-react'
|
|
9
|
+
{% endif %}
|
|
5
10
|
{% for table_name, columns in schemas.items() %}
|
|
6
11
|
import {{ table_name.replace('_', ' ').title().replace(' ', '') }}Crud from './pages/{{ table_name.replace('_', ' ').title().replace(' ', '') }}Crud'
|
|
7
12
|
{% endfor %}
|
|
@@ -26,6 +31,23 @@ const SidebarLink = ({ to, children, icon: Icon }: { to: string; children: React
|
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
function AppContent() {
|
|
34
|
+
{% if auth %}
|
|
35
|
+
const [token, setToken] = useState<string | null>(localStorage.getItem('token'))
|
|
36
|
+
|
|
37
|
+
const handleLogin = (newToken: string) => {
|
|
38
|
+
localStorage.setItem('token', newToken)
|
|
39
|
+
setToken(newToken)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handleLogout = () => {
|
|
43
|
+
localStorage.removeItem('token')
|
|
44
|
+
setToken(null)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!token) {
|
|
48
|
+
return <Login onLogin={handleLogin} />
|
|
49
|
+
}
|
|
50
|
+
{% endif %}
|
|
29
51
|
return (
|
|
30
52
|
<div className="flex min-h-screen bg-slate-950 text-slate-100 font-sans">
|
|
31
53
|
{/* Sidebar */}
|
|
@@ -53,6 +75,15 @@ function AppContent() {
|
|
|
53
75
|
</nav>
|
|
54
76
|
|
|
55
77
|
<div className="mt-auto border-t border-slate-900 pt-4 px-2">
|
|
78
|
+
{% if auth %}
|
|
79
|
+
<button
|
|
80
|
+
onClick={handleLogout}
|
|
81
|
+
className="flex w-full items-center gap-2 px-4 py-2 text-sm text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded-xl transition-all duration-200 mb-4"
|
|
82
|
+
>
|
|
83
|
+
<LogOut className="w-4 h-4" />
|
|
84
|
+
Logout
|
|
85
|
+
</button>
|
|
86
|
+
{% endif %}
|
|
56
87
|
<p className="text-xs text-slate-500">FastAPI + React SPA</p>
|
|
57
88
|
<p className="text-[10px] text-slate-600 mt-1">Status: <span className="text-emerald-500 font-semibold">Online</span></p>
|
|
58
89
|
</div>
|
{sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/src/Dashboard.tsx.jinja2
RENAMED
|
@@ -19,7 +19,7 @@ export default function Dashboard() {
|
|
|
19
19
|
const fetchMetadata = async () => {
|
|
20
20
|
try {
|
|
21
21
|
setLoading(true)
|
|
22
|
-
const res = await fetch(`${API_BASE}/api/metadata`)
|
|
22
|
+
const res = await fetch(`${API_BASE}/api/metadata`{% if auth %}, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }{% endif %})
|
|
23
23
|
if (!res.ok) throw new Error('Failed to fetch system metadata')
|
|
24
24
|
const data = await res.json()
|
|
25
25
|
setMetadata(data)
|
|
@@ -100,7 +100,7 @@ export default function Dashboard() {
|
|
|
100
100
|
</h3>
|
|
101
101
|
<div className="h-72 w-full">
|
|
102
102
|
<ResponsiveContainer width="100%" height="100%">
|
|
103
|
-
<BarChart data={metadata} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
|
103
|
+
<BarChart data={metadata} margin={ { top: 10, right: 10, left: -20, bottom: 0 } }>
|
|
104
104
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
|
105
105
|
<XAxis
|
|
106
106
|
dataKey="name"
|
|
@@ -118,10 +118,10 @@ export default function Dashboard() {
|
|
|
118
118
|
tickFormatter={(val) => val >= 1000 ? `${(val / 1000).toFixed(1)}k` : val}
|
|
119
119
|
/>
|
|
120
120
|
<Tooltip
|
|
121
|
-
cursor={{ fill: '#0f172a' }}
|
|
122
|
-
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', borderRadius: '0.75rem' }}
|
|
123
|
-
itemStyle={{ color: '#38bdf8' }}
|
|
124
|
-
labelStyle={{ color: '#94a3b8', textTransform: 'capitalize', marginBottom: '4px' }}
|
|
121
|
+
cursor={ { fill: '#0f172a' } }
|
|
122
|
+
contentStyle={ { backgroundColor: '#0f172a', borderColor: '#1e293b', borderRadius: '0.75rem' } }
|
|
123
|
+
itemStyle={ { color: '#38bdf8' } }
|
|
124
|
+
labelStyle={ { color: '#94a3b8', textTransform: 'capitalize', marginBottom: '4px' } }
|
|
125
125
|
formatter={(value: number) => [value, 'Records']}
|
|
126
126
|
labelFormatter={(label: string) => label.replace('_', ' ')}
|
|
127
127
|
/>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Database, Lock, User, AlertCircle } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export default function Login({ onLogin }: { onLogin: (token: str) => void }) {
|
|
5
|
+
const [username, setUsername] = useState('');
|
|
6
|
+
const [password, setPassword] = useState('');
|
|
7
|
+
const [error, setError] = useState('');
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
|
|
10
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
setError('');
|
|
13
|
+
setLoading(true);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/auth/token`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
20
|
+
},
|
|
21
|
+
body: new URLSearchParams({
|
|
22
|
+
username,
|
|
23
|
+
password,
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error('Invalid credentials');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const data = await response.json();
|
|
32
|
+
onLogin(data.access_token);
|
|
33
|
+
} catch (err: any) {
|
|
34
|
+
setError(err.message || 'An error occurred during login');
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="min-h-screen bg-slate-950 flex items-center justify-center p-4">
|
|
42
|
+
<div className="w-full max-w-md">
|
|
43
|
+
<div className="bg-slate-900/50 backdrop-blur-xl border border-slate-800 rounded-3xl p-8 shadow-2xl">
|
|
44
|
+
<div className="flex flex-col items-center mb-8">
|
|
45
|
+
<div className="p-3 bg-brand-500/10 text-brand-400 rounded-2xl border border-brand-500/20 mb-4">
|
|
46
|
+
<Database className="w-8 h-8" />
|
|
47
|
+
</div>
|
|
48
|
+
<h1 className="text-2xl font-outfit font-bold text-white mb-2">Welcome Back</h1>
|
|
49
|
+
<p className="text-slate-400 text-sm text-center">Sign in to access your administrative dashboard.</p>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{error && (
|
|
53
|
+
<div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-3 text-red-400 text-sm">
|
|
54
|
+
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
|
55
|
+
<p>{error}</p>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
60
|
+
<div>
|
|
61
|
+
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 ml-1">Username</label>
|
|
62
|
+
<div className="relative">
|
|
63
|
+
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
|
64
|
+
<User className="w-5 h-5 text-slate-500" />
|
|
65
|
+
</div>
|
|
66
|
+
<input
|
|
67
|
+
type="text"
|
|
68
|
+
value={username}
|
|
69
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
70
|
+
className="w-full pl-11 pr-4 py-3 bg-slate-950/50 border border-slate-800 rounded-xl text-white placeholder-slate-600 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-all"
|
|
71
|
+
placeholder="Enter your username"
|
|
72
|
+
required
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div>
|
|
78
|
+
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 ml-1">Password</label>
|
|
79
|
+
<div className="relative">
|
|
80
|
+
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
|
81
|
+
<Lock className="w-5 h-5 text-slate-500" />
|
|
82
|
+
</div>
|
|
83
|
+
<input
|
|
84
|
+
type="password"
|
|
85
|
+
value={password}
|
|
86
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
87
|
+
className="w-full pl-11 pr-4 py-3 bg-slate-950/50 border border-slate-800 rounded-xl text-white placeholder-slate-600 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-all"
|
|
88
|
+
placeholder="••••••••"
|
|
89
|
+
required
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<button
|
|
95
|
+
type="submit"
|
|
96
|
+
disabled={loading}
|
|
97
|
+
className="w-full py-3 px-4 bg-gradient-to-r from-brand-600 to-brand-500 hover:from-brand-500 hover:to-brand-400 text-white rounded-xl font-medium shadow-lg shadow-brand-500/25 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
98
|
+
>
|
|
99
|
+
{loading ? 'Authenticating...' : 'Sign In'}
|
|
100
|
+
</button>
|
|
101
|
+
</form>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
{sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/src/TableCrud.tsx.jinja2
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import React, { useEffect, useState, useCallback } from 'react'
|
|
2
2
|
import { useNavigate } from 'react-router-dom'
|
|
3
3
|
import { Plus, Edit2, Trash2, X, ChevronLeft, ChevronRight, Loader2, Search, Download, ArrowUpDown, ArrowUp, ArrowDown, FileSpreadsheet } from 'lucide-react'
|
|
4
4
|
|
|
@@ -78,7 +78,7 @@ export default function {{ table_name.replace('_', ' ').title().replace(' ', '')
|
|
|
78
78
|
params.set('order_by', sortCol)
|
|
79
79
|
params.set('dir', sortDir)
|
|
80
80
|
}
|
|
81
|
-
const res = await fetch(`${API_BASE}/api/{{ table_name }}?${params}`)
|
|
81
|
+
const res = await fetch(`${API_BASE}/api/{{ table_name }}?${params}`{% if auth %}, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }{% endif %})
|
|
82
82
|
if (!res.ok) throw new Error('Failed to load data from server')
|
|
83
83
|
const data = await res.json()
|
|
84
84
|
setRecords(data)
|
|
@@ -99,7 +99,7 @@ export default function {{ table_name.replace('_', ' ').title().replace(' ', '')
|
|
|
99
99
|
{% for col in columns %}
|
|
100
100
|
{% if col.foreign_key %}
|
|
101
101
|
{% set target_table = col.foreign_key.split('.')[0] %}
|
|
102
|
-
fetch(`${API_BASE}/api/{{ target_table }}?limit=500`)
|
|
102
|
+
fetch(`${API_BASE}/api/{{ target_table }}?limit=500`{% if auth %}, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }{% endif %})
|
|
103
103
|
.then(res => {
|
|
104
104
|
if (!res.ok) throw new Error(`Server responded ${res.status}`)
|
|
105
105
|
return res.json()
|
|
@@ -182,7 +182,10 @@ export default function {{ table_name.replace('_', ' ').title().replace(' ', '')
|
|
|
182
182
|
|
|
183
183
|
const res = await fetch(url, {
|
|
184
184
|
method,
|
|
185
|
-
headers: {
|
|
185
|
+
headers: {
|
|
186
|
+
'Content-Type': 'application/json'{% if auth %},
|
|
187
|
+
'Authorization': `Bearer ${localStorage.getItem('token')}`{% endif %}
|
|
188
|
+
},
|
|
186
189
|
body: JSON.stringify(payload)
|
|
187
190
|
})
|
|
188
191
|
|
|
@@ -200,7 +203,8 @@ export default function {{ table_name.replace('_', ' ').title().replace(' ', '')
|
|
|
200
203
|
const handleDelete = async (id: number) => {
|
|
201
204
|
try {
|
|
202
205
|
const res = await fetch(`${API_BASE}/api/{{ table_name }}/${id}`, {
|
|
203
|
-
method: 'DELETE'
|
|
206
|
+
method: 'DELETE'{% if auth %},
|
|
207
|
+
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }{% endif %}
|
|
204
208
|
})
|
|
205
209
|
if (!res.ok) throw new Error('Failed to delete record')
|
|
206
210
|
setDeleteConfirmId(null)
|
|
@@ -210,8 +214,28 @@ export default function {{ table_name.replace('_', ' ').title().replace(' ', '')
|
|
|
210
214
|
}
|
|
211
215
|
}
|
|
212
216
|
|
|
213
|
-
const handleExport = (fmt: 'csv' | 'xlsx') => {
|
|
217
|
+
const handleExport = async (fmt: 'csv' | 'xlsx') => {
|
|
218
|
+
{% if auth %}
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch(`${API_BASE}/api/{{ table_name }}/export?fmt=${fmt}`, {
|
|
221
|
+
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
|
|
222
|
+
})
|
|
223
|
+
if (!res.ok) throw new Error('Export failed')
|
|
224
|
+
const blob = await res.blob()
|
|
225
|
+
const url = window.URL.createObjectURL(blob)
|
|
226
|
+
const a = document.createElement('a')
|
|
227
|
+
a.href = url
|
|
228
|
+
a.download = `{{ table_name }}.${fmt}`
|
|
229
|
+
document.body.appendChild(a)
|
|
230
|
+
a.click()
|
|
231
|
+
a.remove()
|
|
232
|
+
window.URL.revokeObjectURL(url)
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
alert(err.message || 'Export failed')
|
|
235
|
+
}
|
|
236
|
+
{% else %}
|
|
214
237
|
window.open(`${API_BASE}/api/{{ table_name }}/export?fmt=${fmt}`, '_blank')
|
|
238
|
+
{% endif %}
|
|
215
239
|
}
|
|
216
240
|
|
|
217
241
|
const handleSort = (colName: string) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sirius-cli
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A rapid prototyping tool for generating containerized backend + frontend CRUD stacks from CSV/SQLite files.
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -36,11 +36,13 @@ A rapid prototyping CLI tool designed to take multiple CSV files, Excel spreadsh
|
|
|
36
36
|
|
|
37
37
|
## Installation
|
|
38
38
|
|
|
39
|
-
Install the package
|
|
39
|
+
Install the package directly from PyPI:
|
|
40
40
|
```bash
|
|
41
|
-
pip install -
|
|
41
|
+
pip install sirius-cli
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
*(For local development: clone the repository and run `pip install -e .`)*
|
|
45
|
+
|
|
44
46
|
---
|
|
45
47
|
|
|
46
48
|
## Usage: Creating a New Project (`sirius-init`)
|
|
@@ -14,6 +14,7 @@ sirius_cli.egg-info/requires.txt
|
|
|
14
14
|
sirius_cli.egg-info/top_level.txt
|
|
15
15
|
sirius_cli/templates/docker-compose.yml.jinja2
|
|
16
16
|
sirius_cli/templates/backend/Dockerfile.jinja2
|
|
17
|
+
sirius_cli/templates/backend/auth.py.jinja2
|
|
17
18
|
sirius_cli/templates/backend/database.py.jinja2
|
|
18
19
|
sirius_cli/templates/backend/main.py.jinja2
|
|
19
20
|
sirius_cli/templates/backend/models.py.jinja2
|
|
@@ -31,6 +32,7 @@ sirius_cli/templates/frontend/tsconfig.json.jinja2
|
|
|
31
32
|
sirius_cli/templates/frontend/vite.config.ts.jinja2
|
|
32
33
|
sirius_cli/templates/frontend/src/App.tsx.jinja2
|
|
33
34
|
sirius_cli/templates/frontend/src/Dashboard.tsx.jinja2
|
|
35
|
+
sirius_cli/templates/frontend/src/Login.tsx.jinja2
|
|
34
36
|
sirius_cli/templates/frontend/src/TableCrud.tsx.jinja2
|
|
35
37
|
sirius_cli/templates/frontend/src/index.css.jinja2
|
|
36
38
|
sirius_cli/templates/frontend/src/main.tsx.jinja2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/backend/alembic/script.py.mako.jinja2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/postcss.config.js.jinja2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sirius_cli-0.1.0 → sirius_cli-0.2.1}/sirius_cli/templates/frontend/tailwind.config.js.jinja2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|