From 1fc881024262293a178f5eff3c8025d1577e0477 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Tue, 9 Sep 2025 23:03:08 +0200 Subject: [PATCH] Refactoring code for modular approach --- .env | 9 ++++++-- app/api/v1/admin.py | 4 ++-- app/api/v1/marketplace.py | 20 ++++++++--------- app/api/v1/shops.py | 7 +++--- app/api/v1/stats.py | 6 +++-- app/api/v1/stock.py | 18 +++++++-------- app/core/config.py | 46 +++++++++++++++++++++++++-------------- app/core/database.py | 2 +- app/core/logging.py | 4 ++-- config/settings.py | 32 --------------------------- main.py | 10 ++++----- 11 files changed, 74 insertions(+), 84 deletions(-) delete mode 100644 config/settings.py diff --git a/.env b/.env index eaef3702..e37607b3 100644 --- a/.env +++ b/.env @@ -1,4 +1,9 @@ # .env.example + + PROJECT_NAME: str = "Ecommerce Backend API with Marketplace Support" + DESCRIPTION: str = "Advanced product management system with JWT authentication" + VERSION: str = "0.0.1" + # Database Configuration # DATABASE_URL=postgresql://username:password@localhost:5432/ecommerce_db # For development, you can use SQLite: @@ -16,8 +21,8 @@ DEBUG=False # Rate Limiting RATE_LIMIT_ENABLED=True -DEFAULT_RATE_LIMIT=100 -DEFAULT_WINDOW_SECONDS=3600 +RATE_LIMIT_REQUESTS=100 +RATE_LIMIT_WINDOW=3600 # Logging LOG_LEVEL=INFO diff --git a/app/api/v1/admin.py b/app/api/v1/admin.py index 1ccf3efa..c5197d39 100644 --- a/app/api/v1/admin.py +++ b/app/api/v1/admin.py @@ -3,10 +3,10 @@ from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks from sqlalchemy.orm import Session from app.core.database import get_db -from app.api.deps import get_current_user +from app.api.deps import get_current_user, get_current_admin_user 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, UserResponse, ShopListResponse from models.database_models import User, MarketplaceImportJob, Shop from datetime import datetime import logging diff --git a/app/api/v1/marketplace.py b/app/api/v1/marketplace.py index 60445bce..d5b582ae 100644 --- a/app/api/v1/marketplace.py +++ b/app/api/v1/marketplace.py @@ -8,7 +8,7 @@ from app.tasks.background_tasks import process_marketplace_import from middleware.decorators import rate_limit from models.api_models import MarketplaceImportJobResponse, MarketplaceImportRequest from models.database_models import User -from marketplace_service import marketplace_service +from app.services.marketplace_service import MarketplaceService import logging router = APIRouter() @@ -30,7 +30,7 @@ async def import_products_from_marketplace( 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) + import_job = MarketplaceService.create_import_job(db, request, current_user) # Process in background background_tasks.add_task( @@ -68,8 +68,8 @@ def get_marketplace_import_status( ): """Get status of marketplace import job (Protected)""" try: - job = marketplace_service.get_import_job_by_id(db, job_id, current_user) - return marketplace_service.convert_to_response_model(job) + job = MarketplaceService.get_import_job_by_id(db, job_id, current_user) + return MarketplaceService.convert_to_response_model(job) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @@ -91,7 +91,7 @@ def get_marketplace_import_jobs( ): """Get marketplace import jobs with filtering (Protected)""" try: - jobs = marketplace_service.get_import_jobs( + jobs = MarketplaceService.get_import_jobs( db=db, user=current_user, marketplace=marketplace, @@ -100,7 +100,7 @@ def get_marketplace_import_jobs( limit=limit ) - return [marketplace_service.convert_to_response_model(job) for job in jobs] + return [MarketplaceService.convert_to_response_model(job) for job in jobs] except Exception as e: logger.error(f"Error getting import jobs: {str(e)}") @@ -114,7 +114,7 @@ def get_marketplace_import_stats( ): """Get statistics about marketplace import jobs (Protected)""" try: - stats = marketplace_service.get_job_stats(db, current_user) + stats = MarketplaceService.get_job_stats(db, current_user) return stats except Exception as e: @@ -130,8 +130,8 @@ def cancel_marketplace_import_job( ): """Cancel a pending or running marketplace import job (Protected)""" try: - job = marketplace_service.cancel_import_job(db, job_id, current_user) - return marketplace_service.convert_to_response_model(job) + job = MarketplaceService.cancel_import_job(db, job_id, current_user) + return MarketplaceService.convert_to_response_model(job) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -150,7 +150,7 @@ def delete_marketplace_import_job( ): """Delete a completed marketplace import job (Protected)""" try: - marketplace_service.delete_import_job(db, job_id, current_user) + MarketplaceService.delete_import_job(db, job_id, current_user) return {"message": "Marketplace import job deleted successfully"} except ValueError as e: diff --git a/app/api/v1/shops.py b/app/api/v1/shops.py index 4e5a990d..110b6e64 100644 --- a/app/api/v1/shops.py +++ b/app/api/v1/shops.py @@ -3,11 +3,12 @@ from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks from sqlalchemy.orm import Session from app.core.database import get_db -from app.api.deps import get_current_user +from app.api.deps import get_current_user, get_user_shop from app.tasks.background_tasks import process_marketplace_import from middleware.decorators import rate_limit -from models.api_models import MarketplaceImportJobResponse, MarketplaceImportRequest -from models.database_models import User, MarketplaceImportJob, Shop +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 diff --git a/app/api/v1/stats.py b/app/api/v1/stats.py index 5bc96e15..f097a84c 100644 --- a/app/api/v1/stats.py +++ b/app/api/v1/stats.py @@ -1,13 +1,15 @@ from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks +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.tasks.background_tasks import process_marketplace_import from middleware.decorators import rate_limit -from models.api_models import MarketplaceImportJobResponse, MarketplaceImportRequest -from models.database_models import User, MarketplaceImportJob, Shop +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 diff --git a/app/api/v1/stock.py b/app/api/v1/stock.py index f57a6ff3..483b7e74 100644 --- a/app/api/v1/stock.py +++ b/app/api/v1/stock.py @@ -9,7 +9,7 @@ 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 stock_service import stock_service +from app.services.stock_service import StockService import logging router = APIRouter() @@ -26,7 +26,7 @@ def set_stock( ): """Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)""" try: - result = stock_service.set_stock(db, stock) + result = StockService.set_stock(db, stock) return result except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -43,7 +43,7 @@ def add_stock( ): """Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)""" try: - result = stock_service.add_stock(db, stock) + result = StockService.add_stock(db, stock) return result except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -60,7 +60,7 @@ def remove_stock( ): """Remove quantity from existing stock for a GTIN at a specific location""" try: - result = stock_service.remove_stock(db, stock) + result = StockService.remove_stock(db, stock) return result except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -77,7 +77,7 @@ def get_stock_by_gtin( ): """Get all stock locations and total quantity for a specific GTIN""" try: - result = stock_service.get_stock_by_gtin(db, gtin) + result = StockService.get_stock_by_gtin(db, gtin) return result except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @@ -94,7 +94,7 @@ def get_total_stock( ): """Get total quantity in stock for a specific GTIN""" try: - result = stock_service.get_total_stock(db, gtin) + result = StockService.get_total_stock(db, gtin) return result except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -114,7 +114,7 @@ def get_all_stock( ): """Get all stock entries with optional filtering""" try: - result = stock_service.get_all_stock( + result = StockService.get_all_stock( db=db, skip=skip, limit=limit, @@ -136,7 +136,7 @@ def update_stock( ): """Update stock quantity for a specific stock entry""" try: - result = stock_service.update_stock(db, stock_id, stock_update) + result = StockService.update_stock(db, stock_id, stock_update) return result except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @@ -153,7 +153,7 @@ def delete_stock( ): """Delete a stock entry""" try: - stock_service.delete_stock(db, stock_id) + StockService.delete_stock(db, stock_id) return {"message": "Stock entry deleted successfully"} except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) diff --git a/app/core/config.py b/app/core/config.py index aa7d08d1..8c6496d5 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,26 +1,40 @@ -from pydantic_settings import BaseSettings -from typing import List -import os +# app/core/config.py +from pydantic_settings import BaseSettings # This is the correct import for Pydantic v2 +from typing import Optional, List class Settings(BaseSettings): - PROJECT_NAME: str = "Ecommerce Backend API with Marketplace Support" - DESCRIPTION: str = "Advanced product management system with JWT authentication" - VERSION: str = "2.2.0" + # Project information + project_name: str = "Ecommerce Backend API with Marketplace Support" + description: str = "Advanced product management system with JWT authentication" + version: str = "2.2.0" - DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql://user:password@localhost/ecommerce") - SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-change-in-production") - ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + # Database + database_url: str = "sqlite:///./ecommerce.db" - ALLOWED_HOSTS: List[str] = ["*"] # Configure for production + # JWT + jwt_secret_key: str = "change-this-in-production" + jwt_expire_hours: int = 24 + jwt_expire_minutes: int = 30 - # Rate limiting - RATE_LIMIT_REQUESTS: int = 100 - RATE_LIMIT_WINDOW: int = 3600 + # Middleware + allowed_hosts: List[str] = ["*"] # Configure for production - class Config: - env_file = ".env" - extra = "ignore" + # API + api_host: str = "0.0.0.0" + api_port: int = 8000 + debug: bool = False + + # Rate Limiting + rate_limit_enabled: bool = True + rate_limit_requests: int = 100 + rate_limit_window: int = 3600 + + # Logging + log_level: str = "INFO" + log_file: Optional[str] = None + + model_config = {"env_file": ".env"} # Updated syntax for Pydantic v2 settings = Settings() diff --git a/app/core/database.py b/app/core/database.py index 85a32b28..40901120 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -3,7 +3,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, Session from .config import settings -engine = create_engine(settings.DATABASE_URL) +engine = create_engine(settings.database_url) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() diff --git a/app/core/logging.py b/app/core/logging.py index 29cc8ea0..cef00cad 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -9,12 +9,12 @@ def setup_logging(): """Configure application logging with file and console handlers""" # Create logs directory if it doesn't exist - log_file = Path(settings.LOG_FILE) + log_file = Path(settings.log_file) log_file.parent.mkdir(parents=True, exist_ok=True) # Configure root logger logger = logging.getLogger() - logger.setLevel(getattr(logging, settings.LOG_LEVEL.upper())) + logger.setLevel(getattr(logging, settings.log_level.upper())) # Remove existing handlers for handler in logger.handlers[:]: diff --git a/config/settings.py b/config/settings.py deleted file mode 100644 index 9b2ce534..00000000 --- a/config/settings.py +++ /dev/null @@ -1,32 +0,0 @@ -# config/settings.py -from pydantic_settings import BaseSettings # This is the correct import for Pydantic v2 -from typing import Optional - - -class Settings(BaseSettings): - # Database - database_url: str = "sqlite:///./ecommerce.db" - - # JWT - jwt_secret_key: str = "change-this-in-production" - jwt_expire_hours: int = 24 - jwt_expire_minutes: int = 30 - - # API - api_host: str = "0.0.0.0" - api_port: int = 8000 - debug: bool = False - - # Rate Limiting - rate_limit_enabled: bool = True - default_rate_limit: int = 100 - default_window_seconds: int = 3600 - - # Logging - log_level: str = "INFO" - log_file: Optional[str] = None - - model_config = {"env_file": ".env"} # Updated syntax for Pydantic v2 - - -settings = Settings() diff --git a/main.py b/main.py index 27ce2b4e..7eb60619 100644 --- a/main.py +++ b/main.py @@ -13,16 +13,16 @@ logger = logging.getLogger(__name__) # FastAPI app with lifespan app = FastAPI( - title=settings.PROJECT_NAME, - description=settings.DESCRIPTION, - version=settings.VERSION, + title=settings.project_name, + description=settings.description, + version=settings.version, lifespan=lifespan ) # Add CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=settings.ALLOWED_HOSTS, + allow_origins=settings.allowed_hosts, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -37,7 +37,7 @@ app.include_router(api_router, prefix="/api/v1") @app.get("/") def root(): return { - "message": f"{settings.PROJECT_NAME} v{settings.VERSION}", + "message": f"{settings.project_name} v{settings.version}", "status": "operational", "docs": "/docs", "features": [