fixing DQ issues

This commit is contained in:
2025-09-14 15:47:38 +02:00
parent 3eb18ef91e
commit 0ce708cf09
27 changed files with 430 additions and 214 deletions

View File

@@ -64,7 +64,7 @@ test-admin:
# Code quality (skip check-tools for now) # Code quality (skip check-tools for now)
lint: lint:
@echo Running flake8... @echo Running flake8...
flake8 . --max-line-length=88 --extend-ignore=E203,W503 --exclude=venv,__pycache__,.git flake8 . --max-line-length=120 --extend-ignore=E203,W503,I201,I100 --exclude=venv,__pycache__,.git
@echo Running mypy... @echo Running mypy...
mypy . --ignore-missing-imports --exclude venv mypy . --ignore-missing-imports --exclude venv

View File

@@ -1,3 +1,12 @@
# app/api/deps.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -17,7 +26,7 @@ def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security), credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""Get current authenticated user""" """Get current authenticated user."""
# Check if credentials are provided # Check if credentials are provided
if not credentials: if not credentials:
raise HTTPException(status_code=401, detail="Authorization header required") raise HTTPException(status_code=401, detail="Authorization header required")
@@ -26,7 +35,7 @@ def get_current_user(
def get_current_admin_user(current_user: User = Depends(get_current_user)): def get_current_admin_user(current_user: User = Depends(get_current_user)):
"""Require admin user""" """Require admin user."""
return auth_manager.require_admin(current_user) return auth_manager.require_admin(current_user)
@@ -35,7 +44,7 @@ def get_user_shop(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""Get shop and verify user ownership""" """Get shop and verify user ownership."""
shop = db.query(Shop).filter(Shop.shop_code == shop_code.upper()).first() shop = db.query(Shop).filter(Shop.shop_code == shop_code.upper()).first()
if not shop: if not shop:
raise HTTPException(status_code=404, detail="Shop not found") raise HTTPException(status_code=404, detail="Shop not found")

View File

@@ -1,3 +1,12 @@
# app/api/main.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
from fastapi import APIRouter from fastapi import APIRouter
from app.api.v1 import admin, auth, marketplace, product, shop, stats, stock from app.api.v1 import admin, auth, marketplace, product, shop, stats, stock

View File

@@ -1,3 +1,12 @@
# app/api/v1/admin.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from typing import List, Optional from typing import List, Optional
@@ -7,8 +16,11 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user from app.api.deps import get_current_admin_user
from app.core.database import get_db from app.core.database import get_db
from app.services.admin_service import admin_service from app.services.admin_service import admin_service
from models.api_models import (MarketplaceImportJobResponse, ShopListResponse, from models.api_models import (
UserResponse) MarketplaceImportJobResponse,
ShopListResponse,
UserResponse,
)
from models.database_models import User from models.database_models import User
router = APIRouter() router = APIRouter()
@@ -23,7 +35,7 @@ def get_all_users(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_user),
): ):
"""Get all users (Admin only)""" """Get all users (Admin only)."""
try: try:
users = admin_service.get_all_users(db=db, skip=skip, limit=limit) users = admin_service.get_all_users(db=db, skip=skip, limit=limit)
return [UserResponse.model_validate(user) for user in users] return [UserResponse.model_validate(user) for user in users]
@@ -38,7 +50,7 @@ def toggle_user_status(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_user),
): ):
"""Toggle user active status (Admin only)""" """Toggle user active status (Admin only)."""
try: try:
user, message = admin_service.toggle_user_status(db, user_id, current_admin.id) user, message = admin_service.toggle_user_status(db, user_id, current_admin.id)
return {"message": message} return {"message": message}
@@ -56,7 +68,7 @@ def get_all_shops_admin(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_user),
): ):
"""Get all shops with admin view (Admin only)""" """Get all shops with admin view (Admin only)."""
try: try:
shops, total = admin_service.get_all_shops(db=db, skip=skip, limit=limit) shops, total = admin_service.get_all_shops(db=db, skip=skip, limit=limit)
@@ -72,7 +84,7 @@ def verify_shop(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_user),
): ):
"""Verify/unverify shop (Admin only)""" """Verify/unverify shop (Admin only)."""
try: try:
shop, message = admin_service.verify_shop(db, shop_id) shop, message = admin_service.verify_shop(db, shop_id)
return {"message": message} return {"message": message}
@@ -89,7 +101,7 @@ def toggle_shop_status(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_user),
): ):
"""Toggle shop active status (Admin only)""" """Toggle shop active status (Admin only)."""
try: try:
shop, message = admin_service.toggle_shop_status(db, shop_id) shop, message = admin_service.toggle_shop_status(db, shop_id)
return {"message": message} return {"message": message}
@@ -112,7 +124,7 @@ def get_all_marketplace_import_jobs(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_user),
): ):
"""Get all marketplace import jobs (Admin only)""" """Get all marketplace import jobs (Admin only)."""
try: try:
return admin_service.get_marketplace_import_jobs( return admin_service.get_marketplace_import_jobs(
db=db, db=db,

View File

@@ -1,3 +1,12 @@
# app/api/v1/auth.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
@@ -6,8 +15,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_user from app.api.deps import get_current_user
from app.core.database import get_db from app.core.database import get_db
from app.services.auth_service import auth_service from app.services.auth_service import auth_service
from models.api_models import (LoginResponse, UserLogin, UserRegister, from models.api_models import LoginResponse, UserLogin, UserRegister, UserResponse
UserResponse)
from models.database_models import User from models.database_models import User
router = APIRouter() router = APIRouter()
@@ -17,7 +25,7 @@ logger = logging.getLogger(__name__)
# Authentication Routes # Authentication Routes
@router.post("/auth/register", response_model=UserResponse) @router.post("/auth/register", response_model=UserResponse)
def register_user(user_data: UserRegister, db: Session = Depends(get_db)): def register_user(user_data: UserRegister, db: Session = Depends(get_db)):
"""Register a new user""" """Register a new user."""
try: try:
user = auth_service.register_user(db=db, user_data=user_data) user = auth_service.register_user(db=db, user_data=user_data)
return UserResponse.model_validate(user) return UserResponse.model_validate(user)
@@ -30,7 +38,7 @@ def register_user(user_data: UserRegister, db: Session = Depends(get_db)):
@router.post("/auth/login", response_model=LoginResponse) @router.post("/auth/login", response_model=LoginResponse)
def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)): def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)):
"""Login user and return JWT token""" """Login user and return JWT token."""
try: try:
login_result = auth_service.login_user(db=db, user_credentials=user_credentials) login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
@@ -49,5 +57,5 @@ def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)):
@router.get("/auth/me", response_model=UserResponse) @router.get("/auth/me", response_model=UserResponse)
def get_current_user_info(current_user: User = Depends(get_current_user)): def get_current_user_info(current_user: User = Depends(get_current_user)):
"""Get current user information""" """Get current user information."""
return UserResponse.model_validate(current_user) return UserResponse.model_validate(current_user)

View File

@@ -1,3 +1,12 @@
# app/api/v1/marketplace.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from typing import List, Optional from typing import List, Optional
@@ -9,8 +18,7 @@ from app.core.database import get_db
from app.services.marketplace_service import marketplace_service from app.services.marketplace_service import marketplace_service
from app.tasks.background_tasks import process_marketplace_import from app.tasks.background_tasks import process_marketplace_import
from middleware.decorators import rate_limit from middleware.decorators import rate_limit
from models.api_models import (MarketplaceImportJobResponse, from models.api_models import MarketplaceImportJobResponse, MarketplaceImportRequest
MarketplaceImportRequest)
from models.database_models import User from models.database_models import User
router = APIRouter() router = APIRouter()
@@ -26,7 +34,7 @@ async def import_products_from_marketplace(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Import products from marketplace CSV with background processing (Protected)""" """Import products from marketplace CSV with background processing (Protected)."""
try: try:
logger.info( 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}"
@@ -73,7 +81,7 @@ def get_marketplace_import_status(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get status of marketplace import job (Protected)""" """Get status of marketplace import job (Protected)."""
try: try:
job = marketplace_service.get_import_job_by_id(db, job_id, current_user) job = marketplace_service.get_import_job_by_id(db, job_id, current_user)
return marketplace_service.convert_to_response_model(job) return marketplace_service.convert_to_response_model(job)
@@ -98,7 +106,7 @@ def get_marketplace_import_jobs(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get marketplace import jobs with filtering (Protected)""" """Get marketplace import jobs with filtering (Protected)."""
try: try:
jobs = marketplace_service.get_import_jobs( jobs = marketplace_service.get_import_jobs(
db=db, db=db,
@@ -120,7 +128,7 @@ def get_marketplace_import_jobs(
def get_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)""" """Get statistics about marketplace import jobs (Protected)."""
try: try:
stats = marketplace_service.get_job_stats(db, current_user) stats = marketplace_service.get_job_stats(db, current_user)
return stats return stats
@@ -139,7 +147,7 @@ def cancel_marketplace_import_job(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Cancel a pending or running marketplace import job (Protected)""" """Cancel a pending or running marketplace import job (Protected)."""
try: try:
job = marketplace_service.cancel_import_job(db, job_id, current_user) job = marketplace_service.cancel_import_job(db, job_id, current_user)
return marketplace_service.convert_to_response_model(job) return marketplace_service.convert_to_response_model(job)
@@ -159,7 +167,7 @@ def delete_marketplace_import_job(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Delete a completed marketplace import job (Protected)""" """Delete a completed marketplace import job (Protected)."""
try: try:
marketplace_service.delete_import_job(db, job_id, current_user) marketplace_service.delete_import_job(db, job_id, current_user)
return {"message": "Marketplace import job deleted successfully"} return {"message": "Marketplace import job deleted successfully"}

View File

@@ -1,3 +1,12 @@
# app/api/v1/product.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from typing import Optional from typing import Optional
@@ -8,9 +17,13 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_user from app.api.deps import get_current_user
from app.core.database import get_db from app.core.database import get_db
from app.services.product_service import product_service from app.services.product_service import product_service
from models.api_models import (ProductCreate, ProductDetailResponse, from models.api_models import (
ProductListResponse, ProductResponse, ProductCreate,
ProductUpdate) ProductDetailResponse,
ProductListResponse,
ProductResponse,
ProductUpdate,
)
from models.database_models import User from models.database_models import User
router = APIRouter() router = APIRouter()
@@ -31,8 +44,7 @@ def get_products(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get products with advanced filtering including marketplace and shop (Protected)""" """Get products with advanced filtering including marketplace and shop (Protected)."""
try: try:
products, total = product_service.get_products_with_filters( products, total = product_service.get_products_with_filters(
db=db, db=db,
@@ -60,8 +72,7 @@ def create_product(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Create a new product with validation and marketplace support (Protected)""" """Create a new product with validation and marketplace support (Protected)."""
try: try:
logger.info(f"Starting product creation for ID: {product.product_id}") logger.info(f"Starting product creation for ID: {product.product_id}")
@@ -99,8 +110,7 @@ def get_product(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get product with stock information (Protected)""" """Get product with stock information (Protected)."""
try: try:
product = product_service.get_product_by_id(db, product_id) product = product_service.get_product_by_id(db, product_id)
if not product: if not product:
@@ -127,8 +137,7 @@ def update_product(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Update product with validation and marketplace support (Protected)""" """Update product with validation and marketplace support (Protected)."""
try: try:
product = product_service.get_product_by_id(db, product_id) product = product_service.get_product_by_id(db, product_id)
if not product: if not product:
@@ -152,8 +161,7 @@ def delete_product(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Delete product and associated stock (Protected)""" """Delete product and associated stock (Protected)."""
try: try:
product = product_service.get_product_by_id(db, product_id) product = product_service.get_product_by_id(db, product_id)
if not product: if not product:
@@ -178,8 +186,7 @@ async def export_csv(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Export products as CSV with streaming and marketplace filtering (Protected)""" """Export products as CSV with streaming and marketplace filtering (Protected)."""
try: try:
def generate_csv(): def generate_csv():

View File

@@ -1,21 +1,29 @@
import logging # app/api/v1/shop.py
from datetime import datetime """Summary description ....
from typing import List, Optional
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_user, get_user_shop from app.api.deps import get_current_user, get_user_shop
from app.core.database import get_db from app.core.database import get_db
from app.services.shop_service import shop_service 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 (
from models.api_models import (MarketplaceImportJobResponse, ShopCreate,
MarketplaceImportRequest, ShopCreate, ShopListResponse,
ShopListResponse, ShopProductCreate, ShopProductCreate,
ShopProductResponse, ShopResponse) ShopProductResponse,
from models.database_models import (MarketplaceImportJob, Product, Shop, ShopResponse,
ShopProduct, User) )
from models.database_models import User
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -28,7 +36,7 @@ def create_shop(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Create a new shop (Protected)""" """Create a new shop (Protected)."""
try: try:
shop = shop_service.create_shop( shop = shop_service.create_shop(
db=db, shop_data=shop_data, current_user=current_user db=db, shop_data=shop_data, current_user=current_user
@@ -50,7 +58,7 @@ def get_shops(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get shops with filtering (Protected)""" """Get shops with filtering (Protected)."""
try: try:
shops, total = shop_service.get_shops( shops, total = shop_service.get_shops(
db=db, db=db,
@@ -75,7 +83,7 @@ def get_shop(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get shop details (Protected)""" """Get shop details (Protected)."""
try: try:
shop = shop_service.get_shop_by_code( shop = shop_service.get_shop_by_code(
db=db, shop_code=shop_code, current_user=current_user db=db, shop_code=shop_code, current_user=current_user
@@ -96,7 +104,7 @@ def add_product_to_shop(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Add existing product to shop catalog with shop-specific settings (Protected)""" """Add existing product to shop catalog with shop-specific settings (Protected)."""
try: try:
# Get and verify shop (using existing dependency) # Get and verify shop (using existing dependency)
shop = get_user_shop(shop_code, current_user, db) shop = get_user_shop(shop_code, current_user, db)
@@ -127,7 +135,7 @@ def get_shop_products(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get products in shop catalog (Protected)""" """Get products in shop catalog (Protected)."""
try: try:
# Get shop # Get shop
shop = shop_service.get_shop_by_code( shop = shop_service.get_shop_by_code(

View File

@@ -1,21 +1,26 @@
import logging # app/api/v1/stats.py
from datetime import datetime """Summary description ....
from typing import List, Optional
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query This module provides classes and functions for:
from sqlalchemy import func - ....
- ....
- ....
"""
import logging
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_user from app.api.deps import get_current_user
from app.core.database import get_db from app.core.database import get_db
from app.services.stats_service import stats_service from app.services.stats_service import stats_service
from app.tasks.background_tasks import process_marketplace_import from models.api_models import (
from middleware.decorators import rate_limit MarketplaceStatsResponse,
from models.api_models import (MarketplaceImportJobResponse, StatsResponse,
MarketplaceImportRequest, )
MarketplaceStatsResponse, StatsResponse) from models.database_models import User
from models.database_models import (MarketplaceImportJob, Product, Shop, Stock,
User)
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -26,7 +31,7 @@ logger = logging.getLogger(__name__)
def get_stats( def get_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 comprehensive statistics with marketplace data (Protected)""" """Get comprehensive statistics with marketplace data (Protected)."""
try: try:
stats_data = stats_service.get_comprehensive_stats(db=db) stats_data = stats_service.get_comprehensive_stats(db=db)
@@ -48,7 +53,7 @@ def get_stats(
def get_marketplace_stats( def get_marketplace_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 broken down by marketplace (Protected)""" """Get statistics broken down by marketplace (Protected)."""
try: try:
marketplace_stats = stats_service.get_marketplace_breakdown_stats(db=db) marketplace_stats = stats_service.get_marketplace_breakdown_stats(db=db)

View File

@@ -1,19 +1,29 @@
# app/api/v1/stock.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_user from app.api.deps import get_current_user
from app.core.database import get_db from app.core.database import get_db
from app.services.stock_service import stock_service from app.services.stock_service import stock_service
from app.tasks.background_tasks import process_marketplace_import from models.api_models import (
from middleware.decorators import rate_limit StockAdd,
from models.api_models import (MarketplaceImportJobResponse, StockCreate,
MarketplaceImportRequest, StockAdd, StockCreate, StockResponse,
StockResponse, StockSummaryResponse, StockSummaryResponse,
StockUpdate) StockUpdate,
from models.database_models import MarketplaceImportJob, Shop, User )
from models.database_models import User
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -28,7 +38,7 @@ def set_stock(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)""" """Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)."""
try: try:
result = stock_service.set_stock(db, stock) result = stock_service.set_stock(db, stock)
return result return result
@@ -45,7 +55,7 @@ def add_stock(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)""" """Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)."""
try: try:
result = stock_service.add_stock(db, stock) result = stock_service.add_stock(db, stock)
return result return result
@@ -62,7 +72,7 @@ def remove_stock(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Remove quantity from existing stock for a GTIN at a specific location""" """Remove quantity from existing stock for a GTIN at a specific location."""
try: try:
result = stock_service.remove_stock(db, stock) result = stock_service.remove_stock(db, stock)
return result return result
@@ -79,7 +89,7 @@ def get_stock_by_gtin(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get all stock locations and total quantity for a specific GTIN""" """Get all stock locations and total quantity for a specific GTIN."""
try: try:
result = stock_service.get_stock_by_gtin(db, gtin) result = stock_service.get_stock_by_gtin(db, gtin)
return result return result
@@ -96,7 +106,7 @@ def get_total_stock(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get total quantity in stock for a specific GTIN""" """Get total quantity in stock for a specific GTIN."""
try: try:
result = stock_service.get_total_stock(db, gtin) result = stock_service.get_total_stock(db, gtin)
return result return result
@@ -116,7 +126,7 @@ def get_all_stock(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get all stock entries with optional filtering""" """Get all stock entries with optional filtering."""
try: try:
result = stock_service.get_all_stock( 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
@@ -134,7 +144,7 @@ def update_stock(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Update stock quantity for a specific stock entry""" """Update stock quantity for a specific stock entry."""
try: try:
result = stock_service.update_stock(db, stock_id, stock_update) result = stock_service.update_stock(db, stock_id, stock_update)
return result return result
@@ -151,7 +161,7 @@ def delete_stock(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Delete a stock entry""" """Delete a stock entry."""
try: try:
stock_service.delete_stock(db, stock_id) stock_service.delete_stock(db, stock_id)
return {"message": "Stock entry deleted successfully"} return {"message": "Stock entry deleted successfully"}

View File

@@ -1,4 +1,12 @@
# app/core/config.py # app/core/config.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
from typing import List, Optional from typing import List, Optional
from pydantic_settings import \ from pydantic_settings import \
@@ -6,6 +14,8 @@ from pydantic_settings import \
class Settings(BaseSettings): class Settings(BaseSettings):
"""Settings class inheriting from BaseSettings that allows values to be overridden by environment variables."""
# Project information # Project information
project_name: str = "Ecommerce Backend API with Marketplace Support" project_name: str = "Ecommerce Backend API with Marketplace Support"
description: str = "Advanced product management system with JWT authentication" description: str = "Advanced product management system with JWT authentication"

View File

@@ -1,3 +1,14 @@
# app/core/database.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker from sqlalchemy.orm import declarative_base, sessionmaker
@@ -8,13 +19,18 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base() Base = declarative_base()
logger = logging.getLogger(__name__)
# Database dependency with connection pooling # Database dependency with connection pooling
def get_db(): def get_db():
"""Get database object."""
db = SessionLocal() db = SessionLocal()
try: try:
yield db yield db
except Exception as e: except Exception as e:
logger.error(f"Health check failed: {e}")
db.rollback() db.rollback()
raise raise
finally: finally:

View File

@@ -1,3 +1,12 @@
# app/core/lifespan.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@@ -16,7 +25,7 @@ auth_manager = AuthManager()
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Application lifespan events""" """Application lifespan events."""
# Startup # Startup
app_logger = setup_logging() # Configure logging first app_logger = setup_logging() # Configure logging first
app_logger.info("Starting up ecommerce API") app_logger.info("Starting up ecommerce API")
@@ -43,7 +52,7 @@ async def lifespan(app: FastAPI):
def create_indexes(): def create_indexes():
"""Create database indexes""" """Create database indexes."""
with engine.connect() as conn: with engine.connect() as conn:
try: try:
# User indexes # User indexes

View File

@@ -1,4 +1,12 @@
# app/core/logging.py # app/core/logging.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
import sys import sys
from pathlib import Path from pathlib import Path
@@ -7,8 +15,7 @@ from app.core.config import settings
def setup_logging(): def setup_logging():
"""Configure application logging with file and console handlers""" """Configure application logging with file and console handlers."""
# Create logs directory if it doesn't exist # 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) log_file.parent.mkdir(parents=True, exist_ok=True)

View File

@@ -1,3 +1,12 @@
# app/services/admin_service.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from datetime import datetime from datetime import datetime
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
@@ -12,17 +21,17 @@ logger = logging.getLogger(__name__)
class AdminService: class AdminService:
"""Service class for admin operations following the application's service pattern""" """Service class for admin operations following the application's service pattern."""
def get_all_users(self, db: Session, skip: int = 0, limit: int = 100) -> List[User]: def get_all_users(self, db: Session, skip: int = 0, limit: int = 100) -> List[User]:
"""Get paginated list of all users""" """Get paginated list of all users."""
return db.query(User).offset(skip).limit(limit).all() return db.query(User).offset(skip).limit(limit).all()
def toggle_user_status( def toggle_user_status(
self, db: Session, user_id: int, current_admin_id: int self, db: Session, user_id: int, current_admin_id: int
) -> Tuple[User, str]: ) -> Tuple[User, str]:
""" """
Toggle user active status Toggle user active status.
Args: Args:
db: Database session db: Database session
@@ -59,7 +68,7 @@ class AdminService:
self, db: Session, skip: int = 0, limit: int = 100 self, db: Session, skip: int = 0, limit: int = 100
) -> Tuple[List[Shop], int]: ) -> Tuple[List[Shop], int]:
""" """
Get paginated list of all shops with total count Get paginated list of all shops with total count.
Args: Args:
db: Database session db: Database session
@@ -75,7 +84,7 @@ class AdminService:
def verify_shop(self, db: Session, shop_id: int) -> Tuple[Shop, str]: def verify_shop(self, db: Session, shop_id: int) -> Tuple[Shop, str]:
""" """
Toggle shop verification status Toggle shop verification status.
Args: Args:
db: Database session db: Database session
@@ -102,7 +111,7 @@ class AdminService:
def toggle_shop_status(self, db: Session, shop_id: int) -> Tuple[Shop, str]: def toggle_shop_status(self, db: Session, shop_id: int) -> Tuple[Shop, str]:
""" """
Toggle shop active status Toggle shop active status.
Args: Args:
db: Database session db: Database session
@@ -137,7 +146,7 @@ class AdminService:
limit: int = 100, limit: int = 100,
) -> List[MarketplaceImportJobResponse]: ) -> List[MarketplaceImportJobResponse]:
""" """
Get filtered and paginated marketplace import jobs Get filtered and paginated marketplace import jobs.
Args: Args:
db: Database session db: Database session
@@ -190,19 +199,19 @@ class AdminService:
] ]
def get_user_by_id(self, db: Session, user_id: int) -> Optional[User]: def get_user_by_id(self, db: Session, user_id: int) -> Optional[User]:
"""Get user by ID""" """Get user by ID."""
return db.query(User).filter(User.id == user_id).first() return db.query(User).filter(User.id == user_id).first()
def get_shop_by_id(self, db: Session, shop_id: int) -> Optional[Shop]: def get_shop_by_id(self, db: Session, shop_id: int) -> Optional[Shop]:
"""Get shop by ID""" """Get shop by ID."""
return db.query(Shop).filter(Shop.id == shop_id).first() return db.query(Shop).filter(Shop.id == shop_id).first()
def user_exists(self, db: Session, user_id: int) -> bool: def user_exists(self, db: Session, user_id: int) -> bool:
"""Check if user exists by ID""" """Check if user exists by ID."""
return db.query(User).filter(User.id == user_id).first() is not None return db.query(User).filter(User.id == user_id).first() is not None
def shop_exists(self, db: Session, shop_id: int) -> bool: def shop_exists(self, db: Session, shop_id: int) -> bool:
"""Check if shop exists by ID""" """Check if shop exists by ID."""
return db.query(Shop).filter(Shop.id == shop_id).first() is not None return db.query(Shop).filter(Shop.id == shop_id).first() is not None

View File

@@ -1,3 +1,12 @@
# app/services/auth_service.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
@@ -12,9 +21,10 @@ logger = logging.getLogger(__name__)
class AuthService: class AuthService:
"""Service class for authentication operations following the application's service pattern""" """Service class for authentication operations following the application's service pattern."""
def __init__(self): def __init__(self):
"""Class constructor."""
self.auth_manager = AuthManager() self.auth_manager = AuthManager()
def register_user(self, db: Session, user_data: UserRegister) -> User: def register_user(self, db: Session, user_data: UserRegister) -> User:
@@ -62,7 +72,7 @@ class AuthService:
def login_user(self, db: Session, user_credentials: UserLogin) -> Dict[str, Any]: def login_user(self, db: Session, user_credentials: UserLogin) -> Dict[str, Any]:
""" """
Login user and return JWT token with user data Login user and return JWT token with user data.
Args: Args:
db: Database session db: Database session
@@ -90,33 +100,33 @@ class AuthService:
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]: def get_user_by_email(self, db: Session, email: str) -> Optional[User]:
"""Get user by email""" """Get user by email."""
return db.query(User).filter(User.email == email).first() return db.query(User).filter(User.email == email).first()
def get_user_by_username(self, db: Session, username: str) -> Optional[User]: def get_user_by_username(self, db: Session, username: str) -> Optional[User]:
"""Get user by username""" """Get user by username."""
return db.query(User).filter(User.username == username).first() return db.query(User).filter(User.username == username).first()
def email_exists(self, db: Session, email: str) -> bool: def email_exists(self, db: Session, email: str) -> bool:
"""Check if email already exists""" """Check if email already exists."""
return db.query(User).filter(User.email == email).first() is not None return db.query(User).filter(User.email == email).first() is not None
def username_exists(self, db: Session, username: str) -> bool: def username_exists(self, db: Session, username: str) -> bool:
"""Check if username already exists""" """Check if username already exists."""
return db.query(User).filter(User.username == username).first() is not None return db.query(User).filter(User.username == username).first() is not None
def authenticate_user( def authenticate_user(
self, db: Session, username: str, password: str self, db: Session, username: str, password: str
) -> Optional[User]: ) -> Optional[User]:
"""Authenticate user with username/password""" """Authenticate user with username/password."""
return self.auth_manager.authenticate_user(db, username, password) return self.auth_manager.authenticate_user(db, username, password)
def create_access_token(self, user: User) -> Dict[str, Any]: def create_access_token(self, user: User) -> Dict[str, Any]:
"""Create access token for user""" """Create access token for user."""
return self.auth_manager.create_access_token(user) return self.auth_manager.create_access_token(user)
def hash_password(self, password: str) -> str: def hash_password(self, password: str) -> str:
"""Hash password""" """Hash password."""
return self.auth_manager.hash_password(password) return self.auth_manager.hash_password(password)

View File

@@ -1,3 +1,12 @@
# app/services/marketplace_service.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
@@ -14,6 +23,7 @@ logger = logging.getLogger(__name__)
class MarketplaceService: class MarketplaceService:
def __init__(self): def __init__(self):
"""Class constructor."""
pass pass
def validate_shop_access(self, db: Session, shop_code: str, user: User) -> Shop: def validate_shop_access(self, db: Session, shop_code: str, user: User) -> Shop:
@@ -37,7 +47,7 @@ class MarketplaceService:
def create_import_job( def create_import_job(
self, db: Session, request: MarketplaceImportRequest, user: User self, db: Session, request: MarketplaceImportRequest, user: User
) -> MarketplaceImportJob: ) -> MarketplaceImportJob:
"""Create a new marketplace import job""" """Create a new marketplace import job."""
# Validate shop access first # Validate shop access first
shop = self.validate_shop_access(db, request.shop_code, user) shop = self.validate_shop_access(db, request.shop_code, user)
@@ -65,7 +75,7 @@ class MarketplaceService:
def get_import_job_by_id( def get_import_job_by_id(
self, db: Session, job_id: int, user: User self, db: Session, job_id: int, user: User
) -> MarketplaceImportJob: ) -> MarketplaceImportJob:
"""Get a marketplace import job by ID with access control""" """Get a marketplace import job by ID with access control."""
job = ( job = (
db.query(MarketplaceImportJob) db.query(MarketplaceImportJob)
.filter(MarketplaceImportJob.id == job_id) .filter(MarketplaceImportJob.id == job_id)
@@ -89,7 +99,7 @@ class MarketplaceService:
skip: int = 0, skip: int = 0,
limit: int = 50, limit: int = 50,
) -> List[MarketplaceImportJob]: ) -> List[MarketplaceImportJob]:
"""Get marketplace import jobs with filtering and access control""" """Get marketplace import jobs with filtering and access control."""
query = db.query(MarketplaceImportJob) query = db.query(MarketplaceImportJob)
# Users can only see their own jobs, admins can see all # Users can only see their own jobs, admins can see all
@@ -117,7 +127,7 @@ class MarketplaceService:
def update_job_status( def update_job_status(
self, db: Session, job_id: int, status: str, **kwargs self, db: Session, job_id: int, status: str, **kwargs
) -> MarketplaceImportJob: ) -> MarketplaceImportJob:
"""Update marketplace import job status and other fields""" """Update marketplace import job status and other fields."""
job = ( job = (
db.query(MarketplaceImportJob) db.query(MarketplaceImportJob)
.filter(MarketplaceImportJob.id == job_id) .filter(MarketplaceImportJob.id == job_id)
@@ -151,7 +161,7 @@ class MarketplaceService:
return job return job
def get_job_stats(self, db: Session, user: User) -> dict: def get_job_stats(self, db: Session, user: User) -> dict:
"""Get statistics about marketplace import jobs for a user""" """Get statistics about marketplace import jobs for a user."""
query = db.query(MarketplaceImportJob) query = db.query(MarketplaceImportJob)
# Users can only see their own jobs, admins can see all # Users can only see their own jobs, admins can see all
@@ -177,7 +187,7 @@ class MarketplaceService:
def convert_to_response_model( def convert_to_response_model(
self, job: MarketplaceImportJob self, job: MarketplaceImportJob
) -> MarketplaceImportJobResponse: ) -> MarketplaceImportJobResponse:
"""Convert database model to API response model""" """Convert database model to API response model."""
return MarketplaceImportJobResponse( return MarketplaceImportJobResponse(
job_id=job.id, job_id=job.id,
status=job.status, status=job.status,
@@ -200,7 +210,7 @@ class MarketplaceService:
def cancel_import_job( def cancel_import_job(
self, db: Session, job_id: int, user: User self, db: Session, job_id: int, user: User
) -> MarketplaceImportJob: ) -> MarketplaceImportJob:
"""Cancel a pending or running import job""" """Cancel a pending or running import job."""
job = self.get_import_job_by_id(db, job_id, user) job = self.get_import_job_by_id(db, job_id, user)
if job.status not in ["pending", "running"]: if job.status not in ["pending", "running"]:
@@ -216,7 +226,7 @@ class MarketplaceService:
return job return job
def delete_import_job(self, db: Session, job_id: int, user: User) -> bool: def delete_import_job(self, db: Session, job_id: int, user: User) -> bool:
"""Delete a marketplace import job""" """Delete a marketplace import job."""
job = self.get_import_job_by_id(db, job_id, user) job = self.get_import_job_by_id(db, job_id, user)
# Only allow deletion of completed, failed, or cancelled jobs # Only allow deletion of completed, failed, or cancelled jobs

View File

@@ -1,3 +1,12 @@
# app/services/product_service.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Generator, List, Optional from typing import Generator, List, Optional
@@ -15,11 +24,12 @@ logger = logging.getLogger(__name__)
class ProductService: class ProductService:
def __init__(self): def __init__(self):
"""Class constructor."""
self.gtin_processor = GTINProcessor() self.gtin_processor = GTINProcessor()
self.price_processor = PriceProcessor() self.price_processor = PriceProcessor()
def create_product(self, db: Session, product_data: ProductCreate) -> Product: def create_product(self, db: Session, product_data: ProductCreate) -> Product:
"""Create a new product with validation""" """Create a new product with validation."""
try: try:
# Process and validate GTIN if provided # Process and validate GTIN if provided
if product_data.gtin: if product_data.gtin:
@@ -59,7 +69,7 @@ class ProductService:
raise raise
def get_product_by_id(self, db: Session, product_id: str) -> Optional[Product]: def get_product_by_id(self, db: Session, product_id: str) -> Optional[Product]:
"""Get a product by its ID""" """Get a product by its ID."""
return db.query(Product).filter(Product.product_id == product_id).first() return db.query(Product).filter(Product.product_id == product_id).first()
def get_products_with_filters( def get_products_with_filters(
@@ -74,7 +84,7 @@ class ProductService:
shop_name: Optional[str] = None, shop_name: Optional[str] = None,
search: Optional[str] = None, search: Optional[str] = None,
) -> tuple[List[Product], int]: ) -> tuple[List[Product], int]:
"""Get products with filtering and pagination""" """Get products with filtering and pagination."""
query = db.query(Product) query = db.query(Product)
# Apply filters # Apply filters
@@ -106,7 +116,7 @@ class ProductService:
def update_product( def update_product(
self, db: Session, product_id: str, product_update: ProductUpdate self, db: Session, product_id: str, product_update: ProductUpdate
) -> Product: ) -> Product:
"""Update product with validation""" """Update product with validation."""
product = db.query(Product).filter(Product.product_id == product_id).first() product = db.query(Product).filter(Product.product_id == product_id).first()
if not product: if not product:
raise ValueError("Product not found") raise ValueError("Product not found")
@@ -141,7 +151,7 @@ class ProductService:
return product return product
def delete_product(self, db: Session, product_id: str) -> bool: def delete_product(self, db: Session, product_id: str) -> bool:
"""Delete product and associated stock""" """Delete product and associated stock."""
product = db.query(Product).filter(Product.product_id == product_id).first() product = db.query(Product).filter(Product.product_id == product_id).first()
if not product: if not product:
raise ValueError("Product not found") raise ValueError("Product not found")
@@ -157,7 +167,7 @@ class ProductService:
return True return True
def get_stock_info(self, db: Session, gtin: str) -> Optional[StockSummaryResponse]: def get_stock_info(self, db: Session, gtin: str) -> Optional[StockSummaryResponse]:
"""Get stock information for a product by GTIN""" """Get stock information for a product by GTIN."""
stock_entries = db.query(Stock).filter(Stock.gtin == gtin).all() stock_entries = db.query(Stock).filter(Stock.gtin == gtin).all()
if not stock_entries: if not stock_entries:
return None return None
@@ -178,7 +188,7 @@ class ProductService:
marketplace: Optional[str] = None, marketplace: Optional[str] = None,
shop_name: Optional[str] = None, shop_name: Optional[str] = None,
) -> Generator[str, None, None]: ) -> Generator[str, None, None]:
"""Generate CSV export with streaming for memory efficiency""" """Generate CSV export with streaming for memory efficiency."""
# CSV header # CSV header
yield ( yield (
"product_id,title,description,link,image_link,availability,price,currency,brand," "product_id,title,description,link,image_link,availability,price,currency,brand,"
@@ -214,7 +224,7 @@ class ProductService:
offset += batch_size offset += batch_size
def product_exists(self, db: Session, product_id: str) -> bool: def product_exists(self, db: Session, product_id: str) -> bool:
"""Check if product exists by ID""" """Check if product exists by ID."""
return ( return (
db.query(Product).filter(Product.product_id == product_id).first() db.query(Product).filter(Product.product_id == product_id).first()
is not None is not None

View File

@@ -1,6 +1,15 @@
# app/services/shop_service.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple from typing import List, Optional, Tuple
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import func from sqlalchemy import func
@@ -13,7 +22,7 @@ logger = logging.getLogger(__name__)
class ShopService: class ShopService:
"""Service class for shop operations following the application's service pattern""" """Service class for shop operations following the application's service pattern."""
def create_shop( def create_shop(
self, db: Session, shop_data: ShopCreate, current_user: User self, db: Session, shop_data: ShopCreate, current_user: User
@@ -75,7 +84,7 @@ class ShopService:
verified_only: bool = False, verified_only: bool = False,
) -> Tuple[List[Shop], int]: ) -> Tuple[List[Shop], int]:
""" """
Get shops with filtering Get shops with filtering.
Args: Args:
db: Database session db: Database session
@@ -110,7 +119,7 @@ class ShopService:
def get_shop_by_code(self, db: Session, shop_code: str, current_user: User) -> Shop: def get_shop_by_code(self, db: Session, shop_code: str, current_user: User) -> Shop:
""" """
Get shop by shop code with access control Get shop by shop code with access control.
Args: Args:
db: Database session db: Database session
@@ -145,7 +154,7 @@ class ShopService:
self, db: Session, shop: Shop, shop_product: ShopProductCreate self, db: Session, shop: Shop, shop_product: ShopProductCreate
) -> ShopProduct: ) -> ShopProduct:
""" """
Add existing product to shop catalog with shop-specific settings Add existing product to shop catalog with shop-specific settings.
Args: Args:
db: Database session db: Database session
@@ -211,7 +220,7 @@ class ShopService:
featured_only: bool = False, featured_only: bool = False,
) -> Tuple[List[ShopProduct], int]: ) -> Tuple[List[ShopProduct], int]:
""" """
Get products in shop catalog with filtering Get products in shop catalog with filtering.
Args: Args:
db: Database session db: Database session

View File

@@ -1,21 +1,29 @@
# app/services/stats_service.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from typing import Any, Dict, List from typing import Any, Dict, List
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from models.api_models import MarketplaceStatsResponse, StatsResponse from models.database_models import Product, Stock
from models.database_models import Product, Stock, User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class StatsService: class StatsService:
"""Service class for statistics operations following the application's service pattern""" """Service class for statistics operations following the application's service pattern."""
def get_comprehensive_stats(self, db: Session) -> Dict[str, Any]: def get_comprehensive_stats(self, db: Session) -> Dict[str, Any]:
""" """
Get comprehensive statistics with marketplace data Get comprehensive statistics with marketplace data.
Args: Args:
db: Database session db: Database session
@@ -79,7 +87,7 @@ class StatsService:
def get_marketplace_breakdown_stats(self, db: Session) -> List[Dict[str, Any]]: def get_marketplace_breakdown_stats(self, db: Session) -> List[Dict[str, Any]]:
""" """
Get statistics broken down by marketplace Get statistics broken down by marketplace.
Args: Args:
db: Database session db: Database session
@@ -116,11 +124,11 @@ class StatsService:
return stats_list return stats_list
def get_product_count(self, db: Session) -> int: def get_product_count(self, db: Session) -> int:
"""Get total product count""" """Get total product count."""
return db.query(Product).count() return db.query(Product).count()
def get_unique_brands_count(self, db: Session) -> int: def get_unique_brands_count(self, db: Session) -> int:
"""Get count of unique brands""" """Get count of unique brands."""
return ( return (
db.query(Product.brand) db.query(Product.brand)
.filter(Product.brand.isnot(None), Product.brand != "") .filter(Product.brand.isnot(None), Product.brand != "")
@@ -129,7 +137,7 @@ class StatsService:
) )
def get_unique_categories_count(self, db: Session) -> int: def get_unique_categories_count(self, db: Session) -> int:
"""Get count of unique categories""" """Get count of unique categories."""
return ( return (
db.query(Product.google_product_category) db.query(Product.google_product_category)
.filter( .filter(
@@ -141,7 +149,7 @@ class StatsService:
) )
def get_unique_marketplaces_count(self, db: Session) -> int: def get_unique_marketplaces_count(self, db: Session) -> int:
"""Get count of unique marketplaces""" """Get count of unique marketplaces."""
return ( return (
db.query(Product.marketplace) db.query(Product.marketplace)
.filter(Product.marketplace.isnot(None), Product.marketplace != "") .filter(Product.marketplace.isnot(None), Product.marketplace != "")
@@ -150,7 +158,7 @@ class StatsService:
) )
def get_unique_shops_count(self, db: Session) -> int: def get_unique_shops_count(self, db: Session) -> int:
"""Get count of unique shops""" """Get count of unique shops."""
return ( return (
db.query(Product.shop_name) db.query(Product.shop_name)
.filter(Product.shop_name.isnot(None), Product.shop_name != "") .filter(Product.shop_name.isnot(None), Product.shop_name != "")
@@ -160,7 +168,7 @@ class StatsService:
def get_stock_statistics(self, db: Session) -> Dict[str, int]: def get_stock_statistics(self, db: Session) -> Dict[str, int]:
""" """
Get stock-related statistics Get stock-related statistics.
Args: Args:
db: Database session db: Database session
@@ -177,7 +185,7 @@ class StatsService:
} }
def get_brands_by_marketplace(self, db: Session, marketplace: str) -> List[str]: def get_brands_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
"""Get unique brands for a specific marketplace""" """Get unique brands for a specific marketplace."""
brands = ( brands = (
db.query(Product.brand) db.query(Product.brand)
.filter( .filter(
@@ -191,7 +199,7 @@ class StatsService:
return [brand[0] for brand in brands] return [brand[0] for brand in brands]
def get_shops_by_marketplace(self, db: Session, marketplace: str) -> List[str]: def get_shops_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
"""Get unique shops for a specific marketplace""" """Get unique shops for a specific marketplace."""
shops = ( shops = (
db.query(Product.shop_name) db.query(Product.shop_name)
.filter( .filter(
@@ -205,7 +213,7 @@ class StatsService:
return [shop[0] for shop in shops] return [shop[0] for shop in shops]
def get_products_by_marketplace(self, db: Session, marketplace: str) -> int: def get_products_by_marketplace(self, db: Session, marketplace: str) -> int:
"""Get product count for a specific marketplace""" """Get product count for a specific marketplace."""
return db.query(Product).filter(Product.marketplace == marketplace).count() return db.query(Product).filter(Product.marketplace == marketplace).count()

View File

@@ -1,6 +1,15 @@
# app/services/stock_service.py
"""Summary description ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from datetime import datetime from datetime import datetime
from typing import List, Optional, Tuple from typing import List, Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -13,15 +22,17 @@ logger = logging.getLogger(__name__)
class StockService: class StockService:
"""Service class for stock operations following the application's service pattern."""
def __init__(self): def __init__(self):
"""Class constructor."""
self.gtin_processor = GTINProcessor() self.gtin_processor = GTINProcessor()
def normalize_gtin(self, gtin_value) -> Optional[str]: def normalize_gtin(self, gtin_value) -> Optional[str]:
"""Normalize GTIN format using the GTINProcessor""" """Normalize GTIN format using the GTINProcessor."""
return self.gtin_processor.normalize(gtin_value) return self.gtin_processor.normalize(gtin_value)
def set_stock(self, db: Session, stock_data: StockCreate) -> Stock: def set_stock(self, db: Session, stock_data: StockCreate) -> Stock:
"""Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)""" """Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)."""
normalized_gtin = self.normalize_gtin(stock_data.gtin) normalized_gtin = self.normalize_gtin(stock_data.gtin)
if not normalized_gtin: if not normalized_gtin:
raise ValueError("Invalid GTIN format") raise ValueError("Invalid GTIN format")
@@ -60,7 +71,7 @@ class StockService:
return new_stock return new_stock
def add_stock(self, db: Session, stock_data: StockAdd) -> Stock: def add_stock(self, db: Session, stock_data: StockAdd) -> Stock:
"""Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)""" """Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)."""
normalized_gtin = self.normalize_gtin(stock_data.gtin) normalized_gtin = self.normalize_gtin(stock_data.gtin)
if not normalized_gtin: if not normalized_gtin:
raise ValueError("Invalid GTIN format") raise ValueError("Invalid GTIN format")
@@ -82,7 +93,8 @@ class StockService:
db.commit() db.commit()
db.refresh(existing_stock) db.refresh(existing_stock)
logger.info( 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}: "
f"{old_quantity} + {stock_data.quantity} = {existing_stock.quantity}"
) )
return existing_stock return existing_stock
else: else:
@@ -99,7 +111,7 @@ class StockService:
return new_stock return new_stock
def remove_stock(self, db: Session, stock_data: StockAdd) -> Stock: def remove_stock(self, db: Session, stock_data: StockAdd) -> Stock:
"""Remove quantity from existing stock for a GTIN at a specific location""" """Remove quantity from existing stock for a GTIN at a specific location."""
normalized_gtin = self.normalize_gtin(stock_data.gtin) normalized_gtin = self.normalize_gtin(stock_data.gtin)
if not normalized_gtin: if not normalized_gtin:
raise ValueError("Invalid GTIN format") raise ValueError("Invalid GTIN format")
@@ -131,12 +143,13 @@ class StockService:
db.commit() db.commit()
db.refresh(existing_stock) db.refresh(existing_stock)
logger.info( 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}: "
f"{old_quantity} - {stock_data.quantity} = {existing_stock.quantity}"
) )
return existing_stock return existing_stock
def get_stock_by_gtin(self, db: Session, gtin: str) -> StockSummaryResponse: def get_stock_by_gtin(self, db: Session, gtin: str) -> StockSummaryResponse:
"""Get all stock locations and total quantity for a specific GTIN""" """Get all stock locations and total quantity for a specific GTIN."""
normalized_gtin = self.normalize_gtin(gtin) normalized_gtin = self.normalize_gtin(gtin)
if not normalized_gtin: if not normalized_gtin:
raise ValueError("Invalid GTIN format") raise ValueError("Invalid GTIN format")
@@ -169,7 +182,7 @@ class StockService:
) )
def get_total_stock(self, db: Session, gtin: str) -> dict: def get_total_stock(self, db: Session, gtin: str) -> dict:
"""Get total quantity in stock for a specific GTIN""" """Get total quantity in stock for a specific GTIN."""
normalized_gtin = self.normalize_gtin(gtin) normalized_gtin = self.normalize_gtin(gtin)
if not normalized_gtin: if not normalized_gtin:
raise ValueError("Invalid GTIN format") raise ValueError("Invalid GTIN format")
@@ -196,7 +209,7 @@ class StockService:
location: Optional[str] = None, location: Optional[str] = None,
gtin: Optional[str] = None, gtin: Optional[str] = None,
) -> List[Stock]: ) -> List[Stock]:
"""Get all stock entries with optional filtering""" """Get all stock entries with optional filtering."""
query = db.query(Stock) query = db.query(Stock)
if location: if location:
@@ -212,7 +225,7 @@ class StockService:
def update_stock( def update_stock(
self, db: Session, stock_id: int, stock_update: StockUpdate self, db: Session, stock_id: int, stock_update: StockUpdate
) -> Stock: ) -> Stock:
"""Update stock quantity for a specific stock entry""" """Update stock quantity for a specific stock entry."""
stock_entry = db.query(Stock).filter(Stock.id == stock_id).first() stock_entry = db.query(Stock).filter(Stock.id == stock_id).first()
if not stock_entry: if not stock_entry:
raise ValueError("Stock entry not found") raise ValueError("Stock entry not found")
@@ -228,7 +241,7 @@ class StockService:
return stock_entry return stock_entry
def delete_stock(self, db: Session, stock_id: int) -> bool: def delete_stock(self, db: Session, stock_id: int) -> bool:
"""Delete a stock entry""" """Delete a stock entry."""
stock_entry = db.query(Stock).filter(Stock.id == stock_id).first() stock_entry = db.query(Stock).filter(Stock.id == stock_id).first()
if not stock_entry: if not stock_entry:
raise ValueError("Stock entry not found") raise ValueError("Stock entry not found")
@@ -242,7 +255,7 @@ class StockService:
return True return True
def get_stock_by_id(self, db: Session, stock_id: int) -> Optional[Stock]: def get_stock_by_id(self, db: Session, stock_id: int) -> Optional[Stock]:
"""Get a stock entry by its ID""" """Get a stock entry by its ID."""
return db.query(Stock).filter(Stock.id == stock_id).first() return db.query(Stock).filter(Stock.id == stock_id).first()

View File

@@ -19,9 +19,10 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class AuthManager: class AuthManager:
"""JWT-based authentication manager with bcrypt password hashing""" """JWT-based authentication manager with bcrypt password hashing."""
def __init__(self): def __init__(self):
"""Class constructor."""
self.secret_key = os.getenv( self.secret_key = os.getenv(
"JWT_SECRET_KEY", "your-secret-key-change-in-production-please" "JWT_SECRET_KEY", "your-secret-key-change-in-production-please"
) )

View File

@@ -11,6 +11,7 @@ class RateLimiter:
"""In-memory rate limiter using sliding window""" """In-memory rate limiter using sliding window"""
def __init__(self): def __init__(self):
"""Class constructor."""
# Dictionary to store request timestamps for each client # Dictionary to store request timestamps for each client
self.clients: Dict[str, deque] = defaultdict(lambda: deque()) self.clients: Dict[str, deque] = defaultdict(lambda: deque())
self.cleanup_interval = 3600 # Clean up old entries every hour self.cleanup_interval = 3600 # Clean up old entries every hour

View File

@@ -13,7 +13,7 @@ class TestStockService:
self.service = StockService() self.service = StockService()
def test_normalize_gtin_invalid(self): def test_normalize_gtin_invalid(self):
"""Test GTIN normalization with invalid GTINs""" """Test GTIN normalization with invalid GTINs."""
# Completely invalid values that should return None # Completely invalid values that should return None
assert self.service.normalize_gtin("invalid") is None assert self.service.normalize_gtin("invalid") is None
assert self.service.normalize_gtin("abcdef") is None assert self.service.normalize_gtin("abcdef") is None
@@ -36,7 +36,7 @@ class TestStockService:
assert self.service.normalize_gtin("12345") == "0000000012345" assert self.service.normalize_gtin("12345") == "0000000012345"
def test_normalize_gtin_valid(self): def test_normalize_gtin_valid(self):
"""Test GTIN normalization with valid GTINs""" """Test GTIN normalization with valid GTINs."""
# Test various valid GTIN formats - these should remain unchanged # Test various valid GTIN formats - these should remain unchanged
assert self.service.normalize_gtin("1234567890123") == "1234567890123" # EAN-13 assert self.service.normalize_gtin("1234567890123") == "1234567890123" # EAN-13
assert self.service.normalize_gtin("123456789012") == "123456789012" # UPC-A assert self.service.normalize_gtin("123456789012") == "123456789012" # UPC-A
@@ -63,7 +63,7 @@ class TestStockService:
) # Truncated to 13 ) # Truncated to 13
def test_normalize_gtin_edge_cases(self): def test_normalize_gtin_edge_cases(self):
"""Test GTIN normalization edge cases""" """Test GTIN normalization edge cases."""
# Test numeric inputs # Test numeric inputs
assert self.service.normalize_gtin(1234567890123) == "1234567890123" assert self.service.normalize_gtin(1234567890123) == "1234567890123"
assert self.service.normalize_gtin(123) == "0000000000123" assert self.service.normalize_gtin(123) == "0000000000123"
@@ -80,7 +80,7 @@ class TestStockService:
) # Letters removed ) # Letters removed
def test_set_stock_new_entry(self, db): def test_set_stock_new_entry(self, db):
"""Test setting stock for a new GTIN/location combination""" """Test setting stock for a new GTIN/location combination."""
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
stock_data = StockCreate( stock_data = StockCreate(
gtin="1234567890123", location=f"WAREHOUSE_A_{unique_id}", quantity=100 gtin="1234567890123", location=f"WAREHOUSE_A_{unique_id}", quantity=100
@@ -93,7 +93,7 @@ class TestStockService:
assert result.quantity == 100 assert result.quantity == 100
def test_set_stock_existing_entry(self, db, test_stock): def test_set_stock_existing_entry(self, db, test_stock):
"""Test setting stock for an existing GTIN/location combination""" """Test setting stock for an existing GTIN/location combination."""
stock_data = StockCreate( stock_data = StockCreate(
gtin=test_stock.gtin, gtin=test_stock.gtin,
location=test_stock.location, # Use exact same location as test_stock location=test_stock.location, # Use exact same location as test_stock
@@ -108,7 +108,7 @@ class TestStockService:
assert result.quantity == 200 # Should replace the original quantity assert result.quantity == 200 # Should replace the original quantity
def test_set_stock_invalid_gtin(self, db): def test_set_stock_invalid_gtin(self, db):
"""Test setting stock with invalid GTIN""" """Test setting stock with invalid GTIN."""
stock_data = StockCreate( stock_data = StockCreate(
gtin="invalid_gtin", location="WAREHOUSE_A", quantity=100 gtin="invalid_gtin", location="WAREHOUSE_A", quantity=100
) )
@@ -117,7 +117,7 @@ class TestStockService:
self.service.set_stock(db, stock_data) self.service.set_stock(db, stock_data)
def test_add_stock_new_entry(self, db): def test_add_stock_new_entry(self, db):
"""Test adding stock for a new GTIN/location combination""" """Test adding stock for a new GTIN/location combination."""
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
stock_data = StockAdd( stock_data = StockAdd(
gtin="1234567890123", location=f"WAREHOUSE_B_{unique_id}", quantity=50 gtin="1234567890123", location=f"WAREHOUSE_B_{unique_id}", quantity=50
@@ -130,7 +130,7 @@ class TestStockService:
assert result.quantity == 50 assert result.quantity == 50
def test_add_stock_existing_entry(self, db, test_stock): def test_add_stock_existing_entry(self, db, test_stock):
"""Test adding stock to an existing GTIN/location combination""" """Test adding stock to an existing GTIN/location combination."""
original_quantity = test_stock.quantity original_quantity = test_stock.quantity
stock_data = StockAdd( stock_data = StockAdd(
gtin=test_stock.gtin, gtin=test_stock.gtin,
@@ -145,14 +145,14 @@ class TestStockService:
assert result.quantity == original_quantity + 25 assert result.quantity == original_quantity + 25
def test_add_stock_invalid_gtin(self, db): def test_add_stock_invalid_gtin(self, db):
"""Test adding stock with invalid GTIN""" """Test adding stock with invalid GTIN."""
stock_data = StockAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=50) stock_data = StockAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=50)
with pytest.raises(ValueError, match="Invalid GTIN format"): with pytest.raises(ValueError, match="Invalid GTIN format"):
self.service.add_stock(db, stock_data) self.service.add_stock(db, stock_data)
def test_remove_stock_success(self, db, test_stock): def test_remove_stock_success(self, db, test_stock):
"""Test removing stock successfully""" """Test removing stock successfully."""
original_quantity = test_stock.quantity original_quantity = test_stock.quantity
remove_quantity = min( remove_quantity = min(
10, original_quantity 10, original_quantity
@@ -171,7 +171,7 @@ class TestStockService:
assert result.quantity == original_quantity - remove_quantity assert result.quantity == original_quantity - remove_quantity
def test_remove_stock_insufficient_stock(self, db, test_stock): def test_remove_stock_insufficient_stock(self, db, test_stock):
"""Test removing more stock than available""" """Test removing more stock than available."""
stock_data = StockAdd( stock_data = StockAdd(
gtin=test_stock.gtin, gtin=test_stock.gtin,
location=test_stock.location, # Use exact same location as test_stock location=test_stock.location, # Use exact same location as test_stock
@@ -185,7 +185,7 @@ class TestStockService:
self.service.remove_stock(db, stock_data) self.service.remove_stock(db, stock_data)
def test_remove_stock_nonexistent_entry(self, db): def test_remove_stock_nonexistent_entry(self, db):
"""Test removing stock from non-existent GTIN/location""" """Test removing stock from non-existent GTIN/location."""
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
stock_data = StockAdd( stock_data = StockAdd(
gtin="9999999999999", location=f"NONEXISTENT_{unique_id}", quantity=10 gtin="9999999999999", location=f"NONEXISTENT_{unique_id}", quantity=10
@@ -195,14 +195,14 @@ class TestStockService:
self.service.remove_stock(db, stock_data) self.service.remove_stock(db, stock_data)
def test_remove_stock_invalid_gtin(self, db): def test_remove_stock_invalid_gtin(self, db):
"""Test removing stock with invalid GTIN""" """Test removing stock with invalid GTIN."""
stock_data = StockAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=10) stock_data = StockAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=10)
with pytest.raises(ValueError, match="Invalid GTIN format"): with pytest.raises(ValueError, match="Invalid GTIN format"):
self.service.remove_stock(db, stock_data) self.service.remove_stock(db, stock_data)
def test_get_stock_by_gtin_success(self, db, test_stock, test_product): def test_get_stock_by_gtin_success(self, db, test_stock, test_product):
"""Test getting stock summary by GTIN""" """Test getting stock summary by GTIN."""
result = self.service.get_stock_by_gtin(db, test_stock.gtin) result = self.service.get_stock_by_gtin(db, test_stock.gtin)
assert result.gtin == test_stock.gtin assert result.gtin == test_stock.gtin
@@ -213,7 +213,7 @@ class TestStockService:
assert result.product_title == test_product.title assert result.product_title == test_product.title
def test_get_stock_by_gtin_multiple_locations(self, db, test_product): def test_get_stock_by_gtin_multiple_locations(self, db, test_product):
"""Test getting stock summary with multiple locations""" """Test getting stock summary with multiple locations."""
unique_gtin = test_product.gtin unique_gtin = test_product.gtin
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
@@ -237,17 +237,17 @@ class TestStockService:
assert len(result.locations) == 2 assert len(result.locations) == 2
def test_get_stock_by_gtin_not_found(self, db): def test_get_stock_by_gtin_not_found(self, db):
"""Test getting stock for non-existent GTIN""" """Test getting stock for non-existent GTIN."""
with pytest.raises(ValueError, match="No stock found"): with pytest.raises(ValueError, match="No stock found"):
self.service.get_stock_by_gtin(db, "9999999999999") self.service.get_stock_by_gtin(db, "9999999999999")
def test_get_stock_by_gtin_invalid_gtin(self, db): def test_get_stock_by_gtin_invalid_gtin(self, db):
"""Test getting stock with invalid GTIN""" """Test getting stock with invalid GTIN."""
with pytest.raises(ValueError, match="Invalid GTIN format"): with pytest.raises(ValueError, match="Invalid GTIN format"):
self.service.get_stock_by_gtin(db, "invalid_gtin") self.service.get_stock_by_gtin(db, "invalid_gtin")
def test_get_total_stock_success(self, db, test_stock, test_product): def test_get_total_stock_success(self, db, test_stock, test_product):
"""Test getting total stock for a GTIN""" """Test getting total stock for a GTIN."""
result = self.service.get_total_stock(db, test_stock.gtin) result = self.service.get_total_stock(db, test_stock.gtin)
assert result["gtin"] == test_stock.gtin assert result["gtin"] == test_stock.gtin
@@ -256,19 +256,19 @@ class TestStockService:
assert result["locations_count"] == 1 assert result["locations_count"] == 1
def test_get_total_stock_invalid_gtin(self, db): def test_get_total_stock_invalid_gtin(self, db):
"""Test getting total stock with invalid GTIN""" """Test getting total stock with invalid GTIN."""
with pytest.raises(ValueError, match="Invalid GTIN format"): with pytest.raises(ValueError, match="Invalid GTIN format"):
self.service.get_total_stock(db, "invalid_gtin") self.service.get_total_stock(db, "invalid_gtin")
def test_get_all_stock_no_filters(self, db, test_stock): def test_get_all_stock_no_filters(self, db, test_stock):
"""Test getting all stock without filters""" """Test getting all stock without filters."""
result = self.service.get_all_stock(db) result = self.service.get_all_stock(db)
assert len(result) >= 1 assert len(result) >= 1
assert any(stock.gtin == test_stock.gtin for stock in result) assert any(stock.gtin == test_stock.gtin for stock in result)
def test_get_all_stock_with_location_filter(self, db, test_stock): def test_get_all_stock_with_location_filter(self, db, test_stock):
"""Test getting all stock with location filter""" """Test getting all stock with location filter."""
result = self.service.get_all_stock(db, location=test_stock.location) result = self.service.get_all_stock(db, location=test_stock.location)
assert len(result) >= 1 assert len(result) >= 1
@@ -278,14 +278,14 @@ class TestStockService:
) )
def test_get_all_stock_with_gtin_filter(self, db, test_stock): def test_get_all_stock_with_gtin_filter(self, db, test_stock):
"""Test getting all stock with GTIN filter""" """Test getting all stock with GTIN filter."""
result = self.service.get_all_stock(db, gtin=test_stock.gtin) result = self.service.get_all_stock(db, gtin=test_stock.gtin)
assert len(result) >= 1 assert len(result) >= 1
assert all(stock.gtin == test_stock.gtin for stock in result) assert all(stock.gtin == test_stock.gtin for stock in result)
def test_get_all_stock_with_pagination(self, db): def test_get_all_stock_with_pagination(self, db):
"""Test getting all stock with pagination""" """Test getting all stock with pagination."""
unique_prefix = str(uuid.uuid4())[:8] unique_prefix = str(uuid.uuid4())[:8]
# Create multiple stock entries with unique GTINs and locations # Create multiple stock entries with unique GTINs and locations
@@ -305,7 +305,7 @@ class TestStockService:
) # Should be at most 2, might be less if other records exist ) # Should be at most 2, might be less if other records exist
def test_update_stock_success(self, db, test_stock): def test_update_stock_success(self, db, test_stock):
"""Test updating stock quantity""" """Test updating stock quantity."""
stock_update = StockUpdate(quantity=150) stock_update = StockUpdate(quantity=150)
result = self.service.update_stock(db, test_stock.id, stock_update) result = self.service.update_stock(db, test_stock.id, stock_update)
@@ -314,14 +314,14 @@ class TestStockService:
assert result.quantity == 150 assert result.quantity == 150
def test_update_stock_not_found(self, db): def test_update_stock_not_found(self, db):
"""Test updating non-existent stock entry""" """Test updating non-existent stock entry."""
stock_update = StockUpdate(quantity=150) stock_update = StockUpdate(quantity=150)
with pytest.raises(ValueError, match="Stock entry not found"): with pytest.raises(ValueError, match="Stock entry not found"):
self.service.update_stock(db, 99999, stock_update) self.service.update_stock(db, 99999, stock_update)
def test_delete_stock_success(self, db, test_stock): def test_delete_stock_success(self, db, test_stock):
"""Test deleting stock entry""" """Test deleting stock entry."""
stock_id = test_stock.id stock_id = test_stock.id
result = self.service.delete_stock(db, stock_id) result = self.service.delete_stock(db, stock_id)
@@ -333,12 +333,12 @@ class TestStockService:
assert deleted_stock is None assert deleted_stock is None
def test_delete_stock_not_found(self, db): def test_delete_stock_not_found(self, db):
"""Test deleting non-existent stock entry""" """Test deleting non-existent stock entry."""
with pytest.raises(ValueError, match="Stock entry not found"): with pytest.raises(ValueError, match="Stock entry not found"):
self.service.delete_stock(db, 99999) self.service.delete_stock(db, 99999)
def test_get_stock_by_id_success(self, db, test_stock): def test_get_stock_by_id_success(self, db, test_stock):
"""Test getting stock entry by ID""" """Test getting stock entry by ID."""
result = self.service.get_stock_by_id(db, test_stock.id) result = self.service.get_stock_by_id(db, test_stock.id)
assert result is not None assert result is not None
@@ -346,7 +346,7 @@ class TestStockService:
assert result.gtin == test_stock.gtin assert result.gtin == test_stock.gtin
def test_get_stock_by_id_not_found(self, db): def test_get_stock_by_id_not_found(self, db):
"""Test getting non-existent stock entry by ID""" """Test getting non-existent stock entry by ID."""
result = self.service.get_stock_by_id(db, 99999) result = self.service.get_stock_by_id(db, 99999)
assert result is None assert result is None
@@ -354,7 +354,7 @@ class TestStockService:
@pytest.fixture @pytest.fixture
def test_product_with_stock(db, test_stock): def test_product_with_stock(db, test_stock):
"""Create a test product that corresponds to the test stock""" """Create a test product that corresponds to the test stock."""
product = Product( product = Product(
product_id="STOCK_TEST_001", product_id="STOCK_TEST_001",
title="Stock Test Product", title="Stock Test Product",

View File

@@ -1,4 +1,12 @@
# utils/csv_processor.py # utils/csv_processor.py
"""CSV processor utilities ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from datetime import datetime from datetime import datetime
from io import StringIO from io import StringIO
@@ -15,7 +23,7 @@ logger = logging.getLogger(__name__)
class CSVProcessor: class CSVProcessor:
"""Handles CSV import with robust parsing and batching""" """Handles CSV import with robust parsing and batching."""
ENCODINGS = ["utf-8", "latin-1", "iso-8859-1", "cp1252", "utf-8-sig"] ENCODINGS = ["utf-8", "latin-1", "iso-8859-1", "cp1252", "utf-8-sig"]
@@ -75,13 +83,14 @@ class CSVProcessor:
} }
def __init__(self): def __init__(self):
"""Class constructor."""
from utils.data_processing import GTINProcessor, PriceProcessor from utils.data_processing import GTINProcessor, PriceProcessor
self.gtin_processor = GTINProcessor() self.gtin_processor = GTINProcessor()
self.price_processor = PriceProcessor() self.price_processor = PriceProcessor()
def download_csv(self, url: str) -> str: def download_csv(self, url: str) -> str:
"""Download and decode CSV with multiple encoding attempts""" """Download and decode CSV with multiple encoding attempts."""
try: try:
response = requests.get(url, timeout=30) response = requests.get(url, timeout=30)
response.raise_for_status() response.raise_for_status()
@@ -107,8 +116,7 @@ class CSVProcessor:
raise raise
def parse_csv(self, csv_content: str) -> pd.DataFrame: def parse_csv(self, csv_content: str) -> pd.DataFrame:
"""Parse CSV with multiple separator attempts""" """Parse CSV with multiple separator attempts."""
for config in self.PARSING_CONFIGS: for config in self.PARSING_CONFIGS:
try: try:
df = pd.read_csv( df = pd.read_csv(
@@ -127,7 +135,7 @@ class CSVProcessor:
raise pd.errors.ParserError("Could not parse CSV with any configuration") raise pd.errors.ParserError("Could not parse CSV with any configuration")
def normalize_columns(self, df: pd.DataFrame) -> pd.DataFrame: def normalize_columns(self, df: pd.DataFrame) -> pd.DataFrame:
"""Normalize column names using mapping""" """Normalize column names using mapping."""
# Clean column names # Clean column names
df.columns = df.columns.str.strip() df.columns = df.columns.str.strip()
@@ -138,7 +146,7 @@ class CSVProcessor:
return df return df
def _clean_row_data(self, row_data: Dict[str, Any]) -> Dict[str, Any]: def _clean_row_data(self, row_data: Dict[str, Any]) -> Dict[str, Any]:
"""Process a single row with data normalization""" """Process a single row with data normalization."""
# Handle NaN values # Handle NaN values
processed_data = {k: (v if pd.notna(v) else None) for k, v in row_data.items()} processed_data = {k: (v if pd.notna(v) else None) for k, v in row_data.items()}
@@ -182,7 +190,7 @@ class CSVProcessor:
self, url: str, marketplace: str, shop_name: str, batch_size: int, db: Session self, url: str, marketplace: str, shop_name: str, batch_size: int, db: Session
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Process CSV from URL with marketplace and shop information Process CSV from URL with marketplace and shop information.
Args: Args:
url: URL to the CSV file url: URL to the CSV file
@@ -194,7 +202,6 @@ class CSVProcessor:
Returns: Returns:
Dictionary with processing results Dictionary with processing results
""" """
logger.info( logger.info(
f"Starting marketplace CSV import from {url} for {marketplace} -> {shop_name}" f"Starting marketplace CSV import from {url} for {marketplace} -> {shop_name}"
) )
@@ -239,13 +246,14 @@ class CSVProcessor:
db: Session, db: Session,
batch_num: int, batch_num: int,
) -> Dict[str, int]: ) -> Dict[str, int]:
"""Process a batch of CSV rows with marketplace information""" """Process a batch of CSV rows with marketplace information."""
imported = 0 imported = 0
updated = 0 updated = 0
errors = 0 errors = 0
logger.info( logger.info(
f"Processing batch {batch_num} with {len(batch_df)} rows for {marketplace} -> {shop_name}" f"Processing batch {batch_num} with {len(batch_df)} rows for "
f"{marketplace} -> {shop_name}"
) )
for index, row in batch_df.iterrows(): for index, row in batch_df.iterrows():
@@ -285,7 +293,8 @@ class CSVProcessor:
existing_product.updated_at = datetime.utcnow() existing_product.updated_at = datetime.utcnow()
updated += 1 updated += 1
logger.debug( logger.debug(
f"Updated product {product_data['product_id']} for {marketplace} and shop {shop_name}" f"Updated product {product_data['product_id']} for "
f"{marketplace} and shop {shop_name}"
) )
else: else:
# Create new product # Create new product
@@ -299,8 +308,8 @@ class CSVProcessor:
db.add(new_product) db.add(new_product)
imported += 1 imported += 1
logger.debug( logger.debug(
f"Imported new product {product_data['product_id']} for {marketplace} and shop " f"Imported new product {product_data['product_id']} "
f"{shop_name}" f"for {marketplace} and shop {shop_name}"
) )
except Exception as e: except Exception as e:

View File

@@ -1,4 +1,12 @@
# utils/data_processing.py # utils/data_processing.py
"""Data processing utilities for GTIN validation and price parsing.
This module provides classes and functions for:
- GTIN (Global Trade Item Number) validation and normalization
- Price parsing with currency detection
- Data cleaning and validation utilities
"""
import logging import logging
import re import re
from typing import Optional, Tuple from typing import Optional, Tuple
@@ -9,14 +17,15 @@ logger = logging.getLogger(__name__)
class GTINProcessor: class GTINProcessor:
"""Handles GTIN normalization and validation""" """Handles GTIN normalization and validation."""
VALID_LENGTHS = [8, 12, 13, 14] VALID_LENGTHS = [8, 12, 13, 14]
def normalize(self, gtin_value: any) -> Optional[str]: def normalize(self, gtin_value: any) -> Optional[str]:
""" """
Normalize GTIN to proper format Normalize GTIN to proper format.
Returns None for invalid GTINs
Returns None for invalid GTINs.
""" """
if not gtin_value or pd.isna(gtin_value): if not gtin_value or pd.isna(gtin_value):
return None return None
@@ -63,14 +72,14 @@ class GTINProcessor:
return None return None
def validate(self, gtin: str) -> bool: def validate(self, gtin: str) -> bool:
"""Validate GTIN format""" """Validate GTIN format."""
if not gtin: if not gtin:
return False return False
return len(gtin) in self.VALID_LENGTHS and gtin.isdigit() return len(gtin) in self.VALID_LENGTHS and gtin.isdigit()
class PriceProcessor: class PriceProcessor:
"""Handles price parsing and currency extraction""" """Handles price parsing and currency extraction."""
CURRENCY_PATTERNS = { CURRENCY_PATTERNS = {
# Amount followed by currency # Amount followed by currency
@@ -92,7 +101,8 @@ class PriceProcessor:
self, price_str: any self, price_str: any
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
""" """
Parse price string into (price, currency) tuple Parse price string into (price, currency) tuple.
Returns (None, None) if parsing fails Returns (None, None) if parsing fails
""" """
if not price_str or pd.isna(price_str): if not price_str or pd.isna(price_str):

View File

@@ -1,4 +1,12 @@
# utils/database.py # utils/database.py
"""Database utilities ....
This module provides classes and functions for:
- ....
- ....
- ....
"""
import logging import logging
from sqlalchemy import create_engine from sqlalchemy import create_engine
@@ -9,7 +17,7 @@ logger = logging.getLogger(__name__)
def get_db_engine(database_url: str): def get_db_engine(database_url: str):
"""Create database engine with connection pooling""" """Create database engine with connection pooling."""
if database_url.startswith("sqlite"): if database_url.startswith("sqlite"):
# SQLite configuration # SQLite configuration
engine = create_engine( engine = create_engine(
@@ -26,10 +34,10 @@ def get_db_engine(database_url: str):
echo=False, echo=False,
) )
logger.info(f"Database engine created for: {database_url.split('@')[0]}@...") logger.info(f"Database engine created for: " f"{database_url.split('@')[0]}@...")
return engine return engine
def get_session_local(engine): def get_session_local(engine):
"""Create session factory""" """Create session factory."""
return sessionmaker(autocommit=False, autoflush=False, bind=engine) return sessionmaker(autocommit=False, autoflush=False, bind=engine)