style: apply black and isort formatting across entire codebase

- Standardize quote style (single to double quotes)
- Reorder and group imports alphabetically
- Fix line breaks and indentation for consistency
- Apply PEP 8 formatting standards

Also updated Makefile to exclude both venv and .venv from code quality checks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-28 19:30:17 +01:00
parent 13f0094743
commit 21c13ca39b
236 changed files with 8450 additions and 6545 deletions

View File

@@ -1,14 +1,9 @@
# models/schema/__init__.py
"""API models package - Pydantic models for request/response validation."""
from . import auth
# Import API model modules
from . import base
from . import marketplace_import_job
from . import marketplace_product
from . import stats
from . import inventory
from . import vendor
from . import (auth, base, inventory, marketplace_import_job,
marketplace_product, stats, vendor)
# Common imports for convenience
from .base import * # Base Pydantic models

View File

@@ -12,16 +12,18 @@ This module provides schemas for:
"""
from datetime import datetime
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field, field_validator
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field, field_validator
# ============================================================================
# ADMIN AUDIT LOG SCHEMAS
# ============================================================================
class AdminAuditLogResponse(BaseModel):
"""Response model for admin audit logs."""
id: int
admin_user_id: int
admin_username: Optional[str] = None
@@ -39,6 +41,7 @@ class AdminAuditLogResponse(BaseModel):
class AdminAuditLogFilters(BaseModel):
"""Filters for querying audit logs."""
admin_user_id: Optional[int] = None
action: Optional[str] = None
target_type: Optional[str] = None
@@ -50,6 +53,7 @@ class AdminAuditLogFilters(BaseModel):
class AdminAuditLogListResponse(BaseModel):
"""Paginated list of audit logs."""
logs: List[AdminAuditLogResponse]
total: int
skip: int
@@ -60,8 +64,10 @@ class AdminAuditLogListResponse(BaseModel):
# ADMIN NOTIFICATION SCHEMAS
# ============================================================================
class AdminNotificationCreate(BaseModel):
"""Create admin notification."""
type: str = Field(..., max_length=50, description="Notification type")
priority: str = Field(default="normal", description="Priority level")
title: str = Field(..., max_length=200)
@@ -70,10 +76,10 @@ class AdminNotificationCreate(BaseModel):
action_url: Optional[str] = Field(None, max_length=500)
metadata: Optional[Dict[str, Any]] = None
@field_validator('priority')
@field_validator("priority")
@classmethod
def validate_priority(cls, v):
allowed = ['low', 'normal', 'high', 'critical']
allowed = ["low", "normal", "high", "critical"]
if v not in allowed:
raise ValueError(f"Priority must be one of: {', '.join(allowed)}")
return v
@@ -81,6 +87,7 @@ class AdminNotificationCreate(BaseModel):
class AdminNotificationResponse(BaseModel):
"""Admin notification response."""
id: int
type: str
priority: str
@@ -99,11 +106,13 @@ class AdminNotificationResponse(BaseModel):
class AdminNotificationUpdate(BaseModel):
"""Mark notification as read."""
is_read: bool = True
class AdminNotificationListResponse(BaseModel):
"""Paginated list of notifications."""
notifications: List[AdminNotificationResponse]
total: int
unread_count: int
@@ -115,8 +124,10 @@ class AdminNotificationListResponse(BaseModel):
# ADMIN SETTINGS SCHEMAS
# ============================================================================
class AdminSettingCreate(BaseModel):
"""Create or update admin setting."""
key: str = Field(..., max_length=100, description="Unique setting key")
value: str = Field(..., description="Setting value")
value_type: str = Field(default="string", description="Data type")
@@ -125,25 +136,28 @@ class AdminSettingCreate(BaseModel):
is_encrypted: bool = Field(default=False)
is_public: bool = Field(default=False, description="Can be exposed to frontend")
@field_validator('value_type')
@field_validator("value_type")
@classmethod
def validate_value_type(cls, v):
allowed = ['string', 'integer', 'boolean', 'json', 'float']
allowed = ["string", "integer", "boolean", "json", "float"]
if v not in allowed:
raise ValueError(f"Value type must be one of: {', '.join(allowed)}")
return v
@field_validator('key')
@field_validator("key")
@classmethod
def validate_key_format(cls, v):
# Setting keys should be lowercase with underscores
if not v.replace('_', '').isalnum():
raise ValueError("Setting key must contain only letters, numbers, and underscores")
if not v.replace("_", "").isalnum():
raise ValueError(
"Setting key must contain only letters, numbers, and underscores"
)
return v.lower()
class AdminSettingResponse(BaseModel):
"""Admin setting response."""
id: int
key: str
value: str
@@ -160,12 +174,14 @@ class AdminSettingResponse(BaseModel):
class AdminSettingUpdate(BaseModel):
"""Update admin setting value."""
value: str
description: Optional[str] = None
class AdminSettingListResponse(BaseModel):
"""List of settings by category."""
settings: List[AdminSettingResponse]
total: int
category: Optional[str] = None
@@ -175,8 +191,10 @@ class AdminSettingListResponse(BaseModel):
# PLATFORM ALERT SCHEMAS
# ============================================================================
class PlatformAlertCreate(BaseModel):
"""Create platform alert."""
alert_type: str = Field(..., max_length=50)
severity: str = Field(..., description="Alert severity")
title: str = Field(..., max_length=200)
@@ -185,18 +203,25 @@ class PlatformAlertCreate(BaseModel):
affected_systems: Optional[List[str]] = None
auto_generated: bool = Field(default=True)
@field_validator('severity')
@field_validator("severity")
@classmethod
def validate_severity(cls, v):
allowed = ['info', 'warning', 'error', 'critical']
allowed = ["info", "warning", "error", "critical"]
if v not in allowed:
raise ValueError(f"Severity must be one of: {', '.join(allowed)}")
return v
@field_validator('alert_type')
@field_validator("alert_type")
@classmethod
def validate_alert_type(cls, v):
allowed = ['security', 'performance', 'capacity', 'integration', 'database', 'system']
allowed = [
"security",
"performance",
"capacity",
"integration",
"database",
"system",
]
if v not in allowed:
raise ValueError(f"Alert type must be one of: {', '.join(allowed)}")
return v
@@ -204,6 +229,7 @@ class PlatformAlertCreate(BaseModel):
class PlatformAlertResponse(BaseModel):
"""Platform alert response."""
id: int
alert_type: str
severity: str
@@ -226,12 +252,14 @@ class PlatformAlertResponse(BaseModel):
class PlatformAlertResolve(BaseModel):
"""Resolve platform alert."""
is_resolved: bool = True
resolution_notes: Optional[str] = None
class PlatformAlertListResponse(BaseModel):
"""Paginated list of platform alerts."""
alerts: List[PlatformAlertResponse]
total: int
active_count: int
@@ -244,17 +272,19 @@ class PlatformAlertListResponse(BaseModel):
# BULK OPERATION SCHEMAS
# ============================================================================
class BulkVendorAction(BaseModel):
"""Bulk actions on vendors."""
vendor_ids: List[int] = Field(..., min_length=1, max_length=100)
action: str = Field(..., description="Action to perform")
confirm: bool = Field(default=False, description="Required for destructive actions")
reason: Optional[str] = Field(None, description="Reason for bulk action")
@field_validator('action')
@field_validator("action")
@classmethod
def validate_action(cls, v):
allowed = ['activate', 'deactivate', 'verify', 'unverify', 'delete']
allowed = ["activate", "deactivate", "verify", "unverify", "delete"]
if v not in allowed:
raise ValueError(f"Action must be one of: {', '.join(allowed)}")
return v
@@ -262,6 +292,7 @@ class BulkVendorAction(BaseModel):
class BulkVendorActionResponse(BaseModel):
"""Response for bulk vendor actions."""
successful: List[int]
failed: Dict[int, str] # vendor_id -> error_message
total_processed: int
@@ -271,15 +302,16 @@ class BulkVendorActionResponse(BaseModel):
class BulkUserAction(BaseModel):
"""Bulk actions on users."""
user_ids: List[int] = Field(..., min_length=1, max_length=100)
action: str = Field(..., description="Action to perform")
confirm: bool = Field(default=False)
reason: Optional[str] = None
@field_validator('action')
@field_validator("action")
@classmethod
def validate_action(cls, v):
allowed = ['activate', 'deactivate', 'delete']
allowed = ["activate", "deactivate", "delete"]
if v not in allowed:
raise ValueError(f"Action must be one of: {', '.join(allowed)}")
return v
@@ -287,6 +319,7 @@ class BulkUserAction(BaseModel):
class BulkUserActionResponse(BaseModel):
"""Response for bulk user actions."""
successful: List[int]
failed: Dict[int, str]
total_processed: int
@@ -298,8 +331,10 @@ class BulkUserActionResponse(BaseModel):
# ADMIN DASHBOARD SCHEMAS
# ============================================================================
class AdminDashboardStats(BaseModel):
"""Comprehensive admin dashboard statistics."""
platform: Dict[str, Any]
users: Dict[str, Any]
vendors: Dict[str, Any]
@@ -317,8 +352,10 @@ class AdminDashboardStats(BaseModel):
# SYSTEM HEALTH SCHEMAS
# ============================================================================
class ComponentHealthStatus(BaseModel):
"""Health status for a system component."""
status: str # healthy, degraded, unhealthy
response_time_ms: Optional[float] = None
error_message: Optional[str] = None
@@ -328,6 +365,7 @@ class ComponentHealthStatus(BaseModel):
class SystemHealthResponse(BaseModel):
"""System health check response."""
overall_status: str # healthy, degraded, critical
database: ComponentHealthStatus
redis: ComponentHealthStatus
@@ -342,8 +380,10 @@ class SystemHealthResponse(BaseModel):
# ADMIN SESSION SCHEMAS
# ============================================================================
class AdminSessionResponse(BaseModel):
"""Admin session information."""
id: int
admin_user_id: int
admin_username: Optional[str] = None
@@ -360,6 +400,7 @@ class AdminSessionResponse(BaseModel):
class AdminSessionListResponse(BaseModel):
"""List of admin sessions."""
sessions: List[AdminSessionResponse]
total: int
active_count: int

View File

@@ -17,7 +17,9 @@ class UserRegister(BaseModel):
@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")
raise ValueError(
"Username must contain only letters, numbers, or underscores"
)
return v.lower().strip()
@field_validator("password")
@@ -31,7 +33,9 @@ class UserRegister(BaseModel):
class UserLogin(BaseModel):
email_or_username: str = Field(..., description="Username or email address")
password: str = Field(..., description="Password")
vendor_code: Optional[str] = Field(None, description="Optional vendor code for context")
vendor_code: Optional[str] = Field(
None, description="Optional vendor code for context"
)
@field_validator("email_or_username")
@classmethod

View File

@@ -5,21 +5,24 @@ Pydantic schemas for shopping cart operations.
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field, ConfigDict
from pydantic import BaseModel, ConfigDict, Field
# ============================================================================
# Request Schemas
# ============================================================================
class AddToCartRequest(BaseModel):
"""Request model for adding items to cart."""
product_id: int = Field(..., description="Product ID to add", gt=0)
quantity: int = Field(1, ge=1, description="Quantity to add")
class UpdateCartItemRequest(BaseModel):
"""Request model for updating cart item quantity."""
quantity: int = Field(..., ge=1, description="New quantity (must be >= 1)")
@@ -27,23 +30,30 @@ class UpdateCartItemRequest(BaseModel):
# Response Schemas
# ============================================================================
class CartItemResponse(BaseModel):
"""Response model for a single cart item."""
model_config = ConfigDict(from_attributes=True)
product_id: int = Field(..., description="Product ID")
product_name: str = Field(..., description="Product name")
quantity: int = Field(..., description="Quantity in cart")
price: float = Field(..., description="Price per unit when added to cart")
line_total: float = Field(..., description="Total price for this line (price * quantity)")
line_total: float = Field(
..., description="Total price for this line (price * quantity)"
)
image_url: Optional[str] = Field(None, description="Product image URL")
class CartResponse(BaseModel):
"""Response model for shopping cart."""
vendor_id: int = Field(..., description="Vendor ID")
session_id: str = Field(..., description="Shopping session ID")
items: List[CartItemResponse] = Field(default_factory=list, description="Cart items")
items: List[CartItemResponse] = Field(
default_factory=list, description="Cart items"
)
subtotal: float = Field(..., description="Subtotal of all items")
total: float = Field(..., description="Total amount (currently same as subtotal)")
item_count: int = Field(..., description="Total number of items in cart")
@@ -63,18 +73,22 @@ class CartResponse(BaseModel):
items=items,
subtotal=cart_dict["subtotal"],
total=cart_dict["total"],
item_count=len(items)
item_count=len(items),
)
class CartOperationResponse(BaseModel):
"""Response model for cart operations (add, update, remove)."""
message: str = Field(..., description="Operation result message")
product_id: int = Field(..., description="Product ID affected")
quantity: Optional[int] = Field(None, description="New quantity (for add/update operations)")
quantity: Optional[int] = Field(
None, description="New quantity (for add/update operations)"
)
class ClearCartResponse(BaseModel):
"""Response model for clearing cart."""
message: str = Field(..., description="Operation result message")
items_removed: int = Field(..., description="Number of items removed from cart")

View File

@@ -5,35 +5,34 @@ Pydantic schema for customer-related operations.
from datetime import datetime
from decimal import Decimal
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, EmailStr, Field, field_validator
# ============================================================================
# Customer Registration & Authentication
# ============================================================================
class CustomerRegister(BaseModel):
"""Schema for customer registration."""
email: EmailStr = Field(..., description="Customer email address")
password: str = Field(
...,
min_length=8,
description="Password (minimum 8 characters)"
..., min_length=8, description="Password (minimum 8 characters)"
)
first_name: str = Field(..., min_length=1, max_length=100)
last_name: str = Field(..., min_length=1, max_length=100)
phone: Optional[str] = Field(None, max_length=50)
marketing_consent: bool = Field(default=False)
@field_validator('email')
@field_validator("email")
@classmethod
def email_lowercase(cls, v: str) -> str:
"""Convert email to lowercase."""
return v.lower()
@field_validator('password')
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
"""Validate password strength."""
@@ -55,7 +54,7 @@ class CustomerUpdate(BaseModel):
phone: Optional[str] = Field(None, max_length=50)
marketing_consent: Optional[bool] = None
@field_validator('email')
@field_validator("email")
@classmethod
def email_lowercase(cls, v: Optional[str]) -> Optional[str]:
"""Convert email to lowercase."""
@@ -66,6 +65,7 @@ class CustomerUpdate(BaseModel):
# Customer Response
# ============================================================================
class CustomerResponse(BaseModel):
"""Schema for customer response (excludes password)."""
@@ -84,9 +84,7 @@ class CustomerResponse(BaseModel):
created_at: datetime
updated_at: datetime
model_config = {
"from_attributes": True
}
model_config = {"from_attributes": True}
@property
def full_name(self) -> str:
@@ -110,6 +108,7 @@ class CustomerListResponse(BaseModel):
# Customer Address
# ============================================================================
class CustomerAddressCreate(BaseModel):
"""Schema for creating customer address."""
@@ -159,14 +158,14 @@ class CustomerAddressResponse(BaseModel):
created_at: datetime
updated_at: datetime
model_config = {
"from_attributes": True
}
model_config = {"from_attributes": True}
# ============================================================================
# Customer Preferences
# ============================================================================
class CustomerPreferencesUpdate(BaseModel):
"""Schema for updating customer preferences."""

View File

@@ -1,6 +1,7 @@
# models/schema/inventory.py
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
@@ -11,16 +12,21 @@ class InventoryBase(BaseModel):
class InventoryCreate(InventoryBase):
"""Set exact inventory quantity (replaces existing)."""
quantity: int = Field(..., description="Exact inventory quantity", ge=0)
class InventoryAdjust(InventoryBase):
"""Add or remove inventory quantity."""
quantity: int = Field(..., description="Quantity to add (positive) or remove (negative)")
quantity: int = Field(
..., description="Quantity to add (positive) or remove (negative)"
)
class InventoryUpdate(BaseModel):
"""Update inventory fields."""
quantity: Optional[int] = Field(None, ge=0)
reserved_quantity: Optional[int] = Field(None, ge=0)
location: Optional[str] = None
@@ -28,6 +34,7 @@ class InventoryUpdate(BaseModel):
class InventoryReserve(BaseModel):
"""Reserve inventory for orders."""
product_id: int
location: str
quantity: int = Field(..., gt=0)
@@ -60,6 +67,7 @@ class InventoryLocationResponse(BaseModel):
class ProductInventorySummary(BaseModel):
"""Inventory summary for a product."""
product_id: int
vendor_id: int
product_sku: Optional[str]

View File

@@ -1 +1 @@
# Marketplace import job models
# Marketplace import job models

View File

@@ -1,5 +1,6 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -8,9 +9,12 @@ class MarketplaceImportJobRequest(BaseModel):
Note: vendor_id is injected by middleware, not from request body.
"""
source_url: str = Field(..., description="URL to CSV file from marketplace")
marketplace: str = Field(default="Letzshop", description="Marketplace name")
batch_size: Optional[int] = Field(1000, description="Processing batch size", ge=100, le=10000)
batch_size: Optional[int] = Field(
1000, description="Processing batch size", ge=100, le=10000
)
@field_validator("source_url")
@classmethod
@@ -28,6 +32,7 @@ class MarketplaceImportJobRequest(BaseModel):
class MarketplaceImportJobResponse(BaseModel):
"""Response schema for marketplace import job."""
model_config = ConfigDict(from_attributes=True)
job_id: int
@@ -55,6 +60,7 @@ class MarketplaceImportJobResponse(BaseModel):
class MarketplaceImportJobListResponse(BaseModel):
"""Response schema for list of import jobs."""
jobs: list[MarketplaceImportJobResponse]
total: int
skip: int
@@ -63,6 +69,7 @@ class MarketplaceImportJobListResponse(BaseModel):
class MarketplaceImportJobStatusUpdate(BaseModel):
"""Schema for updating import job status (internal use)."""
status: str
imported_count: Optional[int] = None
updated_count: Optional[int] = None

View File

@@ -1,9 +1,12 @@
# models/schema/marketplace_products.py - Simplified validation
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
from models.schema.inventory import ProductInventorySummary
class MarketplaceProductBase(BaseModel):
marketplace_product_id: Optional[str] = None
title: Optional[str] = None
@@ -45,27 +48,34 @@ class MarketplaceProductBase(BaseModel):
marketplace: Optional[str] = None
vendor_name: Optional[str] = None
class MarketplaceProductCreate(MarketplaceProductBase):
marketplace_product_id: str = Field(..., description="MarketplaceProduct identifier")
marketplace_product_id: str = Field(
..., description="MarketplaceProduct identifier"
)
title: str = Field(..., description="MarketplaceProduct title")
# Removed: min_length constraints and custom validators
# Service will handle empty string validation with proper domain exceptions
class MarketplaceProductUpdate(MarketplaceProductBase):
pass
class MarketplaceProductResponse(MarketplaceProductBase):
model_config = ConfigDict(from_attributes=True)
id: int
created_at: datetime
updated_at: datetime
class MarketplaceProductListResponse(BaseModel):
products: List[MarketplaceProductResponse]
total: int
skip: int
limit: int
class MarketplaceProductDetailResponse(BaseModel):
product: MarketplaceProductResponse
inventory_info: Optional[ProductInventorySummary] = None

View File

@@ -1 +1 @@
# Media/file management models
# Media/file management models

View File

@@ -1 +1 @@
# Monitoring models
# Monitoring models

View File

@@ -1 +1 @@
# Notification models
# Notification models

View File

@@ -4,23 +4,26 @@ Pydantic schema for order operations.
"""
from datetime import datetime
from typing import List, Optional
from decimal import Decimal
from pydantic import BaseModel, Field, ConfigDict
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
# ============================================================================
# Order Item Schemas
# ============================================================================
class OrderItemCreate(BaseModel):
"""Schema for creating an order item."""
product_id: int
quantity: int = Field(..., ge=1)
class OrderItemResponse(BaseModel):
"""Schema for order item response."""
model_config = ConfigDict(from_attributes=True)
id: int
@@ -41,8 +44,10 @@ class OrderItemResponse(BaseModel):
# Order Address Schemas
# ============================================================================
class OrderAddressCreate(BaseModel):
"""Schema for order address (shipping/billing)."""
first_name: str = Field(..., min_length=1, max_length=100)
last_name: str = Field(..., min_length=1, max_length=100)
company: Optional[str] = Field(None, max_length=200)
@@ -55,6 +60,7 @@ class OrderAddressCreate(BaseModel):
class OrderAddressResponse(BaseModel):
"""Schema for order address response."""
model_config = ConfigDict(from_attributes=True)
id: int
@@ -73,8 +79,10 @@ class OrderAddressResponse(BaseModel):
# Order Create/Update Schemas
# ============================================================================
class OrderCreate(BaseModel):
"""Schema for creating an order."""
customer_id: Optional[int] = None # Optional for guest checkout
items: List[OrderItemCreate] = Field(..., min_length=1)
@@ -92,9 +100,9 @@ class OrderCreate(BaseModel):
class OrderUpdate(BaseModel):
"""Schema for updating order status."""
status: Optional[str] = Field(
None,
pattern="^(pending|processing|shipped|delivered|cancelled|refunded)$"
None, pattern="^(pending|processing|shipped|delivered|cancelled|refunded)$"
)
tracking_number: Optional[str] = None
internal_notes: Optional[str] = None
@@ -104,8 +112,10 @@ class OrderUpdate(BaseModel):
# Order Response Schemas
# ============================================================================
class OrderResponse(BaseModel):
"""Schema for order response."""
model_config = ConfigDict(from_attributes=True)
id: int
@@ -141,6 +151,7 @@ class OrderResponse(BaseModel):
class OrderDetailResponse(OrderResponse):
"""Schema for detailed order response with items and addresses."""
items: List[OrderItemResponse]
shipping_address: OrderAddressResponse
billing_address: OrderAddressResponse
@@ -148,6 +159,7 @@ class OrderDetailResponse(OrderResponse):
class OrderListResponse(BaseModel):
"""Schema for paginated order list."""
orders: List[OrderResponse]
total: int
skip: int

View File

@@ -1 +1 @@
# Payment models
# Payment models

View File

@@ -1,14 +1,20 @@
# models/schema/product.py
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
from models.schema.marketplace_product import MarketplaceProductResponse
from models.schema.inventory import InventoryLocationResponse
from models.schema.marketplace_product import MarketplaceProductResponse
class ProductCreate(BaseModel):
marketplace_product_id: int = Field(..., description="MarketplaceProduct ID to add to vendor catalog")
product_id: Optional[str] = Field(None, description="Vendor's internal SKU/product ID")
marketplace_product_id: int = Field(
..., description="MarketplaceProduct ID to add to vendor catalog"
)
product_id: Optional[str] = Field(
None, description="Vendor's internal SKU/product ID"
)
price: Optional[float] = Field(None, ge=0)
sale_price: Optional[float] = Field(None, ge=0)
currency: Optional[str] = None
@@ -59,6 +65,7 @@ class ProductResponse(BaseModel):
class ProductDetailResponse(ProductResponse):
"""Product with full inventory details."""
inventory_locations: List[InventoryLocationResponse] = []

View File

@@ -1 +1 @@
# Search models
# Search models

View File

@@ -1,6 +1,6 @@
import re
from decimal import Decimal
from datetime import datetime
from decimal import Decimal
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
@@ -22,10 +22,12 @@ class MarketplaceStatsResponse(BaseModel):
unique_vendors: int
unique_brands: int
# ============================================================================
# Customer Statistics
# ============================================================================
class CustomerStatsResponse(BaseModel):
"""Schema for customer statistics."""
@@ -42,8 +44,10 @@ class CustomerStatsResponse(BaseModel):
# Order Statistics
# ============================================================================
class OrderStatsResponse(BaseModel):
"""Schema for order statistics."""
total_orders: int
pending_orders: int
processing_orders: int
@@ -53,13 +57,16 @@ class OrderStatsResponse(BaseModel):
total_revenue: Decimal
average_order_value: Decimal
# ============================================================================
# Vendor Statistics
# ============================================================================
class VendorStatsResponse(BaseModel):
"""Vendor statistics response schema."""
total: int = Field(..., description="Total number of vendors")
verified: int = Field(..., description="Number of verified vendors")
pending: int = Field(..., description="Number of pending verification vendors")
inactive: int = Field(..., description="Number of inactive vendors")
inactive: int = Field(..., description="Number of inactive vendors")

View File

@@ -10,33 +10,40 @@ This module defines request/response schemas for:
"""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import List, Optional
from pydantic import BaseModel, EmailStr, Field, field_validator
# ============================================================================
# Role Schemas
# ============================================================================
class RoleBase(BaseModel):
"""Base role schema."""
name: str = Field(..., min_length=1, max_length=100, description="Role name")
permissions: List[str] = Field(default_factory=list, description="List of permission strings")
permissions: List[str] = Field(
default_factory=list, description="List of permission strings"
)
class RoleCreate(RoleBase):
"""Schema for creating a role."""
pass
class RoleUpdate(BaseModel):
"""Schema for updating a role."""
name: Optional[str] = Field(None, min_length=1, max_length=100)
permissions: Optional[List[str]] = None
class RoleResponse(RoleBase):
"""Schema for role response."""
id: int
vendor_id: int
created_at: datetime
@@ -48,6 +55,7 @@ class RoleResponse(RoleBase):
class RoleListResponse(BaseModel):
"""Schema for role list response."""
roles: List[RoleResponse]
total: int
@@ -56,8 +64,10 @@ class RoleListResponse(BaseModel):
# Team Member Schemas
# ============================================================================
class TeamMemberBase(BaseModel):
"""Base team member schema."""
email: EmailStr = Field(..., description="Team member email address")
first_name: Optional[str] = Field(None, max_length=100)
last_name: Optional[str] = Field(None, max_length=100)
@@ -65,30 +75,34 @@ class TeamMemberBase(BaseModel):
class TeamMemberInvite(TeamMemberBase):
"""Schema for inviting a team member."""
role_id: Optional[int] = Field(None, description="Role ID to assign (for preset roles)")
role_name: Optional[str] = Field(None, description="Role name (manager, staff, support, etc.)")
role_id: Optional[int] = Field(
None, description="Role ID to assign (for preset roles)"
)
role_name: Optional[str] = Field(
None, description="Role name (manager, staff, support, etc.)"
)
custom_permissions: Optional[List[str]] = Field(
None,
description="Custom permissions (overrides role preset)"
None, description="Custom permissions (overrides role preset)"
)
@field_validator('role_name')
@field_validator("role_name")
def validate_role_name(cls, v):
"""Validate role name is in allowed presets."""
if v is not None:
allowed_roles = ['manager', 'staff', 'support', 'viewer', 'marketing']
allowed_roles = ["manager", "staff", "support", "viewer", "marketing"]
if v.lower() not in allowed_roles:
raise ValueError(
f"Role name must be one of: {', '.join(allowed_roles)}"
)
return v.lower() if v else v
@field_validator('custom_permissions')
@field_validator("custom_permissions")
def validate_custom_permissions(cls, v, values):
"""Ensure either role_id/role_name OR custom_permissions is provided."""
if v is not None and len(v) > 0:
# If custom permissions provided, role_name should be provided too
if 'role_name' not in values or not values['role_name']:
if "role_name" not in values or not values["role_name"]:
raise ValueError(
"role_name is required when providing custom_permissions"
)
@@ -97,12 +111,14 @@ class TeamMemberInvite(TeamMemberBase):
class TeamMemberUpdate(BaseModel):
"""Schema for updating a team member."""
role_id: Optional[int] = Field(None, description="New role ID")
is_active: Optional[bool] = Field(None, description="Active status")
class TeamMemberResponse(BaseModel):
"""Schema for team member response."""
id: int = Field(..., description="User ID")
email: EmailStr
username: str
@@ -112,15 +128,18 @@ class TeamMemberResponse(BaseModel):
user_type: str = Field(..., description="'owner' or 'member'")
role_name: str = Field(..., description="Role name")
role_id: Optional[int]
permissions: List[str] = Field(default_factory=list, description="User's permissions")
permissions: List[str] = Field(
default_factory=list, description="User's permissions"
)
is_active: bool
is_owner: bool
invitation_pending: bool = Field(
default=False,
description="True if invitation not yet accepted"
default=False, description="True if invitation not yet accepted"
)
invited_at: Optional[datetime] = Field(None, description="When invitation was sent")
accepted_at: Optional[datetime] = Field(None, description="When invitation was accepted")
accepted_at: Optional[datetime] = Field(
None, description="When invitation was accepted"
)
joined_at: datetime = Field(..., description="When user joined vendor")
class Config:
@@ -129,6 +148,7 @@ class TeamMemberResponse(BaseModel):
class TeamMemberListResponse(BaseModel):
"""Schema for team member list response."""
members: List[TeamMemberResponse]
total: int
active_count: int
@@ -139,19 +159,20 @@ class TeamMemberListResponse(BaseModel):
# Invitation Schemas
# ============================================================================
class InvitationAccept(BaseModel):
"""Schema for accepting a team invitation."""
invitation_token: str = Field(..., min_length=32, description="Invitation token from email")
invitation_token: str = Field(
..., min_length=32, description="Invitation token from email"
)
password: str = Field(
...,
min_length=8,
max_length=128,
description="Password for new account"
..., min_length=8, max_length=128, description="Password for new account"
)
first_name: str = Field(..., min_length=1, max_length=100)
last_name: str = Field(..., min_length=1, max_length=100)
@field_validator('password')
@field_validator("password")
def validate_password_strength(cls, v):
"""Validate password meets minimum requirements."""
if len(v) < 8:
@@ -172,18 +193,19 @@ class InvitationAccept(BaseModel):
class InvitationResponse(BaseModel):
"""Schema for invitation response."""
message: str
email: EmailStr
role: str
invitation_token: Optional[str] = Field(
None,
description="Token (only returned in dev/test environments)"
None, description="Token (only returned in dev/test environments)"
)
invitation_sent: bool = Field(default=True)
class InvitationAcceptResponse(BaseModel):
"""Schema for invitation acceptance response."""
message: str
vendor: dict = Field(..., description="Vendor information")
user: dict = Field(..., description="User information")
@@ -194,8 +216,10 @@ class InvitationAcceptResponse(BaseModel):
# Team Statistics Schema
# ============================================================================
class TeamStatistics(BaseModel):
"""Schema for team statistics."""
total_members: int
active_members: int
inactive_members: int
@@ -203,8 +227,7 @@ class TeamStatistics(BaseModel):
owners: int
team_members: int
roles_breakdown: dict = Field(
default_factory=dict,
description="Count of members per role"
default_factory=dict, description="Count of members per role"
)
@@ -212,13 +235,18 @@ class TeamStatistics(BaseModel):
# Bulk Operations Schemas
# ============================================================================
class BulkRemoveRequest(BaseModel):
"""Schema for bulk removing team members."""
user_ids: List[int] = Field(..., min_items=1, description="List of user IDs to remove")
user_ids: List[int] = Field(
..., min_items=1, description="List of user IDs to remove"
)
class BulkRemoveResponse(BaseModel):
"""Schema for bulk remove response."""
success_count: int
failed_count: int
errors: List[dict] = Field(default_factory=list)
@@ -228,21 +256,27 @@ class BulkRemoveResponse(BaseModel):
# Permission Check Schemas
# ============================================================================
class PermissionCheckRequest(BaseModel):
"""Schema for checking permissions."""
permissions: List[str] = Field(..., min_items=1, description="Permissions to check")
class PermissionCheckResponse(BaseModel):
"""Schema for permission check response."""
has_all: bool = Field(..., description="True if user has all permissions")
has_any: bool = Field(..., description="True if user has any permission")
granted: List[str] = Field(default_factory=list, description="Permissions user has")
denied: List[str] = Field(default_factory=list, description="Permissions user lacks")
denied: List[str] = Field(
default_factory=list, description="Permissions user lacks"
)
class UserPermissionsResponse(BaseModel):
"""Schema for user's permissions response."""
permissions: List[str] = Field(default_factory=list)
permission_count: int
is_owner: bool
@@ -253,8 +287,10 @@ class UserPermissionsResponse(BaseModel):
# Error Response Schema
# ============================================================================
class TeamErrorResponse(BaseModel):
"""Schema for team operation errors."""
error_code: str
message: str
details: Optional[dict] = None

View File

@@ -15,7 +15,8 @@ Schemas include:
import re
from datetime import datetime
from typing import Dict, List, Optional, Any
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -27,32 +28,26 @@ class VendorCreate(BaseModel):
...,
description="Unique vendor identifier (e.g., TECHSTORE)",
min_length=2,
max_length=50
max_length=50,
)
subdomain: str = Field(
...,
description="Unique subdomain for the vendor",
min_length=2,
max_length=100
..., description="Unique subdomain for the vendor", min_length=2, max_length=100
)
name: str = Field(
...,
description="Display name of the vendor",
min_length=2,
max_length=255
..., description="Display name of the vendor", min_length=2, max_length=255
)
description: Optional[str] = Field(None, description="Vendor description")
# Owner Information (Creates User Account)
owner_email: str = Field(
...,
description="Email for the vendor owner (used for login and authentication)"
description="Email for the vendor owner (used for login and authentication)",
)
# Business Contact Information (Vendor Fields)
contact_email: Optional[str] = Field(
None,
description="Public business contact email (defaults to owner_email if not provided)"
description="Public business contact email (defaults to owner_email if not provided)",
)
contact_phone: Optional[str] = Field(None, description="Contact phone number")
website: Optional[str] = Field(None, description="Website URL")
@@ -78,8 +73,10 @@ class VendorCreate(BaseModel):
@classmethod
def validate_subdomain(cls, v):
"""Validate subdomain format: lowercase alphanumeric with hyphens."""
if v and not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', v):
raise ValueError("Subdomain must contain only lowercase letters, numbers, and hyphens")
if v and not re.match(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$", v):
raise ValueError(
"Subdomain must contain only lowercase letters, numbers, and hyphens"
)
return v.lower() if v else v
@field_validator("vendor_code")
@@ -104,8 +101,7 @@ class VendorUpdate(BaseModel):
# Business Contact Information (Vendor Fields)
contact_email: Optional[str] = Field(
None,
description="Public business contact email"
None, description="Public business contact email"
)
contact_phone: Optional[str] = None
website: Optional[str] = None
@@ -142,6 +138,7 @@ class VendorUpdate(BaseModel):
class VendorResponse(BaseModel):
"""Standard schema for vendor response data."""
model_config = ConfigDict(from_attributes=True)
id: int
@@ -184,13 +181,9 @@ class VendorDetailResponse(VendorResponse):
"""
owner_email: str = Field(
...,
description="Email of the vendor owner (for login/authentication)"
)
owner_username: str = Field(
...,
description="Username of the vendor owner"
..., description="Email of the vendor owner (for login/authentication)"
)
owner_username: str = Field(..., description="Username of the vendor owner")
class VendorCreateResponse(VendorDetailResponse):
@@ -201,17 +194,14 @@ class VendorCreateResponse(VendorDetailResponse):
"""
temporary_password: str = Field(
...,
description="Temporary password for owner (SHOWN ONLY ONCE)"
)
login_url: Optional[str] = Field(
None,
description="URL for vendor owner to login"
..., description="Temporary password for owner (SHOWN ONLY ONCE)"
)
login_url: Optional[str] = Field(None, description="URL for vendor owner to login")
class VendorListResponse(BaseModel):
"""Schema for paginated vendor list."""
vendors: List[VendorResponse]
total: int
skip: int
@@ -220,6 +210,7 @@ class VendorListResponse(BaseModel):
class VendorSummary(BaseModel):
"""Lightweight vendor summary for dropdowns and quick references."""
model_config = ConfigDict(from_attributes=True)
id: int
@@ -239,20 +230,17 @@ class VendorTransferOwnership(BaseModel):
"""
new_owner_user_id: int = Field(
...,
description="ID of the user who will become the new owner",
gt=0
..., description="ID of the user who will become the new owner", gt=0
)
confirm_transfer: bool = Field(
...,
description="Must be true to confirm ownership transfer"
..., description="Must be true to confirm ownership transfer"
)
transfer_reason: Optional[str] = Field(
None,
max_length=500,
description="Reason for ownership transfer (for audit logs)"
description="Reason for ownership transfer (for audit logs)",
)
@field_validator("confirm_transfer")
@@ -273,12 +261,10 @@ class VendorTransferOwnershipResponse(BaseModel):
vendor_name: str
old_owner: Dict[str, Any] = Field(
...,
description="Information about the previous owner"
..., description="Information about the previous owner"
)
new_owner: Dict[str, Any] = Field(
...,
description="Information about the new owner"
..., description="Information about the new owner"
)
transferred_at: datetime

View File

@@ -12,7 +12,8 @@ Schemas include:
import re
from datetime import datetime
from typing import List, Optional, Dict
from typing import Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -23,14 +24,13 @@ class VendorDomainCreate(BaseModel):
...,
description="Custom domain (e.g., myshop.com or shop.mybrand.com)",
min_length=3,
max_length=255
max_length=255,
)
is_primary: bool = Field(
default=False,
description="Set as primary domain for the vendor"
default=False, description="Set as primary domain for the vendor"
)
@field_validator('domain')
@field_validator("domain")
@classmethod
def validate_domain(cls, v: str) -> str:
"""Validate and normalize domain."""
@@ -44,20 +44,22 @@ class VendorDomainCreate(BaseModel):
domain = domain.lower().strip()
# Basic validation
if not domain or '/' in domain:
if not domain or "/" in domain:
raise ValueError("Invalid domain format")
if '.' not in domain:
if "." not in domain:
raise ValueError("Domain must have at least one dot")
# Check for reserved subdomains
reserved = ['www', 'admin', 'api', 'mail', 'smtp', 'ftp', 'cpanel', 'webmail']
first_part = domain.split('.')[0]
reserved = ["www", "admin", "api", "mail", "smtp", "ftp", "cpanel", "webmail"]
first_part = domain.split(".")[0]
if first_part in reserved:
raise ValueError(f"Domain cannot start with reserved subdomain: {first_part}")
raise ValueError(
f"Domain cannot start with reserved subdomain: {first_part}"
)
# Validate domain format (basic regex)
domain_pattern = r'^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$'
domain_pattern = r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$"
if not re.match(domain_pattern, domain):
raise ValueError("Invalid domain format")
@@ -75,6 +77,7 @@ class VendorDomainUpdate(BaseModel):
class VendorDomainResponse(BaseModel):
"""Standard schema for vendor domain response."""
model_config = ConfigDict(from_attributes=True)
id: int

View File

@@ -3,12 +3,14 @@
Pydantic schemas for vendor theme operations.
"""
from typing import Dict, Optional, List
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
class VendorThemeColors(BaseModel):
"""Color scheme for vendor theme."""
primary: Optional[str] = Field(None, description="Primary brand color")
secondary: Optional[str] = Field(None, description="Secondary color")
accent: Optional[str] = Field(None, description="Accent/CTA color")
@@ -19,12 +21,14 @@ class VendorThemeColors(BaseModel):
class VendorThemeFonts(BaseModel):
"""Typography settings for vendor theme."""
heading: Optional[str] = Field(None, description="Font for headings")
body: Optional[str] = Field(None, description="Font for body text")
class VendorThemeBranding(BaseModel):
"""Branding assets for vendor theme."""
logo: Optional[str] = Field(None, description="Logo URL")
logo_dark: Optional[str] = Field(None, description="Dark mode logo URL")
favicon: Optional[str] = Field(None, description="Favicon URL")
@@ -33,36 +37,54 @@ class VendorThemeBranding(BaseModel):
class VendorThemeLayout(BaseModel):
"""Layout settings for vendor theme."""
style: Optional[str] = Field(None, description="Product layout style (grid, list, masonry)")
header: Optional[str] = Field(None, description="Header style (fixed, static, transparent)")
product_card: Optional[str] = Field(None, description="Product card style (modern, classic, minimal)")
style: Optional[str] = Field(
None, description="Product layout style (grid, list, masonry)"
)
header: Optional[str] = Field(
None, description="Header style (fixed, static, transparent)"
)
product_card: Optional[str] = Field(
None, description="Product card style (modern, classic, minimal)"
)
class VendorThemeUpdate(BaseModel):
"""Schema for updating vendor theme (partial updates allowed)."""
theme_name: Optional[str] = Field(None, description="Theme preset name")
colors: Optional[Dict[str, str]] = Field(None, description="Color scheme")
fonts: Optional[Dict[str, str]] = Field(None, description="Font settings")
branding: Optional[Dict[str, Optional[str]]] = Field(None, description="Branding assets")
branding: Optional[Dict[str, Optional[str]]] = Field(
None, description="Branding assets"
)
layout: Optional[Dict[str, str]] = Field(None, description="Layout settings")
custom_css: Optional[str] = Field(None, description="Custom CSS rules")
social_links: Optional[Dict[str, str]] = Field(None, description="Social media links")
social_links: Optional[Dict[str, str]] = Field(
None, description="Social media links"
)
class VendorThemeResponse(BaseModel):
"""Schema for vendor theme response."""
theme_name: str = Field(..., description="Theme name")
colors: Dict[str, str] = Field(..., description="Color scheme")
fonts: Dict[str, str] = Field(..., description="Font settings")
branding: Dict[str, Optional[str]] = Field(..., description="Branding assets")
layout: Dict[str, str] = Field(..., description="Layout settings")
social_links: Optional[Dict[str, str]] = Field(default_factory=dict, description="Social links")
social_links: Optional[Dict[str, str]] = Field(
default_factory=dict, description="Social links"
)
custom_css: Optional[str] = Field(None, description="Custom CSS")
css_variables: Optional[Dict[str, str]] = Field(None, description="CSS custom properties")
css_variables: Optional[Dict[str, str]] = Field(
None, description="CSS custom properties"
)
class ThemePresetPreview(BaseModel):
"""Preview information for a theme preset."""
name: str = Field(..., description="Preset name")
description: str = Field(..., description="Preset description")
primary_color: str = Field(..., description="Primary color")
@@ -75,10 +97,12 @@ class ThemePresetPreview(BaseModel):
class ThemePresetResponse(BaseModel):
"""Response after applying a preset."""
message: str = Field(..., description="Success message")
theme: VendorThemeResponse = Field(..., description="Applied theme")
class ThemePresetListResponse(BaseModel):
"""List of available theme presets."""
presets: List[ThemePresetPreview] = Field(..., description="Available presets")