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 ## 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
View File
@@ -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
View File
@@ -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)
+2 -1
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
from .color_palette import router
+1 -7
View File
@@ -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)
+1 -1
View File
@@ -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
} }
+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"
}
}
+2 -1
View File
@@ -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
+1 -3
View File
@@ -10,6 +10,4 @@ if __name__ == "__main__":
host=settings.HOST, host=settings.HOST,
port=settings.PORT, port=settings.PORT,
reload=settings.APP_ENV == "development", reload=settings.APP_ENV == "development",
) )
# Force Reload