Spaces:
Sleeping
Sleeping
Update backend with high-quality catalog and fixes
Browse files- app/api/admin.py +195 -1
- app/api/products.py +1 -124
- app/models/product.py +16 -0
- final_audit_report.json +0 -0
- product_quality_20260311_071055.log +0 -0
- product_quality_20260311_071142.log +0 -0
- product_quality_20260311_071259.log +0 -0
- product_quality_20260311_071329.log +0 -0
- real_products_1000.json +0 -0
- real_products_1000_validation.json +0 -0
- seed_orchestrator.py +55 -0
app/api/admin.py
CHANGED
|
@@ -6,8 +6,9 @@ from datetime import datetime, timedelta
|
|
| 6 |
from app.db.base import get_db
|
| 7 |
from app.models.user import User, UserRole
|
| 8 |
from app.models.order import Order, OrderStatus, PaymentDetail
|
| 9 |
-
from app.models.product import Product
|
| 10 |
from app.api.auth import get_current_user
|
|
|
|
| 11 |
|
| 12 |
router = APIRouter(prefix="/admin", tags=["Administration"])
|
| 13 |
|
|
@@ -300,3 +301,196 @@ def get_order_payment_details(
|
|
| 300 |
"statusCode": 200
|
| 301 |
}
|
| 302 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from app.db.base import get_db
|
| 7 |
from app.models.user import User, UserRole
|
| 8 |
from app.models.order import Order, OrderStatus, PaymentDetail
|
| 9 |
+
from app.models.product import Product, ProductImage, ProductAudit
|
| 10 |
from app.api.auth import get_current_user
|
| 11 |
+
import json
|
| 12 |
|
| 13 |
router = APIRouter(prefix="/admin", tags=["Administration"])
|
| 14 |
|
|
|
|
| 301 |
"statusCode": 200
|
| 302 |
}
|
| 303 |
|
| 304 |
+
@router.get("/products/deleted")
|
| 305 |
+
def get_deleted_products(
|
| 306 |
+
current_user_id: int = Depends(get_current_user),
|
| 307 |
+
db: Session = Depends(get_db)
|
| 308 |
+
):
|
| 309 |
+
"""List all soft-deleted products pending administrative review."""
|
| 310 |
+
check_admin(current_user_id, db)
|
| 311 |
+
|
| 312 |
+
deleted_products = db.query(Product).filter(Product.deleted_at.isnot(None)).all()
|
| 313 |
+
|
| 314 |
+
results = [{
|
| 315 |
+
"id": p.id,
|
| 316 |
+
"name_en": p.name_en,
|
| 317 |
+
"name_ar": p.name_ar,
|
| 318 |
+
"price": p.price,
|
| 319 |
+
"deleted_at": p.deleted_at.isoformat() if p.deleted_at else None,
|
| 320 |
+
"deletion_reason": p.deletion_reason,
|
| 321 |
+
"quality_score": p.quality_score,
|
| 322 |
+
"image_url": p.images[0].image_url if p.images else None
|
| 323 |
+
} for p in deleted_products]
|
| 324 |
+
|
| 325 |
+
return {
|
| 326 |
+
"isSuccess": True,
|
| 327 |
+
"value": {"products": results},
|
| 328 |
+
"statusCode": 200
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
@router.post("/products/{product_id}/restore")
|
| 332 |
+
def restore_product(
|
| 333 |
+
product_id: int,
|
| 334 |
+
current_user_id: int = Depends(get_current_user),
|
| 335 |
+
db: Session = Depends(get_db)
|
| 336 |
+
):
|
| 337 |
+
"""Restore a soft-deleted product and log the action."""
|
| 338 |
+
admin = check_admin(current_user_id, db)
|
| 339 |
+
product = db.query(Product).filter(Product.id == product_id).first()
|
| 340 |
+
|
| 341 |
+
if not product:
|
| 342 |
+
raise HTTPException(status_code=404, detail="Product not found")
|
| 343 |
+
|
| 344 |
+
if not product.deleted_at:
|
| 345 |
+
return {"isSuccess": False, "message": "Product is not deleted", "statusCode": 400}
|
| 346 |
+
|
| 347 |
+
# Log the restore action
|
| 348 |
+
audit = ProductAudit(
|
| 349 |
+
product_id=product.id,
|
| 350 |
+
action="RESTORE",
|
| 351 |
+
reason="Manual restoration by admin",
|
| 352 |
+
performed_by=admin.id,
|
| 353 |
+
created_at=datetime.utcnow()
|
| 354 |
+
)
|
| 355 |
+
db.add(audit)
|
| 356 |
+
|
| 357 |
+
product.deleted_at = None
|
| 358 |
+
product.deletion_reason = None
|
| 359 |
+
product.deleted_by = None
|
| 360 |
+
product.is_active = True
|
| 361 |
+
|
| 362 |
+
db.commit()
|
| 363 |
+
|
| 364 |
+
return {
|
| 365 |
+
"isSuccess": True,
|
| 366 |
+
"value": {"message": f"Product {product.id} restored successfully"},
|
| 367 |
+
"statusCode": 200
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
@router.delete("/products/{product_id}/final")
|
| 371 |
+
def permanently_delete_product(
|
| 372 |
+
product_id: int,
|
| 373 |
+
current_user_id: int = Depends(get_current_user),
|
| 374 |
+
db: Session = Depends(get_db)
|
| 375 |
+
):
|
| 376 |
+
"""Permanently delete a product from the database after review."""
|
| 377 |
+
check_admin(current_user_id, db)
|
| 378 |
+
product = db.query(Product).filter(Product.id == product_id).first()
|
| 379 |
+
|
| 380 |
+
if not product:
|
| 381 |
+
raise HTTPException(status_code=404, detail="Product not found")
|
| 382 |
+
|
| 383 |
+
db.delete(product)
|
| 384 |
+
db.commit()
|
| 385 |
+
|
| 386 |
+
return {
|
| 387 |
+
"isSuccess": True,
|
| 388 |
+
"value": {"message": f"Product {product_id} permanently deleted"},
|
| 389 |
+
"statusCode": 200
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
@router.get("/products/{product_id}/audit")
|
| 393 |
+
def get_product_audit_logs(
|
| 394 |
+
product_id: int,
|
| 395 |
+
current_user_id: int = Depends(get_current_user),
|
| 396 |
+
db: Session = Depends(get_db)
|
| 397 |
+
):
|
| 398 |
+
"""Get all audit logs for a specific product."""
|
| 399 |
+
check_admin(current_user_id, db)
|
| 400 |
+
|
| 401 |
+
logs = db.query(ProductAudit).filter(ProductAudit.product_id == product_id).order_by(ProductAudit.created_at.desc()).all()
|
| 402 |
+
|
| 403 |
+
results = [{
|
| 404 |
+
"id": l.id,
|
| 405 |
+
"action": l.action,
|
| 406 |
+
"reason": l.reason,
|
| 407 |
+
"created_at": l.created_at.isoformat(),
|
| 408 |
+
"snapshot": l.snapshot
|
| 409 |
+
} for l in logs]
|
| 410 |
+
|
| 411 |
+
return {
|
| 412 |
+
"isSuccess": True,
|
| 413 |
+
"value": {"logs": results},
|
| 414 |
+
"statusCode": 200
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
@router.post("/products/bulk-restore")
|
| 418 |
+
def bulk_restore_products(
|
| 419 |
+
product_ids: list[int],
|
| 420 |
+
current_user_id: int = Depends(get_current_user),
|
| 421 |
+
db: Session = Depends(get_db)
|
| 422 |
+
):
|
| 423 |
+
"""Restore multiple soft-deleted products."""
|
| 424 |
+
admin = check_admin(current_user_id, db)
|
| 425 |
+
|
| 426 |
+
products = db.query(Product).filter(Product.id.in_(product_ids)).all()
|
| 427 |
+
products_to_restore = [p for p in products if p.deleted_at]
|
| 428 |
+
|
| 429 |
+
for product in products_to_restore:
|
| 430 |
+
product.deleted_at = None
|
| 431 |
+
product.deletion_reason = None
|
| 432 |
+
product.deleted_by = None
|
| 433 |
+
product.is_active = True
|
| 434 |
+
|
| 435 |
+
audit = ProductAudit(
|
| 436 |
+
product_id=product.id,
|
| 437 |
+
action="RESTORE",
|
| 438 |
+
reason="Bulk restoration by admin",
|
| 439 |
+
performed_by=admin.id,
|
| 440 |
+
created_at=datetime.utcnow()
|
| 441 |
+
)
|
| 442 |
+
db.add(audit)
|
| 443 |
+
|
| 444 |
+
db.commit()
|
| 445 |
+
return {
|
| 446 |
+
"isSuccess": True,
|
| 447 |
+
"value": {"message": f"Successfully restored {len(products_to_restore)} products"},
|
| 448 |
+
"statusCode": 200
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
@router.post("/products/bulk-hard-delete")
|
| 452 |
+
def bulk_hard_delete_products(
|
| 453 |
+
product_ids: list[int],
|
| 454 |
+
current_user_id: int = Depends(get_current_user),
|
| 455 |
+
db: Session = Depends(get_db)
|
| 456 |
+
):
|
| 457 |
+
"""Permanently delete multiple products."""
|
| 458 |
+
check_admin(current_user_id, db)
|
| 459 |
+
|
| 460 |
+
# We can delete them all in one go or loop if we want to log (hard delete has no audit after delete)
|
| 461 |
+
db.query(Product).filter(Product.id.in_(product_ids)).delete(synchronize_session=False)
|
| 462 |
+
db.commit()
|
| 463 |
+
|
| 464 |
+
return {
|
| 465 |
+
"isSuccess": True,
|
| 466 |
+
"value": {"message": f"Successfully deleted {len(product_ids)} products permanently"},
|
| 467 |
+
"statusCode": 200
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
@router.get("/quality/summary")
|
| 471 |
+
def get_quality_summary(
|
| 472 |
+
current_user_id: int = Depends(get_current_user),
|
| 473 |
+
db: Session = Depends(get_db)
|
| 474 |
+
):
|
| 475 |
+
"""Get a summary of product quality issues."""
|
| 476 |
+
check_admin(current_user_id, db)
|
| 477 |
+
|
| 478 |
+
total_products = db.query(func.count(Product.id)).scalar()
|
| 479 |
+
deleted_products = db.query(func.count(Product.id)).filter(Product.deleted_at.isnot(None)).scalar()
|
| 480 |
+
active_products = db.query(func.count(Product.id)).filter(Product.deleted_at.is_ (None)).scalar()
|
| 481 |
+
|
| 482 |
+
# Average quality score of active products
|
| 483 |
+
avg_score_val = db.query(func.avg(Product.quality_score)).filter(Product.deleted_at.is_(None)).scalar()
|
| 484 |
+
avg_score: float = float(avg_score_val) if avg_score_val is not None else 0.0
|
| 485 |
+
|
| 486 |
+
return {
|
| 487 |
+
"isSuccess": True,
|
| 488 |
+
"value": {
|
| 489 |
+
"total_products": total_products,
|
| 490 |
+
"deleted_count": deleted_products,
|
| 491 |
+
"active_count": active_products,
|
| 492 |
+
"average_quality_score": float(f"{avg_score:.2f}")
|
| 493 |
+
},
|
| 494 |
+
"statusCode": 200
|
| 495 |
+
}
|
| 496 |
+
|
app/api/products.py
CHANGED
|
@@ -329,127 +329,4 @@ async def list_deleted_products(
|
|
| 329 |
)
|
| 330 |
|
| 331 |
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
@router.post("/admin/bulk-delete", response_model=AuthResponse)
|
| 336 |
-
async def bulk_soft_delete_products(
|
| 337 |
-
product_ids: List[int],
|
| 338 |
-
reason: str = "Bulk deletion by admin",
|
| 339 |
-
db: Session = Depends(get_db),
|
| 340 |
-
user_id: int = Depends(get_current_user),
|
| 341 |
-
):
|
| 342 |
-
"""Soft delete multiple products at once."""
|
| 343 |
-
deleted_count = 0
|
| 344 |
-
|
| 345 |
-
for product_id in product_ids:
|
| 346 |
-
product = db.query(Product).filter(Product.id == product_id).first()
|
| 347 |
-
if product and product.deleted_at is None:
|
| 348 |
-
product.deleted_at = datetime.now(timezone.utc)
|
| 349 |
-
product.deleted_by = user_id
|
| 350 |
-
product.deletion_reason = reason
|
| 351 |
-
deleted_count += 1
|
| 352 |
-
|
| 353 |
-
db.commit()
|
| 354 |
-
|
| 355 |
-
return AuthResponse(
|
| 356 |
-
isSuccess=True,
|
| 357 |
-
value={"deleted_count": deleted_count, "requested_count": len(product_ids)},
|
| 358 |
-
statusCode=200,
|
| 359 |
-
)
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
@router.post("/admin/bulk-restore", response_model=AuthResponse)
|
| 363 |
-
async def bulk_restore_products(
|
| 364 |
-
product_ids: List[int],
|
| 365 |
-
db: Session = Depends(get_db),
|
| 366 |
-
user_id: int = Depends(get_current_user),
|
| 367 |
-
):
|
| 368 |
-
"""Restore multiple soft-deleted products."""
|
| 369 |
-
restored_count = 0
|
| 370 |
-
|
| 371 |
-
for product_id in product_ids:
|
| 372 |
-
product = db.query(Product).filter(Product.id == product_id).first()
|
| 373 |
-
if product and product.deleted_at is not None:
|
| 374 |
-
product.deleted_at = None
|
| 375 |
-
product.deleted_by = None
|
| 376 |
-
product.deletion_reason = None
|
| 377 |
-
restored_count += 1
|
| 378 |
-
|
| 379 |
-
db.commit()
|
| 380 |
-
|
| 381 |
-
return AuthResponse(
|
| 382 |
-
isSuccess=True,
|
| 383 |
-
value={"restored_count": restored_count, "requested_count": len(product_ids)},
|
| 384 |
-
statusCode=200,
|
| 385 |
-
)
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
@router.post("/admin/bulk-hard-delete", response_model=AuthResponse)
|
| 389 |
-
async def bulk_hard_delete_products(
|
| 390 |
-
product_ids: List[int],
|
| 391 |
-
db: Session = Depends(get_db),
|
| 392 |
-
user_id: int = Depends(get_current_user),
|
| 393 |
-
):
|
| 394 |
-
"""Permanently delete multiple products (hard delete)."""
|
| 395 |
-
deleted_count = 0
|
| 396 |
-
|
| 397 |
-
for product_id in product_ids:
|
| 398 |
-
product = db.query(Product).filter(Product.id == product_id).first()
|
| 399 |
-
if product:
|
| 400 |
-
db.delete(product)
|
| 401 |
-
deleted_count += 1
|
| 402 |
-
|
| 403 |
-
db.commit()
|
| 404 |
-
|
| 405 |
-
return AuthResponse(
|
| 406 |
-
isSuccess=True,
|
| 407 |
-
value={"deleted_count": deleted_count, "requested_count": len(product_ids)},
|
| 408 |
-
statusCode=200,
|
| 409 |
-
)
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
@router.get("/admin/quality/summary", response_model=AuthResponse)
|
| 413 |
-
async def get_quality_summary(
|
| 414 |
-
db: Session = Depends(get_db),
|
| 415 |
-
user_id: int = Depends(get_current_user),
|
| 416 |
-
):
|
| 417 |
-
"""Get quality statistics summary."""
|
| 418 |
-
from sqlalchemy import func
|
| 419 |
-
|
| 420 |
-
try:
|
| 421 |
-
total_products = db.query(Product).filter(Product.deleted_at == None).count()
|
| 422 |
-
total_deleted = db.query(Product).filter(Product.deleted_at != None).count()
|
| 423 |
-
|
| 424 |
-
avg_quality = db.query(func.avg(Product.quality_score)).filter(
|
| 425 |
-
Product.deleted_at == None
|
| 426 |
-
).scalar() or 0
|
| 427 |
-
|
| 428 |
-
low_quality = db.query(Product).filter(
|
| 429 |
-
Product.deleted_at == None,
|
| 430 |
-
Product.quality_score < 70
|
| 431 |
-
).count()
|
| 432 |
-
|
| 433 |
-
no_images = 0
|
| 434 |
-
products = db.query(Product).filter(Product.deleted_at == None).all()
|
| 435 |
-
for p in products:
|
| 436 |
-
if not p.images or len(p.images) == 0:
|
| 437 |
-
no_images += 1
|
| 438 |
-
|
| 439 |
-
return AuthResponse(
|
| 440 |
-
isSuccess=True,
|
| 441 |
-
value={
|
| 442 |
-
"total_active": total_products,
|
| 443 |
-
"total_deleted": total_deleted,
|
| 444 |
-
"avg_quality_score": round(float(avg_quality), 1),
|
| 445 |
-
"low_quality_products": low_quality,
|
| 446 |
-
"products_without_images": no_images,
|
| 447 |
-
},
|
| 448 |
-
statusCode=200,
|
| 449 |
-
)
|
| 450 |
-
except Exception as e:
|
| 451 |
-
return AuthResponse(
|
| 452 |
-
isSuccess=False,
|
| 453 |
-
value={"error": str(e)},
|
| 454 |
-
statusCode=500,
|
| 455 |
-
)
|
|
|
|
| 329 |
)
|
| 330 |
|
| 331 |
|
| 332 |
+
# End of products.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/models/product.py
CHANGED
|
@@ -39,6 +39,22 @@ class Product(Base):
|
|
| 39 |
|
| 40 |
category = relationship("Category", back_populates="products")
|
| 41 |
images = relationship("ProductImage", back_populates="product", cascade="all, delete-orphan")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
class ProductImage(Base):
|
|
|
|
| 39 |
|
| 40 |
category = relationship("Category", back_populates="products")
|
| 41 |
images = relationship("ProductImage", back_populates="product", cascade="all, delete-orphan")
|
| 42 |
+
audit_logs = relationship("ProductAudit", back_populates="product", cascade="all, delete-orphan")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class ProductAudit(Base):
|
| 46 |
+
__tablename__ = "product_audit_logs"
|
| 47 |
+
__table_args__ = {'extend_existing': True}
|
| 48 |
+
|
| 49 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 50 |
+
product_id = Column(Integer, ForeignKey("products.id", ondelete="CASCADE"), nullable=False)
|
| 51 |
+
action = Column(String(50), nullable=False) # 'SOFT_DELETE', 'RESTORE', 'HARD_DELETE_INIT', etc.
|
| 52 |
+
reason = Column(String(500), nullable=True)
|
| 53 |
+
performed_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
| 54 |
+
snapshot = Column(JSON, nullable=True) # Full data backup before action
|
| 55 |
+
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
|
| 56 |
+
|
| 57 |
+
product = relationship("Product", back_populates="audit_logs")
|
| 58 |
|
| 59 |
|
| 60 |
class ProductImage(Base):
|
final_audit_report.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
product_quality_20260311_071055.log
ADDED
|
File without changes
|
product_quality_20260311_071142.log
ADDED
|
File without changes
|
product_quality_20260311_071259.log
ADDED
|
File without changes
|
product_quality_20260311_071329.log
ADDED
|
File without changes
|
real_products_1000.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
real_products_1000_validation.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
seed_orchestrator.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import asyncio
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
# Set up paths - we assume this script runs from the backend directory
|
| 8 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 9 |
+
|
| 10 |
+
# Set logging
|
| 11 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
async def main():
|
| 15 |
+
try:
|
| 16 |
+
from app.db.base import SessionLocal, engine, Base
|
| 17 |
+
from app.api.seed import seed_database
|
| 18 |
+
# Models are imported within seed_database and checker
|
| 19 |
+
except ImportError as e:
|
| 20 |
+
logger.error(f"Import failed: {e}")
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
# Seed database logic will handle clearing tables if clear=True is passed
|
| 24 |
+
# Ensure tables exist
|
| 25 |
+
logger.info("Initializing database tables...")
|
| 26 |
+
Base.metadata.create_all(bind=engine)
|
| 27 |
+
|
| 28 |
+
db = SessionLocal()
|
| 29 |
+
try:
|
| 30 |
+
logger.info("Step 1: Seeding database with 1000 models...")
|
| 31 |
+
result = await seed_database(clear=True, db=db)
|
| 32 |
+
logger.info(f"Seed Result: {result.value.get('message')} - {result.value.get('inserted_new')} inserted")
|
| 33 |
+
|
| 34 |
+
logger.info("Step 2: Running dynamic quality audit on DB...")
|
| 35 |
+
# Import checker with correct path
|
| 36 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "scripts"))
|
| 37 |
+
from product_quality_checker import ProductQualityChecker
|
| 38 |
+
|
| 39 |
+
checker = ProductQualityChecker(db)
|
| 40 |
+
report = checker.run_quality_check()
|
| 41 |
+
|
| 42 |
+
logger.info(f"Audit Complete: {report.total_valid} valid, {report.total_invalid} soft-deleted")
|
| 43 |
+
|
| 44 |
+
# Save final report
|
| 45 |
+
with open("final_audit_report.json", "w", encoding="utf-8") as f:
|
| 46 |
+
import json
|
| 47 |
+
json.dump(report.to_dict(), f, indent=2, ensure_ascii=False)
|
| 48 |
+
|
| 49 |
+
except Exception as e:
|
| 50 |
+
logger.exception(f"Orchestration failed: {e}")
|
| 51 |
+
finally:
|
| 52 |
+
db.close()
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
asyncio.run(main())
|