code quality run

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

View File

@@ -1,28 +1,35 @@
# models/api_models.py - Updated with Marketplace Support and Pydantic v2
from pydantic import BaseModel, Field, field_validator, EmailStr, ConfigDict
from typing import Optional, List
from datetime import datetime
import re
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
# User Authentication Models
class UserRegister(BaseModel):
email: EmailStr = Field(..., description="Valid email address")
username: str = Field(..., min_length=3, max_length=50, description="Username (3-50 characters)")
password: str = Field(..., min_length=6, description="Password (minimum 6 characters)")
username: str = Field(
..., min_length=3, max_length=50, description="Username (3-50 characters)"
)
password: str = Field(
..., min_length=6, description="Password (minimum 6 characters)"
)
@field_validator('username')
@field_validator("username")
@classmethod
def validate_username(cls, v):
if not re.match(r'^[a-zA-Z0-9_]+$', v):
raise ValueError('Username must contain only letters, numbers, or underscores')
if not re.match(r"^[a-zA-Z0-9_]+$", v):
raise ValueError(
"Username must contain only letters, numbers, or underscores"
)
return v.lower().strip()
@field_validator('password')
@field_validator("password")
@classmethod
def validate_password(cls, v):
if len(v) < 6:
raise ValueError('Password must be at least 6 characters long')
raise ValueError("Password must be at least 6 characters long")
return v
@@ -30,7 +37,7 @@ class UserLogin(BaseModel):
username: str = Field(..., description="Username")
password: str = Field(..., description="Password")
@field_validator('username')
@field_validator("username")
@classmethod
def validate_username(cls, v):
return v.strip()
@@ -58,27 +65,38 @@ class LoginResponse(BaseModel):
# NEW: Shop models
class ShopCreate(BaseModel):
shop_code: str = Field(..., min_length=3, max_length=50, description="Unique shop code (e.g., TECHSTORE)")
shop_name: str = Field(..., min_length=1, max_length=200, description="Display name of the shop")
description: Optional[str] = Field(None, max_length=2000, description="Shop description")
shop_code: str = Field(
...,
min_length=3,
max_length=50,
description="Unique shop code (e.g., TECHSTORE)",
)
shop_name: str = Field(
..., min_length=1, max_length=200, description="Display name of the shop"
)
description: Optional[str] = Field(
None, max_length=2000, description="Shop description"
)
contact_email: Optional[str] = None
contact_phone: Optional[str] = None
website: Optional[str] = None
business_address: Optional[str] = None
tax_number: Optional[str] = None
@field_validator('shop_code')
@field_validator("shop_code")
def validate_shop_code(cls, v):
# Convert to uppercase and check format
v = v.upper().strip()
if not v.replace('_', '').replace('-', '').isalnum():
raise ValueError('Shop code must be alphanumeric (underscores and hyphens allowed)')
if not v.replace("_", "").replace("-", "").isalnum():
raise ValueError(
"Shop code must be alphanumeric (underscores and hyphens allowed)"
)
return v
@field_validator('contact_email')
@field_validator("contact_email")
def validate_contact_email(cls, v):
if v and ('@' not in v or '.' not in v):
raise ValueError('Invalid email format')
if v and ("@" not in v or "." not in v):
raise ValueError("Invalid email format")
return v.lower() if v else v
@@ -91,10 +109,10 @@ class ShopUpdate(BaseModel):
business_address: Optional[str] = None
tax_number: Optional[str] = None
@field_validator('contact_email')
@field_validator("contact_email")
def validate_contact_email(cls, v):
if v and ('@' not in v or '.' not in v):
raise ValueError('Invalid email format')
if v and ("@" not in v or "." not in v):
raise ValueError("Invalid email format")
return v.lower() if v else v
@@ -172,11 +190,11 @@ class ProductCreate(ProductBase):
product_id: str = Field(..., min_length=1, description="Product ID is required")
title: str = Field(..., min_length=1, description="Title is required")
@field_validator('product_id', 'title')
@field_validator("product_id", "title")
@classmethod
def validate_required_fields(cls, v):
if not v or not v.strip():
raise ValueError('Field cannot be empty')
raise ValueError("Field cannot be empty")
return v.strip()
@@ -195,15 +213,25 @@ class ProductResponse(ProductBase):
# NEW: Shop Product models
class ShopProductCreate(BaseModel):
product_id: str = Field(..., description="Product ID to add to shop")
shop_product_id: Optional[str] = Field(None, description="Shop's internal product ID")
shop_price: Optional[float] = Field(None, ge=0, description="Shop-specific price override")
shop_sale_price: Optional[float] = Field(None, ge=0, description="Shop-specific sale price")
shop_product_id: Optional[str] = Field(
None, description="Shop's internal product ID"
)
shop_price: Optional[float] = Field(
None, ge=0, description="Shop-specific price override"
)
shop_sale_price: Optional[float] = Field(
None, ge=0, description="Shop-specific sale price"
)
shop_currency: Optional[str] = Field(None, description="Shop-specific currency")
shop_availability: Optional[str] = Field(None, description="Shop-specific availability")
shop_availability: Optional[str] = Field(
None, description="Shop-specific availability"
)
shop_condition: Optional[str] = Field(None, description="Shop-specific condition")
is_featured: bool = Field(False, description="Featured product flag")
min_quantity: int = Field(1, ge=1, description="Minimum order quantity")
max_quantity: Optional[int] = Field(None, ge=1, description="Maximum order quantity")
max_quantity: Optional[int] = Field(
None, ge=1, description="Maximum order quantity"
)
class ShopProductResponse(BaseModel):
@@ -270,28 +298,40 @@ class StockSummaryResponse(BaseModel):
# Marketplace Import Models
class MarketplaceImportRequest(BaseModel):
url: str = Field(..., description="URL to CSV file from marketplace")
marketplace: str = Field(default="Letzshop", description="Name of the marketplace (e.g., Letzshop, Amazon, eBay)")
marketplace: str = Field(
default="Letzshop",
description="Name of the marketplace (e.g., Letzshop, Amazon, eBay)",
)
shop_code: str = Field(..., description="Shop code to associate products with")
batch_size: Optional[int] = Field(1000, gt=0, le=10000, description="Batch size for processing")
batch_size: Optional[int] = Field(
1000, gt=0, le=10000, description="Batch size for processing"
)
@field_validator('url')
@field_validator("url")
@classmethod
def validate_url(cls, v):
if not v.startswith(('http://', 'https://')):
raise ValueError('URL must start with http:// or https://')
if not v.startswith(("http://", "https://")):
raise ValueError("URL must start with http:// or https://")
return v
@field_validator('marketplace')
@field_validator("marketplace")
@classmethod
def validate_marketplace(cls, v):
# You can add validation for supported marketplaces here
supported_marketplaces = ['Letzshop', 'Amazon', 'eBay', 'Etsy', 'Shopify', 'Other']
supported_marketplaces = [
"Letzshop",
"Amazon",
"eBay",
"Etsy",
"Shopify",
"Other",
]
if v not in supported_marketplaces:
# For now, allow any marketplace but log it
pass
return v.strip()
@field_validator('shop_code')
@field_validator("shop_code")
@classmethod
def validate_shop_code(cls, v):
return v.upper().strip()

View File

@@ -1,8 +1,10 @@
# models/database_models.py - Updated with Marketplace Support
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey, UniqueConstraint, Index
from sqlalchemy.orm import relationship
from datetime import datetime
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index,
Integer, String, Text, UniqueConstraint)
from sqlalchemy.orm import relationship
# Import Base from the central database module instead of creating a new one
from app.core.database import Base
@@ -18,10 +20,14 @@ class User(Base):
is_active = Column(Boolean, default=True, nullable=False)
last_login = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
updated_at = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
# Relationships
marketplace_import_jobs = relationship("MarketplaceImportJob", back_populates="user")
marketplace_import_jobs = relationship(
"MarketplaceImportJob", back_populates="user"
)
owned_shops = relationship("Shop", back_populates="owner")
def __repr__(self):
@@ -32,7 +38,9 @@ class Shop(Base):
__tablename__ = "shops"
id = Column(Integer, primary_key=True, index=True)
shop_code = Column(String, unique=True, index=True, nullable=False) # e.g., "TECHSTORE", "FASHIONHUB"
shop_code = Column(
String, unique=True, index=True, nullable=False
) # e.g., "TECHSTORE", "FASHIONHUB"
shop_name = Column(String, nullable=False) # Display name
description = Column(Text)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
@@ -57,7 +65,9 @@ class Shop(Base):
# Relationships
owner = relationship("User", back_populates="owned_shops")
shop_products = relationship("ShopProduct", back_populates="shop")
marketplace_import_jobs = relationship("MarketplaceImportJob", back_populates="shop")
marketplace_import_jobs = relationship(
"MarketplaceImportJob", back_populates="shop"
)
class Product(Base):
@@ -103,26 +113,40 @@ class Product(Base):
currency = Column(String)
# New marketplace fields
marketplace = Column(String, index=True, nullable=True, default="Letzshop") # Index for marketplace filtering
marketplace = Column(
String, index=True, nullable=True, default="Letzshop"
) # Index for marketplace filtering
shop_name = Column(String, index=True, nullable=True) # Index for shop filtering
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
updated_at = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
# Relationship to stock (one-to-many via GTIN)
stock_entries = relationship("Stock", foreign_keys="Stock.gtin", primaryjoin="Product.gtin == Stock.gtin",
viewonly=True)
stock_entries = relationship(
"Stock",
foreign_keys="Stock.gtin",
primaryjoin="Product.gtin == Stock.gtin",
viewonly=True,
)
shop_products = relationship("ShopProduct", back_populates="product")
# Additional indexes for marketplace queries
__table_args__ = (
Index('idx_marketplace_shop', 'marketplace', 'shop_name'), # Composite index for marketplace+shop queries
Index('idx_marketplace_brand', 'marketplace', 'brand'), # Composite index for marketplace+brand queries
Index(
"idx_marketplace_shop", "marketplace", "shop_name"
), # Composite index for marketplace+shop queries
Index(
"idx_marketplace_brand", "marketplace", "brand"
), # Composite index for marketplace+brand queries
)
def __repr__(self):
return (f"<Product(product_id='{self.product_id}', title='{self.title}', marketplace='{self.marketplace}', "
f"shop='{self.shop_name}')>")
return (
f"<Product(product_id='{self.product_id}', title='{self.title}', marketplace='{self.marketplace}', "
f"shop='{self.shop_name}')>"
)
class ShopProduct(Base):
@@ -159,9 +183,9 @@ class ShopProduct(Base):
# Constraints
__table_args__ = (
UniqueConstraint('shop_id', 'product_id', name='uq_shop_product'),
Index('idx_shop_product_active', 'shop_id', 'is_active'),
Index('idx_shop_product_featured', 'shop_id', 'is_featured'),
UniqueConstraint("shop_id", "product_id", name="uq_shop_product"),
Index("idx_shop_product_active", "shop_id", "is_active"),
Index("idx_shop_product_featured", "shop_id", "is_featured"),
)
@@ -169,22 +193,28 @@ class Stock(Base):
__tablename__ = "stock"
id = Column(Integer, primary_key=True, index=True)
gtin = Column(String, index=True, nullable=False) # Foreign key relationship would be ideal
gtin = Column(
String, index=True, nullable=False
) # Foreign key relationship would be ideal
location = Column(String, nullable=False, index=True)
quantity = Column(Integer, nullable=False, default=0)
reserved_quantity = Column(Integer, default=0) # For orders being processed
shop_id = Column(Integer, ForeignKey("shops.id")) # Optional: shop-specific stock
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
updated_at = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
# Relationships
shop = relationship("Shop")
# Composite unique constraint to prevent duplicate GTIN-location combinations
__table_args__ = (
UniqueConstraint('gtin', 'location', name='uq_stock_gtin_location'),
Index('idx_stock_gtin_location', 'gtin', 'location'), # Composite index for efficient queries
UniqueConstraint("gtin", "location", name="uq_stock_gtin_location"),
Index(
"idx_stock_gtin_location", "gtin", "location"
), # Composite index for efficient queries
)
def __repr__(self):
@@ -195,13 +225,20 @@ class MarketplaceImportJob(Base):
__tablename__ = "marketplace_import_jobs"
id = Column(Integer, primary_key=True, index=True)
status = Column(String, nullable=False,
default="pending") # pending, processing, completed, failed, completed_with_errors
status = Column(
String, nullable=False, default="pending"
) # pending, processing, completed, failed, completed_with_errors
source_url = Column(String, nullable=False)
marketplace = Column(String, nullable=False, index=True, default="Letzshop") # Index for marketplace filtering
marketplace = Column(
String, nullable=False, index=True, default="Letzshop"
) # Index for marketplace filtering
shop_name = Column(String, nullable=False, index=True) # Index for shop filtering
shop_id = Column(Integer, ForeignKey("shops.id"), nullable=False) # Add proper foreign key
user_id = Column(Integer, ForeignKey('users.id'), nullable=False) # Foreign key to users table
shop_id = Column(
Integer, ForeignKey("shops.id"), nullable=False
) # Add proper foreign key
user_id = Column(
Integer, ForeignKey("users.id"), nullable=False
) # Foreign key to users table
# Results
imported_count = Column(Integer, default=0)
@@ -223,11 +260,15 @@ class MarketplaceImportJob(Base):
# Additional indexes for marketplace import job queries
__table_args__ = (
Index('idx_marketplace_import_user_marketplace', 'user_id', 'marketplace'), # User's marketplace imports
Index('idx_marketplace_import_shop_status', 'status'), # Shop import status
Index('idx_marketplace_import_shop_id', 'shop_id'),
Index(
"idx_marketplace_import_user_marketplace", "user_id", "marketplace"
), # User's marketplace imports
Index("idx_marketplace_import_shop_status", "status"), # Shop import status
Index("idx_marketplace_import_shop_id", "shop_id"),
)
def __repr__(self):
return (f"<MarketplaceImportJob(id={self.id}, marketplace='{self.marketplace}', shop='{self.shop_name}', "
f"status='{self.status}', imported={self.imported_count})>")
return (
f"<MarketplaceImportJob(id={self.id}, marketplace='{self.marketplace}', shop='{self.shop_name}', "
f"status='{self.status}', imported={self.imported_count})>"
)