code quality run

This commit is contained in:
2025-09-13 21:58:54 +02:00
parent 0dfd885847
commit 3eb18ef91e
63 changed files with 1802 additions and 1289 deletions

View File

@@ -1,10 +1,11 @@
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session
from app.core.database import get_db
from models.database_models import User, Shop
from middleware.auth import AuthManager
from middleware.rate_limiter import RateLimiter
from models.database_models import Shop, User
# Set auto_error=False to prevent automatic 403 responses
security = HTTPBearer(auto_error=False)
@@ -13,8 +14,8 @@ rate_limiter = RateLimiter()
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db),
):
"""Get current authenticated user"""
# Check if credentials are provided
@@ -30,9 +31,9 @@ def get_current_admin_user(current_user: User = Depends(get_current_user)):
def get_user_shop(
shop_code: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
shop_code: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Get shop and verify user ownership"""
shop = db.query(Shop).filter(Shop.shop_code == shop_code.upper()).first()

View File

@@ -1,5 +1,6 @@
from fastapi import APIRouter
from app.api.v1 import auth, product, stock, shop, marketplace, admin, stats
from app.api.v1 import admin, auth, marketplace, product, shop, stats, stock
api_router = APIRouter()
@@ -9,6 +10,5 @@ api_router.include_router(auth.router, tags=["authentication"])
api_router.include_router(marketplace.router, tags=["marketplace"])
api_router.include_router(product.router, tags=["product"])
api_router.include_router(shop.router, tags=["shop"])
api_router.include_router(stats.router, tags=["statistics"])
api_router.include_router(stats.router, tags=["statistics"])
api_router.include_router(stock.router, tags=["stock"])

View File

@@ -1,13 +1,15 @@
import logging
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.api.deps import get_current_admin_user
from app.core.database import get_db
from app.services.admin_service import admin_service
from models.api_models import MarketplaceImportJobResponse, UserResponse, ShopListResponse
from models.api_models import (MarketplaceImportJobResponse, ShopListResponse,
UserResponse)
from models.database_models import User
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -16,10 +18,10 @@ logger = logging.getLogger(__name__)
# Admin-only routes
@router.get("/admin/users", response_model=List[UserResponse])
def get_all_users(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Get all users (Admin only)"""
try:
@@ -32,9 +34,9 @@ def get_all_users(
@router.put("/admin/users/{user_id}/status")
def toggle_user_status(
user_id: int,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
user_id: int,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Toggle user active status (Admin only)"""
try:
@@ -49,21 +51,16 @@ def toggle_user_status(
@router.get("/admin/shops", response_model=ShopListResponse)
def get_all_shops_admin(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Get all shops with admin view (Admin only)"""
try:
shops, total = admin_service.get_all_shops(db=db, skip=skip, limit=limit)
return ShopListResponse(
shops=shops,
total=total,
skip=skip,
limit=limit
)
return ShopListResponse(shops=shops, total=total, skip=skip, limit=limit)
except Exception as e:
logger.error(f"Error getting shops: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
@@ -71,9 +68,9 @@ def get_all_shops_admin(
@router.put("/admin/shops/{shop_id}/verify")
def verify_shop(
shop_id: int,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
shop_id: int,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Verify/unverify shop (Admin only)"""
try:
@@ -88,9 +85,9 @@ def verify_shop(
@router.put("/admin/shops/{shop_id}/status")
def toggle_shop_status(
shop_id: int,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
shop_id: int,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Toggle shop active status (Admin only)"""
try:
@@ -103,15 +100,17 @@ def toggle_shop_status(
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/admin/marketplace-import-jobs", response_model=List[MarketplaceImportJobResponse])
@router.get(
"/admin/marketplace-import-jobs", response_model=List[MarketplaceImportJobResponse]
)
def get_all_marketplace_import_jobs(
marketplace: Optional[str] = Query(None),
shop_name: Optional[str] = Query(None),
status: Optional[str] = Query(None),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
marketplace: Optional[str] = Query(None),
shop_name: Optional[str] = Query(None),
status: Optional[str] = Query(None),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Get all marketplace import jobs (Admin only)"""
try:
@@ -121,7 +120,7 @@ def get_all_marketplace_import_jobs(
shop_name=shop_name,
status=status,
skip=skip,
limit=limit
limit=limit,
)
except Exception as e:
logger.error(f"Error getting marketplace import jobs: {str(e)}")

View File

@@ -1,11 +1,14 @@
import logging
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.auth_service import auth_service
from models.api_models import UserRegister, UserLogin, UserResponse, LoginResponse
from models.api_models import (LoginResponse, UserLogin, UserRegister,
UserResponse)
from models.database_models import User
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -35,7 +38,7 @@ def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)):
access_token=login_result["token_data"]["access_token"],
token_type=login_result["token_data"]["token_type"],
expires_in=login_result["token_data"]["expires_in"],
user=UserResponse.model_validate(login_result["user"])
user=UserResponse.model_validate(login_result["user"]),
)
except HTTPException:
raise

View File

@@ -1,15 +1,17 @@
import logging
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.marketplace_service import marketplace_service
from app.tasks.background_tasks import process_marketplace_import
from middleware.decorators import rate_limit
from models.api_models import MarketplaceImportJobResponse, MarketplaceImportRequest
from models.api_models import (MarketplaceImportJobResponse,
MarketplaceImportRequest)
from models.database_models import User
from app.services.marketplace_service import marketplace_service
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -19,15 +21,16 @@ logger = logging.getLogger(__name__)
@router.post("/marketplace/import-product", response_model=MarketplaceImportJobResponse)
@rate_limit(max_requests=10, window_seconds=3600) # Limit marketplace imports
async def import_products_from_marketplace(
request: MarketplaceImportRequest,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
request: MarketplaceImportRequest,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Import products from marketplace CSV with background processing (Protected)"""
try:
logger.info(
f"Starting marketplace import: {request.marketplace} -> {request.shop_code} by user {current_user.username}")
f"Starting marketplace import: {request.marketplace} -> {request.shop_code} by user {current_user.username}"
)
# Create import job through service
import_job = marketplace_service.create_import_job(db, request, current_user)
@@ -39,7 +42,7 @@ async def import_products_from_marketplace(
request.url,
request.marketplace,
request.shop_code,
request.batch_size or 1000
request.batch_size or 1000,
)
return MarketplaceImportJobResponse(
@@ -50,7 +53,7 @@ async def import_products_from_marketplace(
shop_id=import_job.shop_id,
shop_name=import_job.shop_name,
message=f"Marketplace import started from {request.marketplace}. Check status with "
f"/import-status/{import_job.id}"
f"/import-status/{import_job.id}",
)
except ValueError as e:
@@ -62,11 +65,13 @@ async def import_products_from_marketplace(
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/marketplace/import-status/{job_id}", response_model=MarketplaceImportJobResponse)
@router.get(
"/marketplace/import-status/{job_id}", response_model=MarketplaceImportJobResponse
)
def get_marketplace_import_status(
job_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
job_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get status of marketplace import job (Protected)"""
try:
@@ -82,14 +87,16 @@ def get_marketplace_import_status(
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/marketplace/import-jobs", response_model=List[MarketplaceImportJobResponse])
@router.get(
"/marketplace/import-jobs", response_model=List[MarketplaceImportJobResponse]
)
def get_marketplace_import_jobs(
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get marketplace import jobs with filtering (Protected)"""
try:
@@ -99,7 +106,7 @@ def get_marketplace_import_jobs(
marketplace=marketplace,
shop_name=shop_name,
skip=skip,
limit=limit
limit=limit,
)
return [marketplace_service.convert_to_response_model(job) for job in jobs]
@@ -111,8 +118,7 @@ def get_marketplace_import_jobs(
@router.get("/marketplace/marketplace-import-stats")
def get_marketplace_import_stats(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
):
"""Get statistics about marketplace import jobs (Protected)"""
try:
@@ -124,11 +130,14 @@ def get_marketplace_import_stats(
raise HTTPException(status_code=500, detail="Internal server error")
@router.put("/marketplace/import-jobs/{job_id}/cancel", response_model=MarketplaceImportJobResponse)
@router.put(
"/marketplace/import-jobs/{job_id}/cancel",
response_model=MarketplaceImportJobResponse,
)
def cancel_marketplace_import_job(
job_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
job_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Cancel a pending or running marketplace import job (Protected)"""
try:
@@ -146,9 +155,9 @@ def cancel_marketplace_import_job(
@router.delete("/marketplace/import-jobs/{job_id}")
def delete_marketplace_import_job(
job_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
job_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Delete a completed marketplace import job (Protected)"""
try:

View File

@@ -1,17 +1,17 @@
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.api.deps import get_current_user
from models.api_models import (ProductListResponse, ProductResponse, ProductCreate, ProductDetailResponse,
from app.core.database import get_db
from app.services.product_service import product_service
from models.api_models import (ProductCreate, ProductDetailResponse,
ProductListResponse, ProductResponse,
ProductUpdate)
from models.database_models import User
import logging
from app.services.product_service import product_service
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -20,16 +20,16 @@ logger = logging.getLogger(__name__)
# Enhanced Product Routes with Marketplace Support
@router.get("/product", response_model=ProductListResponse)
def get_products(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
brand: Optional[str] = Query(None),
category: Optional[str] = Query(None),
availability: Optional[str] = Query(None),
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
search: Optional[str] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
brand: Optional[str] = Query(None),
category: Optional[str] = Query(None),
availability: Optional[str] = Query(None),
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
search: Optional[str] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get products with advanced filtering including marketplace and shop (Protected)"""
@@ -43,14 +43,11 @@ def get_products(
availability=availability,
marketplace=marketplace,
shop_name=shop_name,
search=search
search=search,
)
return ProductListResponse(
products=products,
total=total,
skip=skip,
limit=limit
products=products, total=total, skip=skip, limit=limit
)
except Exception as e:
logger.error(f"Error getting products: {str(e)}")
@@ -59,9 +56,9 @@ def get_products(
@router.post("/product", response_model=ProductResponse)
def create_product(
product: ProductCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
product: ProductCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Create a new product with validation and marketplace support (Protected)"""
@@ -75,7 +72,9 @@ def create_product(
if existing:
logger.info("Product already exists, raising 400 error")
raise HTTPException(status_code=400, detail="Product with this ID already exists")
raise HTTPException(
status_code=400, detail="Product with this ID already exists"
)
logger.info("No existing product found, proceeding to create...")
db_product = product_service.create_product(db, product)
@@ -93,11 +92,12 @@ def create_product(
logger.error(f"Unexpected error: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/product/{product_id}", response_model=ProductDetailResponse)
def get_product(
product_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
product_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get product with stock information (Protected)"""
@@ -111,10 +111,7 @@ def get_product(
if product.gtin:
stock_info = product_service.get_stock_info(db, product.gtin)
return ProductDetailResponse(
product=product,
stock_info=stock_info
)
return ProductDetailResponse(product=product, stock_info=stock_info)
except HTTPException:
raise
@@ -125,10 +122,10 @@ def get_product(
@router.put("/product/{product_id}", response_model=ProductResponse)
def update_product(
product_id: str,
product_update: ProductUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
product_id: str,
product_update: ProductUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Update product with validation and marketplace support (Protected)"""
@@ -151,9 +148,9 @@ def update_product(
@router.delete("/product/{product_id}")
def delete_product(
product_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
product_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Delete product and associated stock (Protected)"""
@@ -176,19 +173,18 @@ def delete_product(
# Export with streaming for large datasets (Protected)
@router.get("/export-csv")
async def export_csv(
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Export products as CSV with streaming and marketplace filtering (Protected)"""
try:
def generate_csv():
return product_service.generate_csv_export(
db=db,
marketplace=marketplace,
shop_name=shop_name
db=db, marketplace=marketplace, shop_name=shop_name
)
filename = "products_export"
@@ -201,7 +197,7 @@ async def export_csv(
return StreamingResponse(
generate_csv(),
media_type="text/csv",
headers={"Content-Disposition": f"attachment; filename={filename}"}
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
except Exception as e:

View File

@@ -1,17 +1,21 @@
import logging
from datetime import datetime
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.api.deps import get_current_user, get_user_shop
from app.core.database import get_db
from app.services.shop_service import shop_service
from app.tasks.background_tasks import process_marketplace_import
from middleware.decorators import rate_limit
from models.api_models import MarketplaceImportJobResponse, MarketplaceImportRequest, ShopResponse, ShopCreate, \
ShopListResponse, ShopProductResponse, ShopProductCreate
from models.database_models import User, MarketplaceImportJob, Shop, Product, ShopProduct
from datetime import datetime
import logging
from models.api_models import (MarketplaceImportJobResponse,
MarketplaceImportRequest, ShopCreate,
ShopListResponse, ShopProductCreate,
ShopProductResponse, ShopResponse)
from models.database_models import (MarketplaceImportJob, Product, Shop,
ShopProduct, User)
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -20,13 +24,15 @@ logger = logging.getLogger(__name__)
# Shop Management Routes
@router.post("/shop", response_model=ShopResponse)
def create_shop(
shop_data: ShopCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
shop_data: ShopCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Create a new shop (Protected)"""
try:
shop = shop_service.create_shop(db=db, shop_data=shop_data, current_user=current_user)
shop = shop_service.create_shop(
db=db, shop_data=shop_data, current_user=current_user
)
return ShopResponse.model_validate(shop)
except HTTPException:
raise
@@ -37,12 +43,12 @@ def create_shop(
@router.get("/shop", response_model=ShopListResponse)
def get_shops(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
active_only: bool = Query(True),
verified_only: bool = Query(False),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
active_only: bool = Query(True),
verified_only: bool = Query(False),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get shops with filtering (Protected)"""
try:
@@ -52,15 +58,10 @@ def get_shops(
skip=skip,
limit=limit,
active_only=active_only,
verified_only=verified_only
verified_only=verified_only,
)
return ShopListResponse(
shops=shops,
total=total,
skip=skip,
limit=limit
)
return ShopListResponse(shops=shops, total=total, skip=skip, limit=limit)
except HTTPException:
raise
except Exception as e:
@@ -69,10 +70,16 @@ def get_shops(
@router.get("/shop/{shop_code}", response_model=ShopResponse)
def get_shop(shop_code: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
def get_shop(
shop_code: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get shop details (Protected)"""
try:
shop = shop_service.get_shop_by_code(db=db, shop_code=shop_code, current_user=current_user)
shop = shop_service.get_shop_by_code(
db=db, shop_code=shop_code, current_user=current_user
)
return ShopResponse.model_validate(shop)
except HTTPException:
raise
@@ -84,10 +91,10 @@ def get_shop(shop_code: str, db: Session = Depends(get_db), current_user: User =
# Shop Product Management
@router.post("/shop/{shop_code}/products", response_model=ShopProductResponse)
def add_product_to_shop(
shop_code: str,
shop_product: ShopProductCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
shop_code: str,
shop_product: ShopProductCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Add existing product to shop catalog with shop-specific settings (Protected)"""
try:
@@ -96,9 +103,7 @@ def add_product_to_shop(
# Add product to shop
new_shop_product = shop_service.add_product_to_shop(
db=db,
shop=shop,
shop_product=shop_product
db=db, shop=shop, shop_product=shop_product
)
# Return with product details
@@ -114,18 +119,20 @@ def add_product_to_shop(
@router.get("/shop/{shop_code}/products")
def get_shop_products(
shop_code: str,
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
active_only: bool = Query(True),
featured_only: bool = Query(False),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
shop_code: str,
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
active_only: bool = Query(True),
featured_only: bool = Query(False),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get products in shop catalog (Protected)"""
try:
# Get shop
shop = shop_service.get_shop_by_code(db=db, shop_code=shop_code, current_user=current_user)
shop = shop_service.get_shop_by_code(
db=db, shop_code=shop_code, current_user=current_user
)
# Get shop products
shop_products, total = shop_service.get_shop_products(
@@ -135,7 +142,7 @@ def get_shop_products(
skip=skip,
limit=limit,
active_only=active_only,
featured_only=featured_only
featured_only=featured_only,
)
# Format response
@@ -150,7 +157,7 @@ def get_shop_products(
"total": total,
"skip": skip,
"limit": limit,
"shop": ShopResponse.model_validate(shop)
"shop": ShopResponse.model_validate(shop),
}
except HTTPException:
raise

View File

@@ -1,18 +1,21 @@
import logging
from datetime import datetime
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.stats_service import stats_service
from app.tasks.background_tasks import process_marketplace_import
from middleware.decorators import rate_limit
from models.api_models import MarketplaceImportJobResponse, MarketplaceImportRequest, StatsResponse, \
MarketplaceStatsResponse
from models.database_models import User, MarketplaceImportJob, Shop, Product, Stock
from datetime import datetime
import logging
from models.api_models import (MarketplaceImportJobResponse,
MarketplaceImportRequest,
MarketplaceStatsResponse, StatsResponse)
from models.database_models import (MarketplaceImportJob, Product, Shop, Stock,
User)
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -20,7 +23,9 @@ logger = logging.getLogger(__name__)
# Enhanced Statistics with Marketplace Support
@router.get("/stats", response_model=StatsResponse)
def get_stats(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
def get_stats(
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
):
"""Get comprehensive statistics with marketplace data (Protected)"""
try:
stats_data = stats_service.get_comprehensive_stats(db=db)
@@ -32,7 +37,7 @@ def get_stats(db: Session = Depends(get_db), current_user: User = Depends(get_cu
unique_marketplaces=stats_data["unique_marketplaces"],
unique_shops=stats_data["unique_shops"],
total_stock_entries=stats_data["total_stock_entries"],
total_inventory_quantity=stats_data["total_inventory_quantity"]
total_inventory_quantity=stats_data["total_inventory_quantity"],
)
except Exception as e:
logger.error(f"Error getting comprehensive stats: {str(e)}")
@@ -40,7 +45,9 @@ def get_stats(db: Session = Depends(get_db), current_user: User = Depends(get_cu
@router.get("/stats/marketplace", response_model=List[MarketplaceStatsResponse])
def get_marketplace_stats(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
def get_marketplace_stats(
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
):
"""Get statistics broken down by marketplace (Protected)"""
try:
marketplace_stats = stats_service.get_marketplace_breakdown_stats(db=db)
@@ -50,8 +57,9 @@ def get_marketplace_stats(db: Session = Depends(get_db), current_user: User = De
marketplace=stat["marketplace"],
total_products=stat["total_products"],
unique_shops=stat["unique_shops"],
unique_brands=stat["unique_brands"]
) for stat in marketplace_stats
unique_brands=stat["unique_brands"],
)
for stat in marketplace_stats
]
except Exception as e:
logger.error(f"Error getting marketplace stats: {str(e)}")

View File

@@ -1,16 +1,19 @@
import logging
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.stock_service import stock_service
from app.tasks.background_tasks import process_marketplace_import
from middleware.decorators import rate_limit
from models.api_models import (MarketplaceImportJobResponse, MarketplaceImportRequest, StockResponse,
StockSummaryResponse, StockCreate, StockAdd, StockUpdate)
from models.database_models import User, MarketplaceImportJob, Shop
from app.services.stock_service import stock_service
import logging
from models.api_models import (MarketplaceImportJobResponse,
MarketplaceImportRequest, StockAdd, StockCreate,
StockResponse, StockSummaryResponse,
StockUpdate)
from models.database_models import MarketplaceImportJob, Shop, User
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -18,11 +21,12 @@ logger = logging.getLogger(__name__)
# Stock Management Routes (Protected)
@router.post("/stock", response_model=StockResponse)
def set_stock(
stock: StockCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
stock: StockCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)"""
try:
@@ -37,9 +41,9 @@ def set_stock(
@router.post("/stock/add", response_model=StockResponse)
def add_stock(
stock: StockAdd,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
stock: StockAdd,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)"""
try:
@@ -54,9 +58,9 @@ def add_stock(
@router.post("/stock/remove", response_model=StockResponse)
def remove_stock(
stock: StockAdd,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
stock: StockAdd,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Remove quantity from existing stock for a GTIN at a specific location"""
try:
@@ -71,9 +75,9 @@ def remove_stock(
@router.get("/stock/{gtin}", response_model=StockSummaryResponse)
def get_stock_by_gtin(
gtin: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
gtin: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get all stock locations and total quantity for a specific GTIN"""
try:
@@ -88,9 +92,9 @@ def get_stock_by_gtin(
@router.get("/stock/{gtin}/total")
def get_total_stock(
gtin: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
gtin: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get total quantity in stock for a specific GTIN"""
try:
@@ -105,21 +109,17 @@ def get_total_stock(
@router.get("/stock", response_model=List[StockResponse])
def get_all_stock(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
location: Optional[str] = Query(None, description="Filter by location"),
gtin: Optional[str] = Query(None, description="Filter by GTIN"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
location: Optional[str] = Query(None, description="Filter by location"),
gtin: Optional[str] = Query(None, description="Filter by GTIN"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get all stock entries with optional filtering"""
try:
result = stock_service.get_all_stock(
db=db,
skip=skip,
limit=limit,
location=location,
gtin=gtin
db=db, skip=skip, limit=limit, location=location, gtin=gtin
)
return result
except Exception as e:
@@ -129,10 +129,10 @@ def get_all_stock(
@router.put("/stock/{stock_id}", response_model=StockResponse)
def update_stock(
stock_id: int,
stock_update: StockUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
stock_id: int,
stock_update: StockUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Update stock quantity for a specific stock entry"""
try:
@@ -147,9 +147,9 @@ def update_stock(
@router.delete("/stock/{stock_id}")
def delete_stock(
stock_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
stock_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Delete a stock entry"""
try:

View File

@@ -1,6 +1,8 @@
# app/core/config.py
from pydantic_settings import BaseSettings # This is the correct import for Pydantic v2
from typing import Optional, List
from typing import List, Optional
from pydantic_settings import \
BaseSettings # This is the correct import for Pydantic v2
class Settings(BaseSettings):

View File

@@ -1,6 +1,6 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import declarative_base, sessionmaker
from .config import settings
engine = create_engine(settings.database_url)

View File

@@ -1,11 +1,14 @@
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlalchemy import text
from .logging import setup_logging
from .database import engine, SessionLocal
from models.database_models import Base
import logging
from middleware.auth import AuthManager
from models.database_models import Base
from .database import SessionLocal, engine
from .logging import setup_logging
logger = logging.getLogger(__name__)
auth_manager = AuthManager()
@@ -44,15 +47,29 @@ def create_indexes():
with engine.connect() as conn:
try:
# User indexes
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_user_email ON users(email)"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_user_username ON users(username)"))
conn.execute(
text("CREATE INDEX IF NOT EXISTS idx_user_email ON users(email)")
)
conn.execute(
text("CREATE INDEX IF NOT EXISTS idx_user_username ON users(username)")
)
# Product indexes
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_product_gtin ON products(gtin)"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_product_marketplace ON products(marketplace)"))
conn.execute(
text("CREATE INDEX IF NOT EXISTS idx_product_gtin ON products(gtin)")
)
conn.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_product_marketplace ON products(marketplace)"
)
)
# Stock indexes
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_stock_gtin_location ON stock(gtin, location)"))
conn.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_stock_gtin_location ON stock(gtin, location)"
)
)
conn.commit()
logger.info("Database indexes created successfully")

View File

@@ -2,6 +2,7 @@
import logging
import sys
from pathlib import Path
from app.core.config import settings
@@ -22,7 +23,7 @@ def setup_logging():
# Create formatters
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Console handler

View File

@@ -1,11 +1,12 @@
from sqlalchemy.orm import Session
from fastapi import HTTPException
from datetime import datetime
import logging
from datetime import datetime
from typing import List, Optional, Tuple
from models.database_models import User, MarketplaceImportJob, Shop
from fastapi import HTTPException
from sqlalchemy.orm import Session
from models.api_models import MarketplaceImportJobResponse
from models.database_models import MarketplaceImportJob, Shop, User
logger = logging.getLogger(__name__)
@@ -17,7 +18,9 @@ class AdminService:
"""Get paginated list of all users"""
return db.query(User).offset(skip).limit(limit).all()
def toggle_user_status(self, db: Session, user_id: int, current_admin_id: int) -> Tuple[User, str]:
def toggle_user_status(
self, db: Session, user_id: int, current_admin_id: int
) -> Tuple[User, str]:
"""
Toggle user active status
@@ -37,7 +40,9 @@ class AdminService:
raise HTTPException(status_code=404, detail="User not found")
if user.id == current_admin_id:
raise HTTPException(status_code=400, detail="Cannot deactivate your own account")
raise HTTPException(
status_code=400, detail="Cannot deactivate your own account"
)
user.is_active = not user.is_active
user.updated_at = datetime.utcnow()
@@ -45,10 +50,14 @@ class AdminService:
db.refresh(user)
status = "activated" if user.is_active else "deactivated"
logger.info(f"User {user.username} has been {status} by admin {current_admin_id}")
logger.info(
f"User {user.username} has been {status} by admin {current_admin_id}"
)
return user, f"User {user.username} has been {status}"
def get_all_shops(self, db: Session, skip: int = 0, limit: int = 100) -> Tuple[List[Shop], int]:
def get_all_shops(
self, db: Session, skip: int = 0, limit: int = 100
) -> Tuple[List[Shop], int]:
"""
Get paginated list of all shops with total count
@@ -119,13 +128,13 @@ class AdminService:
return shop, f"Shop {shop.shop_code} has been {status}"
def get_marketplace_import_jobs(
self,
db: Session,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
status: Optional[str] = None,
skip: int = 0,
limit: int = 100
self,
db: Session,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
status: Optional[str] = None,
skip: int = 0,
limit: int = 100,
) -> List[MarketplaceImportJobResponse]:
"""
Get filtered and paginated marketplace import jobs
@@ -145,14 +154,21 @@ class AdminService:
# Apply filters
if marketplace:
query = query.filter(MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%"))
query = query.filter(
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
)
if shop_name:
query = query.filter(MarketplaceImportJob.shop_name.ilike(f"%{shop_name}%"))
if status:
query = query.filter(MarketplaceImportJob.status == status)
# Order by creation date and apply pagination
jobs = query.order_by(MarketplaceImportJob.created_at.desc()).offset(skip).limit(limit).all()
jobs = (
query.order_by(MarketplaceImportJob.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return [
MarketplaceImportJobResponse(
@@ -168,8 +184,9 @@ class AdminService:
error_message=job.error_message,
created_at=job.created_at,
started_at=job.started_at,
completed_at=job.completed_at
) for job in jobs
completed_at=job.completed_at,
)
for job in jobs
]
def get_user_by_id(self, db: Session, user_id: int) -> Optional[User]:

View File

@@ -1,11 +1,12 @@
from sqlalchemy.orm import Session
from fastapi import HTTPException
import logging
from typing import Optional, Dict, Any
from typing import Any, Dict, Optional
from fastapi import HTTPException
from sqlalchemy.orm import Session
from models.database_models import User
from models.api_models import UserRegister, UserLogin
from middleware.auth import AuthManager
from models.api_models import UserLogin, UserRegister
from models.database_models import User
logger = logging.getLogger(__name__)
@@ -36,7 +37,9 @@ class AuthService:
raise HTTPException(status_code=400, detail="Email already registered")
# Check if username already exists
existing_username = db.query(User).filter(User.username == user_data.username).first()
existing_username = (
db.query(User).filter(User.username == user_data.username).first()
)
if existing_username:
raise HTTPException(status_code=400, detail="Username already taken")
@@ -47,7 +50,7 @@ class AuthService:
username=user_data.username,
hashed_password=hashed_password,
role="user",
is_active=True
is_active=True,
)
db.add(new_user)
@@ -71,19 +74,20 @@ class AuthService:
Raises:
HTTPException: If authentication fails
"""
user = self.auth_manager.authenticate_user(db, user_credentials.username, user_credentials.password)
user = self.auth_manager.authenticate_user(
db, user_credentials.username, user_credentials.password
)
if not user:
raise HTTPException(status_code=401, detail="Incorrect username or password")
raise HTTPException(
status_code=401, detail="Incorrect username or password"
)
# Create access token
token_data = self.auth_manager.create_access_token(user)
logger.info(f"User logged in: {user.username}")
return {
"token_data": token_data,
"user": user
}
return {"token_data": token_data, "user": user}
def get_user_by_email(self, db: Session, email: str) -> Optional[User]:
"""Get user by email"""
@@ -101,7 +105,9 @@ class AuthService:
"""Check if username already exists"""
return db.query(User).filter(User.username == username).first() is not None
def authenticate_user(self, db: Session, username: str, password: str) -> Optional[User]:
def authenticate_user(
self, db: Session, username: str, password: str
) -> Optional[User]:
"""Authenticate user with username/password"""
return self.auth_manager.authenticate_user(db, username, password)

View File

@@ -1,10 +1,13 @@
import logging
from datetime import datetime
from typing import List, Optional
from sqlalchemy import func
from sqlalchemy.orm import Session
from models.api_models import (MarketplaceImportJobResponse,
MarketplaceImportRequest)
from models.database_models import MarketplaceImportJob, Shop, User
from models.api_models import MarketplaceImportRequest, MarketplaceImportJobResponse
from typing import Optional, List
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
@@ -17,9 +20,11 @@ class MarketplaceService:
"""Validate that the shop exists and user has access to it"""
# Explicit type hint to help type checker shop: Optional[Shop]
# Use case-insensitive query to handle both uppercase and lowercase codes
shop: Optional[Shop] = db.query(Shop).filter(
func.upper(Shop.shop_code) == shop_code.upper()
).first()
shop: Optional[Shop] = (
db.query(Shop)
.filter(func.upper(Shop.shop_code) == shop_code.upper())
.first()
)
if not shop:
raise ValueError("Shop not found")
@@ -30,10 +35,7 @@ class MarketplaceService:
return shop
def create_import_job(
self,
db: Session,
request: MarketplaceImportRequest,
user: User
self, db: Session, request: MarketplaceImportRequest, user: User
) -> MarketplaceImportJob:
"""Create a new marketplace import job"""
# Validate shop access first
@@ -47,7 +49,7 @@ class MarketplaceService:
shop_id=shop.id, # Foreign key to shops table
shop_name=shop.shop_name, # Use shop.shop_name (the display name)
user_id=user.id,
created_at=datetime.utcnow()
created_at=datetime.utcnow(),
)
db.add(import_job)
@@ -55,13 +57,20 @@ class MarketplaceService:
db.refresh(import_job)
logger.info(
f"Created marketplace import job {import_job.id}: {request.marketplace} -> {shop.shop_name} (shop_code: {shop.shop_code}) by user {user.username}")
f"Created marketplace import job {import_job.id}: {request.marketplace} -> {shop.shop_name} (shop_code: {shop.shop_code}) by user {user.username}"
)
return import_job
def get_import_job_by_id(self, db: Session, job_id: int, user: User) -> MarketplaceImportJob:
def get_import_job_by_id(
self, db: Session, job_id: int, user: User
) -> MarketplaceImportJob:
"""Get a marketplace import job by ID with access control"""
job = db.query(MarketplaceImportJob).filter(MarketplaceImportJob.id == job_id).first()
job = (
db.query(MarketplaceImportJob)
.filter(MarketplaceImportJob.id == job_id)
.first()
)
if not job:
raise ValueError("Marketplace import job not found")
@@ -72,13 +81,13 @@ class MarketplaceService:
return job
def get_import_jobs(
self,
db: Session,
user: User,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
skip: int = 0,
limit: int = 50
self,
db: Session,
user: User,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
skip: int = 0,
limit: int = 50,
) -> List[MarketplaceImportJob]:
"""Get marketplace import jobs with filtering and access control"""
query = db.query(MarketplaceImportJob)
@@ -89,44 +98,51 @@ class MarketplaceService:
# Apply filters
if marketplace:
query = query.filter(MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%"))
query = query.filter(
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
)
if shop_name:
query = query.filter(MarketplaceImportJob.shop_name.ilike(f"%{shop_name}%"))
# Order by creation date (newest first) and apply pagination
jobs = query.order_by(MarketplaceImportJob.created_at.desc()).offset(skip).limit(limit).all()
jobs = (
query.order_by(MarketplaceImportJob.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return jobs
def update_job_status(
self,
db: Session,
job_id: int,
status: str,
**kwargs
self, db: Session, job_id: int, status: str, **kwargs
) -> MarketplaceImportJob:
"""Update marketplace import job status and other fields"""
job = db.query(MarketplaceImportJob).filter(MarketplaceImportJob.id == job_id).first()
job = (
db.query(MarketplaceImportJob)
.filter(MarketplaceImportJob.id == job_id)
.first()
)
if not job:
raise ValueError("Marketplace import job not found")
job.status = status
# Update optional fields if provided
if 'imported_count' in kwargs:
job.imported_count = kwargs['imported_count']
if 'updated_count' in kwargs:
job.updated_count = kwargs['updated_count']
if 'total_processed' in kwargs:
job.total_processed = kwargs['total_processed']
if 'error_count' in kwargs:
job.error_count = kwargs['error_count']
if 'error_message' in kwargs:
job.error_message = kwargs['error_message']
if 'started_at' in kwargs:
job.started_at = kwargs['started_at']
if 'completed_at' in kwargs:
job.completed_at = kwargs['completed_at']
if "imported_count" in kwargs:
job.imported_count = kwargs["imported_count"]
if "updated_count" in kwargs:
job.updated_count = kwargs["updated_count"]
if "total_processed" in kwargs:
job.total_processed = kwargs["total_processed"]
if "error_count" in kwargs:
job.error_count = kwargs["error_count"]
if "error_message" in kwargs:
job.error_message = kwargs["error_message"]
if "started_at" in kwargs:
job.started_at = kwargs["started_at"]
if "completed_at" in kwargs:
job.completed_at = kwargs["completed_at"]
db.commit()
db.refresh(job)
@@ -145,7 +161,9 @@ class MarketplaceService:
total_jobs = query.count()
pending_jobs = query.filter(MarketplaceImportJob.status == "pending").count()
running_jobs = query.filter(MarketplaceImportJob.status == "running").count()
completed_jobs = query.filter(MarketplaceImportJob.status == "completed").count()
completed_jobs = query.filter(
MarketplaceImportJob.status == "completed"
).count()
failed_jobs = query.filter(MarketplaceImportJob.status == "failed").count()
return {
@@ -153,17 +171,21 @@ class MarketplaceService:
"pending_jobs": pending_jobs,
"running_jobs": running_jobs,
"completed_jobs": completed_jobs,
"failed_jobs": failed_jobs
"failed_jobs": failed_jobs,
}
def convert_to_response_model(self, job: MarketplaceImportJob) -> MarketplaceImportJobResponse:
def convert_to_response_model(
self, job: MarketplaceImportJob
) -> MarketplaceImportJobResponse:
"""Convert database model to API response model"""
return MarketplaceImportJobResponse(
job_id=job.id,
status=job.status,
marketplace=job.marketplace,
shop_id=job.shop_id,
shop_code=job.shop.shop_code if job.shop else None, # Add this optional field via relationship
shop_code=(
job.shop.shop_code if job.shop else None
), # Add this optional field via relationship
shop_name=job.shop_name,
imported=job.imported_count or 0,
updated=job.updated_count or 0,
@@ -172,10 +194,12 @@ class MarketplaceService:
error_message=job.error_message,
created_at=job.created_at,
started_at=job.started_at,
completed_at=job.completed_at
completed_at=job.completed_at,
)
def cancel_import_job(self, db: Session, job_id: int, user: User) -> MarketplaceImportJob:
def cancel_import_job(
self, db: Session, job_id: int, user: User
) -> MarketplaceImportJob:
"""Cancel a pending or running import job"""
job = self.get_import_job_by_id(db, job_id, user)
@@ -197,7 +221,9 @@ class MarketplaceService:
# Only allow deletion of completed, failed, or cancelled jobs
if job.status in ["pending", "running"]:
raise ValueError(f"Cannot delete job with status: {job.status}. Cancel it first.")
raise ValueError(
f"Cannot delete job with status: {job.status}. Cancel it first."
)
db.delete(job)
db.commit()

View File

@@ -1,11 +1,14 @@
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from models.database_models import Product, Stock
from models.api_models import ProductCreate, ProductUpdate, StockLocationResponse, StockSummaryResponse
from utils.data_processing import GTINProcessor, PriceProcessor
from typing import Optional, List, Generator
from datetime import datetime
import logging
from datetime import datetime
from typing import Generator, List, Optional
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from models.api_models import (ProductCreate, ProductUpdate,
StockLocationResponse, StockSummaryResponse)
from models.database_models import Product, Stock
from utils.data_processing import GTINProcessor, PriceProcessor
logger = logging.getLogger(__name__)
@@ -27,7 +30,9 @@ class ProductService:
# Process price if provided
if product_data.price:
parsed_price, currency = self.price_processor.parse_price_currency(product_data.price)
parsed_price, currency = self.price_processor.parse_price_currency(
product_data.price
)
if parsed_price:
product_data.price = parsed_price
product_data.currency = currency
@@ -58,16 +63,16 @@ class ProductService:
return db.query(Product).filter(Product.product_id == product_id).first()
def get_products_with_filters(
self,
db: Session,
skip: int = 0,
limit: int = 100,
brand: Optional[str] = None,
category: Optional[str] = None,
availability: Optional[str] = None,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
search: Optional[str] = None
self,
db: Session,
skip: int = 0,
limit: int = 100,
brand: Optional[str] = None,
category: Optional[str] = None,
availability: Optional[str] = None,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
search: Optional[str] = None,
) -> tuple[List[Product], int]:
"""Get products with filtering and pagination"""
query = db.query(Product)
@@ -87,10 +92,10 @@ class ProductService:
# Search in title, description, marketplace, and shop_name
search_term = f"%{search}%"
query = query.filter(
(Product.title.ilike(search_term)) |
(Product.description.ilike(search_term)) |
(Product.marketplace.ilike(search_term)) |
(Product.shop_name.ilike(search_term))
(Product.title.ilike(search_term))
| (Product.description.ilike(search_term))
| (Product.marketplace.ilike(search_term))
| (Product.shop_name.ilike(search_term))
)
total = query.count()
@@ -98,7 +103,9 @@ class ProductService:
return products, total
def update_product(self, db: Session, product_id: str, product_update: ProductUpdate) -> Product:
def update_product(
self, db: Session, product_id: str, product_update: ProductUpdate
) -> Product:
"""Update product with validation"""
product = db.query(Product).filter(Product.product_id == product_id).first()
if not product:
@@ -116,7 +123,9 @@ class ProductService:
# Process price if being updated
if "price" in update_data and update_data["price"]:
parsed_price, currency = self.price_processor.parse_price_currency(update_data["price"])
parsed_price, currency = self.price_processor.parse_price_currency(
update_data["price"]
)
if parsed_price:
update_data["price"] = parsed_price
update_data["currency"] = currency
@@ -160,21 +169,21 @@ class ProductService:
]
return StockSummaryResponse(
gtin=gtin,
total_quantity=total_quantity,
locations=locations
gtin=gtin, total_quantity=total_quantity, locations=locations
)
def generate_csv_export(
self,
db: Session,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None
self,
db: Session,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
) -> Generator[str, None, None]:
"""Generate CSV export with streaming for memory efficiency"""
# CSV header
yield ("product_id,title,description,link,image_link,availability,price,currency,brand,"
"gtin,marketplace,shop_name\n")
yield (
"product_id,title,description,link,image_link,availability,price,currency,brand,"
"gtin,marketplace,shop_name\n"
)
batch_size = 1000
offset = 0
@@ -194,17 +203,22 @@ class ProductService:
for product in products:
# Create CSV row with marketplace fields
row = (f'"{product.product_id}","{product.title or ""}","{product.description or ""}",'
f'"{product.link or ""}","{product.image_link or ""}","{product.availability or ""}",'
f'"{product.price or ""}","{product.currency or ""}","{product.brand or ""}",'
f'"{product.gtin or ""}","{product.marketplace or ""}","{product.shop_name or ""}"\n')
row = (
f'"{product.product_id}","{product.title or ""}","{product.description or ""}",'
f'"{product.link or ""}","{product.image_link or ""}","{product.availability or ""}",'
f'"{product.price or ""}","{product.currency or ""}","{product.brand or ""}",'
f'"{product.gtin or ""}","{product.marketplace or ""}","{product.shop_name or ""}"\n'
)
yield row
offset += batch_size
def product_exists(self, db: Session, product_id: str) -> bool:
"""Check if product exists by ID"""
return db.query(Product).filter(Product.product_id == product_id).first() is not None
return (
db.query(Product).filter(Product.product_id == product_id).first()
is not None
)
# Create service instance

View File

@@ -1,12 +1,13 @@
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from fastapi import HTTPException
from sqlalchemy import func
from sqlalchemy.orm import Session
from fastapi import HTTPException
from datetime import datetime
import logging
from typing import List, Optional, Tuple, Dict, Any
from models.database_models import User, Shop, Product, ShopProduct
from models.api_models import ShopCreate, ShopProductCreate
from models.database_models import Product, Shop, ShopProduct, User
logger = logging.getLogger(__name__)
@@ -14,7 +15,9 @@ logger = logging.getLogger(__name__)
class ShopService:
"""Service class for shop operations following the application's service pattern"""
def create_shop(self, db: Session, shop_data: ShopCreate, current_user: User) -> Shop:
def create_shop(
self, db: Session, shop_data: ShopCreate, current_user: User
) -> Shop:
"""
Create a new shop
@@ -33,39 +36,43 @@ class ShopService:
normalized_shop_code = shop_data.shop_code.upper()
# Check if shop code already exists (case-insensitive check against existing data)
existing_shop = db.query(Shop).filter(
func.upper(Shop.shop_code) == normalized_shop_code
).first()
existing_shop = (
db.query(Shop)
.filter(func.upper(Shop.shop_code) == normalized_shop_code)
.first()
)
if existing_shop:
raise HTTPException(status_code=400, detail="Shop code already exists")
# Create shop with uppercase code
shop_dict = shop_data.model_dump() # Fixed deprecated .dict() method
shop_dict['shop_code'] = normalized_shop_code # Store as uppercase
shop_dict["shop_code"] = normalized_shop_code # Store as uppercase
new_shop = Shop(
**shop_dict,
owner_id=current_user.id,
is_active=True,
is_verified=(current_user.role == "admin")
is_verified=(current_user.role == "admin"),
)
db.add(new_shop)
db.commit()
db.refresh(new_shop)
logger.info(f"New shop created: {new_shop.shop_code} by {current_user.username}")
logger.info(
f"New shop created: {new_shop.shop_code} by {current_user.username}"
)
return new_shop
def get_shops(
self,
db: Session,
current_user: User,
skip: int = 0,
limit: int = 100,
active_only: bool = True,
verified_only: bool = False
self,
db: Session,
current_user: User,
skip: int = 0,
limit: int = 100,
active_only: bool = True,
verified_only: bool = False,
) -> Tuple[List[Shop], int]:
"""
Get shops with filtering
@@ -86,8 +93,8 @@ class ShopService:
# Non-admin users can only see active and verified shops, plus their own
if current_user.role != "admin":
query = query.filter(
(Shop.is_active == True) &
((Shop.is_verified == True) | (Shop.owner_id == current_user.id))
(Shop.is_active == True)
& ((Shop.is_verified == True) | (Shop.owner_id == current_user.id))
)
else:
# Admin can apply filters
@@ -117,22 +124,25 @@ class ShopService:
HTTPException: If shop not found or access denied
"""
# Explicit type hint to help type checker shop: Optional[Shop]
shop: Optional[Shop] = db.query(Shop).filter(func.upper(Shop.shop_code) == shop_code.upper()).first()
shop: Optional[Shop] = (
db.query(Shop)
.filter(func.upper(Shop.shop_code) == shop_code.upper())
.first()
)
if not shop:
raise HTTPException(status_code=404, detail="Shop not found")
# Non-admin users can only see active verified shops or their own shops
if current_user.role != "admin":
if not shop.is_active or (not shop.is_verified and shop.owner_id != current_user.id):
if not shop.is_active or (
not shop.is_verified and shop.owner_id != current_user.id
):
raise HTTPException(status_code=404, detail="Shop not found")
return shop
def add_product_to_shop(
self,
db: Session,
shop: Shop,
shop_product: ShopProductCreate
self, db: Session, shop: Shop, shop_product: ShopProductCreate
) -> ShopProduct:
"""
Add existing product to shop catalog with shop-specific settings
@@ -149,24 +159,35 @@ class ShopService:
HTTPException: If product not found or already in shop
"""
# Check if product exists
product = db.query(Product).filter(Product.product_id == shop_product.product_id).first()
product = (
db.query(Product)
.filter(Product.product_id == shop_product.product_id)
.first()
)
if not product:
raise HTTPException(status_code=404, detail="Product not found in marketplace catalog")
raise HTTPException(
status_code=404, detail="Product not found in marketplace catalog"
)
# Check if product already in shop
existing_shop_product = db.query(ShopProduct).filter(
ShopProduct.shop_id == shop.id,
ShopProduct.product_id == product.id
).first()
existing_shop_product = (
db.query(ShopProduct)
.filter(
ShopProduct.shop_id == shop.id, ShopProduct.product_id == product.id
)
.first()
)
if existing_shop_product:
raise HTTPException(status_code=400, detail="Product already in shop catalog")
raise HTTPException(
status_code=400, detail="Product already in shop catalog"
)
# Create shop-product association
new_shop_product = ShopProduct(
shop_id=shop.id,
product_id=product.id,
**shop_product.model_dump(exclude={'product_id'})
**shop_product.model_dump(exclude={"product_id"}),
)
db.add(new_shop_product)
@@ -180,14 +201,14 @@ class ShopService:
return new_shop_product
def get_shop_products(
self,
db: Session,
shop: Shop,
current_user: User,
skip: int = 0,
limit: int = 100,
active_only: bool = True,
featured_only: bool = False
self,
db: Session,
shop: Shop,
current_user: User,
skip: int = 0,
limit: int = 100,
active_only: bool = True,
featured_only: bool = False,
) -> Tuple[List[ShopProduct], int]:
"""
Get products in shop catalog with filtering
@@ -239,10 +260,14 @@ class ShopService:
def product_in_shop(self, db: Session, shop_id: int, product_id: int) -> bool:
"""Check if product is already in shop"""
return db.query(ShopProduct).filter(
ShopProduct.shop_id == shop_id,
ShopProduct.product_id == product_id
).first() is not None
return (
db.query(ShopProduct)
.filter(
ShopProduct.shop_id == shop_id, ShopProduct.product_id == product_id
)
.first()
is not None
)
def is_shop_owner(self, shop: Shop, user: User) -> bool:
"""Check if user is shop owner"""

View File

@@ -1,10 +1,11 @@
import logging
from typing import Any, Dict, List
from sqlalchemy import func
from sqlalchemy.orm import Session
import logging
from typing import List, Dict, Any
from models.database_models import User, Product, Stock
from models.api_models import StatsResponse, MarketplaceStatsResponse
from models.api_models import MarketplaceStatsResponse, StatsResponse
from models.database_models import Product, Stock, User
logger = logging.getLogger(__name__)
@@ -25,26 +26,37 @@ class StatsService:
# Use more efficient queries with proper indexes
total_products = db.query(Product).count()
unique_brands = db.query(Product.brand).filter(
Product.brand.isnot(None),
Product.brand != ""
).distinct().count()
unique_brands = (
db.query(Product.brand)
.filter(Product.brand.isnot(None), Product.brand != "")
.distinct()
.count()
)
unique_categories = db.query(Product.google_product_category).filter(
Product.google_product_category.isnot(None),
Product.google_product_category != ""
).distinct().count()
unique_categories = (
db.query(Product.google_product_category)
.filter(
Product.google_product_category.isnot(None),
Product.google_product_category != "",
)
.distinct()
.count()
)
# New marketplace statistics
unique_marketplaces = db.query(Product.marketplace).filter(
Product.marketplace.isnot(None),
Product.marketplace != ""
).distinct().count()
unique_marketplaces = (
db.query(Product.marketplace)
.filter(Product.marketplace.isnot(None), Product.marketplace != "")
.distinct()
.count()
)
unique_shops = db.query(Product.shop_name).filter(
Product.shop_name.isnot(None),
Product.shop_name != ""
).distinct().count()
unique_shops = (
db.query(Product.shop_name)
.filter(Product.shop_name.isnot(None), Product.shop_name != "")
.distinct()
.count()
)
# Stock statistics
total_stock_entries = db.query(Stock).count()
@@ -57,10 +69,12 @@ class StatsService:
"unique_marketplaces": unique_marketplaces,
"unique_shops": unique_shops,
"total_stock_entries": total_stock_entries,
"total_inventory_quantity": total_inventory
"total_inventory_quantity": total_inventory,
}
logger.info(f"Generated comprehensive stats: {total_products} products, {unique_marketplaces} marketplaces")
logger.info(
f"Generated comprehensive stats: {total_products} products, {unique_marketplaces} marketplaces"
)
return stats_data
def get_marketplace_breakdown_stats(self, db: Session) -> List[Dict[str, Any]]:
@@ -74,25 +88,31 @@ class StatsService:
List of dictionaries containing marketplace statistics
"""
# Query to get stats per marketplace
marketplace_stats = db.query(
Product.marketplace,
func.count(Product.id).label('total_products'),
func.count(func.distinct(Product.shop_name)).label('unique_shops'),
func.count(func.distinct(Product.brand)).label('unique_brands')
).filter(
Product.marketplace.isnot(None)
).group_by(Product.marketplace).all()
marketplace_stats = (
db.query(
Product.marketplace,
func.count(Product.id).label("total_products"),
func.count(func.distinct(Product.shop_name)).label("unique_shops"),
func.count(func.distinct(Product.brand)).label("unique_brands"),
)
.filter(Product.marketplace.isnot(None))
.group_by(Product.marketplace)
.all()
)
stats_list = [
{
"marketplace": stat.marketplace,
"total_products": stat.total_products,
"unique_shops": stat.unique_shops,
"unique_brands": stat.unique_brands
} for stat in marketplace_stats
"unique_brands": stat.unique_brands,
}
for stat in marketplace_stats
]
logger.info(f"Generated marketplace breakdown stats for {len(stats_list)} marketplaces")
logger.info(
f"Generated marketplace breakdown stats for {len(stats_list)} marketplaces"
)
return stats_list
def get_product_count(self, db: Session) -> int:
@@ -101,31 +121,42 @@ class StatsService:
def get_unique_brands_count(self, db: Session) -> int:
"""Get count of unique brands"""
return db.query(Product.brand).filter(
Product.brand.isnot(None),
Product.brand != ""
).distinct().count()
return (
db.query(Product.brand)
.filter(Product.brand.isnot(None), Product.brand != "")
.distinct()
.count()
)
def get_unique_categories_count(self, db: Session) -> int:
"""Get count of unique categories"""
return db.query(Product.google_product_category).filter(
Product.google_product_category.isnot(None),
Product.google_product_category != ""
).distinct().count()
return (
db.query(Product.google_product_category)
.filter(
Product.google_product_category.isnot(None),
Product.google_product_category != "",
)
.distinct()
.count()
)
def get_unique_marketplaces_count(self, db: Session) -> int:
"""Get count of unique marketplaces"""
return db.query(Product.marketplace).filter(
Product.marketplace.isnot(None),
Product.marketplace != ""
).distinct().count()
return (
db.query(Product.marketplace)
.filter(Product.marketplace.isnot(None), Product.marketplace != "")
.distinct()
.count()
)
def get_unique_shops_count(self, db: Session) -> int:
"""Get count of unique shops"""
return db.query(Product.shop_name).filter(
Product.shop_name.isnot(None),
Product.shop_name != ""
).distinct().count()
return (
db.query(Product.shop_name)
.filter(Product.shop_name.isnot(None), Product.shop_name != "")
.distinct()
.count()
)
def get_stock_statistics(self, db: Session) -> Dict[str, int]:
"""
@@ -142,25 +173,35 @@ class StatsService:
return {
"total_stock_entries": total_stock_entries,
"total_inventory_quantity": total_inventory
"total_inventory_quantity": total_inventory,
}
def get_brands_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
"""Get unique brands for a specific marketplace"""
brands = db.query(Product.brand).filter(
Product.marketplace == marketplace,
Product.brand.isnot(None),
Product.brand != ""
).distinct().all()
brands = (
db.query(Product.brand)
.filter(
Product.marketplace == marketplace,
Product.brand.isnot(None),
Product.brand != "",
)
.distinct()
.all()
)
return [brand[0] for brand in brands]
def get_shops_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
"""Get unique shops for a specific marketplace"""
shops = db.query(Product.shop_name).filter(
Product.marketplace == marketplace,
Product.shop_name.isnot(None),
Product.shop_name != ""
).distinct().all()
shops = (
db.query(Product.shop_name)
.filter(
Product.marketplace == marketplace,
Product.shop_name.isnot(None),
Product.shop_name != "",
)
.distinct()
.all()
)
return [shop[0] for shop in shops]
def get_products_by_marketplace(self, db: Session, marketplace: str) -> int:

View File

@@ -1,10 +1,13 @@
from sqlalchemy.orm import Session
from models.database_models import Stock, Product
from models.api_models import StockCreate, StockAdd, StockUpdate, StockLocationResponse, StockSummaryResponse
from utils.data_processing import GTINProcessor
from typing import Optional, List, Tuple
from datetime import datetime
import logging
from datetime import datetime
from typing import List, Optional, Tuple
from sqlalchemy.orm import Session
from models.api_models import (StockAdd, StockCreate, StockLocationResponse,
StockSummaryResponse, StockUpdate)
from models.database_models import Product, Stock
from utils.data_processing import GTINProcessor
logger = logging.getLogger(__name__)
@@ -26,10 +29,11 @@ class StockService:
location = stock_data.location.strip().upper()
# Check if stock entry already exists for this GTIN and location
existing_stock = db.query(Stock).filter(
Stock.gtin == normalized_gtin,
Stock.location == location
).first()
existing_stock = (
db.query(Stock)
.filter(Stock.gtin == normalized_gtin, Stock.location == location)
.first()
)
if existing_stock:
# Update existing stock (SET to exact quantity)
@@ -39,19 +43,20 @@ class StockService:
db.commit()
db.refresh(existing_stock)
logger.info(
f"Updated stock for GTIN {normalized_gtin} at {location}: {old_quantity}{stock_data.quantity}")
f"Updated stock for GTIN {normalized_gtin} at {location}: {old_quantity}{stock_data.quantity}"
)
return existing_stock
else:
# Create new stock entry
new_stock = Stock(
gtin=normalized_gtin,
location=location,
quantity=stock_data.quantity
gtin=normalized_gtin, location=location, quantity=stock_data.quantity
)
db.add(new_stock)
db.commit()
db.refresh(new_stock)
logger.info(f"Created new stock for GTIN {normalized_gtin} at {location}: {stock_data.quantity}")
logger.info(
f"Created new stock for GTIN {normalized_gtin} at {location}: {stock_data.quantity}"
)
return new_stock
def add_stock(self, db: Session, stock_data: StockAdd) -> Stock:
@@ -63,10 +68,11 @@ class StockService:
location = stock_data.location.strip().upper()
# Check if stock entry already exists for this GTIN and location
existing_stock = db.query(Stock).filter(
Stock.gtin == normalized_gtin,
Stock.location == location
).first()
existing_stock = (
db.query(Stock)
.filter(Stock.gtin == normalized_gtin, Stock.location == location)
.first()
)
if existing_stock:
# Add to existing stock
@@ -76,19 +82,20 @@ class StockService:
db.commit()
db.refresh(existing_stock)
logger.info(
f"Added stock for GTIN {normalized_gtin} at {location}: {old_quantity} + {stock_data.quantity} = {existing_stock.quantity}")
f"Added stock for GTIN {normalized_gtin} at {location}: {old_quantity} + {stock_data.quantity} = {existing_stock.quantity}"
)
return existing_stock
else:
# Create new stock entry with the quantity
new_stock = Stock(
gtin=normalized_gtin,
location=location,
quantity=stock_data.quantity
gtin=normalized_gtin, location=location, quantity=stock_data.quantity
)
db.add(new_stock)
db.commit()
db.refresh(new_stock)
logger.info(f"Created new stock for GTIN {normalized_gtin} at {location}: {stock_data.quantity}")
logger.info(
f"Created new stock for GTIN {normalized_gtin} at {location}: {stock_data.quantity}"
)
return new_stock
def remove_stock(self, db: Session, stock_data: StockAdd) -> Stock:
@@ -100,18 +107,22 @@ class StockService:
location = stock_data.location.strip().upper()
# Find existing stock entry
existing_stock = db.query(Stock).filter(
Stock.gtin == normalized_gtin,
Stock.location == location
).first()
existing_stock = (
db.query(Stock)
.filter(Stock.gtin == normalized_gtin, Stock.location == location)
.first()
)
if not existing_stock:
raise ValueError(f"No stock found for GTIN {normalized_gtin} at location {location}")
raise ValueError(
f"No stock found for GTIN {normalized_gtin} at location {location}"
)
# Check if we have enough stock to remove
if existing_stock.quantity < stock_data.quantity:
raise ValueError(
f"Insufficient stock. Available: {existing_stock.quantity}, Requested to remove: {stock_data.quantity}")
f"Insufficient stock. Available: {existing_stock.quantity}, Requested to remove: {stock_data.quantity}"
)
# Remove from existing stock
old_quantity = existing_stock.quantity
@@ -120,7 +131,8 @@ class StockService:
db.commit()
db.refresh(existing_stock)
logger.info(
f"Removed stock for GTIN {normalized_gtin} at {location}: {old_quantity} - {stock_data.quantity} = {existing_stock.quantity}")
f"Removed stock for GTIN {normalized_gtin} at {location}: {old_quantity} - {stock_data.quantity} = {existing_stock.quantity}"
)
return existing_stock
def get_stock_by_gtin(self, db: Session, gtin: str) -> StockSummaryResponse:
@@ -141,10 +153,9 @@ class StockService:
for entry in stock_entries:
total_quantity += entry.quantity
locations.append(StockLocationResponse(
location=entry.location,
quantity=entry.quantity
))
locations.append(
StockLocationResponse(location=entry.location, quantity=entry.quantity)
)
# Try to get product title for reference
product = db.query(Product).filter(Product.gtin == normalized_gtin).first()
@@ -154,7 +165,7 @@ class StockService:
gtin=normalized_gtin,
total_quantity=total_quantity,
locations=locations,
product_title=product_title
product_title=product_title,
)
def get_total_stock(self, db: Session, gtin: str) -> dict:
@@ -174,16 +185,16 @@ class StockService:
"gtin": normalized_gtin,
"total_quantity": total_quantity,
"product_title": product.title if product else None,
"locations_count": len(total_stock)
"locations_count": len(total_stock),
}
def get_all_stock(
self,
db: Session,
skip: int = 0,
limit: int = 100,
location: Optional[str] = None,
gtin: Optional[str] = None
self,
db: Session,
skip: int = 0,
limit: int = 100,
location: Optional[str] = None,
gtin: Optional[str] = None,
) -> List[Stock]:
"""Get all stock entries with optional filtering"""
query = db.query(Stock)
@@ -198,7 +209,9 @@ class StockService:
return query.offset(skip).limit(limit).all()
def update_stock(self, db: Session, stock_id: int, stock_update: StockUpdate) -> Stock:
def update_stock(
self, db: Session, stock_id: int, stock_update: StockUpdate
) -> Stock:
"""Update stock quantity for a specific stock entry"""
stock_entry = db.query(Stock).filter(Stock.id == stock_id).first()
if not stock_entry:
@@ -209,7 +222,9 @@ class StockService:
db.commit()
db.refresh(stock_entry)
logger.info(f"Updated stock entry {stock_id} to quantity {stock_update.quantity}")
logger.info(
f"Updated stock entry {stock_id} to quantity {stock_update.quantity}"
)
return stock_entry
def delete_stock(self, db: Session, stock_id: int) -> bool:

View File

@@ -1,6 +1,7 @@
# app/tasks/background_tasks.py
import logging
from datetime import datetime
from app.core.database import SessionLocal
from models.database_models import MarketplaceImportJob
from utils.csv_processor import CSVProcessor
@@ -9,11 +10,7 @@ logger = logging.getLogger(__name__)
async def process_marketplace_import(
job_id: int,
url: str,
marketplace: str,
shop_name: str,
batch_size: int = 1000
job_id: int, url: str, marketplace: str, shop_name: str, batch_size: int = 1000
):
"""Background task to process marketplace CSV import"""
db = SessionLocal()
@@ -22,7 +19,11 @@ async def process_marketplace_import(
try:
# Update job status
job = db.query(MarketplaceImportJob).filter(MarketplaceImportJob.id == job_id).first()
job = (
db.query(MarketplaceImportJob)
.filter(MarketplaceImportJob.id == job_id)
.first()
)
if not job:
logger.error(f"Import job {job_id} not found")
return
@@ -70,7 +71,7 @@ async def process_marketplace_import(
finally:
# Close the database session only if it's not a mock
# In tests, we use the same session so we shouldn't close it
if hasattr(db, 'close') and callable(getattr(db, 'close')):
if hasattr(db, "close") and callable(getattr(db, "close")):
try:
db.close()
except Exception as close_error: