API and database models refactoring

This commit is contained in:
2025-09-20 20:17:16 +02:00
parent 91a0a13daa
commit c494c5b5c6
65 changed files with 931 additions and 487 deletions

View File

@@ -123,7 +123,7 @@ import sys
# Add your project directory to the Python path
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from models.database_models import Base
from models.database import Base
from config.settings import settings
# Alembic Config object

View File

@@ -10,7 +10,7 @@ from alembic import context
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from app.core.config import settings
from models.database_models import Base
from models.database import Base
# Alembic Config object
config = context.config

View File

@@ -10,7 +10,7 @@ from alembic import context
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from app.core.config import settings
from models.database_models import Base
from models.database.base import Base
# Alembic Config object
config = context.config

View File

@@ -14,7 +14,8 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from middleware.auth import AuthManager
from middleware.rate_limiter import RateLimiter
from models.database_models import Shop, User
from models.database.user import User
from models.database.shop import Shop
# Set auto_error=False to prevent automatic 403 responses
security = HTTPBearer(auto_error=False)

View File

@@ -16,9 +16,10 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user
from app.core.database import get_db
from app.services.admin_service import admin_service
from models.api_models import (MarketplaceImportJobResponse, ShopListResponse,
UserResponse)
from models.database_models import User
from models.api.marketplace import MarketplaceImportJobResponse
from models.api.shop import ShopListResponse
from models.api.auth import UserResponse
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)

View File

@@ -15,9 +15,9 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.auth_service import auth_service
from models.api_models import (LoginResponse, UserLogin, UserRegister,
from models.api.auth import (LoginResponse, UserLogin, UserRegister,
UserResponse)
from models.database_models import User
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)

View File

@@ -18,9 +18,9 @@ from app.core.database import get_db
from app.services.marketplace_service import marketplace_service
from app.tasks.background_tasks import process_marketplace_import
from middleware.decorators import rate_limit
from models.api_models import (MarketplaceImportJobResponse,
from models.api.marketplace import (MarketplaceImportJobResponse,
MarketplaceImportRequest)
from models.database_models import User
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)

View File

@@ -17,10 +17,10 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.product_service import product_service
from models.api_models import (ProductCreate, ProductDetailResponse,
from models.api.product import (ProductCreate, ProductDetailResponse,
ProductListResponse, ProductResponse,
ProductUpdate)
from models.database_models import User
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)

View File

@@ -15,9 +15,9 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_user, get_user_shop
from app.core.database import get_db
from app.services.shop_service import shop_service
from models.api_models import (ShopCreate, ShopListResponse, ShopProductCreate,
from models.api.shop import (ShopCreate, ShopListResponse, ShopProductCreate,
ShopProductResponse, ShopResponse)
from models.database_models import User
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)

View File

@@ -16,8 +16,8 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.stats_service import stats_service
from models.api_models import MarketplaceStatsResponse, StatsResponse
from models.database_models import User
from models.api.stats import MarketplaceStatsResponse, StatsResponse
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)

View File

@@ -16,9 +16,9 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.stock_service import stock_service
from models.api_models import (StockAdd, StockCreate, StockResponse,
from models.api.stock import (StockAdd, StockCreate, StockResponse,
StockSummaryResponse, StockUpdate)
from models.database_models import User
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)

View File

@@ -14,7 +14,7 @@ from fastapi import FastAPI
from sqlalchemy import text
from middleware.auth import AuthManager
from models.database_models import Base
from models.database.base import Base
from .database import SessionLocal, engine
from .logging import setup_logging

View File

@@ -14,8 +14,10 @@ from typing import List, Optional, Tuple
from fastapi import HTTPException
from sqlalchemy.orm import Session
from models.api_models import MarketplaceImportJobResponse
from models.database_models import MarketplaceImportJob, Shop, User
from models.api.marketplace import MarketplaceImportJobResponse
from models.database.marketplace import MarketplaceImportJob
from models.database.shop import Shop
from models.database.user import User
logger = logging.getLogger(__name__)

View File

@@ -14,8 +14,8 @@ from fastapi import HTTPException
from sqlalchemy.orm import Session
from middleware.auth import AuthManager
from models.api_models import UserLogin, UserRegister
from models.database_models import User
from models.api.auth import UserLogin, UserRegister
from models.database.user import User
logger = logging.getLogger(__name__)

View File

@@ -14,9 +14,11 @@ from typing import List, Optional
from sqlalchemy import func
from sqlalchemy.orm import Session
from models.api_models import (MarketplaceImportJobResponse,
from models.api.marketplace import (MarketplaceImportJobResponse,
MarketplaceImportRequest)
from models.database_models import MarketplaceImportJob, Shop, User
from models.database.marketplace import MarketplaceImportJob
from models.database.shop import Shop
from models.database.user import User
logger = logging.getLogger(__name__)

View File

@@ -14,9 +14,10 @@ from typing import Generator, List, Optional
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from models.api_models import (ProductCreate, ProductUpdate,
StockLocationResponse, StockSummaryResponse)
from models.database_models import Product, Stock
from models.api.product import ProductCreate, ProductUpdate
from models.api.stock import StockLocationResponse, StockSummaryResponse
from models.database.product import Product
from models.database.stock import Stock
from utils.data_processing import GTINProcessor, PriceProcessor
logger = logging.getLogger(__name__)

View File

@@ -14,8 +14,10 @@ from fastapi import HTTPException
from sqlalchemy import func
from sqlalchemy.orm import Session
from models.api_models import ShopCreate, ShopProductCreate
from models.database_models import Product, Shop, ShopProduct, User
from models.api.shop import ShopCreate, ShopProductCreate
from models.database.product import Product
from models.database.shop import Shop, ShopProduct
from models.database.user import User
logger = logging.getLogger(__name__)

View File

@@ -13,7 +13,8 @@ from typing import Any, Dict, List
from sqlalchemy import func
from sqlalchemy.orm import Session
from models.database_models import Product, Stock
from models.database.product import Product
from models.database.stock import Stock
logger = logging.getLogger(__name__)

View File

@@ -13,9 +13,10 @@ from typing import List, Optional
from sqlalchemy.orm import Session
from models.api_models import (StockAdd, StockCreate, StockLocationResponse,
from models.api.stock import (StockAdd, StockCreate, StockLocationResponse,
StockSummaryResponse, StockUpdate)
from models.database_models import Product, Stock
from models.database.product import Product
from models.database.stock import Stock
from utils.data_processing import GTINProcessor
logger = logging.getLogger(__name__)

View File

@@ -11,7 +11,7 @@ import logging
from datetime import datetime
from app.core.database import SessionLocal
from models.database_models import MarketplaceImportJob
from models.database.marketplace import MarketplaceImportJob
from utils.csv_processor import CSVProcessor
logger = logging.getLogger(__name__)

View File

@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
import pytest
from app.tasks.background_tasks import process_marketplace_import
from models.database_models import MarketplaceImportJob
from models.database.marketplace import MarketplaceImportJob
class TestBackgroundTasks:

View File

@@ -4,7 +4,7 @@ from io import StringIO
import pytest
from models.database_models import Product
from models.database.product import Product
class TestExportFunctionality:

View File

@@ -1,7 +1,7 @@
# tests/test_filtering.py
import pytest
from models.database_models import Product
from models.database.product import Product
class TestFiltering:

View File

@@ -18,7 +18,7 @@ from jose import jwt
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from models.database_models import User
from models.database.user import User
logger = logging.getLogger(__name__)

0
models/api/__init__.py Normal file
View File

62
models/api/auth.py Normal file
View File

@@ -0,0 +1,62 @@
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)"
)
@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"
)
return v.lower().strip()
@field_validator("password")
@classmethod
def validate_password(cls, v):
if len(v) < 6:
raise ValueError("Password must be at least 6 characters long")
return v
class UserLogin(BaseModel):
username: str = Field(..., description="Username")
password: str = Field(..., description="Password")
@field_validator("username")
@classmethod
def validate_username(cls, v):
return v.strip()
class UserResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: str
username: str
role: str
is_active: bool
last_login: Optional[datetime] = None
created_at: datetime
updated_at: datetime
class LoginResponse(BaseModel):
access_token: str
token_type: str = "bearer"
expires_in: int
user: UserResponse

16
models/api/base.py Normal file
View File

@@ -0,0 +1,16 @@
from typing import List, TypeVar, Generic
from pydantic import BaseModel
T = TypeVar('T')
class ListResponse(BaseModel, Generic[T]):
"""Generic list response model"""
items: List[T]
total: int
skip: int
limit: int
class StatusResponse(BaseModel):
"""Generic status response"""
success: bool
message: str

65
models/api/marketplace.py Normal file
View File

@@ -0,0 +1,65 @@
import re
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
# 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)",
)
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"
)
@field_validator("url")
@classmethod
def validate_url(cls, v):
if not v.startswith(("http://", "https://")):
raise ValueError("URL must start with http:// or https://")
return v
@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",
]
if v not in supported_marketplaces:
# For now, allow any marketplace but log it
pass
return v.strip()
@field_validator("shop_code")
@classmethod
def validate_shop_code(cls, v):
return v.upper().strip()
class MarketplaceImportJobResponse(BaseModel):
job_id: int
status: str
marketplace: str
shop_id: int
shop_code: Optional[str] = None # Will be populated from shop relationship
shop_name: str
message: Optional[str] = None
imported: Optional[int] = 0
updated: Optional[int] = 0
total_processed: Optional[int] = 0
error_count: Optional[int] = 0
error_message: Optional[str] = None
created_at: Optional[datetime] = None
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None

83
models/api/product.py Normal file
View File

@@ -0,0 +1,83 @@
import re
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
from models.api.stock import StockSummaryResponse
class ProductBase(BaseModel):
product_id: Optional[str] = None
title: Optional[str] = None
description: Optional[str] = None
link: Optional[str] = None
image_link: Optional[str] = None
availability: Optional[str] = None
price: Optional[str] = None
brand: Optional[str] = None
gtin: Optional[str] = None
mpn: Optional[str] = None
condition: Optional[str] = None
adult: Optional[str] = None
multipack: Optional[int] = None
is_bundle: Optional[str] = None
age_group: Optional[str] = None
color: Optional[str] = None
gender: Optional[str] = None
material: Optional[str] = None
pattern: Optional[str] = None
size: Optional[str] = None
size_type: Optional[str] = None
size_system: Optional[str] = None
item_group_id: Optional[str] = None
google_product_category: Optional[str] = None
product_type: Optional[str] = None
custom_label_0: Optional[str] = None
custom_label_1: Optional[str] = None
custom_label_2: Optional[str] = None
custom_label_3: Optional[str] = None
custom_label_4: Optional[str] = None
additional_image_link: Optional[str] = None
sale_price: Optional[str] = None
unit_pricing_measure: Optional[str] = None
unit_pricing_base_measure: Optional[str] = None
identifier_exists: Optional[str] = None
shipping: Optional[str] = None
currency: Optional[str] = None
# New marketplace fields
marketplace: Optional[str] = None
shop_name: Optional[str] = None
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")
@classmethod
def validate_required_fields(cls, v):
if not v or not v.strip():
raise ValueError("Field cannot be empty")
return v.strip()
class ProductUpdate(ProductBase):
pass
class ProductResponse(ProductBase):
model_config = ConfigDict(from_attributes=True)
id: int
created_at: datetime
updated_at: datetime
class ProductListResponse(BaseModel):
products: List[ProductResponse]
total: int
skip: int
limit: int
class ProductDetailResponse(BaseModel):
product: ProductResponse
stock_info: Optional[StockSummaryResponse] = None

125
models/api/shop.py Normal file
View File

@@ -0,0 +1,125 @@
import re
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
from models.api.product import ProductResponse
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"
)
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")
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)"
)
return v
@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")
return v.lower() if v else v
class ShopUpdate(BaseModel):
shop_name: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=2000)
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("contact_email")
def validate_contact_email(cls, v):
if v and ("@" not in v or "." not in v):
raise ValueError("Invalid email format")
return v.lower() if v else v
class ShopResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
shop_code: str
shop_name: str
description: Optional[str]
owner_id: int
contact_email: Optional[str]
contact_phone: Optional[str]
website: Optional[str]
business_address: Optional[str]
tax_number: Optional[str]
is_active: bool
is_verified: bool
created_at: datetime
updated_at: datetime
class ShopListResponse(BaseModel):
shops: List[ShopResponse]
total: int
skip: int
limit: int
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_currency: Optional[str] = Field(None, description="Shop-specific currency")
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"
)
class ShopProductResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
shop_id: int
product: ProductResponse
shop_product_id: Optional[str]
shop_price: Optional[float]
shop_sale_price: Optional[float]
shop_currency: Optional[str]
shop_availability: Optional[str]
shop_condition: Optional[str]
is_featured: bool
is_active: bool
min_quantity: int
max_quantity: Optional[int]
created_at: datetime
updated_at: datetime

22
models/api/stats.py Normal file
View File

@@ -0,0 +1,22 @@
import re
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
class StatsResponse(BaseModel):
total_products: int
unique_brands: int
unique_categories: int
unique_marketplaces: int = 0
unique_shops: int = 0
total_stock_entries: int = 0
total_inventory_quantity: int = 0
class MarketplaceStatsResponse(BaseModel):
marketplace: str
total_products: int
unique_shops: int
unique_brands: int

46
models/api/stock.py Normal file
View File

@@ -0,0 +1,46 @@
import re
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
# Stock Models
class StockBase(BaseModel):
gtin: str = Field(..., min_length=1, description="GTIN is required")
location: str = Field(..., min_length=1, description="Location is required")
class StockCreate(StockBase):
quantity: int = Field(ge=0, description="Quantity must be non-negative")
class StockAdd(StockBase):
quantity: int = Field(gt=0, description="Quantity to add must be positive")
class StockUpdate(BaseModel):
quantity: int = Field(ge=0, description="Quantity must be non-negative")
class StockResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
gtin: str
location: str
quantity: int
created_at: datetime
updated_at: datetime
class StockLocationResponse(BaseModel):
location: str
quantity: int
class StockSummaryResponse(BaseModel):
gtin: str
total_quantity: int
locations: List[StockLocationResponse]
product_title: Optional[str] = None

View File

10
models/database/base.py Normal file
View File

@@ -0,0 +1,10 @@
from datetime import datetime
from sqlalchemy import Column, DateTime
from app.core.database import Base
class TimestampMixin:
"""Mixin to add created_at and updated_at timestamps to models"""
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)

View File

@@ -0,0 +1,61 @@
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
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
source_url = Column(String, nullable=False)
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
# Results
imported_count = Column(Integer, default=0)
updated_count = Column(Integer, default=0)
error_count = Column(Integer, default=0)
total_processed = Column(Integer, default=0)
# Error handling
error_message = Column(String)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
started_at = Column(DateTime)
completed_at = Column(DateTime)
# Relationship to user
user = relationship("User", foreign_keys=[user_id])
shop = relationship("Shop", back_populates="marketplace_import_jobs")
# 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"),
)
def __repr__(self):
return (
f"<MarketplaceImportJob(id={self.id}, marketplace='{self.marketplace}', shop='{self.shop_name}', "
f"status='{self.status}', imported={self.imported_count})>"
)

View File

@@ -0,0 +1,88 @@
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
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
product_id = Column(String, unique=True, index=True, nullable=False)
title = Column(String, nullable=False)
description = Column(String)
link = Column(String)
image_link = Column(String)
availability = Column(String, index=True) # Index for filtering
price = Column(String)
brand = Column(String, index=True) # Index for filtering
gtin = Column(String, index=True) # Index for stock lookups
mpn = Column(String)
condition = Column(String)
adult = Column(String)
multipack = Column(Integer)
is_bundle = Column(String)
age_group = Column(String)
color = Column(String)
gender = Column(String)
material = Column(String)
pattern = Column(String)
size = Column(String)
size_type = Column(String)
size_system = Column(String)
item_group_id = Column(String)
google_product_category = Column(String, index=True) # Index for filtering
product_type = Column(String)
custom_label_0 = Column(String)
custom_label_1 = Column(String)
custom_label_2 = Column(String)
custom_label_3 = Column(String)
custom_label_4 = Column(String)
additional_image_link = Column(String)
sale_price = Column(String)
unit_pricing_measure = Column(String)
unit_pricing_base_measure = Column(String)
identifier_exists = Column(String)
shipping = Column(String)
currency = Column(String)
# New marketplace fields
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
)
# Relationship to stock (one-to-many via GTIN)
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
)
def __repr__(self):
return (
f"<Product(product_id='{self.product_id}', title='{self.title}', marketplace='{self.marketplace}', "
f"shop='{self.shop_name}')>"
)

84
models/database/shop.py Normal file
View File

@@ -0,0 +1,84 @@
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
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_name = Column(String, nullable=False) # Display name
description = Column(Text)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Contact information
contact_email = Column(String)
contact_phone = Column(String)
website = Column(String)
# Business information
business_address = Column(Text)
tax_number = Column(String)
# Status
is_active = Column(Boolean, default=True)
is_verified = Column(Boolean, default=False)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
owner = relationship("User", back_populates="owned_shops")
shop_products = relationship("ShopProduct", back_populates="shop")
marketplace_import_jobs = relationship(
"MarketplaceImportJob", back_populates="shop"
)
class ShopProduct(Base):
__tablename__ = "shop_products"
id = Column(Integer, primary_key=True, index=True)
shop_id = Column(Integer, ForeignKey("shops.id"), nullable=False)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
# Shop-specific overrides (can override the main product data)
shop_product_id = Column(String) # Shop's internal product ID
shop_price = Column(Float) # Override main product price
shop_sale_price = Column(Float)
shop_currency = Column(String)
shop_availability = Column(String) # Override availability
shop_condition = Column(String)
# Shop-specific metadata
is_featured = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
display_order = Column(Integer, default=0)
# Inventory management
min_quantity = Column(Integer, default=1)
max_quantity = Column(Integer)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
shop = relationship("Shop", back_populates="shop_products")
product = relationship("Product", back_populates="shop_products")
# 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"),
)

40
models/database/stock.py Normal file
View File

@@ -0,0 +1,40 @@
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
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
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
)
# 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
)
def __repr__(self):
return f"<Stock(gtin='{self.gtin}', location='{self.location}', quantity={self.quantity})>"

33
models/database/user.py Normal file
View File

@@ -0,0 +1,33 @@
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
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
username = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
role = Column(String, nullable=False, default="user") # user, admin, shop_owner
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
)
# Relationships
marketplace_import_jobs = relationship(
"MarketplaceImportJob", back_populates="user"
)
owned_shops = relationship("Shop", back_populates="owner")
def __repr__(self):
return f"<User(username='{self.username}', email='{self.email}', role='{self.role}')>"

View File

@@ -152,7 +152,7 @@ class StatusResponse(BaseModel):
```python
# In route files
from models.database import User, Product, Stock
from models.database.user import User, Product, Stock
from models.api.auth import UserLogin, UserResponse
from models.api.product import ProductCreate, ProductListResponse

View File

@@ -1,6 +1,6 @@
# restructure_models.ps1 - PowerShell script to restructure models from single files to domain-organized structure
# restructure_models.ps1 - Final working script using temp files
Write-Host "🔄 Starting models restructure..." -ForegroundColor Cyan
Write-Host "📄 Starting models restructure..." -ForegroundColor Cyan
# Create new directory structure for models
Write-Host "📁 Creating models directory structure..." -ForegroundColor Yellow
@@ -10,13 +10,19 @@ $modelDirectories = @(
)
foreach ($dir in $modelDirectories) {
if (!(Test-Path $dir)) {
New-Item -Path $dir -ItemType Directory -Force | Out-Null
Write-Host " Created: $dir" -ForegroundColor Gray
Write-Host " Created: $dir" -ForegroundColor Green
} else {
Write-Host " Exists: $dir" -ForegroundColor Gray
}
}
# Backup original model files
Write-Host "💾 Backing up original model files..." -ForegroundColor Yellow
New-Item -Path "models\backup" -ItemType Directory -Force | Out-Null
if (!(Test-Path "models\backup")) {
New-Item -Path "models\backup" -ItemType Directory -Force | Out-Null
}
$originalFiles = @("models\database_models.py", "models\api_models.py")
foreach ($file in $originalFiles) {
@@ -26,11 +32,23 @@ foreach ($file in $originalFiles) {
}
}
# Create database models files
Write-Host "📄 Creating database model files..." -ForegroundColor Yellow
# models/database/__init__.py
$databaseInit = @"
# Function to create files using temp file approach
function New-PythonFile {
param(
[string]$Path,
[string]$TempContent
)
$tempFile = [System.IO.Path]::GetTempFileName()
$TempContent | Out-File $tempFile -Encoding UTF8
Move-Item $tempFile $Path -Force
}
# Create models/database/__init__.py
Write-Host " Creating models\database\__init__.py..." -ForegroundColor Gray
$initContent = @"
# models/database/__init__.py
from .user import User
from .product import Product
@@ -46,431 +64,98 @@ __all__ = [
"MarketplaceImportJob",
]
"@
New-PythonFile -Path "models\database\__init__.py" -TempContent $initContent
$databaseInit | Out-File -FilePath "models\database\__init__.py" -Encoding UTF8
# Create models/database/base.py
Write-Host " Creating models\database\base.py..." -ForegroundColor Gray
$baseContent = 'LS0gLS0tQkVHSU4gUFlUSE9OIENPREUtLS0tLQojIG1vZGVscy9kYXRhYmFzZS9iYXNlLnB5CmZyb20gZGF0ZXRpbWUgaW1wb3J0IGRhdGV0aW1lCmZyb20gc3FsYWxjaGVteSBpbXBvcnQgQ29sdW1uLCBEYXRlVGltZQpmcm9tIGFwcC5jb3JlLmRhdGFiYXNlIGltcG9ydCBCYXNlCgoKY2xhc3MgVGltZXN0YW1wTWl4aW46CiAgICAiIiJNaXhpbiB0byBhZGQgY3JlYXRlZF9hdCBhbmQgdXBkYXRlZF9hdCB0aW1lc3RhbXBzIHRvIG1vZGVscyIiIgogICAgY3JlYXRlZF9hdCA9IENvbHVtbihEYXRlVGltZSwgZGVmYXVsdD1kYXRldGltZS51dGNub3csIG51bGxhYmxlPUZhbHNlKQogICAgdXBkYXRlZF9hdCA9IENvbHVtbigKICAgICAgICBEYXRlVGltZSwgZGVmYXVsdD1kYXRldGltZS51dGNub3csIG9udXBkYXRlPWRhdGV0aW1lLnV0Y25vdywgbnVsbGFibGU9RmFsc2UKICAgICkKLS0tLS1FTkQgUFlUSE9OIENPREUtLS0tLQo='
$decodedBase = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($baseContent))
$decodedBase = $decodedBase.Replace('-----BEGIN PYTHON CODE-----', '').Replace('-----END PYTHON CODE-----', '').Trim()
$decodedBase | Out-File "models\database\base.py" -Encoding UTF8
# models/database/base.py
$databaseBase = @"
# models/database/base.py
from datetime import datetime
from sqlalchemy import Column, DateTime
from app.core.database import Base
class TimestampMixin:
"""Mixin to add created_at and updated_at timestamps to models"""
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
"@
$databaseBase | Out-File -FilePath "models\database\base.py" -Encoding UTF8
# models/database/user.py
$userModel = @"
# models/database/user.py
from datetime import datetime
from sqlalchemy import Boolean, Column, DateTime, Integer, String
from sqlalchemy.orm import relationship
from app.core.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
username = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
role = Column(String, nullable=False, default="user") # user, admin, shop_owner
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
)
# Relationships
marketplace_import_jobs = relationship(
"MarketplaceImportJob", back_populates="user"
)
owned_shops = relationship("Shop", back_populates="owner")
def __repr__(self):
return f"<User(username='{self.username}', email='{self.email}', role='{self.role}')>"
"@
$userModel | Out-File -FilePath "models\database\user.py" -Encoding UTF8
# models/database/product.py
$productModel = @"
# models/database/product.py
from datetime import datetime
from sqlalchemy import Column, DateTime, Index, Integer, String
from sqlalchemy.orm import relationship
from app.core.database import Base
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
product_id = Column(String, unique=True, index=True, nullable=False)
title = Column(String, nullable=False)
description = Column(String)
link = Column(String)
image_link = Column(String)
availability = Column(String, index=True) # Index for filtering
price = Column(String)
brand = Column(String, index=True) # Index for filtering
gtin = Column(String, index=True) # Index for stock lookups
mpn = Column(String)
condition = Column(String)
adult = Column(String)
multipack = Column(Integer)
is_bundle = Column(String)
age_group = Column(String)
color = Column(String)
gender = Column(String)
material = Column(String)
pattern = Column(String)
size = Column(String)
size_type = Column(String)
size_system = Column(String)
item_group_id = Column(String)
google_product_category = Column(String, index=True) # Index for filtering
product_type = Column(String)
custom_label_0 = Column(String)
custom_label_1 = Column(String)
custom_label_2 = Column(String)
custom_label_3 = Column(String)
custom_label_4 = Column(String)
additional_image_link = Column(String)
sale_price = Column(String)
unit_pricing_measure = Column(String)
unit_pricing_base_measure = Column(String)
identifier_exists = Column(String)
shipping = Column(String)
currency = Column(String)
# New marketplace fields
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
)
# Relationship to stock (one-to-many via GTIN)
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
)
def __repr__(self):
return (
f"<Product(product_id='{self.product_id}', title='{self.title}', marketplace='{self.marketplace}', "
f"shop='{self.shop_name}')>"
)
"@
$productModel | Out-File -FilePath "models\database\product.py" -Encoding UTF8
# models/database/shop.py
$shopModel = @"
# models/database/shop.py
from datetime import datetime
from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Index, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import relationship
from app.core.database import Base
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_name = Column(String, nullable=False) # Display name
description = Column(Text)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Contact information
contact_email = Column(String)
contact_phone = Column(String)
website = Column(String)
# Business information
business_address = Column(Text)
tax_number = Column(String)
# Status
is_active = Column(Boolean, default=True)
is_verified = Column(Boolean, default=False)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
owner = relationship("User", back_populates="owned_shops")
shop_products = relationship("ShopProduct", back_populates="shop")
marketplace_import_jobs = relationship(
"MarketplaceImportJob", back_populates="shop"
)
class ShopProduct(Base):
__tablename__ = "shop_products"
id = Column(Integer, primary_key=True, index=True)
shop_id = Column(Integer, ForeignKey("shops.id"), nullable=False)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
# Shop-specific overrides (can override the main product data)
shop_product_id = Column(String) # Shop's internal product ID
shop_price = Column(Float) # Override main product price
shop_sale_price = Column(Float)
shop_currency = Column(String)
shop_availability = Column(String) # Override availability
shop_condition = Column(String)
# Shop-specific metadata
is_featured = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
display_order = Column(Integer, default=0)
# Inventory management
min_quantity = Column(Integer, default=1)
max_quantity = Column(Integer)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
shop = relationship("Shop", back_populates="shop_products")
product = relationship("Product", back_populates="shop_products")
# 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"),
)
"@
$shopModel | Out-File -FilePath "models\database\shop.py" -Encoding UTF8
# models/database/stock.py
$stockModel = @"
# models/database/stock.py
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Index, Integer, String, UniqueConstraint
from sqlalchemy.orm import relationship
from app.core.database import Base
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
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
)
# 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
)
def __repr__(self):
return f"<Stock(gtin='{self.gtin}', location='{self.location}', quantity={self.quantity})>"
"@
$stockModel | Out-File -FilePath "models\database\stock.py" -Encoding UTF8
# models/database/marketplace.py
$marketplaceModel = @"
# models/database/marketplace.py
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Index, Integer, String
from sqlalchemy.orm import relationship
from app.core.database import Base
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
source_url = Column(String, nullable=False)
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
# Results
imported_count = Column(Integer, default=0)
updated_count = Column(Integer, default=0)
error_count = Column(Integer, default=0)
total_processed = Column(Integer, default=0)
# Error handling
error_message = Column(String)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
started_at = Column(DateTime)
completed_at = Column(DateTime)
# Relationship to user
user = relationship("User", foreign_keys=[user_id])
shop = relationship("Shop", back_populates="marketplace_import_jobs")
# 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"),
)
def __repr__(self):
return (
f"<MarketplaceImportJob(id={self.id}, marketplace='{self.marketplace}', shop='{self.shop_name}', "
f"status='{self.status}', imported={self.imported_count})>"
)
"@
$marketplaceModel | Out-File -FilePath "models\database\marketplace.py" -Encoding UTF8
# Create API models files
Write-Host "📄 Creating API model files..." -ForegroundColor Yellow
# Create models/database/user.py
Write-Host " Creating models\database\user.py..." -ForegroundColor Gray
$userContent = 'LS0tLS1CRUdJTiBQWVRIT04gQ09ERS0tLS0tCiMgbW9kZWxzL2RhdGFiYXNlL3VzZXIucHkKZnJvbSBkYXRldGltZSBpbXBvcnQgZGF0ZXRpbWUKZnJvbSBzcWxhbGNoZW15IGltcG9ydCBCb29sZWFuLCBDb2x1bW4sIERhdGVUaW1lLCBJbnRlZ2VyLCBTdHJpbmcKZnJvbSBzcWxhbGNoZW15Lm9ybSBpbXBvcnQgcmVsYXRpb25zaGlwCmZyb20gYXBwLmNvcmUuZGF0YWJhc2UgaW1wb3J0IEJhc2UKCgpjbGFzcyBVc2VyKEJhc2UpOgogICAgX190YWJsZW5hbWVfXyA9ICJ1c2VycyIKCiAgICBpZCA9IENvbHVtbihJbnRlZ2VyLCBwcmltYXJ5X2tleT1UcnVlLCBpbmRleD1UcnVlKQogICAgZW1haWwgPSBDb2x1bW4oU3RyaW5nLCB1bmlxdWU9VHJ1ZSwgaW5kZXg9VHJ1ZSwgbnVsbGFibGU9RmFsc2UpCiAgICB1c2VybmFtZSA9IENvbHVtbihTdHJpbmcsIHVuaXF1ZT1UcnVlLCBpbmRleD1UcnVlLCBudWxsYWJsZT1GYWxzZSkKICAgIGhhc2hlZF9wYXNzd29yZCA9IENvbHVtbihTdHJpbmcsIG51bGxhYmxlPUZhbHNlKQogICAgcm9sZSA9IENvbHVtbihTdHJpbmcsIG51bGxhYmxlPUZhbHNlLCBkZWZhdWx0PSJ1c2VyIikgICMgdXNlciwgYWRtaW4sIHNob3Bfb3duZXIKICAgIGlzX2FjdGl2ZSA9IENvbHVtbihCb29sZWFuLCBkZWZhdWx0PVRydWUsIG51bGxhYmxlPUZhbHNlKQogICAgbGFzdF9sb2dpbiA9IENvbHVtbihEYXRlVGltZSwgbnVsbGFibGU9VHJ1ZSkKICAgIGNyZWF0ZWRfYXQgPSBDb2x1bW4oRGF0ZVRpbWUsIGRlZmF1bHQ9ZGF0ZXRpbWUudXRjbm93LCBudWxsYWJsZT1GYWxzZSkKICAgIHVwZGF0ZWRfYXQgPSBDb2x1bW4oCiAgICAgICAgRGF0ZVRpbWUsIGRlZmF1bHQ9ZGF0ZXRpbWUudXRjbm93LCBvbnVwZGF0ZT1kYXRldGltZS51dGNub3csIG51bGxhYmxlPUZhbHNlCiAgICApCgogICAgIyBSZWxhdGlvbnNoaXBzCiAgICBtYXJrZXRwbGFjZV9pbXBvcnRfam9icyA9IHJlbGF0aW9uc2hpcCgKICAgICAgICAiTWFya2V0cGxhY2VJbXBvcnRKb2IiLCBiYWNrX3BvcHVsYXRlcz0idXNlciIKICAgICkKICAgIG93bmVkX3Nob3BzID0gcmVsYXRpb25zaGlwKCJTaG9wIiwgYmFja19wb3B1bGF0ZXM9Im93bmVyIikKCiAgICBkZWYgX19yZXByX18oc2VsZik6CiAgICAgICAgcmV0dXJuIGYiPFVzZXIodXNlcm5hbWU9J3tzZWxmLnVzZXJuYW1lfScsIGVtYWlsPSd7c2VsZi5lbWFpbH0nLCByb2xlPSd7c2VsZi5yb2xlfScpPiIKLS0tLS1FTkQgUFlUSE9OIENPREUtLS0tLQo='
$decodedUser = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($userContent))
$decodedUser = $decodedUser.Replace('-----BEGIN PYTHON CODE-----', '').Replace('-----END PYTHON CODE-----', '').Trim()
$decodedUser | Out-File "models\database\user.py" -Encoding UTF8
# Create models/api/__init__.py
Write-Host " Creating models\api\__init__.py..." -ForegroundColor Gray
$apiInitContent = @"
# models/api/__init__.py
$apiInit = @"
# models/api/__init__.py
from .auth import UserRegister, UserLogin, UserResponse, LoginResponse
from .product import ProductBase, ProductCreate, ProductUpdate, ProductResponse, ProductListResponse, ProductDetailResponse
from .shop import ShopCreate, ShopUpdate, ShopResponse, ShopListResponse, ShopProductCreate, ShopProductResponse
from .stock import StockBase, StockCreate, StockAdd, StockUpdate, StockResponse, StockLocationResponse, StockSummaryResponse
from .marketplace import MarketplaceImportRequest, MarketplaceImportJobResponse
from .stats import StatsResponse, MarketplaceStatsResponse
# Import statements will be added as you create the individual API model files
# Example:
# from .auth import UserRegister, UserLogin, UserResponse, LoginResponse
__all__ = [
# Auth models
"UserRegister", "UserLogin", "UserResponse", "LoginResponse",
# Product models
"ProductBase", "ProductCreate", "ProductUpdate", "ProductResponse",
"ProductListResponse", "ProductDetailResponse",
# Shop models
"ShopCreate", "ShopUpdate", "ShopResponse", "ShopListResponse",
"ShopProductCreate", "ShopProductResponse",
# Stock models
"StockBase", "StockCreate", "StockAdd", "StockUpdate", "StockResponse",
"StockLocationResponse", "StockSummaryResponse",
# Marketplace models
"MarketplaceImportRequest", "MarketplaceImportJobResponse",
# Stats models
"StatsResponse", "MarketplaceStatsResponse",
# Add your API model imports here as you create them
]
"@
New-PythonFile -Path "models\api\__init__.py" -TempContent $apiInitContent
$apiInit | Out-File -FilePath "models\api\__init__.py" -Encoding UTF8
# Create models/api/base.py
Write-Host " Creating models\api\base.py..." -ForegroundColor Gray
$apiBaseContent = 'LS0tLS1CRUdJTiBQWVRIT04gQ09ERS0tLS0tCiMgbW9kZWxzL2FwaS9iYXNlLnB5CmZyb20gdHlwaW5nIGltcG9ydCBMaXN0LCBUeXBlVmFyLCBHZW5lcmljCmZyb20gcHlkYW50aWMgaW1wb3J0IEJhc2VNb2RlbAoKVCA9IFR5cGVWYXIoJ1QnKQoKY2xhc3MgTGlzdFJlc3BvbnNlKEJhc2VNb2RlbCwgR2VuZXJpY1tUXSk6CiAgICAiIiJHZW5lcmljIGxpc3QgcmVzcG9uc2UgbW9kZWwiIiIKICAgIGl0ZW1zOiBMaXN0W1RdCiAgICB0b3RhbDogaW50CiAgICBza2lwOiBpbnQKICAgIGxpbWl0OiBpbnQKCmNsYXNzIFN0YXR1c1Jlc3BvbnNlKEJhc2VNb2RlbCk6CiAgICAiIiJHZW5lcmljIHN0YXR1cyByZXNwb25zZSIiIgogICAgc3VjY2VzczogYm9vbAogICAgbWVzc2FnZTogc3RyCi0tLS0tRU5EIFBZVEhPTiBDT0RFLS0tLS0K'
$decodedApi = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($apiBaseContent))
$decodedApi = $decodedApi.Replace('-----BEGIN PYTHON CODE-----', '').Replace('-----END PYTHON CODE-----', '').Trim()
$decodedApi | Out-File "models\api\base.py" -Encoding UTF8
# models/api/base.py
$apiBase = @"
# models/api/base.py
from typing import List, TypeVar, Generic
from pydantic import BaseModel
Write-Host "✅ Basic model structure created!" -ForegroundColor Green
Write-Host ""
Write-Host "🔍 Testing file creation..." -ForegroundColor Yellow
T = TypeVar('T')
# Check if files were created
$createdFiles = @(
"models\database\__init__.py",
"models\database\base.py",
"models\database\user.py",
"models\api\__init__.py",
"models\api\base.py"
)
class ListResponse(BaseModel, Generic[T]):
"""Generic list response model"""
items: List[T]
total: int
skip: int
limit: int
foreach ($file in $createdFiles) {
if (Test-Path $file) {
$size = (Get-Item $file).Length
Write-Host "$file ($size bytes)" -ForegroundColor Green
} else {
Write-Host "$file (missing)" -ForegroundColor Red
}
}
class StatusResponse(BaseModel):
"""Generic status response"""
success: bool
message: str
"@
Write-Host ""
Write-Host "🧪 Testing Python imports..." -ForegroundColor Yellow
try {
$pythonTest = 'import sys; sys.path.append("."); from models.database.user import User; print("SUCCESS: Database models import working")'
$testResult = python -c $pythonTest 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "✅ Python import test passed!" -ForegroundColor Green
} else {
Write-Host "⚠️ Python import test failed (may need dependencies)" -ForegroundColor Yellow
}
} catch {
Write-Host "⚠️ Python not available for testing" -ForegroundColor Yellow
}
$apiBase | Out-File -FilePath "models\api\base.py" -Encoding UTF8
Write-Host "✅ Models directory structure created!" -ForegroundColor Green
Write-Host ""
Write-Host "📋 Next steps:" -ForegroundColor Cyan
Write-Host "1. You'll need to manually extract the remaining API model classes from api_models.py:" -ForegroundColor White
Write-Host " - Auth models → models\api\auth.py" -ForegroundColor Gray
Write-Host " - Product models → models\api\product.py" -ForegroundColor Gray
Write-Host " - Shop models → models\api\shop.py" -ForegroundColor Gray
Write-Host " - Stock models → models\api\stock.py" -ForegroundColor Gray
Write-Host " - Marketplace models → models\api\marketplace.py" -ForegroundColor Gray
Write-Host " - Stats models → models\api\stats.py" -ForegroundColor Gray
Write-Host "1. Complete the remaining database models by copying from your original files:" -ForegroundColor White
Write-Host " - models\database\product.py" -ForegroundColor Gray
Write-Host " - models\database\shop.py" -ForegroundColor Gray
Write-Host " - models\database\stock.py" -ForegroundColor Gray
Write-Host " - models\database\marketplace.py" -ForegroundColor Gray
Write-Host ""
Write-Host "2. Update imports throughout your application:" -ForegroundColor White
Write-Host " Old: from models.database_models import User, Product" -ForegroundColor Gray
Write-Host " New: from models.database import User, Product" -ForegroundColor Gray
Write-Host "2. Extract API model classes from your api_models.py to separate files:" -ForegroundColor White
Write-Host " - models\api\auth.py" -ForegroundColor Gray
Write-Host " - models\api\product.py" -ForegroundColor Gray
Write-Host " - models\api\shop.py" -ForegroundColor Gray
Write-Host " - models\api\stock.py" -ForegroundColor Gray
Write-Host " - models\api\marketplace.py" -ForegroundColor Gray
Write-Host " - models\api\stats.py" -ForegroundColor Gray
Write-Host ""
Write-Host " Old: from models.api_models import UserCreate, ProductResponse" -ForegroundColor Gray
Write-Host " New: from models.api import UserRegister, ProductResponse" -ForegroundColor Gray
Write-Host "3. Update imports throughout your application:" -ForegroundColor White
Write-Host " Old: from models.database.user import User" -ForegroundColor Gray
Write-Host " New: from models.database.user import User" -ForegroundColor Gray
Write-Host ""
Write-Host "3. Test imports after restructure:" -ForegroundColor White
Write-Host " python -c \"from models.database import User, Product, Shop\"" -ForegroundColor Yellow
Write-Host " python -c \"from models.api import UserRegister, ProductResponse\"" -ForegroundColor Yellow
Write-Host ""
Write-Host "4. Files to update (search and replace imports):" -ForegroundColor White
Write-Host " - All route files in api/v1/" -ForegroundColor Gray
Write-Host " - Service files in app/services/" -ForegroundColor Gray
Write-Host " - Test files" -ForegroundColor Gray
Write-Host " - Any utility files" -ForegroundColor Gray
Write-Host ""
Write-Host "✨ Database models restructure completed!" -ForegroundColor Green
Write-Host "✨ Foundation completed successfully!" -ForegroundColor Green
Write-Host "📚 Original files backed up in models\backup\" -ForegroundColor Green

View File

@@ -148,11 +148,11 @@ After running the script, update imports throughout your codebase:
```python
# Old imports
from models.database_models import User, Product, Shop
from models.api_models import UserRegister, ProductResponse
from models.database.user import User, Product, Shop
from models.api import UserRegister, ProductResponse
# New imports
from models.database import User, Product, Shop
from models.database.user import User, Product, Shop
from models.api import UserRegister, ProductResponse
```

View File

@@ -45,7 +45,7 @@ import sys
# Add your project directory to the Python path
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from models.database_models import Base
from models.database import Base
from config.settings import settings
# Alembic Config object

View File

@@ -8,8 +8,11 @@ from sqlalchemy.pool import StaticPool
from app.core.database import Base, get_db
from main import app
# Import all models to ensure they're registered with Base metadata
from models.database_models import (MarketplaceImportJob, Product, Shop,
ShopProduct, Stock, User)
from models.database.marketplace import MarketplaceImportJob
from models.database.product import Product
from models.database.shop import Shop,ShopProduct
from models.database.stock import Stock
from models.database.user import User
# Use in-memory SQLite database for tests
SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///:memory:"

View File

@@ -4,7 +4,7 @@ import uuid
import pytest
from middleware.auth import AuthManager
from models.database_models import User
from models.database.user import User
@pytest.fixture(scope="session")

View File

@@ -1,7 +1,7 @@
# tests/fixtures/marketplace_fixtures.py
import pytest
from models.database_models import MarketplaceImportJob
from models.database.marketplace import MarketplaceImportJob
@pytest.fixture

View File

@@ -3,7 +3,7 @@ import uuid
import pytest
from models.database_models import Product
from models.database.product import Product
@pytest.fixture

View File

@@ -3,7 +3,8 @@ import uuid
import pytest
from models.database_models import Shop, ShopProduct, Stock
from models.database.shop import Shop,ShopProduct
from models.database.stock import Stock
@pytest.fixture

View File

@@ -1,7 +1,8 @@
# tests/integration/api/v1/test_pagination.py
import pytest
from models.database_models import Product, Shop
from models.database.product import Product
from models.database.shop import Shop
@pytest.mark.integration

View File

@@ -1,7 +1,7 @@
# tests/test_stock_endpoints.py
import pytest
from models.database_models import Stock
from models.database.stock import Stock
class TestStockAPI:

View File

@@ -3,7 +3,7 @@ import time
import pytest
from models.database_models import Product
from models.database.product import Product
@pytest.mark.performance

View File

@@ -1,7 +1,10 @@
# tests/unit/models/test_database_models.py
import pytest
from models.database_models import Product, Shop, Stock, User
from models.database.product import Product
from models.database.shop import Shop
from models.database.stock import Stock
from models.database.user import User
@pytest.mark.unit

View File

@@ -3,7 +3,8 @@ import pytest
from fastapi import HTTPException
from app.services.admin_service import AdminService
from models.database_models import MarketplaceImportJob, Shop
from models.database.marketplace import MarketplaceImportJob
from models.database.shop import Shop
@pytest.mark.unit

View File

@@ -3,8 +3,8 @@ import pytest
from fastapi import HTTPException
from app.services.auth_service import AuthService
from models.api_models import UserLogin, UserRegister
from models.database_models import User
from models.api.auth import UserLogin, UserRegister
from models.database.user import User
@pytest.mark.unit
@pytest.mark.auth

View File

@@ -5,8 +5,10 @@ from datetime import datetime
import pytest
from app.services.marketplace_service import MarketplaceService
from models.api_models import MarketplaceImportRequest
from models.database_models import MarketplaceImportJob, Shop, User
from models.api.marketplace import MarketplaceImportRequest
from models.database.marketplace import MarketplaceImportJob
from models.database.shop import Shop
from models.database.user import User
@pytest.mark.unit
@pytest.mark.marketplace

View File

@@ -2,8 +2,8 @@
import pytest
from app.services.product_service import ProductService
from models.api_models import ProductCreate
from models.database_models import Product
from models.api.product import ProductCreate
from models.database.product import Product
@pytest.mark.unit
@pytest.mark.products

View File

@@ -3,7 +3,7 @@ import pytest
from fastapi import HTTPException
from app.services.shop_service import ShopService
from models.api_models import ShopCreate, ShopProductCreate
from models.api.shop import ShopCreate, ShopProductCreate
@pytest.mark.unit
@pytest.mark.shops

View File

@@ -2,7 +2,8 @@
import pytest
from app.services.stats_service import StatsService
from models.database_models import Product, Stock
from models.database.product import Product
from models.database.stock import Stock
@pytest.mark.unit
@pytest.mark.stats

View File

@@ -4,8 +4,9 @@ import uuid
import pytest
from app.services.stock_service import StockService
from models.api_models import StockAdd, StockCreate, StockUpdate
from models.database_models import Product, Stock
from models.api.stock import StockAdd, StockCreate, StockUpdate
from models.database.product import Product
from models.database.stock import Stock
@pytest.mark.unit
@pytest.mark.stock

View File

@@ -83,7 +83,7 @@ class TestCSVProcessor:
mock_get.return_value = mock_response
with pytest.raises(Exception):
self.processor._download_csv("http://example.com/nonexistent.csv")
self.processor.download_csv("http://example.com/nonexistent.csv")
def test_parse_csv_content(self):
"""Test CSV content parsing"""

View File

@@ -116,7 +116,7 @@ tests/
```python
# tests/fixtures/auth_fixtures.py
import pytest
from models.database import User
from models.database.user import User
from utils.auth import create_access_token
@pytest.fixture

View File

@@ -603,7 +603,7 @@ volumes:
```python
# Migrate existing products to demo shop
from models.database_models import Product, Shop, ShopProduct
from models.database import Product, Shop, ShopProduct
from sqlalchemy.orm import Session
def migrate_to_shops(db: Session):

View File

@@ -17,7 +17,7 @@ import requests
from sqlalchemy import literal
from sqlalchemy.orm import Session
from models.database_models import Product
from models.database.product import Product
logger = logging.getLogger(__name__)