Files
bakery-ia/scripts/generate_deletion_service.py

271 lines
8.6 KiB
Python
Raw Normal View History

2025-10-31 11:54:19 +01:00
#!/usr/bin/env python3
"""
Quick script to generate deletion service boilerplate
Usage: python generate_deletion_service.py <service_name> <model1,model2,model3>
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 <service_name> <model1,model2,model3>")
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/<router>.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/<router>.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()