feat: implement pydantic multi env setup and removed node based version
This commit is contained in:
@@ -20,9 +20,10 @@ A multi-tenant SaaS backend built with FastAPI, PostgreSQL, and SQLAlchemy.
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python 3.12+
|
||||||
|
- PostgreSQL 12+
|
||||||
- Python 3.10+
|
- Python 3.10+
|
||||||
- PostgreSQL 12+
|
- PostgreSQL 12+
|
||||||
- Node.js (for npm scripts)
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ python -m venv venv
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
npm install # For cross-env support in npm scripts
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Environment Configuration
|
### 4. Environment Configuration
|
||||||
@@ -112,11 +113,11 @@ alembic revision --autogenerate -m "Initial schema"
|
|||||||
#### Run Migrations
|
#### Run Migrations
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using npm scripts (recommended - handles APP_ENV automatically)
|
# Using manage.py (recommended - handles APP_ENV automatically)
|
||||||
npm run migrate:local
|
python manage.py migrate --env local
|
||||||
npm run migrate:dev
|
python manage.py migrate --env development
|
||||||
npm run migrate:prod
|
python manage.py migrate --env production
|
||||||
npm run migrate:test
|
python manage.py migrate --env testing
|
||||||
|
|
||||||
# Or using alembic directly
|
# Or using alembic directly
|
||||||
$env:APP_ENV="local" # Set environment first
|
$env:APP_ENV="local" # Set environment first
|
||||||
@@ -148,20 +149,20 @@ After running migrations, seed the database with initial data:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Seed super admin user
|
# Seed super admin user
|
||||||
npm run seed:superadmin:local
|
python manage.py seed superadmin --env local
|
||||||
|
|
||||||
# Seed default color palettes
|
# Seed default color palettes
|
||||||
npm run seed:palettes:local
|
python manage.py seed palettes --env local
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. Run the Application
|
### 7. Run the Application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using npm scripts (recommended)
|
# Using manage.py (recommended)
|
||||||
npm run local # Local environment
|
python manage.py run --env local # Local environment
|
||||||
npm run dev # Development environment
|
python manage.py run --env development # Development environment
|
||||||
npm run prod # Production environment
|
python manage.py run --env production # Production environment
|
||||||
npm run test # Testing environment
|
python manage.py run --env testing # Testing environment
|
||||||
|
|
||||||
# Or using Python directly
|
# Or using Python directly
|
||||||
python run.py
|
python run.py
|
||||||
@@ -209,7 +210,8 @@ backend/
|
|||||||
│ ├── seed_palettes.py
|
│ ├── seed_palettes.py
|
||||||
│ └── seed_superadmin.py
|
│ └── seed_superadmin.py
|
||||||
├── alembic.ini # Alembic configuration
|
├── alembic.ini # Alembic configuration
|
||||||
├── package.json # NPM scripts
|
├── alembic.ini # Alembic configuration
|
||||||
|
├── manage.py # Management CLI script
|
||||||
├── requirements.txt # Python dependencies
|
├── requirements.txt # Python dependencies
|
||||||
└── run.py # Application entry point
|
└── run.py # Application entry point
|
||||||
```
|
```
|
||||||
|
|||||||
+6
-12
@@ -5,21 +5,21 @@ import logging
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from app.config.settings import settings
|
from app.config.settings import settings
|
||||||
from app.config.database import engine
|
from app.config.database import engine
|
||||||
|
|
||||||
# Import models for Alembic
|
|
||||||
import app.models.auth.user_model
|
import app.models.auth.user_model
|
||||||
import app.models.auth.role_model
|
import app.models.auth.role_model
|
||||||
import app.models.auth.tenant_model
|
import app.models.auth.tenant_model
|
||||||
import app.models.theme.color_palette_model
|
import app.models.theme.color_palette_model
|
||||||
|
|
||||||
# New Module Integration Models
|
|
||||||
import app.models.auth.module_model
|
import app.models.auth.module_model
|
||||||
import app.models.auth.module_environment_model
|
import app.models.auth.module_environment_model
|
||||||
import app.models.auth.tenant_module_model
|
import app.models.auth.tenant_module_model
|
||||||
import app.models.auth.sso_grant_model
|
import app.models.auth.sso_grant_model
|
||||||
import app.models.auth.access_model # Re-import to ensure updates are picked up
|
import app.models.auth.access_model
|
||||||
import app.models.system.event_log_model
|
import app.models.system.event_log_model
|
||||||
|
import asyncio
|
||||||
|
from app.services.auth.event_service import EventService
|
||||||
|
from app.config.database import SessionLocal
|
||||||
|
from app.core.redis import redis_client, sync_redis_client
|
||||||
|
from fastapi.concurrency import run_in_threadpool
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -141,11 +141,6 @@ def create_app() -> FastAPI:
|
|||||||
f"{settings.PROJECT_NAME} v{settings.VERSION} started ({settings.APP_ENV})"
|
f"{settings.PROJECT_NAME} v{settings.VERSION} started ({settings.APP_ENV})"
|
||||||
)
|
)
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from app.services.auth.event_service import EventService
|
|
||||||
from app.config.database import SessionLocal
|
|
||||||
from app.core.redis import redis_client, sync_redis_client
|
|
||||||
from fastapi.concurrency import run_in_threadpool
|
|
||||||
|
|
||||||
async def redis_event_consumer():
|
async def redis_event_consumer():
|
||||||
logger.info("Redis Event Consumer STARTED")
|
logger.info("Redis Event Consumer STARTED")
|
||||||
@@ -185,7 +180,6 @@ def create_app() -> FastAPI:
|
|||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def shutdown_event():
|
async def shutdown_event():
|
||||||
logger.info("Shutting down...")
|
logger.info("Shutting down...")
|
||||||
from app.core.redis import redis_client
|
|
||||||
await redis_client.close()
|
await redis_client.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from fastapi import HTTPException, status, BackgroundTasks
|
from fastapi import HTTPException, status, BackgroundTasks
|
||||||
from fastapi import HTTPException, status
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import uuid
|
import uuid
|
||||||
from app.models.auth.user_model import User
|
from app.models.auth.user_model import User
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from app.schemas.theme.color_palette_schema import (
|
|||||||
)
|
)
|
||||||
from app.services.theme.color_palette_service import PaletteService
|
from app.services.theme.color_palette_service import PaletteService
|
||||||
|
|
||||||
|
|
||||||
class PaletteController:
|
class PaletteController:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_palettes(db: Session, current_user: User) -> List[ColorPalette]:
|
def get_all_palettes(db: Session, current_user: User) -> List[ColorPalette]:
|
||||||
@@ -35,4 +34,4 @@ class PaletteController:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_palette(db: Session, palette_id: UUID):
|
def delete_palette(db: Session, palette_id: UUID):
|
||||||
return PaletteService.delete_palette(db, palette_id)
|
return PaletteService.delete_palette(db, palette_id)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
from .auth import router
|
from .auth import router
|
||||||
from .tenant import router
|
from .tenant import router
|
||||||
from .role import router
|
from .role import router
|
||||||
|
from .user import router
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .color_palette import router
|
||||||
@@ -14,14 +14,12 @@ from app.models.auth.user_model import User
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/get", response_model=List[ColorPaletteResponse])
|
@router.get("/get", response_model=List[ColorPaletteResponse])
|
||||||
def get_all_palettes(
|
def get_all_palettes(
|
||||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
return PaletteController.get_all_palettes(db, current_user)
|
return PaletteController.get_all_palettes(db, current_user)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/get/{palette_id}", response_model=ColorPaletteResponse)
|
@router.get("/get/{palette_id}", response_model=ColorPaletteResponse)
|
||||||
def get_palette(
|
def get_palette(
|
||||||
palette_id: UUID,
|
palette_id: UUID,
|
||||||
@@ -30,7 +28,6 @@ def get_palette(
|
|||||||
):
|
):
|
||||||
return PaletteController.get_palette(db, palette_id)
|
return PaletteController.get_palette(db, palette_id)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/create", response_model=ColorPaletteResponse, status_code=status.HTTP_201_CREATED
|
"/create", response_model=ColorPaletteResponse, status_code=status.HTTP_201_CREATED
|
||||||
)
|
)
|
||||||
@@ -42,12 +39,10 @@ def create_palette(
|
|||||||
):
|
):
|
||||||
return PaletteController.create_palette(db, data, current_user)
|
return PaletteController.create_palette(db, data, current_user)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/update/users/me/preferences", deprecated=True)
|
@router.put("/update/users/me/preferences", deprecated=True)
|
||||||
def update_user_preference():
|
def update_user_preference():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@router.put("/update/{palette_id}", response_model=ColorPaletteResponse)
|
@router.put("/update/{palette_id}", response_model=ColorPaletteResponse)
|
||||||
def update_palette(
|
def update_palette(
|
||||||
palette_id: UUID,
|
palette_id: UUID,
|
||||||
@@ -58,7 +53,6 @@ def update_palette(
|
|||||||
):
|
):
|
||||||
return PaletteController.update_palette(db, palette_id, data)
|
return PaletteController.update_palette(db, palette_id, data)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/delete/{palette_id}", status_code=status.HTTP_200_OK)
|
@router.delete("/delete/{palette_id}", status_code=status.HTTP_200_OK)
|
||||||
def delete_palette(
|
def delete_palette(
|
||||||
palette_id: UUID,
|
palette_id: UUID,
|
||||||
@@ -66,4 +60,4 @@ def delete_palette(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_=Depends(require_access("superadmin.palette.delete")),
|
_=Depends(require_access("superadmin.palette.delete")),
|
||||||
):
|
):
|
||||||
return PaletteController.delete_palette(db, palette_id)
|
return PaletteController.delete_palette(db, palette_id)
|
||||||
@@ -173,7 +173,7 @@ class TenantService:
|
|||||||
if event_targets:
|
if event_targets:
|
||||||
payload = {
|
payload = {
|
||||||
"tenant_id": str(tenant.id),
|
"tenant_id": str(tenant.id),
|
||||||
"tenant_name": tenant.tenant_name, # Note: using current name (might be old if not updated yet, but usually distinct requests)
|
"tenant_name": tenant.tenant_name,
|
||||||
"provisioning_id": provisioning_id,
|
"provisioning_id": provisioning_id,
|
||||||
"targets": event_targets
|
"targets": event_targets
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import typer
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
app = typer.Typer(help="Management script for SaaS Backend")
|
||||||
|
seed_app = typer.Typer(help="Seeding commands")
|
||||||
|
app.add_typer(seed_app, name="seed")
|
||||||
|
|
||||||
|
def run_command(args: list[str], env_name: str):
|
||||||
|
"""
|
||||||
|
Helper to run commands with a specific APP_ENV.
|
||||||
|
Replaces "python" with sys.executable to ensure the same interpreter is used.
|
||||||
|
"""
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["APP_ENV"] = env_name
|
||||||
|
|
||||||
|
if args[0] == "python":
|
||||||
|
args[0] = sys.executable
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(args, env=env, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
sys.exit(e.returncode)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def run(
|
||||||
|
env: str = typer.Option("local", "--env", "-e", help="Environment to run in (local, development, production, testing)")
|
||||||
|
):
|
||||||
|
"""Start the FastAPI server."""
|
||||||
|
run_command(["python", "run.py"], env)
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def migrate(
|
||||||
|
env: str = typer.Option("local", "--env", "-e", help="Environment to run in (local, development, production, testing)")
|
||||||
|
):
|
||||||
|
"""Run Alembic migrations (upgrade head)."""
|
||||||
|
run_command(["python", "-m", "alembic", "upgrade", "head"], env)
|
||||||
|
|
||||||
|
@seed_app.command("palettes")
|
||||||
|
def seed_palettes(
|
||||||
|
env: str = typer.Option("local", "--env", "-e", help="Environment to run in (local, development, production, testing)")
|
||||||
|
):
|
||||||
|
"""Seed palettes database."""
|
||||||
|
run_command(["python", "scripts/seed_palettes.py"], env)
|
||||||
|
|
||||||
|
@seed_app.command("superadmin")
|
||||||
|
def seed_superadmin(
|
||||||
|
env: str = typer.Option("local", "--env", "-e", help="Environment to run in (local, development, production, testing)")
|
||||||
|
):
|
||||||
|
"""Seed superadmin user."""
|
||||||
|
run_command(["python", "scripts/seed_superadmin.py"], env)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
Generated
-105
@@ -1,105 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "saas-backend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "saas-backend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"cross-env": "^7.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cross-env": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cross-spawn": "^7.0.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"cross-env": "src/bin/cross-env.js",
|
|
||||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.14",
|
|
||||||
"npm": ">=6",
|
|
||||||
"yarn": ">=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cross-spawn": {
|
|
||||||
"version": "7.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
||||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.1.0",
|
|
||||||
"shebang-command": "^2.0.0",
|
|
||||||
"which": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/isexe": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/path-key": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shebang-command": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"shebang-regex": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shebang-regex": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"isexe": "^2.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"node-which": "bin/node-which"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "saas-backend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Backend for SaaS architecture",
|
|
||||||
"main": "run.py",
|
|
||||||
"scripts": {
|
|
||||||
"start": "python run.py",
|
|
||||||
"local": "cross-env APP_ENV=local python run.py",
|
|
||||||
"dev": "cross-env APP_ENV=development python run.py",
|
|
||||||
"prod": "cross-env APP_ENV=production python run.py",
|
|
||||||
"test": "cross-env APP_ENV=testing python run.py",
|
|
||||||
"migrate:local": "cross-env APP_ENV=local python -m alembic upgrade head",
|
|
||||||
"migrate:dev": "cross-env APP_ENV=development python -m alembic upgrade head",
|
|
||||||
"migrate:prod": "cross-env APP_ENV=production python -m alembic upgrade head",
|
|
||||||
"migrate:test": "cross-env APP_ENV=testing python -m alembic upgrade head",
|
|
||||||
"seed:palettes:local": "cross-env APP_ENV=local python scripts/seed_palettes.py",
|
|
||||||
"seed:palettes:dev": "cross-env APP_ENV=development python scripts/seed_palettes.py",
|
|
||||||
"seed:palettes:prod": "cross-env APP_ENV=production python scripts/seed_palettes.py",
|
|
||||||
"seed:palettes:test": "cross-env APP_ENV=testing python scripts/seed_palettes.py",
|
|
||||||
"seed:superadmin:local": "cross-env APP_ENV=local python scripts/seed_superadmin.py",
|
|
||||||
"seed:superadmin:dev": "cross-env APP_ENV=development python scripts/seed_superadmin.py",
|
|
||||||
"seed:superadmin:prod": "cross-env APP_ENV=production python scripts/seed_superadmin.py",
|
|
||||||
"seed:superadmin:test": "cross-env APP_ENV=testing python scripts/seed_superadmin.py"
|
|
||||||
},
|
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
|
||||||
"cross-env": "^7.0.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-1
@@ -9,4 +9,5 @@ bcrypt>=4.0.1
|
|||||||
pyjwt>=2.8.0
|
pyjwt>=2.8.0
|
||||||
email-validator>=2.1.0
|
email-validator>=2.1.0
|
||||||
redis==7.1.0
|
redis==7.1.0
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
|
typer==0.21.1
|
||||||
Reference in New Issue
Block a user