#!/usr/bin/env python3 """ Quick script to generate deletion service boilerplate Usage: python generate_deletion_service.py Example: python generate_deletion_service.py pos POSConfiguration,POSTransaction,POSSession """ import sys import os from pathlib import Path def generate_deletion_service(service_name: str, models: list[str]): """Generate deletion service file from template""" service_class = f"{service_name.title().replace('_', '')}TenantDeletionService" model_imports = ", ".join(models) # Build preview section preview_code = [] delete_code = [] for i, model in enumerate(models): model_lower = model.lower().replace('_', ' ') model_plural = f"{model_lower}s" if not model_lower.endswith('s') else model_lower preview_code.append(f""" # Count {model_plural} try: {model.lower()}_count = await self.db.scalar( select(func.count({model}.id)).where({model}.tenant_id == tenant_id) ) preview["{model_plural}"] = {model.lower()}_count or 0 except Exception: preview["{model_plural}"] = 0 # Table might not exist """) delete_code.append(f""" # Delete {model_plural} try: {model.lower()}_delete = await self.db.execute( delete({model}).where({model}.tenant_id == tenant_id) ) result.add_deleted_items("{model_plural}", {model.lower()}_delete.rowcount) logger.info("Deleted {model_plural} for tenant", tenant_id=tenant_id, count={model.lower()}_delete.rowcount) except Exception as e: logger.error("Error deleting {model_plural}", tenant_id=tenant_id, error=str(e)) result.add_error(f"{model} deletion: {{str(e)}}") """) template = f'''""" {service_name.title()} Service - Tenant Data Deletion Handles deletion of all {service_name}-related data for a tenant """ from typing import Dict from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, delete, func import structlog from shared.services.tenant_deletion import BaseTenantDataDeletionService, TenantDataDeletionResult logger = structlog.get_logger() class {service_class}(BaseTenantDataDeletionService): """Service for deleting all {service_name}-related data for a tenant""" def __init__(self, db_session: AsyncSession): super().__init__("{service_name}-service") self.db = db_session async def get_tenant_data_preview(self, tenant_id: str) -> Dict[str, int]: """Get counts of what would be deleted""" try: preview = {{}} # Import models here to avoid circular imports from app.models import {model_imports} {"".join(preview_code)} return preview except Exception as e: logger.error("Error getting deletion preview", tenant_id=tenant_id, error=str(e)) return {{}} async def delete_tenant_data(self, tenant_id: str) -> TenantDataDeletionResult: """Delete all data for a tenant""" result = TenantDataDeletionResult(tenant_id, self.service_name) try: # Import models here to avoid circular imports from app.models import {model_imports} {"".join(delete_code)} # Commit all deletions await self.db.commit() logger.info("Tenant data deletion completed", tenant_id=tenant_id, deleted_counts=result.deleted_counts) except Exception as e: logger.error("Fatal error during tenant data deletion", tenant_id=tenant_id, error=str(e)) await self.db.rollback() result.add_error(f"Fatal error: {{str(e)}}") return result ''' return template def generate_api_endpoints(service_name: str): """Generate API endpoint code""" service_class = f"{service_name.title().replace('_', '')}TenantDeletionService" template = f''' # ===== Tenant Data Deletion Endpoints ===== @router.delete("/tenant/{{tenant_id}}") async def delete_tenant_data( tenant_id: str, current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Delete all {service_name}-related data for a tenant Only accessible by internal services (called during tenant deletion) """ logger.info(f"Tenant data deletion request received for tenant: {{tenant_id}}") # Only allow internal service calls if current_user.get("type") != "service": raise HTTPException( status_code=403, detail="This endpoint is only accessible to internal services" ) try: from app.services.tenant_deletion_service import {service_class} deletion_service = {service_class}(db) result = await deletion_service.safe_delete_tenant_data(tenant_id) return {{ "message": "Tenant data deletion completed in {service_name}-service", "summary": result.to_dict() }} except Exception as e: logger.error(f"Tenant data deletion failed for {{tenant_id}}: {{e}}") raise HTTPException( status_code=500, detail=f"Failed to delete tenant data: {{str(e)}}" ) @router.get("/tenant/{{tenant_id}}/deletion-preview") async def preview_tenant_data_deletion( tenant_id: str, current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Preview what data would be deleted for a tenant (dry-run) Accessible by internal services and tenant admins """ # Allow internal services and admins is_service = current_user.get("type") == "service" is_admin = current_user.get("role") in ["owner", "admin"] if not (is_service or is_admin): raise HTTPException( status_code=403, detail="Insufficient permissions" ) try: from app.services.tenant_deletion_service import {service_class} deletion_service = {service_class}(db) preview = await deletion_service.get_tenant_data_preview(tenant_id) return {{ "tenant_id": tenant_id, "service": "{service_name}-service", "data_counts": preview, "total_items": sum(preview.values()) }} except Exception as e: logger.error(f"Deletion preview failed for {{tenant_id}}: {{e}}") raise HTTPException( status_code=500, detail=f"Failed to get deletion preview: {{str(e)}}" ) ''' return template def main(): if len(sys.argv) < 3: print("Usage: python generate_deletion_service.py ") print("Example: python generate_deletion_service.py pos POSConfiguration,POSTransaction,POSSession") sys.exit(1) service_name = sys.argv[1] models = [m.strip() for m in sys.argv[2].split(',')] # Generate service file service_code = generate_deletion_service(service_name, models) # Generate API endpoints api_code = generate_api_endpoints(service_name) # Output files service_dir = Path(f"services/{service_name}/app/services") print(f"\n{'='*80}") print(f"Generated code for {service_name} service with models: {', '.join(models)}") print(f"{'='*80}\n") print("1. DELETION SERVICE FILE:") print(f" Location: {service_dir}/tenant_deletion_service.py") print("-" * 80) print(service_code) print() print("\n2. API ENDPOINTS TO ADD:") print(f" Add to: services/{service_name}/app/api/.py") print("-" * 80) print(api_code) print() # Optionally write files write = input("\nWrite files to disk? (y/n): ").lower().strip() if write == 'y': # Create service file service_dir.mkdir(parents=True, exist_ok=True) service_file = service_dir / "tenant_deletion_service.py" with open(service_file, 'w') as f: f.write(service_code) print(f"\n✅ Created: {service_file}") print(f"\n⚠️ Next steps:") print(f" 1. Review and customize {service_file}") print(f" 2. Add the API endpoints to services/{service_name}/app/api/.py") print(f" 3. Test with: curl -X GET 'http://localhost:8000/api/v1/{service_name}/tenant/{{id}}/deletion-preview'") else: print("\n✅ Files not written. Copy the code above manually.") if __name__ == "__main__": main()