feat: implement pydantic multi env setup and removed node based version

This commit is contained in:
Furqan-14
2026-02-05 14:30:07 +05:30
parent 2678e9548a
commit 9b41301b09
13 changed files with 90 additions and 177 deletions
+17 -15
View File
@@ -20,9 +20,10 @@ A multi-tenant SaaS backend built with FastAPI, PostgreSQL, and SQLAlchemy.
## Prerequisites
- Python 3.12+
- PostgreSQL 12+
- Python 3.10+
- PostgreSQL 12+
- Node.js (for npm scripts)
## Getting Started
@@ -44,7 +45,7 @@ python -m venv venv
```bash
pip install -r requirements.txt
npm install # For cross-env support in npm scripts
pip install -r requirements.txt
```
### 4. Environment Configuration
@@ -112,11 +113,11 @@ alembic revision --autogenerate -m "Initial schema"
#### Run Migrations
```bash
# Using npm scripts (recommended - handles APP_ENV automatically)
npm run migrate:local
npm run migrate:dev
npm run migrate:prod
npm run migrate:test
# Using manage.py (recommended - handles APP_ENV automatically)
python manage.py migrate --env local
python manage.py migrate --env development
python manage.py migrate --env production
python manage.py migrate --env testing
# Or using alembic directly
$env:APP_ENV="local" # Set environment first
@@ -148,20 +149,20 @@ After running migrations, seed the database with initial data:
```bash
# Seed super admin user
npm run seed:superadmin:local
python manage.py seed superadmin --env local
# Seed default color palettes
npm run seed:palettes:local
python manage.py seed palettes --env local
```
### 7. Run the Application
```bash
# Using npm scripts (recommended)
npm run local # Local environment
npm run dev # Development environment
npm run prod # Production environment
npm run test # Testing environment
# Using manage.py (recommended)
python manage.py run --env local # Local environment
python manage.py run --env development # Development environment
python manage.py run --env production # Production environment
python manage.py run --env testing # Testing environment
# Or using Python directly
python run.py
@@ -209,7 +210,8 @@ backend/
│ ├── seed_palettes.py
│ └── seed_superadmin.py
├── alembic.ini # Alembic configuration
├── package.json # NPM scripts
├── alembic.ini # Alembic configuration
├── manage.py # Management CLI script
├── requirements.txt # Python dependencies
└── run.py # Application entry point
```
+6 -12
View File
@@ -5,21 +5,21 @@ import logging
from sqlalchemy import text
from app.config.settings import settings
from app.config.database import engine
# Import models for Alembic
import app.models.auth.user_model
import app.models.auth.role_model
import app.models.auth.tenant_model
import app.models.theme.color_palette_model
# New Module Integration Models
import app.models.auth.module_model
import app.models.auth.module_environment_model
import app.models.auth.tenant_module_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 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
logging.basicConfig(
@@ -141,11 +141,6 @@ def create_app() -> FastAPI:
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():
logger.info("Redis Event Consumer STARTED")
@@ -185,7 +180,6 @@ def create_app() -> FastAPI:
@app.on_event("shutdown")
async def shutdown_event():
logger.info("Shutting down...")
from app.core.redis import redis_client
await redis_client.close()
-1
View File
@@ -1,6 +1,5 @@
from sqlalchemy.orm import Session
from fastapi import HTTPException, status, BackgroundTasks
from fastapi import HTTPException, status
from typing import List, Optional
import uuid
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
class PaletteController:
@staticmethod
def get_all_palettes(db: Session, current_user: User) -> List[ColorPalette]:
+1
View File
@@ -1,3 +1,4 @@
from .auth import router
from .tenant import router
from .role import router
from .user import router
+1
View File
@@ -0,0 +1 @@
from .color_palette import router
-6
View File
@@ -14,14 +14,12 @@ from app.models.auth.user_model import User
router = APIRouter()
@router.get("/get", response_model=List[ColorPaletteResponse])
def get_all_palettes(
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
):
return PaletteController.get_all_palettes(db, current_user)
@router.get("/get/{palette_id}", response_model=ColorPaletteResponse)
def get_palette(
palette_id: UUID,
@@ -30,7 +28,6 @@ def get_palette(
):
return PaletteController.get_palette(db, palette_id)
@router.post(
"/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)
@router.put("/update/users/me/preferences", deprecated=True)
def update_user_preference():
pass
@router.put("/update/{palette_id}", response_model=ColorPaletteResponse)
def update_palette(
palette_id: UUID,
@@ -58,7 +53,6 @@ def update_palette(
):
return PaletteController.update_palette(db, palette_id, data)
@router.delete("/delete/{palette_id}", status_code=status.HTTP_200_OK)
def delete_palette(
palette_id: UUID,
+1 -1
View File
@@ -173,7 +173,7 @@ class TenantService:
if event_targets:
payload = {
"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,
"targets": event_targets
}
+58
View File
@@ -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()
-105
View File
@@ -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"
}
}
}
}
-29
View File
@@ -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"
}
}
+1
View File
@@ -10,3 +10,4 @@ pyjwt>=2.8.0
email-validator>=2.1.0
redis==7.1.0
requests==2.32.5
typer==0.21.1
-2
View File
@@ -11,5 +11,3 @@ if __name__ == "__main__":
port=settings.PORT,
reload=settings.APP_ENV == "development",
)
# Force Reload