diff --git a/Makefile b/Makefile index d201da6a..2e174aca 100644 --- a/Makefile +++ b/Makefile @@ -175,24 +175,24 @@ test-inventory: # ============================================================================= format: - @echo "Running black..." - $(PYTHON) -m black . --exclude '/(\.)?venv/' - @echo "Running isort..." - $(PYTHON) -m isort . --skip venv --skip .venv + @echo "Formatting code with ruff..." + $(PYTHON) -m ruff format . lint: - @echo "Running linting..." - $(PYTHON) -m ruff check . --exclude venv --exclude .venv - $(PYTHON) -m mypy . --ignore-missing-imports --exclude '.*(\.)?venv.*' + @echo "Linting code with ruff..." + $(PYTHON) -m ruff check . --fix + @echo "Type checking with mypy..." + $(PYTHON) -m mypy . -lint-flake8: - @echo "Running linting..." - $(PYTHON) -m flake8 . --max-line-length=120 --extend-ignore=E203,W503,I201,I100 --exclude=venv,.venv,__pycache__,.git - $(PYTHON) -m mypy . --ignore-missing-imports --exclude '.*(\.)?venv.*' +lint-strict: + @echo "Linting (no auto-fix)..." + $(PYTHON) -m ruff check . + @echo "Type checking with mypy..." + $(PYTHON) -m mypy . check: format lint -ci: format lint test-coverage +ci: lint-strict test-coverage qa: format lint test-coverage docs-check @echo "Quality assurance checks completed!" @@ -330,10 +330,11 @@ help: @echo " test-fast - Run fast tests only" @echo "" @echo "=== CODE QUALITY ===" - @echo " format - Format code (black + isort)" - @echo " lint - Run linting (ruff + mypy)" + @echo " format - Format code with ruff" + @echo " lint - Lint and auto-fix with ruff + mypy" + @echo " lint-strict - Lint without auto-fix + mypy" @echo " check - Format + lint" - @echo " ci - Full CI pipeline" + @echo " ci - Full CI pipeline (strict)" @echo " qa - Quality assurance" @echo "" @echo "=== DOCUMENTATION ===" diff --git a/alembic/env.py b/alembic/env.py index 8605ecc3..8217dcbe 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -40,9 +40,13 @@ print("=" * 70) # ADMIN MODELS # ---------------------------------------------------------------------------- try: - from models.database.admin import (AdminAuditLog, AdminNotification, - AdminSession, AdminSetting, - PlatformAlert) + from models.database.admin import ( + AdminAuditLog, + AdminNotification, + AdminSession, + AdminSetting, + PlatformAlert, + ) print(" āœ“ Admin models imported (5 models)") print(" - AdminAuditLog") @@ -176,8 +180,8 @@ except ImportError as e: # SUMMARY # ============================================================================ print("=" * 70) -print(f"[ALEMBIC] Model import completed") -print(f"[ALEMBIC] Tables detected in metadata:") +print("[ALEMBIC] Model import completed") +print("[ALEMBIC] Tables detected in metadata:") print("=" * 70) if Base.metadata.tables: diff --git a/app/api/deps.py b/app/api/deps.py index f2761a9a..3b2413fc 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -32,18 +32,20 @@ The cookie path restrictions prevent cross-context cookie leakage: """ import logging -from typing import Optional +from datetime import UTC from fastapi import Cookie, Depends, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.orm import Session from app.core.database import get_db -from app.exceptions import (AdminRequiredException, - InsufficientPermissionsException, - InvalidTokenException, - UnauthorizedVendorAccessException, - VendorNotFoundException) +from app.exceptions import ( + AdminRequiredException, + InsufficientPermissionsException, + InvalidTokenException, + UnauthorizedVendorAccessException, + VendorNotFoundException, +) from middleware.auth import AuthManager from middleware.rate_limiter import RateLimiter from models.database.user import User @@ -62,11 +64,11 @@ logger = logging.getLogger(__name__) def _get_token_from_request( - credentials: Optional[HTTPAuthorizationCredentials], - cookie_value: Optional[str], + credentials: HTTPAuthorizationCredentials | None, + cookie_value: str | None, cookie_name: str, request_path: str, -) -> tuple[Optional[str], Optional[str]]: +) -> tuple[str | None, str | None]: """ Extract token from Authorization header or cookie. @@ -86,7 +88,7 @@ def _get_token_from_request( if credentials: logger.debug(f"Token found in Authorization header for {request_path}") return credentials.credentials, "header" - elif cookie_value: + if cookie_value: logger.debug(f"Token found in {cookie_name} cookie for {request_path}") return cookie_value, "cookie" @@ -118,8 +120,8 @@ def _validate_user_token(token: str, db: Session) -> User: def get_current_admin_from_cookie_or_header( request: Request, - credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), - admin_token: Optional[str] = Cookie(None), + credentials: HTTPAuthorizationCredentials | None = Depends(security), + admin_token: str | None = Cookie(None), db: Session = Depends(get_db), ) -> User: """ @@ -205,8 +207,8 @@ def get_current_admin_api( def get_current_vendor_from_cookie_or_header( request: Request, - credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), - vendor_token: Optional[str] = Cookie(None), + credentials: HTTPAuthorizationCredentials | None = Depends(security), + vendor_token: str | None = Cookie(None), db: Session = Depends(get_db), ) -> User: """ @@ -305,8 +307,8 @@ def get_current_vendor_api( def get_current_customer_from_cookie_or_header( request: Request, - credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), - customer_token: Optional[str] = Cookie(None), + credentials: HTTPAuthorizationCredentials | None = Depends(security), + customer_token: str | None = Cookie(None), db: Session = Depends(get_db), ): """ @@ -331,7 +333,7 @@ def get_current_customer_from_cookie_or_header( Raises: InvalidTokenException: If no token or invalid token """ - from datetime import datetime, timezone + from datetime import datetime from jose import JWTError, jwt @@ -365,8 +367,8 @@ def get_current_customer_from_cookie_or_header( # Verify token hasn't expired exp = payload.get("exp") - if exp and datetime.fromtimestamp(exp, tz=timezone.utc) < datetime.now( - timezone.utc + if exp and datetime.fromtimestamp(exp, tz=UTC) < datetime.now( + UTC ): logger.warning(f"Expired customer token for customer_id={customer_id}") raise InvalidTokenException("Token has expired") @@ -694,10 +696,10 @@ def get_user_permissions( def get_current_admin_optional( request: Request, - credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), - admin_token: Optional[str] = Cookie(None), + credentials: HTTPAuthorizationCredentials | None = Depends(security), + admin_token: str | None = Cookie(None), db: Session = Depends(get_db), -) -> Optional[User]: +) -> User | None: """ Get current admin user from admin_token cookie or Authorization header. @@ -741,10 +743,10 @@ def get_current_admin_optional( def get_current_vendor_optional( request: Request, - credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), - vendor_token: Optional[str] = Cookie(None), + credentials: HTTPAuthorizationCredentials | None = Depends(security), + vendor_token: str | None = Cookie(None), db: Session = Depends(get_db), -) -> Optional[User]: +) -> User | None: """ Get current vendor user from vendor_token cookie or Authorization header. @@ -788,10 +790,10 @@ def get_current_vendor_optional( def get_current_customer_optional( request: Request, - credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), - customer_token: Optional[str] = Cookie(None), + credentials: HTTPAuthorizationCredentials | None = Depends(security), + customer_token: str | None = Cookie(None), db: Session = Depends(get_db), -) -> Optional[User]: +) -> User | None: """ Get current customer user from customer_token cookie or Authorization header. diff --git a/app/api/v1/admin/__init__.py b/app/api/v1/admin/__init__.py index a7b9d009..2d261c8a 100644 --- a/app/api/v1/admin/__init__.py +++ b/app/api/v1/admin/__init__.py @@ -24,9 +24,21 @@ IMPORTANT: from fastapi import APIRouter # Import all admin routers -from . import (audit, auth, code_quality, content_pages, dashboard, - marketplace, monitoring, notifications, settings, users, - vendor_domains, vendor_themes, vendors) +from . import ( + audit, + auth, + code_quality, + content_pages, + dashboard, + marketplace, + monitoring, + notifications, + settings, + users, + vendor_domains, + vendor_themes, + vendors, +) # Create admin router router = APIRouter() diff --git a/app/api/v1/admin/audit.py b/app/api/v1/admin/audit.py index d3d91511..bf78a8eb 100644 --- a/app/api/v1/admin/audit.py +++ b/app/api/v1/admin/audit.py @@ -10,7 +10,6 @@ Provides endpoints for: import logging from datetime import datetime -from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session @@ -19,9 +18,11 @@ from app.api.deps import get_current_admin_api from app.core.database import get_db from app.services.admin_audit_service import admin_audit_service from models.database.user import User -from models.schema.admin import (AdminAuditLogFilters, - AdminAuditLogListResponse, - AdminAuditLogResponse) +from models.schema.admin import ( + AdminAuditLogFilters, + AdminAuditLogListResponse, + AdminAuditLogResponse, +) router = APIRouter(prefix="/audit") logger = logging.getLogger(__name__) @@ -29,11 +30,11 @@ logger = logging.getLogger(__name__) @router.get("/logs", response_model=AdminAuditLogListResponse) def get_audit_logs( - admin_user_id: Optional[int] = Query(None, description="Filter by admin user"), - action: Optional[str] = Query(None, description="Filter by action type"), - target_type: Optional[str] = Query(None, description="Filter by target type"), - date_from: Optional[datetime] = Query(None, description="Filter from date"), - date_to: Optional[datetime] = Query(None, description="Filter to date"), + admin_user_id: int | None = Query(None, description="Filter by admin user"), + action: str | None = Query(None, description="Filter by action type"), + target_type: str | None = Query(None, description="Filter by target type"), + date_from: datetime | None = Query(None, description="Filter from date"), + date_to: datetime | None = Query(None, description="Filter to date"), skip: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(100, ge=1, le=1000, description="Maximum records to return"), db: Session = Depends(get_db), diff --git a/app/api/v1/admin/code_quality.py b/app/api/v1/admin/code_quality.py index 8193638b..bc82d4e8 100644 --- a/app/api/v1/admin/code_quality.py +++ b/app/api/v1/admin/code_quality.py @@ -4,7 +4,6 @@ RESTful API for architecture validation and violation management """ from datetime import datetime -from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field @@ -32,7 +31,7 @@ class ScanResponse(BaseModel): warnings: int duration_seconds: float triggered_by: str - git_commit_hash: Optional[str] + git_commit_hash: str | None class Config: from_attributes = True @@ -49,13 +48,13 @@ class ViolationResponse(BaseModel): file_path: str line_number: int message: str - context: Optional[str] - suggestion: Optional[str] + context: str | None + suggestion: str | None status: str - assigned_to: Optional[int] - resolved_at: Optional[str] - resolved_by: Optional[int] - resolution_note: Optional[str] + assigned_to: int | None + resolved_at: str | None + resolved_by: int | None + resolution_note: str | None created_at: str class Config: @@ -83,7 +82,7 @@ class AssignViolationRequest(BaseModel): """Request model for assigning a violation""" user_id: int = Field(..., description="User ID to assign to") - due_date: Optional[datetime] = Field(None, description="Due date for resolution") + due_date: datetime | None = Field(None, description="Due date for resolution") priority: str = Field( "medium", description="Priority level (low, medium, high, critical)" ) @@ -123,7 +122,7 @@ class DashboardStatsResponse(BaseModel): by_rule: dict by_module: dict top_files: list - last_scan: Optional[str] + last_scan: str | None # API Endpoints @@ -189,17 +188,17 @@ async def list_scans( @router.get("/violations", response_model=ViolationListResponse) async def list_violations( - scan_id: Optional[int] = Query( + scan_id: int | None = Query( None, description="Filter by scan ID (defaults to latest)" ), - severity: Optional[str] = Query( + severity: str | None = Query( None, description="Filter by severity (error, warning)" ), - status: Optional[str] = Query( + status: str | None = Query( None, description="Filter by status (open, assigned, resolved, ignored)" ), - rule_id: Optional[str] = Query(None, description="Filter by rule ID"), - file_path: Optional[str] = Query( + rule_id: str | None = Query(None, description="Filter by rule ID"), + file_path: str | None = Query( None, description="Filter by file path (partial match)" ), page: int = Query(1, ge=1, description="Page number"), diff --git a/app/api/v1/admin/content_pages.py b/app/api/v1/admin/content_pages.py index aceb521c..653c8397 100644 --- a/app/api/v1/admin/content_pages.py +++ b/app/api/v1/admin/content_pages.py @@ -9,7 +9,6 @@ Platform administrators can: """ import logging -from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field @@ -46,17 +45,17 @@ class ContentPageCreate(BaseModel): max_length=50, description="Template name (default, minimal, modern)", ) - meta_description: Optional[str] = Field( + meta_description: str | None = Field( None, max_length=300, description="SEO meta description" ) - meta_keywords: Optional[str] = Field( + meta_keywords: str | None = Field( None, max_length=300, description="SEO keywords" ) is_published: bool = Field(default=False, description="Publish immediately") show_in_footer: bool = Field(default=True, description="Show in footer navigation") show_in_header: bool = Field(default=False, description="Show in header navigation") display_order: int = Field(default=0, description="Display order (lower = first)") - vendor_id: Optional[int] = Field( + vendor_id: int | None = Field( None, description="Vendor ID (None for platform default)" ) @@ -64,32 +63,32 @@ class ContentPageCreate(BaseModel): class ContentPageUpdate(BaseModel): """Schema for updating a content page.""" - title: Optional[str] = Field(None, max_length=200) - content: Optional[str] = None - content_format: Optional[str] = None - template: Optional[str] = Field(None, max_length=50) - meta_description: Optional[str] = Field(None, max_length=300) - meta_keywords: Optional[str] = Field(None, max_length=300) - is_published: Optional[bool] = None - show_in_footer: Optional[bool] = None - show_in_header: Optional[bool] = None - display_order: Optional[int] = None + title: str | None = Field(None, max_length=200) + content: str | None = None + content_format: str | None = None + template: str | None = Field(None, max_length=50) + meta_description: str | None = Field(None, max_length=300) + meta_keywords: str | None = Field(None, max_length=300) + is_published: bool | None = None + show_in_footer: bool | None = None + show_in_header: bool | None = None + display_order: int | None = None class ContentPageResponse(BaseModel): """Schema for content page response.""" id: int - vendor_id: Optional[int] - vendor_name: Optional[str] + vendor_id: int | None + vendor_name: str | None slug: str title: str content: str content_format: str - meta_description: Optional[str] - meta_keywords: Optional[str] + meta_description: str | None + meta_keywords: str | None is_published: bool - published_at: Optional[str] + published_at: str | None display_order: int show_in_footer: bool show_in_header: bool @@ -97,8 +96,8 @@ class ContentPageResponse(BaseModel): is_vendor_override: bool created_at: str updated_at: str - created_by: Optional[int] - updated_by: Optional[int] + created_by: int | None + updated_by: int | None # ============================================================================ @@ -106,7 +105,7 @@ class ContentPageResponse(BaseModel): # ============================================================================ -@router.get("/platform", response_model=List[ContentPageResponse]) +@router.get("/platform", response_model=list[ContentPageResponse]) def list_platform_pages( include_unpublished: bool = Query(False, description="Include draft pages"), current_user: User = Depends(get_current_admin_api), @@ -161,9 +160,9 @@ def create_platform_page( # ============================================================================ -@router.get("/", response_model=List[ContentPageResponse]) +@router.get("/", response_model=list[ContentPageResponse]) def list_all_pages( - vendor_id: Optional[int] = Query(None, description="Filter by vendor ID"), + vendor_id: int | None = Query(None, description="Filter by vendor ID"), include_unpublished: bool = Query(False, description="Include draft pages"), current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), @@ -256,4 +255,4 @@ def delete_page( if not success: raise HTTPException(status_code=404, detail="Content page not found") - return None + return diff --git a/app/api/v1/admin/dashboard.py b/app/api/v1/admin/dashboard.py index 1ca77a50..0a599156 100644 --- a/app/api/v1/admin/dashboard.py +++ b/app/api/v1/admin/dashboard.py @@ -4,7 +4,6 @@ Admin dashboard and statistics endpoints. """ import logging -from typing import List from fastapi import APIRouter, Depends from sqlalchemy.orm import Session @@ -57,7 +56,7 @@ def get_comprehensive_stats( ) -@router.get("/stats/marketplace", response_model=List[MarketplaceStatsResponse]) +@router.get("/stats/marketplace", response_model=list[MarketplaceStatsResponse]) def get_marketplace_stats( db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), diff --git a/app/api/v1/admin/marketplace.py b/app/api/v1/admin/marketplace.py index b9456a53..3319bb51 100644 --- a/app/api/v1/admin/marketplace.py +++ b/app/api/v1/admin/marketplace.py @@ -4,7 +4,6 @@ Marketplace import job monitoring endpoints for admin. """ import logging -from typing import List, Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session @@ -20,11 +19,11 @@ router = APIRouter(prefix="/marketplace-import-jobs") logger = logging.getLogger(__name__) -@router.get("", response_model=List[MarketplaceImportJobResponse]) +@router.get("", response_model=list[MarketplaceImportJobResponse]) def get_all_marketplace_import_jobs( - marketplace: Optional[str] = Query(None), - vendor_name: Optional[str] = Query(None), - status: Optional[str] = Query(None), + marketplace: str | None = Query(None), + vendor_name: str | None = Query(None), + status: str | None = Query(None), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=100), db: Session = Depends(get_db), diff --git a/app/api/v1/admin/notifications.py b/app/api/v1/admin/notifications.py index 780885b7..090b2205 100644 --- a/app/api/v1/admin/notifications.py +++ b/app/api/v1/admin/notifications.py @@ -9,7 +9,6 @@ Provides endpoints for: """ import logging -from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session @@ -17,12 +16,13 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db from models.database.user import User -from models.schema.admin import (AdminNotificationCreate, - AdminNotificationListResponse, - AdminNotificationResponse, - PlatformAlertCreate, - PlatformAlertListResponse, - PlatformAlertResolve, PlatformAlertResponse) +from models.schema.admin import ( + AdminNotificationListResponse, + PlatformAlertCreate, + PlatformAlertListResponse, + PlatformAlertResolve, + PlatformAlertResponse, +) router = APIRouter(prefix="/notifications") logger = logging.getLogger(__name__) @@ -35,8 +35,8 @@ logger = logging.getLogger(__name__) @router.get("", response_model=AdminNotificationListResponse) def get_notifications( - priority: Optional[str] = Query(None, description="Filter by priority"), - is_read: Optional[bool] = Query(None, description="Filter by read status"), + priority: str | None = Query(None, description="Filter by priority"), + is_read: bool | None = Query(None, description="Filter by read status"), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), db: Session = Depends(get_db), @@ -87,8 +87,8 @@ def mark_all_as_read( @router.get("/alerts", response_model=PlatformAlertListResponse) def get_platform_alerts( - severity: Optional[str] = Query(None, description="Filter by severity"), - is_resolved: Optional[bool] = Query( + severity: str | None = Query(None, description="Filter by severity"), + is_resolved: bool | None = Query( None, description="Filter by resolution status" ), skip: int = Query(0, ge=0), diff --git a/app/api/v1/admin/settings.py b/app/api/v1/admin/settings.py index 025b2a3c..f0806e90 100644 --- a/app/api/v1/admin/settings.py +++ b/app/api/v1/admin/settings.py @@ -9,7 +9,6 @@ Provides endpoints for: """ import logging -from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session @@ -19,8 +18,12 @@ from app.core.database import get_db from app.services.admin_audit_service import admin_audit_service from app.services.admin_settings_service import admin_settings_service from models.database.user import User -from models.schema.admin import (AdminSettingCreate, AdminSettingListResponse, - AdminSettingResponse, AdminSettingUpdate) +from models.schema.admin import ( + AdminSettingCreate, + AdminSettingListResponse, + AdminSettingResponse, + AdminSettingUpdate, +) router = APIRouter(prefix="/settings") logger = logging.getLogger(__name__) @@ -28,8 +31,8 @@ logger = logging.getLogger(__name__) @router.get("", response_model=AdminSettingListResponse) def get_all_settings( - category: Optional[str] = Query(None, description="Filter by category"), - is_public: Optional[bool] = Query(None, description="Filter by public flag"), + category: str | None = Query(None, description="Filter by category"), + is_public: bool | None = Query(None, description="Filter by public flag"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): diff --git a/app/api/v1/admin/users.py b/app/api/v1/admin/users.py index 2cadd850..962aee6e 100644 --- a/app/api/v1/admin/users.py +++ b/app/api/v1/admin/users.py @@ -4,7 +4,6 @@ User management endpoints for admin. """ import logging -from typing import List from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session @@ -20,7 +19,7 @@ router = APIRouter(prefix="/users") logger = logging.getLogger(__name__) -@router.get("", response_model=List[UserResponse]) +@router.get("", response_model=list[UserResponse]) def get_all_users( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), diff --git a/app/api/v1/admin/vendor_domains.py b/app/api/v1/admin/vendor_domains.py index c33e1786..c3c00755 100644 --- a/app/api/v1/admin/vendor_domains.py +++ b/app/api/v1/admin/vendor_domains.py @@ -10,9 +10,8 @@ Follows the architecture pattern: """ import logging -from typing import List -from fastapi import APIRouter, Body, Depends, Path, Query +from fastapi import APIRouter, Body, Depends, Path from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api @@ -21,13 +20,15 @@ from app.exceptions import VendorNotFoundException from app.services.vendor_domain_service import vendor_domain_service from models.database.user import User from models.database.vendor import Vendor -from models.schema.vendor_domain import (DomainDeletionResponse, - DomainVerificationInstructions, - DomainVerificationResponse, - VendorDomainCreate, - VendorDomainListResponse, - VendorDomainResponse, - VendorDomainUpdate) +from models.schema.vendor_domain import ( + DomainDeletionResponse, + DomainVerificationInstructions, + DomainVerificationResponse, + VendorDomainCreate, + VendorDomainListResponse, + VendorDomainResponse, + VendorDomainUpdate, +) router = APIRouter(prefix="/vendors") logger = logging.getLogger(__name__) diff --git a/app/api/v1/admin/vendor_themes.py b/app/api/v1/admin/vendor_themes.py index 3a0fa06e..6c45c590 100644 --- a/app/api/v1/admin/vendor_themes.py +++ b/app/api/v1/admin/vendor_themes.py @@ -20,8 +20,11 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, get_db from app.services.vendor_theme_service import vendor_theme_service from models.database.user import User -from models.schema.vendor_theme import (ThemePresetListResponse, - VendorThemeResponse, VendorThemeUpdate) +from models.schema.vendor_theme import ( + ThemePresetListResponse, + VendorThemeResponse, + VendorThemeUpdate, +) router = APIRouter(prefix="/vendor-themes") logger = logging.getLogger(__name__) diff --git a/app/api/v1/admin/vendors.py b/app/api/v1/admin/vendors.py index 58b1f615..6066906e 100644 --- a/app/api/v1/admin/vendors.py +++ b/app/api/v1/admin/vendors.py @@ -4,7 +4,7 @@ Vendor management endpoints for admin. """ import logging -from typing import Optional +from datetime import UTC from fastapi import APIRouter, Body, Depends, Path, Query from sqlalchemy import func @@ -12,18 +12,21 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db -from app.exceptions import (ConfirmationRequiredException, - VendorNotFoundException) +from app.exceptions import ConfirmationRequiredException, VendorNotFoundException from app.services.admin_service import admin_service from app.services.stats_service import stats_service from models.database.user import User from models.database.vendor import Vendor from models.schema.stats import VendorStatsResponse -from models.schema.vendor import (VendorCreate, VendorCreateResponse, - VendorDetailResponse, VendorListResponse, - VendorResponse, VendorTransferOwnership, - VendorTransferOwnershipResponse, - VendorUpdate) +from models.schema.vendor import ( + VendorCreate, + VendorCreateResponse, + VendorDetailResponse, + VendorListResponse, + VendorTransferOwnership, + VendorTransferOwnershipResponse, + VendorUpdate, +) router = APIRouter(prefix="/vendors") logger = logging.getLogger(__name__) @@ -126,9 +129,9 @@ def create_vendor_with_owner( def get_all_vendors_admin( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), - search: Optional[str] = Query(None, description="Search by name or vendor code"), - is_active: Optional[bool] = Query(None), - is_verified: Optional[bool] = Query(None), + search: str | None = Query(None, description="Search by name or vendor code"), + is_active: bool | None = Query(None), + is_verified: bool | None = Query(None), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): @@ -282,7 +285,7 @@ def transfer_vendor_ownership( - `confirm_transfer`: Must be true - `transfer_reason`: Optional reason for audit trail """ - from datetime import datetime, timezone + from datetime import datetime vendor = _get_vendor_by_identifier(db, vendor_identifier) vendor, old_owner, new_owner = admin_service.transfer_vendor_ownership( @@ -304,7 +307,7 @@ def transfer_vendor_ownership( "username": new_owner.username, "email": new_owner.email, }, - transferred_at=datetime.now(timezone.utc), + transferred_at=datetime.now(UTC), transfer_reason=transfer_data.transfer_reason, ) diff --git a/app/api/v1/shop/cart.py b/app/api/v1/shop/cart.py index ac51667c..e05ba7c7 100644 --- a/app/api/v1/shop/cart.py +++ b/app/api/v1/shop/cart.py @@ -14,9 +14,13 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.services.cart_service import cart_service -from models.schema.cart import (AddToCartRequest, CartOperationResponse, - CartResponse, ClearCartResponse, - UpdateCartItemRequest) +from models.schema.cart import ( + AddToCartRequest, + CartOperationResponse, + CartResponse, + ClearCartResponse, + UpdateCartItemRequest, +) router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/api/v1/shop/content_pages.py b/app/api/v1/shop/content_pages.py index 48e19d64..8ab17249 100644 --- a/app/api/v1/shop/content_pages.py +++ b/app/api/v1/shop/content_pages.py @@ -7,7 +7,6 @@ No authentication required. """ import logging -from typing import List from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel @@ -52,7 +51,7 @@ class ContentPageListItem(BaseModel): # ============================================================================ -@router.get("/navigation", response_model=List[ContentPageListItem]) +@router.get("/navigation", response_model=list[ContentPageListItem]) def get_navigation_pages(request: Request, db: Session = Depends(get_db)): """ Get list of content pages for navigation (footer/header). diff --git a/app/api/v1/shop/orders.py b/app/api/v1/shop/orders.py index 74ac25bf..ea7dd648 100644 --- a/app/api/v1/shop/orders.py +++ b/app/api/v1/shop/orders.py @@ -8,7 +8,6 @@ Requires customer authentication for most operations. """ import logging -from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request from sqlalchemy.orm import Session @@ -19,8 +18,12 @@ from app.services.customer_service import customer_service from app.services.order_service import order_service from models.database.customer import Customer from models.database.user import User -from models.schema.order import (OrderCreate, OrderDetailResponse, - OrderListResponse, OrderResponse) +from models.schema.order import ( + OrderCreate, + OrderDetailResponse, + OrderListResponse, + OrderResponse, +) router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/api/v1/shop/products.py b/app/api/v1/shop/products.py index 37836bec..cf6f76e7 100644 --- a/app/api/v1/shop/products.py +++ b/app/api/v1/shop/products.py @@ -8,15 +8,17 @@ No authentication required. """ import logging -from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request from sqlalchemy.orm import Session from app.core.database import get_db from app.services.product_service import product_service -from models.schema.product import (ProductDetailResponse, ProductListResponse, - ProductResponse) +from models.schema.product import ( + ProductDetailResponse, + ProductListResponse, + ProductResponse, +) router = APIRouter() logger = logging.getLogger(__name__) @@ -27,8 +29,8 @@ def get_product_catalog( request: Request, skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), - search: Optional[str] = Query(None, description="Search products by name"), - is_featured: Optional[bool] = Query( + search: str | None = Query(None, description="Search products by name"), + is_featured: bool | None = Query( None, description="Filter by featured products" ), db: Session = Depends(get_db), diff --git a/app/api/v1/vendor/__init__.py b/app/api/v1/vendor/__init__.py index b984af4f..90fefb44 100644 --- a/app/api/v1/vendor/__init__.py +++ b/app/api/v1/vendor/__init__.py @@ -13,9 +13,24 @@ IMPORTANT: from fastapi import APIRouter # Import all sub-routers (JSON API only) -from . import (analytics, auth, content_pages, customers, dashboard, info, - inventory, marketplace, media, notifications, orders, payments, - products, profile, settings, team) +from . import ( + analytics, + auth, + content_pages, + customers, + dashboard, + info, + inventory, + marketplace, + media, + notifications, + orders, + payments, + products, + profile, + settings, + team, +) # Create vendor router router = APIRouter() diff --git a/app/api/v1/vendor/content_pages.py b/app/api/v1/vendor/content_pages.py index 4a552898..4f50d8c0 100644 --- a/app/api/v1/vendor/content_pages.py +++ b/app/api/v1/vendor/content_pages.py @@ -9,7 +9,6 @@ Vendors can: """ import logging -from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field @@ -41,10 +40,10 @@ class VendorContentPageCreate(BaseModel): content_format: str = Field( default="html", description="Content format: html or markdown" ) - meta_description: Optional[str] = Field( + meta_description: str | None = Field( None, max_length=300, description="SEO meta description" ) - meta_keywords: Optional[str] = Field( + meta_keywords: str | None = Field( None, max_length=300, description="SEO keywords" ) is_published: bool = Field(default=False, description="Publish immediately") @@ -56,31 +55,31 @@ class VendorContentPageCreate(BaseModel): class VendorContentPageUpdate(BaseModel): """Schema for updating a vendor content page.""" - title: Optional[str] = Field(None, max_length=200) - content: Optional[str] = None - content_format: Optional[str] = None - meta_description: Optional[str] = Field(None, max_length=300) - meta_keywords: Optional[str] = Field(None, max_length=300) - is_published: Optional[bool] = None - show_in_footer: Optional[bool] = None - show_in_header: Optional[bool] = None - display_order: Optional[int] = None + title: str | None = Field(None, max_length=200) + content: str | None = None + content_format: str | None = None + meta_description: str | None = Field(None, max_length=300) + meta_keywords: str | None = Field(None, max_length=300) + is_published: bool | None = None + show_in_footer: bool | None = None + show_in_header: bool | None = None + display_order: int | None = None class ContentPageResponse(BaseModel): """Schema for content page response.""" id: int - vendor_id: Optional[int] - vendor_name: Optional[str] + vendor_id: int | None + vendor_name: str | None slug: str title: str content: str content_format: str - meta_description: Optional[str] - meta_keywords: Optional[str] + meta_description: str | None + meta_keywords: str | None is_published: bool - published_at: Optional[str] + published_at: str | None display_order: int show_in_footer: bool show_in_header: bool @@ -88,8 +87,8 @@ class ContentPageResponse(BaseModel): is_vendor_override: bool created_at: str updated_at: str - created_by: Optional[int] - updated_by: Optional[int] + created_by: int | None + updated_by: int | None # ============================================================================ @@ -97,7 +96,7 @@ class ContentPageResponse(BaseModel): # ============================================================================ -@router.get("/", response_model=List[ContentPageResponse]) +@router.get("/", response_model=list[ContentPageResponse]) def list_vendor_pages( include_unpublished: bool = Query(False, description="Include draft pages"), current_user: User = Depends(get_current_vendor_api), @@ -120,7 +119,7 @@ def list_vendor_pages( return [page.to_dict() for page in pages] -@router.get("/overrides", response_model=List[ContentPageResponse]) +@router.get("/overrides", response_model=list[ContentPageResponse]) def list_vendor_overrides( include_unpublished: bool = Query(False, description="Include draft pages"), current_user: User = Depends(get_current_vendor_api), @@ -284,4 +283,4 @@ def delete_vendor_page( # Delete content_page_service.delete_page(db, page_id) - return None + return diff --git a/app/api/v1/vendor/customers.py b/app/api/v1/vendor/customers.py index 3a267c17..823bb277 100644 --- a/app/api/v1/vendor/customers.py +++ b/app/api/v1/vendor/customers.py @@ -5,7 +5,6 @@ Vendor customer management endpoints. """ import logging -from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session @@ -24,8 +23,8 @@ logger = logging.getLogger(__name__) def get_vendor_customers( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), - search: Optional[str] = Query(None), - is_active: Optional[bool] = Query(None), + search: str | None = Query(None), + is_active: bool | None = Query(None), vendor: Vendor = Depends(require_vendor_context()), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), diff --git a/app/api/v1/vendor/dashboard.py b/app/api/v1/vendor/dashboard.py index b49351a6..98b9586e 100644 --- a/app/api/v1/vendor/dashboard.py +++ b/app/api/v1/vendor/dashboard.py @@ -11,9 +11,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_api from app.core.database import get_db from app.services.stats_service import stats_service -from middleware.vendor_context import require_vendor_context from models.database.user import User -from models.database.vendor import Vendor router = APIRouter(prefix="/dashboard") logger = logging.getLogger(__name__) diff --git a/app/api/v1/vendor/info.py b/app/api/v1/vendor/info.py index e84d0a8d..53c46476 100644 --- a/app/api/v1/vendor/info.py +++ b/app/api/v1/vendor/info.py @@ -16,7 +16,7 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.exceptions import VendorNotFoundException from models.database.vendor import Vendor -from models.schema.vendor import VendorDetailResponse, VendorResponse +from models.schema.vendor import VendorDetailResponse router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/api/v1/vendor/inventory.py b/app/api/v1/vendor/inventory.py index 0cb49c2c..9e6a13f0 100644 --- a/app/api/v1/vendor/inventory.py +++ b/app/api/v1/vendor/inventory.py @@ -1,6 +1,5 @@ # app/api/v1/vendor/inventory.py import logging -from typing import List, Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session @@ -11,10 +10,15 @@ from app.services.inventory_service import inventory_service from middleware.vendor_context import require_vendor_context from models.database.user import User from models.database.vendor import Vendor -from models.schema.inventory import (InventoryAdjust, InventoryCreate, - InventoryListResponse, InventoryReserve, - InventoryResponse, InventoryUpdate, - ProductInventorySummary) +from models.schema.inventory import ( + InventoryAdjust, + InventoryCreate, + InventoryListResponse, + InventoryReserve, + InventoryResponse, + InventoryUpdate, + ProductInventorySummary, +) router = APIRouter() logger = logging.getLogger(__name__) @@ -90,8 +94,8 @@ def get_product_inventory( def get_vendor_inventory( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), - location: Optional[str] = Query(None), - low_stock: Optional[int] = Query(None, ge=0), + location: str | None = Query(None), + low_stock: int | None = Query(None, ge=0), vendor: Vendor = Depends(require_vendor_context()), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), diff --git a/app/api/v1/vendor/marketplace.py b/app/api/v1/vendor/marketplace.py index 9084bfd2..12001170 100644 --- a/app/api/v1/vendor/marketplace.py +++ b/app/api/v1/vendor/marketplace.py @@ -5,22 +5,22 @@ Vendor context is automatically injected by middleware. """ import logging -from typing import List, Optional from fastapi import APIRouter, BackgroundTasks, Depends, Query from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_api from app.core.database import get_db -from app.services.marketplace_import_job_service import \ - marketplace_import_job_service +from app.services.marketplace_import_job_service import marketplace_import_job_service from app.tasks.background_tasks import process_marketplace_import from middleware.decorators import rate_limit from middleware.vendor_context import require_vendor_context # IMPORTANT from models.database.user import User from models.database.vendor import Vendor -from models.schema.marketplace_import_job import (MarketplaceImportJobRequest, - MarketplaceImportJobResponse) +from models.schema.marketplace_import_job import ( + MarketplaceImportJobRequest, + MarketplaceImportJobResponse, +) router = APIRouter() logger = logging.getLogger(__name__) @@ -93,9 +93,9 @@ def get_marketplace_import_status( return marketplace_import_job_service.convert_to_response_model(job) -@router.get("/imports", response_model=List[MarketplaceImportJobResponse]) +@router.get("/imports", response_model=list[MarketplaceImportJobResponse]) def get_marketplace_import_jobs( - marketplace: Optional[str] = Query(None, description="Filter by marketplace"), + marketplace: str | None = Query(None, description="Filter by marketplace"), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), vendor: Vendor = Depends(require_vendor_context()), diff --git a/app/api/v1/vendor/media.py b/app/api/v1/vendor/media.py index 900bbb6e..dc5aaf6d 100644 --- a/app/api/v1/vendor/media.py +++ b/app/api/v1/vendor/media.py @@ -5,7 +5,6 @@ Vendor media and file management endpoints. """ import logging -from typing import Optional from fastapi import APIRouter, Depends, File, Query, UploadFile from sqlalchemy.orm import Session @@ -24,8 +23,8 @@ logger = logging.getLogger(__name__) def get_media_library( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), - media_type: Optional[str] = Query(None, description="image, video, document"), - search: Optional[str] = Query(None), + media_type: str | None = Query(None, description="image, video, document"), + search: str | None = Query(None), vendor: Vendor = Depends(require_vendor_context()), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), @@ -52,7 +51,7 @@ def get_media_library( @router.post("/upload") async def upload_media( file: UploadFile = File(...), - folder: Optional[str] = Query(None, description="products, general, etc."), + folder: str | None = Query(None, description="products, general, etc."), vendor: Vendor = Depends(require_vendor_context()), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), @@ -78,7 +77,7 @@ async def upload_media( @router.post("/upload/multiple") async def upload_multiple_media( files: list[UploadFile] = File(...), - folder: Optional[str] = Query(None), + folder: str | None = Query(None), vendor: Vendor = Depends(require_vendor_context()), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), diff --git a/app/api/v1/vendor/notifications.py b/app/api/v1/vendor/notifications.py index 2b6a8205..26610a74 100644 --- a/app/api/v1/vendor/notifications.py +++ b/app/api/v1/vendor/notifications.py @@ -5,7 +5,6 @@ Vendor notification management endpoints. """ import logging -from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session @@ -24,7 +23,7 @@ logger = logging.getLogger(__name__) def get_notifications( skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), - unread_only: Optional[bool] = Query(False), + unread_only: bool | None = Query(False), vendor: Vendor = Depends(require_vendor_context()), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), diff --git a/app/api/v1/vendor/orders.py b/app/api/v1/vendor/orders.py index a32b4253..74d91f83 100644 --- a/app/api/v1/vendor/orders.py +++ b/app/api/v1/vendor/orders.py @@ -4,9 +4,8 @@ Vendor order management endpoints. """ import logging -from typing import Optional -from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_api @@ -14,9 +13,13 @@ from app.core.database import get_db from app.services.order_service import order_service from middleware.vendor_context import require_vendor_context from models.database.user import User -from models.database.vendor import Vendor, VendorUser -from models.schema.order import (OrderDetailResponse, OrderListResponse, - OrderResponse, OrderUpdate) +from models.database.vendor import Vendor +from models.schema.order import ( + OrderDetailResponse, + OrderListResponse, + OrderResponse, + OrderUpdate, +) router = APIRouter(prefix="/orders") logger = logging.getLogger(__name__) @@ -26,8 +29,8 @@ logger = logging.getLogger(__name__) def get_vendor_orders( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), - status: Optional[str] = Query(None, description="Filter by order status"), - customer_id: Optional[int] = Query(None, description="Filter by customer"), + status: str | None = Query(None, description="Filter by order status"), + customer_id: int | None = Query(None, description="Filter by customer"), vendor: Vendor = Depends(require_vendor_context()), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), diff --git a/app/api/v1/vendor/products.py b/app/api/v1/vendor/products.py index f2263ef3..ed04f2fd 100644 --- a/app/api/v1/vendor/products.py +++ b/app/api/v1/vendor/products.py @@ -4,7 +4,6 @@ Vendor product catalog management endpoints. """ import logging -from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session @@ -15,9 +14,13 @@ from app.services.product_service import product_service from middleware.vendor_context import require_vendor_context from models.database.user import User from models.database.vendor import Vendor -from models.schema.product import (ProductCreate, ProductDetailResponse, - ProductListResponse, ProductResponse, - ProductUpdate) +from models.schema.product import ( + ProductCreate, + ProductDetailResponse, + ProductListResponse, + ProductResponse, + ProductUpdate, +) router = APIRouter(prefix="/products") logger = logging.getLogger(__name__) @@ -27,8 +30,8 @@ logger = logging.getLogger(__name__) def get_vendor_products( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), - is_active: Optional[bool] = Query(None), - is_featured: Optional[bool] = Query(None), + is_active: bool | None = Query(None), + is_featured: bool | None = Query(None), vendor: Vendor = Depends(require_vendor_context()), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), diff --git a/app/api/v1/vendor/team.py b/app/api/v1/vendor/team.py index fe84c858..73d0f127 100644 --- a/app/api/v1/vendor/team.py +++ b/app/api/v1/vendor/team.py @@ -11,25 +11,34 @@ Implements complete team management with: """ import logging -from typing import List from fastapi import APIRouter, Depends, Request from sqlalchemy.orm import Session -from app.api.deps import (get_current_vendor_api, get_user_permissions, - require_vendor_owner, require_vendor_permission) +from app.api.deps import ( + get_current_vendor_api, + get_user_permissions, + require_vendor_owner, + require_vendor_permission, +) from app.core.database import get_db from app.core.permissions import VendorPermissions from app.services.vendor_team_service import vendor_team_service from models.database.user import User -from models.database.vendor import Vendor -from models.schema.team import (BulkRemoveRequest, BulkRemoveResponse, - InvitationAccept, InvitationAcceptResponse, - InvitationResponse, RoleListResponse, - RoleResponse, TeamMemberInvite, - TeamMemberListResponse, TeamMemberResponse, - TeamMemberUpdate, TeamStatistics, - UserPermissionsResponse) +from models.schema.team import ( + BulkRemoveRequest, + BulkRemoveResponse, + InvitationAccept, + InvitationAcceptResponse, + InvitationResponse, + RoleListResponse, + TeamMemberInvite, + TeamMemberListResponse, + TeamMemberResponse, + TeamMemberUpdate, + TeamStatistics, + UserPermissionsResponse, +) router = APIRouter(prefix="/team") logger = logging.getLogger(__name__) @@ -382,7 +391,7 @@ def list_roles( @router.get("/me/permissions", response_model=UserPermissionsResponse) def get_my_permissions( request: Request, - permissions: List[str] = Depends(get_user_permissions), + permissions: list[str] = Depends(get_user_permissions), current_user: User = Depends(get_current_vendor_api), ): """ diff --git a/app/core/config.py b/app/core/config.py index ed0b2641..915bee47 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -13,7 +13,6 @@ Note: Environment detection is handled by app.core.environment module. This module focuses purely on configuration storage and validation. """ -from typing import List, Optional from pydantic_settings import BaseSettings @@ -82,7 +81,7 @@ class Settings(BaseSettings): # ============================================================================= # MIDDLEWARE & SECURITY # ============================================================================= - allowed_hosts: List[str] = ["*"] # Configure for production + allowed_hosts: list[str] = ["*"] # Configure for production # Rate Limiting rate_limit_enabled: bool = True @@ -93,7 +92,7 @@ class Settings(BaseSettings): # LOGGING # ============================================================================= log_level: str = "INFO" - log_file: Optional[str] = None + log_file: str | None = None # ============================================================================= # PLATFORM DOMAIN CONFIGURATION @@ -138,9 +137,13 @@ settings = Settings() # ENVIRONMENT UTILITIES - Module-level functions # ============================================================================= # Import environment detection utilities -from app.core.environment import (get_environment, is_development, - is_production, is_staging, - should_use_secure_cookies) +from app.core.environment import ( + get_environment, + is_development, + is_production, + is_staging, + should_use_secure_cookies, +) def get_current_environment() -> str: @@ -188,7 +191,7 @@ def is_staging_environment() -> bool: # ============================================================================= -def validate_production_settings() -> List[str]: +def validate_production_settings() -> list[str]: """ Validate settings for production environment. diff --git a/app/core/environment.py b/app/core/environment.py index 0a430311..1c80991c 100644 --- a/app/core/environment.py +++ b/app/core/environment.py @@ -31,18 +31,18 @@ def get_environment() -> EnvironmentType: env = os.getenv("ENV", "").lower() if env in ["development", "dev", "local"]: return "development" - elif env in ["staging", "stage"]: + if env in ["staging", "stage"]: return "staging" - elif env in ["production", "prod"]: + if env in ["production", "prod"]: return "production" # Priority 2: ENVIRONMENT variable env = os.getenv("ENVIRONMENT", "").lower() if env in ["development", "dev", "local"]: return "development" - elif env in ["staging", "stage"]: + if env in ["staging", "stage"]: return "staging" - elif env in ["production", "prod"]: + if env in ["production", "prod"]: return "production" # Priority 3: Auto-detect from common indicators diff --git a/app/core/lifespan.py b/app/core/lifespan.py index f26a8363..a4e9dddb 100644 --- a/app/core/lifespan.py +++ b/app/core/lifespan.py @@ -16,7 +16,7 @@ from sqlalchemy import text from middleware.auth import AuthManager -from .database import SessionLocal, engine +from .database import engine from .logging import setup_logging # Remove this import if not needed: from models.database.base import Base @@ -60,7 +60,6 @@ def check_database_ready(): def get_migration_status(): """Get current Alembic migration status.""" try: - from alembic import command from alembic.config import Config alembic_cfg = Config("alembic.ini") diff --git a/app/core/permissions.py b/app/core/permissions.py index 2a04968c..be02f0ef 100644 --- a/app/core/permissions.py +++ b/app/core/permissions.py @@ -7,8 +7,8 @@ This module defines: - Permission groups (for easier role creation) - Permission checking utilities """ + from enum import Enum -from typing import List, Set class VendorPermissions(str, Enum): @@ -78,10 +78,10 @@ class PermissionGroups: """Pre-defined permission groups for common roles.""" # Full access (for owners) - OWNER: Set[str] = set(p.value for p in VendorPermissions) + OWNER: set[str] = set(p.value for p in VendorPermissions) # Manager - Can do most things except team management and critical settings - MANAGER: Set[str] = { + MANAGER: set[str] = { VendorPermissions.DASHBOARD_VIEW.value, VendorPermissions.PRODUCTS_VIEW.value, VendorPermissions.PRODUCTS_CREATE.value, @@ -113,7 +113,7 @@ class PermissionGroups: } # Staff - Can view and edit products/orders but limited access - STAFF: Set[str] = { + STAFF: set[str] = { VendorPermissions.DASHBOARD_VIEW.value, VendorPermissions.PRODUCTS_VIEW.value, VendorPermissions.PRODUCTS_CREATE.value, @@ -127,7 +127,7 @@ class PermissionGroups: } # Support - Can view and assist with orders/customers - SUPPORT: Set[str] = { + SUPPORT: set[str] = { VendorPermissions.DASHBOARD_VIEW.value, VendorPermissions.PRODUCTS_VIEW.value, VendorPermissions.ORDERS_VIEW.value, @@ -137,7 +137,7 @@ class PermissionGroups: } # Viewer - Read-only access - VIEWER: Set[str] = { + VIEWER: set[str] = { VendorPermissions.DASHBOARD_VIEW.value, VendorPermissions.PRODUCTS_VIEW.value, VendorPermissions.STOCK_VIEW.value, @@ -147,7 +147,7 @@ class PermissionGroups: } # Marketing - Focused on marketing and customer communication - MARKETING: Set[str] = { + MARKETING: set[str] = { VendorPermissions.DASHBOARD_VIEW.value, VendorPermissions.CUSTOMERS_VIEW.value, VendorPermissions.CUSTOMERS_EXPORT.value, @@ -162,34 +162,34 @@ class PermissionChecker: """Utility class for permission checking.""" @staticmethod - def has_permission(permissions: List[str], required_permission: str) -> bool: + def has_permission(permissions: list[str], required_permission: str) -> bool: """Check if a permission list contains a required permission.""" return required_permission in permissions @staticmethod def has_any_permission( - permissions: List[str], required_permissions: List[str] + permissions: list[str], required_permissions: list[str] ) -> bool: """Check if a permission list contains ANY of the required permissions.""" return any(perm in permissions for perm in required_permissions) @staticmethod def has_all_permissions( - permissions: List[str], required_permissions: List[str] + permissions: list[str], required_permissions: list[str] ) -> bool: """Check if a permission list contains ALL of the required permissions.""" return all(perm in permissions for perm in required_permissions) @staticmethod def get_missing_permissions( - permissions: List[str], required_permissions: List[str] - ) -> List[str]: + permissions: list[str], required_permissions: list[str] + ) -> list[str]: """Get list of missing permissions.""" return [perm for perm in required_permissions if perm not in permissions] # Helper function to get permissions for a role preset -def get_preset_permissions(preset_name: str) -> Set[str]: +def get_preset_permissions(preset_name: str) -> set[str]: """ Get permissions for a preset role. diff --git a/app/exceptions/__init__.py b/app/exceptions/__init__.py index fa7f17f1..8dc654ff 100644 --- a/app/exceptions/__init__.py +++ b/app/exceptions/__init__.py @@ -7,108 +7,178 @@ messages, and HTTP status mappings. """ # Admin exceptions -from .admin import (AdminOperationException, BulkOperationException, - CannotModifyAdminException, CannotModifySelfException, - ConfirmationRequiredException, InvalidAdminActionException, - UserNotFoundException, UserStatusChangeException, - VendorVerificationException) +from .admin import ( + AdminOperationException, + BulkOperationException, + CannotModifyAdminException, + CannotModifySelfException, + ConfirmationRequiredException, + InvalidAdminActionException, + UserNotFoundException, + UserStatusChangeException, + VendorVerificationException, +) + # Authentication exceptions -from .auth import (AdminRequiredException, InsufficientPermissionsException, - InvalidCredentialsException, InvalidTokenException, - TokenExpiredException, UserAlreadyExistsException, - UserNotActiveException) +from .auth import ( + AdminRequiredException, + InsufficientPermissionsException, + InvalidCredentialsException, + InvalidTokenException, + TokenExpiredException, + UserAlreadyExistsException, + UserNotActiveException, +) + # Base exceptions -from .base import (AuthenticationException, AuthorizationException, - BusinessLogicException, ConflictException, - ExternalServiceException, RateLimitException, - ResourceNotFoundException, ServiceUnavailableException, - ValidationException, WizamartException) +from .base import ( + AuthenticationException, + AuthorizationException, + BusinessLogicException, + ConflictException, + ExternalServiceException, + RateLimitException, + ResourceNotFoundException, + ServiceUnavailableException, + ValidationException, + WizamartException, +) + # Cart exceptions -from .cart import (CartItemNotFoundException, CartValidationException, - EmptyCartException, InsufficientInventoryForCartException, - InvalidCartQuantityException, - ProductNotAvailableForCartException) +from .cart import ( + CartItemNotFoundException, + CartValidationException, + EmptyCartException, + InsufficientInventoryForCartException, + InvalidCartQuantityException, + ProductNotAvailableForCartException, +) + # Customer exceptions -from .customer import (CustomerAlreadyExistsException, - CustomerAuthorizationException, - CustomerNotActiveException, CustomerNotFoundException, - CustomerValidationException, - DuplicateCustomerEmailException, - InvalidCustomerCredentialsException) +from .customer import ( + CustomerAlreadyExistsException, + CustomerAuthorizationException, + CustomerNotActiveException, + CustomerNotFoundException, + CustomerValidationException, + DuplicateCustomerEmailException, + InvalidCustomerCredentialsException, +) + # Inventory exceptions -from .inventory import (InsufficientInventoryException, - InvalidInventoryOperationException, - InvalidQuantityException, InventoryNotFoundException, - InventoryValidationException, - LocationNotFoundException, NegativeInventoryException) +from .inventory import ( + InsufficientInventoryException, + InvalidInventoryOperationException, + InvalidQuantityException, + InventoryNotFoundException, + InventoryValidationException, + LocationNotFoundException, + NegativeInventoryException, +) + # Marketplace import job exceptions -from .marketplace_import_job import (ImportJobAlreadyProcessingException, - ImportJobCannotBeCancelledException, - ImportJobCannotBeDeletedException, - ImportJobNotFoundException, - ImportJobNotOwnedException, - ImportRateLimitException, - InvalidImportDataException, - InvalidMarketplaceException, - MarketplaceConnectionException, - MarketplaceDataParsingException, - MarketplaceImportException) +from .marketplace_import_job import ( + ImportJobAlreadyProcessingException, + ImportJobCannotBeCancelledException, + ImportJobCannotBeDeletedException, + ImportJobNotFoundException, + ImportJobNotOwnedException, + ImportRateLimitException, + InvalidImportDataException, + InvalidMarketplaceException, + MarketplaceConnectionException, + MarketplaceDataParsingException, + MarketplaceImportException, +) + # Marketplace product exceptions -from .marketplace_product import (InvalidGTINException, - InvalidMarketplaceProductDataException, - MarketplaceProductAlreadyExistsException, - MarketplaceProductCSVImportException, - MarketplaceProductNotFoundException, - MarketplaceProductValidationException) +from .marketplace_product import ( + InvalidGTINException, + InvalidMarketplaceProductDataException, + MarketplaceProductAlreadyExistsException, + MarketplaceProductCSVImportException, + MarketplaceProductNotFoundException, + MarketplaceProductValidationException, +) + # Order exceptions -from .order import (InvalidOrderStatusException, OrderAlreadyExistsException, - OrderCannotBeCancelledException, OrderNotFoundException, - OrderValidationException) +from .order import ( + InvalidOrderStatusException, + OrderAlreadyExistsException, + OrderCannotBeCancelledException, + OrderNotFoundException, + OrderValidationException, +) + # Product exceptions -from .product import (CannotDeleteProductWithInventoryException, - CannotDeleteProductWithOrdersException, - InvalidProductDataException, - ProductAlreadyExistsException, ProductNotActiveException, - ProductNotFoundException, ProductNotInCatalogException, - ProductValidationException) +from .product import ( + CannotDeleteProductWithInventoryException, + CannotDeleteProductWithOrdersException, + InvalidProductDataException, + ProductAlreadyExistsException, + ProductNotActiveException, + ProductNotFoundException, + ProductNotInCatalogException, + ProductValidationException, +) + # Team exceptions -from .team import (CannotModifyOwnRoleException, CannotRemoveOwnerException, - InsufficientTeamPermissionsException, - InvalidInvitationDataException, - InvalidInvitationTokenException, InvalidRoleException, - MaxTeamMembersReachedException, RoleNotFoundException, - TeamInvitationAlreadyAcceptedException, - TeamInvitationExpiredException, - TeamInvitationNotFoundException, - TeamMemberAlreadyExistsException, - TeamMemberNotFoundException, TeamValidationException, - UnauthorizedTeamActionException) +from .team import ( + CannotModifyOwnRoleException, + CannotRemoveOwnerException, + InsufficientTeamPermissionsException, + InvalidInvitationDataException, + InvalidInvitationTokenException, + InvalidRoleException, + MaxTeamMembersReachedException, + RoleNotFoundException, + TeamInvitationAlreadyAcceptedException, + TeamInvitationExpiredException, + TeamInvitationNotFoundException, + TeamMemberAlreadyExistsException, + TeamMemberNotFoundException, + TeamValidationException, + UnauthorizedTeamActionException, +) + # Vendor exceptions -from .vendor import (InvalidVendorDataException, MaxVendorsReachedException, - UnauthorizedVendorAccessException, - VendorAlreadyExistsException, VendorNotActiveException, - VendorNotFoundException, VendorNotVerifiedException, - VendorValidationException) +from .vendor import ( + InvalidVendorDataException, + MaxVendorsReachedException, + UnauthorizedVendorAccessException, + VendorAlreadyExistsException, + VendorNotActiveException, + VendorNotFoundException, + VendorNotVerifiedException, + VendorValidationException, +) + # Vendor domain exceptions -from .vendor_domain import (DNSVerificationException, - DomainAlreadyVerifiedException, - DomainNotVerifiedException, - DomainVerificationFailedException, - InvalidDomainFormatException, - MaxDomainsReachedException, - MultiplePrimaryDomainsException, - ReservedDomainException, - UnauthorizedDomainAccessException, - VendorDomainAlreadyExistsException, - VendorDomainNotFoundException) +from .vendor_domain import ( + DNSVerificationException, + DomainAlreadyVerifiedException, + DomainNotVerifiedException, + DomainVerificationFailedException, + InvalidDomainFormatException, + MaxDomainsReachedException, + MultiplePrimaryDomainsException, + ReservedDomainException, + UnauthorizedDomainAccessException, + VendorDomainAlreadyExistsException, + VendorDomainNotFoundException, +) + # Vendor theme exceptions -from .vendor_theme import (InvalidColorFormatException, - InvalidFontFamilyException, - InvalidThemeDataException, ThemeOperationException, - ThemePresetAlreadyAppliedException, - ThemePresetNotFoundException, - ThemeValidationException, - VendorThemeNotFoundException) +from .vendor_theme import ( + InvalidColorFormatException, + InvalidFontFamilyException, + InvalidThemeDataException, + ThemeOperationException, + ThemePresetAlreadyAppliedException, + ThemePresetNotFoundException, + ThemeValidationException, + VendorThemeNotFoundException, +) __all__ = [ # Base exceptions diff --git a/app/exceptions/admin.py b/app/exceptions/admin.py index c98abdb0..c385f925 100644 --- a/app/exceptions/admin.py +++ b/app/exceptions/admin.py @@ -3,10 +3,14 @@ Admin operations specific exceptions. """ -from typing import Any, Dict, Optional +from typing import Any -from .base import (AuthorizationException, BusinessLogicException, - ResourceNotFoundException, ValidationException) +from .base import ( + AuthorizationException, + BusinessLogicException, + ResourceNotFoundException, + ValidationException, +) class UserNotFoundException(ResourceNotFoundException): @@ -36,7 +40,7 @@ class UserStatusChangeException(BusinessLogicException): user_id: int, current_status: str, attempted_action: str, - reason: Optional[str] = None, + reason: str | None = None, ): message = f"Cannot {attempted_action} user {user_id} (current status: {current_status})" if reason: @@ -61,7 +65,7 @@ class ShopVerificationException(BusinessLogicException): self, shop_id: int, reason: str, - current_verification_status: Optional[bool] = None, + current_verification_status: bool | None = None, ): details = { "shop_id": shop_id, @@ -85,8 +89,8 @@ class AdminOperationException(BusinessLogicException): self, operation: str, reason: str, - target_type: Optional[str] = None, - target_id: Optional[str] = None, + target_type: str | None = None, + target_id: str | None = None, ): message = f"Admin operation '{operation}' failed: {reason}" @@ -142,7 +146,7 @@ class InvalidAdminActionException(ValidationException): self, action: str, reason: str, - valid_actions: Optional[list] = None, + valid_actions: list | None = None, ): details = { "action": action, @@ -167,7 +171,7 @@ class BulkOperationException(BusinessLogicException): operation: str, total_items: int, failed_items: int, - errors: Optional[Dict[str, Any]] = None, + errors: dict[str, Any] | None = None, ): message = f"Bulk {operation} completed with errors: {failed_items}/{total_items} failed" @@ -194,7 +198,7 @@ class ConfirmationRequiredException(BusinessLogicException): def __init__( self, operation: str, - message: Optional[str] = None, + message: str | None = None, confirmation_param: str = "confirm", ): if not message: @@ -217,7 +221,7 @@ class VendorVerificationException(BusinessLogicException): self, vendor_id: int, reason: str, - current_verification_status: Optional[bool] = None, + current_verification_status: bool | None = None, ): details = { "vendor_id": vendor_id, diff --git a/app/exceptions/auth.py b/app/exceptions/auth.py index 48c96c15..8af58f40 100644 --- a/app/exceptions/auth.py +++ b/app/exceptions/auth.py @@ -3,10 +3,8 @@ Authentication and authorization specific exceptions. """ -from typing import Optional -from .base import (AuthenticationException, AuthorizationException, - ConflictException) +from .base import AuthenticationException, AuthorizationException, ConflictException class InvalidCredentialsException(AuthenticationException): @@ -45,7 +43,7 @@ class InsufficientPermissionsException(AuthorizationException): def __init__( self, message: str = "Insufficient permissions for this action", - required_permission: Optional[str] = None, + required_permission: str | None = None, ): details = {} if required_permission: @@ -84,7 +82,7 @@ class UserAlreadyExistsException(ConflictException): def __init__( self, message: str = "User already exists", - field: Optional[str] = None, + field: str | None = None, ): details = {} if field: diff --git a/app/exceptions/base.py b/app/exceptions/base.py index 5cfe05a3..b8eeb371 100644 --- a/app/exceptions/base.py +++ b/app/exceptions/base.py @@ -8,7 +8,7 @@ This module provides classes and functions for: - Standardized error response structure """ -from typing import Any, Dict, Optional +from typing import Any class WizamartException(Exception): @@ -19,7 +19,7 @@ class WizamartException(Exception): message: str, error_code: str, status_code: int = 400, - details: Optional[Dict[str, Any]] = None, + details: dict[str, Any] | None = None, ): self.message = message self.error_code = error_code @@ -27,7 +27,7 @@ class WizamartException(Exception): self.details = details or {} super().__init__(self.message) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert exception to dictionary for JSON response.""" result = { "error_code": self.error_code, @@ -45,8 +45,8 @@ class ValidationException(WizamartException): def __init__( self, message: str, - field: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, + field: str | None = None, + details: dict[str, Any] | None = None, ): validation_details = details or {} if field: @@ -67,7 +67,7 @@ class AuthenticationException(WizamartException): self, message: str = "Authentication failed", error_code: str = "AUTHENTICATION_FAILED", - details: Optional[Dict[str, Any]] = None, + details: dict[str, Any] | None = None, ): super().__init__( message=message, @@ -84,7 +84,7 @@ class AuthorizationException(WizamartException): self, message: str = "Access denied", error_code: str = "AUTHORIZATION_FAILED", - details: Optional[Dict[str, Any]] = None, + details: dict[str, Any] | None = None, ): super().__init__( message=message, @@ -101,8 +101,8 @@ class ResourceNotFoundException(WizamartException): self, resource_type: str, identifier: str, - message: Optional[str] = None, - error_code: Optional[str] = None, + message: str | None = None, + error_code: str | None = None, ): if not message: message = f"{resource_type} with identifier '{identifier}' not found" @@ -127,7 +127,7 @@ class ConflictException(WizamartException): self, message: str, error_code: str = "RESOURCE_CONFLICT", - details: Optional[Dict[str, Any]] = None, + details: dict[str, Any] | None = None, ): super().__init__( message=message, @@ -144,7 +144,7 @@ class BusinessLogicException(WizamartException): self, message: str, error_code: str, - details: Optional[Dict[str, Any]] = None, + details: dict[str, Any] | None = None, ): super().__init__( message=message, @@ -162,7 +162,7 @@ class ExternalServiceException(WizamartException): message: str, service_name: str, error_code: str = "EXTERNAL_SERVICE_ERROR", - details: Optional[Dict[str, Any]] = None, + details: dict[str, Any] | None = None, ): service_details = details or {} service_details["service_name"] = service_name @@ -181,8 +181,8 @@ class RateLimitException(WizamartException): def __init__( self, message: str = "Rate limit exceeded", - retry_after: Optional[int] = None, - details: Optional[Dict[str, Any]] = None, + retry_after: int | None = None, + details: dict[str, Any] | None = None, ): rate_limit_details = details or {} if retry_after: diff --git a/app/exceptions/cart.py b/app/exceptions/cart.py index 6a980730..fd62b890 100644 --- a/app/exceptions/cart.py +++ b/app/exceptions/cart.py @@ -3,10 +3,8 @@ Shopping cart specific exceptions. """ -from typing import Optional -from .base import (BusinessLogicException, ResourceNotFoundException, - ValidationException) +from .base import BusinessLogicException, ResourceNotFoundException, ValidationException class CartItemNotFoundException(ResourceNotFoundException): @@ -36,8 +34,8 @@ class CartValidationException(ValidationException): def __init__( self, message: str = "Cart validation failed", - field: Optional[str] = None, - details: Optional[dict] = None, + field: str | None = None, + details: dict | None = None, ): super().__init__( message=message, @@ -75,7 +73,7 @@ class InvalidCartQuantityException(ValidationException): """Raised when cart quantity is invalid.""" def __init__( - self, quantity: int, min_quantity: int = 1, max_quantity: Optional[int] = None + self, quantity: int, min_quantity: int = 1, max_quantity: int | None = None ): if quantity < min_quantity: message = f"Quantity must be at least {min_quantity}" diff --git a/app/exceptions/customer.py b/app/exceptions/customer.py index a9f50ace..055cf6ff 100644 --- a/app/exceptions/customer.py +++ b/app/exceptions/customer.py @@ -3,11 +3,15 @@ Customer management specific exceptions. """ -from typing import Any, Dict, Optional +from typing import Any -from .base import (AuthenticationException, BusinessLogicException, - ConflictException, ResourceNotFoundException, - ValidationException) +from .base import ( + AuthenticationException, + BusinessLogicException, + ConflictException, + ResourceNotFoundException, + ValidationException, +) class CustomerNotFoundException(ResourceNotFoundException): @@ -71,8 +75,8 @@ class CustomerValidationException(ValidationException): def __init__( self, message: str = "Customer validation failed", - field: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, + field: str | None = None, + details: dict[str, Any] | None = None, ): super().__init__(message=message, field=field, details=details) self.error_code = "CUSTOMER_VALIDATION_FAILED" diff --git a/app/exceptions/error_renderer.py b/app/exceptions/error_renderer.py index cc804722..8a983d33 100644 --- a/app/exceptions/error_renderer.py +++ b/app/exceptions/error_renderer.py @@ -5,9 +5,10 @@ Error Page Renderer Renders context-aware error pages using Jinja2 templates. Handles fallback logic and context-specific customization. """ + import logging from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any from fastapi import Request from fastapi.responses import HTMLResponse @@ -60,7 +61,7 @@ class ErrorPageRenderer: status_code: int, error_code: str, message: str, - details: Optional[Dict[str, Any]] = None, + details: dict[str, Any] | None = None, show_debug: bool = False, ) -> HTMLResponse: """ @@ -190,9 +191,9 @@ class ErrorPageRenderer: status_code: int, error_code: str, message: str, - details: Optional[Dict[str, Any]], + details: dict[str, Any] | None, show_debug: bool, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Prepare data dictionary for error template.""" # Get friendly status name status_name = ErrorPageRenderer.STATUS_CODE_NAMES.get( @@ -229,7 +230,7 @@ class ErrorPageRenderer: @staticmethod def _get_context_data( request: Request, context_type: RequestContext - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get context-specific data for error templates.""" data = {} diff --git a/app/exceptions/handler.py b/app/exceptions/handler.py index c1b38edf..c5b4dc0e 100644 --- a/app/exceptions/handler.py +++ b/app/exceptions/handler.py @@ -11,7 +11,6 @@ This module provides classes and functions for: """ import logging -from typing import Union from fastapi import HTTPException, Request from fastapi.exceptions import RequestValidationError @@ -280,7 +279,7 @@ def setup_exception_handlers(app): request=request, status_code=404, error_code="ENDPOINT_NOT_FOUND", - message=f"The page you're looking for doesn't exist or has been moved.", + message="The page you're looking for doesn't exist or has been moved.", details={"path": request.url.path}, show_debug=True, ) @@ -364,10 +363,10 @@ def _redirect_to_login(request: Request) -> RedirectResponse: if context_type == RequestContext.ADMIN: logger.debug("Redirecting to /admin/login") return RedirectResponse(url="/admin/login", status_code=302) - elif context_type == RequestContext.VENDOR_DASHBOARD: + if context_type == RequestContext.VENDOR_DASHBOARD: logger.debug("Redirecting to /vendor/login") return RedirectResponse(url="/vendor/login", status_code=302) - elif context_type == RequestContext.SHOP: + if context_type == RequestContext.SHOP: # For shop context, redirect to shop login (customer login) # Calculate base_url for proper routing (supports domain, subdomain, and path-based access) vendor = getattr(request.state, "vendor", None) @@ -390,10 +389,9 @@ def _redirect_to_login(request: Request) -> RedirectResponse: login_url = f"{base_url}shop/account/login" logger.debug(f"Redirecting to {login_url}") return RedirectResponse(url=login_url, status_code=302) - else: - # Fallback to root for unknown contexts - logger.debug("Unknown context, redirecting to /") - return RedirectResponse(url="/", status_code=302) + # Fallback to root for unknown contexts + logger.debug("Unknown context, redirecting to /") + return RedirectResponse(url="/", status_code=302) # Utility functions for common exception scenarios diff --git a/app/exceptions/inventory.py b/app/exceptions/inventory.py index c2c73fd7..e62c73a8 100644 --- a/app/exceptions/inventory.py +++ b/app/exceptions/inventory.py @@ -3,10 +3,9 @@ Inventory management specific exceptions. """ -from typing import Any, Dict, Optional +from typing import Any -from .base import (BusinessLogicException, ResourceNotFoundException, - ValidationException) +from .base import BusinessLogicException, ResourceNotFoundException, ValidationException class InventoryNotFoundException(ResourceNotFoundException): @@ -58,8 +57,8 @@ class InvalidInventoryOperationException(ValidationException): def __init__( self, message: str, - operation: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, + operation: str | None = None, + details: dict[str, Any] | None = None, ): if not details: details = {} @@ -80,8 +79,8 @@ class InventoryValidationException(ValidationException): def __init__( self, message: str = "Inventory validation failed", - field: Optional[str] = None, - validation_errors: Optional[Dict[str, str]] = None, + field: str | None = None, + validation_errors: dict[str, str] | None = None, ): details = {} if validation_errors: diff --git a/app/exceptions/marketplace_import_job.py b/app/exceptions/marketplace_import_job.py index c442e1f9..abf5a9e3 100644 --- a/app/exceptions/marketplace_import_job.py +++ b/app/exceptions/marketplace_import_job.py @@ -3,11 +3,15 @@ Marketplace import specific exceptions. """ -from typing import Any, Dict, Optional +from typing import Any -from .base import (AuthorizationException, BusinessLogicException, - ExternalServiceException, ResourceNotFoundException, - ValidationException) +from .base import ( + AuthorizationException, + BusinessLogicException, + ExternalServiceException, + ResourceNotFoundException, + ValidationException, +) class MarketplaceImportException(BusinessLogicException): @@ -17,8 +21,8 @@ class MarketplaceImportException(BusinessLogicException): self, message: str, error_code: str = "MARKETPLACE_IMPORT_ERROR", - marketplace: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, + marketplace: str | None = None, + details: dict[str, Any] | None = None, ): if not details: details = {} @@ -48,7 +52,7 @@ class ImportJobNotFoundException(ResourceNotFoundException): class ImportJobNotOwnedException(AuthorizationException): """Raised when user tries to access import job they don't own.""" - def __init__(self, job_id: int, user_id: Optional[int] = None): + def __init__(self, job_id: int, user_id: int | None = None): details = {"job_id": job_id} if user_id: details["user_id"] = user_id @@ -66,9 +70,9 @@ class InvalidImportDataException(ValidationException): def __init__( self, message: str = "Invalid import data", - field: Optional[str] = None, - row_number: Optional[int] = None, - details: Optional[Dict[str, Any]] = None, + field: str | None = None, + row_number: int | None = None, + details: dict[str, Any] | None = None, ): if not details: details = {} @@ -132,7 +136,7 @@ class MarketplaceDataParsingException(ValidationException): self, marketplace: str, message: str = "Failed to parse marketplace data", - details: Optional[Dict[str, Any]] = None, + details: dict[str, Any] | None = None, ): if not details: details = {} @@ -152,7 +156,7 @@ class ImportRateLimitException(BusinessLogicException): self, max_imports: int, time_window: str, - retry_after: Optional[int] = None, + retry_after: int | None = None, ): details = { "max_imports": max_imports, @@ -172,7 +176,7 @@ class ImportRateLimitException(BusinessLogicException): class InvalidMarketplaceException(ValidationException): """Raised when marketplace is not supported.""" - def __init__(self, marketplace: str, supported_marketplaces: Optional[list] = None): + def __init__(self, marketplace: str, supported_marketplaces: list | None = None): details = {"marketplace": marketplace} if supported_marketplaces: details["supported_marketplaces"] = supported_marketplaces diff --git a/app/exceptions/marketplace_product.py b/app/exceptions/marketplace_product.py index 8fbc3e71..a5b47e32 100644 --- a/app/exceptions/marketplace_product.py +++ b/app/exceptions/marketplace_product.py @@ -3,10 +3,14 @@ MarketplaceProduct management specific exceptions. """ -from typing import Any, Dict, Optional +from typing import Any -from .base import (BusinessLogicException, ConflictException, - ResourceNotFoundException, ValidationException) +from .base import ( + BusinessLogicException, + ConflictException, + ResourceNotFoundException, + ValidationException, +) class MarketplaceProductNotFoundException(ResourceNotFoundException): @@ -38,8 +42,8 @@ class InvalidMarketplaceProductDataException(ValidationException): def __init__( self, message: str = "Invalid product data", - field: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, + field: str | None = None, + details: dict[str, Any] | None = None, ): super().__init__( message=message, @@ -55,8 +59,8 @@ class MarketplaceProductValidationException(ValidationException): def __init__( self, message: str, - field: Optional[str] = None, - validation_errors: Optional[Dict[str, str]] = None, + field: str | None = None, + validation_errors: dict[str, str] | None = None, ): details = {} if validation_errors: @@ -88,8 +92,8 @@ class MarketplaceProductCSVImportException(BusinessLogicException): def __init__( self, message: str = "MarketplaceProduct CSV import failed", - row_number: Optional[int] = None, - errors: Optional[Dict[str, Any]] = None, + row_number: int | None = None, + errors: dict[str, Any] | None = None, ): details = {} if row_number: diff --git a/app/exceptions/order.py b/app/exceptions/order.py index d5b74705..8a72de98 100644 --- a/app/exceptions/order.py +++ b/app/exceptions/order.py @@ -3,10 +3,8 @@ Order management specific exceptions. """ -from typing import Optional -from .base import (BusinessLogicException, ResourceNotFoundException, - ValidationException) +from .base import BusinessLogicException, ResourceNotFoundException, ValidationException class OrderNotFoundException(ResourceNotFoundException): @@ -35,7 +33,7 @@ class OrderAlreadyExistsException(ValidationException): class OrderValidationException(ValidationException): """Raised when order data validation fails.""" - def __init__(self, message: str, details: Optional[dict] = None): + def __init__(self, message: str, details: dict | None = None): super().__init__( message=message, error_code="ORDER_VALIDATION_FAILED", details=details ) diff --git a/app/exceptions/product.py b/app/exceptions/product.py index 72a3d8ba..72e8d377 100644 --- a/app/exceptions/product.py +++ b/app/exceptions/product.py @@ -3,16 +3,19 @@ Product (vendor catalog) specific exceptions. """ -from typing import Optional -from .base import (BusinessLogicException, ConflictException, - ResourceNotFoundException, ValidationException) +from .base import ( + BusinessLogicException, + ConflictException, + ResourceNotFoundException, + ValidationException, +) class ProductNotFoundException(ResourceNotFoundException): """Raised when a product is not found in vendor catalog.""" - def __init__(self, product_id: int, vendor_id: Optional[int] = None): + def __init__(self, product_id: int, vendor_id: int | None = None): details = {"product_id": product_id} if vendor_id: details["vendor_id"] = vendor_id @@ -79,8 +82,8 @@ class InvalidProductDataException(ValidationException): def __init__( self, message: str = "Invalid product data", - field: Optional[str] = None, - details: Optional[dict] = None, + field: str | None = None, + details: dict | None = None, ): super().__init__( message=message, @@ -96,8 +99,8 @@ class ProductValidationException(ValidationException): def __init__( self, message: str = "Product validation failed", - field: Optional[str] = None, - validation_errors: Optional[dict] = None, + field: str | None = None, + validation_errors: dict | None = None, ): details = {} if validation_errors: diff --git a/app/exceptions/team.py b/app/exceptions/team.py index 21283db1..70f229d2 100644 --- a/app/exceptions/team.py +++ b/app/exceptions/team.py @@ -3,17 +3,21 @@ Team management specific exceptions. """ -from typing import Any, Dict, Optional +from typing import Any -from .base import (AuthorizationException, BusinessLogicException, - ConflictException, ResourceNotFoundException, - ValidationException) +from .base import ( + AuthorizationException, + BusinessLogicException, + ConflictException, + ResourceNotFoundException, + ValidationException, +) class TeamMemberNotFoundException(ResourceNotFoundException): """Raised when a team member is not found.""" - def __init__(self, user_id: int, vendor_id: Optional[int] = None): + def __init__(self, user_id: int, vendor_id: int | None = None): details = {"user_id": user_id} if vendor_id: details["vendor_id"] = vendor_id @@ -63,7 +67,7 @@ class TeamInvitationExpiredException(BusinessLogicException): def __init__(self, invitation_token: str): super().__init__( - message=f"Team invitation has expired", + message="Team invitation has expired", error_code="TEAM_INVITATION_EXPIRED", details={"invitation_token": invitation_token}, ) @@ -86,8 +90,8 @@ class UnauthorizedTeamActionException(AuthorizationException): def __init__( self, action: str, - user_id: Optional[int] = None, - required_permission: Optional[str] = None, + user_id: int | None = None, + required_permission: str | None = None, ): details = {"action": action} if user_id: @@ -130,7 +134,7 @@ class CannotModifyOwnRoleException(BusinessLogicException): class RoleNotFoundException(ResourceNotFoundException): """Raised when a role is not found.""" - def __init__(self, role_id: int, vendor_id: Optional[int] = None): + def __init__(self, role_id: int, vendor_id: int | None = None): details = {"role_id": role_id} if vendor_id: details["vendor_id"] = vendor_id @@ -153,8 +157,8 @@ class InvalidRoleException(ValidationException): def __init__( self, message: str = "Invalid role data", - field: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, + field: str | None = None, + details: dict[str, Any] | None = None, ): super().__init__( message=message, @@ -170,8 +174,8 @@ class InsufficientTeamPermissionsException(AuthorizationException): def __init__( self, required_permission: str, - user_id: Optional[int] = None, - action: Optional[str] = None, + user_id: int | None = None, + action: str | None = None, ): details = {"required_permission": required_permission} if user_id: @@ -208,8 +212,8 @@ class TeamValidationException(ValidationException): def __init__( self, message: str = "Team operation validation failed", - field: Optional[str] = None, - validation_errors: Optional[Dict[str, str]] = None, + field: str | None = None, + validation_errors: dict[str, str] | None = None, ): details = {} if validation_errors: @@ -229,8 +233,8 @@ class InvalidInvitationDataException(ValidationException): def __init__( self, message: str = "Invalid invitation data", - field: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, + field: str | None = None, + details: dict[str, Any] | None = None, ): super().__init__( message=message, @@ -255,7 +259,7 @@ class InvalidInvitationTokenException(ValidationException): def __init__( self, message: str = "Invalid or expired invitation token", - invitation_token: Optional[str] = None, + invitation_token: str | None = None, ): details = {} if invitation_token: diff --git a/app/exceptions/vendor.py b/app/exceptions/vendor.py index 44380e67..d10f42ab 100644 --- a/app/exceptions/vendor.py +++ b/app/exceptions/vendor.py @@ -3,11 +3,15 @@ Vendor management specific exceptions. """ -from typing import Any, Dict, Optional +from typing import Any -from .base import (AuthorizationException, BusinessLogicException, - ConflictException, ResourceNotFoundException, - ValidationException) +from .base import ( + AuthorizationException, + BusinessLogicException, + ConflictException, + ResourceNotFoundException, + ValidationException, +) class VendorNotFoundException(ResourceNotFoundException): @@ -63,7 +67,7 @@ class VendorNotVerifiedException(BusinessLogicException): class UnauthorizedVendorAccessException(AuthorizationException): """Raised when user tries to access vendor they don't own.""" - def __init__(self, vendor_code: str, user_id: Optional[int] = None): + def __init__(self, vendor_code: str, user_id: int | None = None): details = {"vendor_code": vendor_code} if user_id: details["user_id"] = user_id @@ -81,8 +85,8 @@ class InvalidVendorDataException(ValidationException): def __init__( self, message: str = "Invalid vendor data", - field: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, + field: str | None = None, + details: dict[str, Any] | None = None, ): super().__init__( message=message, @@ -98,8 +102,8 @@ class VendorValidationException(ValidationException): def __init__( self, message: str = "Vendor validation failed", - field: Optional[str] = None, - validation_errors: Optional[Dict[str, str]] = None, + field: str | None = None, + validation_errors: dict[str, str] | None = None, ): details = {} if validation_errors: @@ -134,7 +138,7 @@ class IncompleteVendorDataException(ValidationException): class MaxVendorsReachedException(BusinessLogicException): """Raised when user tries to create more vendors than allowed.""" - def __init__(self, max_vendors: int, user_id: Optional[int] = None): + def __init__(self, max_vendors: int, user_id: int | None = None): details = {"max_vendors": max_vendors} if user_id: details["user_id"] = user_id diff --git a/app/exceptions/vendor_domain.py b/app/exceptions/vendor_domain.py index 8a8e704b..147cc26d 100644 --- a/app/exceptions/vendor_domain.py +++ b/app/exceptions/vendor_domain.py @@ -3,11 +3,14 @@ Vendor domain management specific exceptions. """ -from typing import Any, Dict, Optional -from .base import (BusinessLogicException, ConflictException, - ExternalServiceException, ResourceNotFoundException, - ValidationException) +from .base import ( + BusinessLogicException, + ConflictException, + ExternalServiceException, + ResourceNotFoundException, + ValidationException, +) class VendorDomainNotFoundException(ResourceNotFoundException): @@ -30,7 +33,7 @@ class VendorDomainNotFoundException(ResourceNotFoundException): class VendorDomainAlreadyExistsException(ConflictException): """Raised when trying to add a domain that already exists.""" - def __init__(self, domain: str, existing_vendor_id: Optional[int] = None): + def __init__(self, domain: str, existing_vendor_id: int | None = None): details = {"domain": domain} if existing_vendor_id: details["existing_vendor_id"] = existing_vendor_id @@ -104,7 +107,7 @@ class MultiplePrimaryDomainsException(BusinessLogicException): def __init__(self, vendor_id: int): super().__init__( - message=f"Vendor can only have one primary domain", + message="Vendor can only have one primary domain", error_code="MULTIPLE_PRIMARY_DOMAINS", details={"vendor_id": vendor_id}, ) diff --git a/app/exceptions/vendor_theme.py b/app/exceptions/vendor_theme.py index 08000432..35b58222 100644 --- a/app/exceptions/vendor_theme.py +++ b/app/exceptions/vendor_theme.py @@ -3,10 +3,13 @@ Vendor theme management specific exceptions. """ -from typing import Any, Dict, Optional +from typing import Any -from .base import (BusinessLogicException, ConflictException, - ResourceNotFoundException, ValidationException) +from .base import ( + BusinessLogicException, + ResourceNotFoundException, + ValidationException, +) class VendorThemeNotFoundException(ResourceNotFoundException): @@ -27,8 +30,8 @@ class InvalidThemeDataException(ValidationException): def __init__( self, message: str = "Invalid theme data", - field: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, + field: str | None = None, + details: dict[str, Any] | None = None, ): super().__init__( message=message, @@ -41,7 +44,7 @@ class InvalidThemeDataException(ValidationException): class ThemePresetNotFoundException(ResourceNotFoundException): """Raised when a theme preset is not found.""" - def __init__(self, preset_name: str, available_presets: Optional[list] = None): + def __init__(self, preset_name: str, available_presets: list | None = None): details = {"preset_name": preset_name} if available_presets: details["available_presets"] = available_presets @@ -61,8 +64,8 @@ class ThemeValidationException(ValidationException): def __init__( self, message: str = "Theme validation failed", - field: Optional[str] = None, - validation_errors: Optional[Dict[str, str]] = None, + field: str | None = None, + validation_errors: dict[str, str] | None = None, ): details = {} if validation_errors: diff --git a/app/models/architecture_scan.py b/app/models/architecture_scan.py index 403fbcbf..d0af9569 100644 --- a/app/models/architecture_scan.py +++ b/app/models/architecture_scan.py @@ -3,8 +3,17 @@ Architecture Scan Models Database models for tracking code quality scans and violations """ -from sqlalchemy import (JSON, Boolean, Column, DateTime, Float, ForeignKey, - Integer, String, Text) +from sqlalchemy import ( + JSON, + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Integer, + String, + Text, +) from sqlalchemy.orm import relationship from sqlalchemy.sql import func diff --git a/app/routes/admin_pages.py b/app/routes/admin_pages.py index 736b27db..4cb6f26e 100644 --- a/app/routes/admin_pages.py +++ b/app/routes/admin_pages.py @@ -30,15 +30,17 @@ Routes: - GET /code-quality/violations/{violation_id} → Violation details (auth required) """ -from typing import Optional from fastapi import APIRouter, Depends, Path, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy.orm import Session -from app.api.deps import (get_current_admin_from_cookie_or_header, - get_current_admin_optional, get_db) +from app.api.deps import ( + get_current_admin_from_cookie_or_header, + get_current_admin_optional, + get_db, +) from models.database.user import User router = APIRouter() @@ -52,7 +54,7 @@ templates = Jinja2Templates(directory="app/templates") @router.get("/", response_class=RedirectResponse, include_in_schema=False) async def admin_root( - current_user: Optional[User] = Depends(get_current_admin_optional), + current_user: User | None = Depends(get_current_admin_optional), ): """ Redirect /admin/ based on authentication status. @@ -69,7 +71,7 @@ async def admin_root( @router.get("/login", response_class=HTMLResponse, include_in_schema=False) async def admin_login_page( - request: Request, current_user: Optional[User] = Depends(get_current_admin_optional) + request: Request, current_user: User | None = Depends(get_current_admin_optional) ): """ Render admin login page. diff --git a/app/routes/shop_pages.py b/app/routes/shop_pages.py index 8103d809..364a31e8 100644 --- a/app/routes/shop_pages.py +++ b/app/routes/shop_pages.py @@ -129,7 +129,7 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d ) except Exception as e: logger.error( - f"[SHOP_CONTEXT] Failed to load navigation pages", + "[SHOP_CONTEXT] Failed to load navigation pages", extra={"error": str(e), "vendor_id": vendor.id if vendor else None}, ) @@ -149,7 +149,7 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d context.update(extra_context) logger.debug( - f"[SHOP_CONTEXT] Context built", + "[SHOP_CONTEXT] Context built", extra={ "vendor_id": vendor.id if vendor else None, "vendor_name": vendor.name if vendor else None, @@ -179,7 +179,7 @@ async def shop_products_page(request: Request, db: Session = Depends(get_db)): Shows featured products and categories. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -203,7 +203,7 @@ async def shop_product_detail_page( Shows product information, images, reviews, and buy options. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -227,7 +227,7 @@ async def shop_category_page( Shows all products in a specific category. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -247,7 +247,7 @@ async def shop_cart_page(request: Request): Shows cart items and allows quantity updates. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -265,7 +265,7 @@ async def shop_checkout_page(request: Request): Handles shipping, payment, and order confirmation. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -283,7 +283,7 @@ async def shop_search_page(request: Request): Shows products matching search query. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -306,7 +306,7 @@ async def shop_register_page(request: Request): No authentication required. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -326,7 +326,7 @@ async def shop_login_page(request: Request): No authentication required. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -348,7 +348,7 @@ async def shop_forgot_password_page(request: Request): Allows customers to reset their password. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -373,7 +373,7 @@ async def shop_account_root(request: Request): Redirect /shop/account or /shop/account/ to dashboard. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -414,7 +414,7 @@ async def shop_account_dashboard_page( Requires customer authentication. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -439,7 +439,7 @@ async def shop_orders_page( Requires customer authentication. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -467,7 +467,7 @@ async def shop_order_detail_page( Requires customer authentication. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -493,7 +493,7 @@ async def shop_profile_page( Requires customer authentication. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -518,7 +518,7 @@ async def shop_addresses_page( Requires customer authentication. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -543,7 +543,7 @@ async def shop_wishlist_page( Requires customer authentication. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -568,7 +568,7 @@ async def shop_settings_page( Requires customer authentication. """ logger.debug( - f"[SHOP_HANDLER] shop_products_page REACHED", + "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), @@ -609,7 +609,7 @@ async def generic_content_page( from fastapi import HTTPException logger.debug( - f"[SHOP_HANDLER] generic_content_page REACHED", + "[SHOP_HANDLER] generic_content_page REACHED", extra={ "path": request.url.path, "slug": slug, @@ -628,7 +628,7 @@ async def generic_content_page( if not page: logger.warning( - f"[SHOP_HANDLER] Content page not found", + "[SHOP_HANDLER] Content page not found", extra={ "slug": slug, "vendor_id": vendor_id, @@ -638,7 +638,7 @@ async def generic_content_page( raise HTTPException(status_code=404, detail=f"Page not found: {slug}") logger.info( - f"[SHOP_HANDLER] Content page found", + "[SHOP_HANDLER] Content page found", extra={ "slug": slug, "page_id": page.id, @@ -709,14 +709,14 @@ async def debug_context(request: Request):
{json.dumps(debug_info, indent=2)}

Status

-

- Vendor: {'āœ“ Found' if vendor else 'āœ— Not Found'} +

+ Vendor: {"āœ“ Found" if vendor else "āœ— Not Found"}

-

- Theme: {'āœ“ Found' if theme else 'āœ— Not Found'} +

+ Theme: {"āœ“ Found" if theme else "āœ— Not Found"}

-

- Context Type: {str(getattr(request.state, 'context_type', 'NOT SET'))} +

+ Context Type: {str(getattr(request.state, "context_type", "NOT SET"))}

diff --git a/app/routes/vendor_pages.py b/app/routes/vendor_pages.py index 41dd702d..e6cc71d5 100644 --- a/app/routes/vendor_pages.py +++ b/app/routes/vendor_pages.py @@ -22,15 +22,17 @@ Routes: """ import logging -from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Path, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy.orm import Session -from app.api.deps import (get_current_vendor_from_cookie_or_header, - get_current_vendor_optional, get_db) +from app.api.deps import ( + get_current_vendor_from_cookie_or_header, + get_current_vendor_optional, + get_db, +) from app.services.content_page_service import content_page_service from models.database.user import User @@ -57,7 +59,7 @@ async def vendor_root_no_slash(vendor_code: str = Path(..., description="Vendor @router.get("/{vendor_code}/", response_class=RedirectResponse, include_in_schema=False) async def vendor_root( vendor_code: str = Path(..., description="Vendor code"), - current_user: Optional[User] = Depends(get_current_vendor_optional), + current_user: User | None = Depends(get_current_vendor_optional), ): """ Redirect /vendor/{code}/ based on authentication status. @@ -78,7 +80,7 @@ async def vendor_root( async def vendor_login_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), - current_user: Optional[User] = Depends(get_current_vendor_optional), + current_user: User | None = Depends(get_current_vendor_optional), ): """ Render vendor login page. @@ -374,7 +376,7 @@ async def vendor_content_page( shadowing other specific routes. """ logger.debug( - f"[VENDOR_HANDLER] vendor_content_page REACHED", + "[VENDOR_HANDLER] vendor_content_page REACHED", extra={ "path": request.url.path, "vendor_code": vendor_code, diff --git a/app/services/admin_audit_service.py b/app/services/admin_audit_service.py index 503a2c32..e6eeb4c6 100644 --- a/app/services/admin_audit_service.py +++ b/app/services/admin_audit_service.py @@ -9,10 +9,9 @@ This module provides functions for: """ import logging -from datetime import datetime, timezone -from typing import Any, Dict, List, Optional +from typing import Any -from sqlalchemy import and_, or_ +from sqlalchemy import and_ from sqlalchemy.orm import Session from app.exceptions import AdminOperationException @@ -33,10 +32,10 @@ class AdminAuditService: action: str, target_type: str, target_id: str, - details: Optional[Dict[str, Any]] = None, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - request_id: Optional[str] = None, + details: dict[str, Any] | None = None, + ip_address: str | None = None, + user_agent: str | None = None, + request_id: str | None = None, ) -> AdminAuditLog: """ Log an admin action to the audit trail. @@ -85,7 +84,7 @@ class AdminAuditService: def get_audit_logs( self, db: Session, filters: AdminAuditLogFilters - ) -> List[AdminAuditLogResponse]: + ) -> list[AdminAuditLogResponse]: """ Get filtered admin audit logs with pagination. @@ -187,14 +186,14 @@ class AdminAuditService: def get_recent_actions_by_admin( self, db: Session, admin_user_id: int, limit: int = 10 - ) -> List[AdminAuditLogResponse]: + ) -> list[AdminAuditLogResponse]: """Get recent actions by a specific admin.""" filters = AdminAuditLogFilters(admin_user_id=admin_user_id, limit=limit) return self.get_audit_logs(db, filters) def get_actions_by_target( self, db: Session, target_type: str, target_id: str, limit: int = 50 - ) -> List[AdminAuditLogResponse]: + ) -> list[AdminAuditLogResponse]: """Get all actions performed on a specific target.""" try: logs = ( diff --git a/app/services/admin_service.py b/app/services/admin_service.py index 65637df8..c2b2afa9 100644 --- a/app/services/admin_service.py +++ b/app/services/admin_service.py @@ -13,17 +13,21 @@ This module provides classes and functions for: import logging import secrets import string -from datetime import datetime, timezone -from typing import List, Optional, Tuple +from datetime import UTC, datetime from sqlalchemy import func, or_ from sqlalchemy.orm import Session -from app.exceptions import (AdminOperationException, CannotModifySelfException, - UserNotFoundException, UserStatusChangeException, - ValidationException, VendorAlreadyExistsException, - VendorNotFoundException, - VendorVerificationException) +from app.exceptions import ( + AdminOperationException, + CannotModifySelfException, + UserNotFoundException, + UserStatusChangeException, + ValidationException, + VendorAlreadyExistsException, + VendorNotFoundException, + VendorVerificationException, +) from models.database.marketplace_import_job import MarketplaceImportJob from models.database.user import User from models.database.vendor import Role, Vendor, VendorUser @@ -40,7 +44,7 @@ class AdminService: # USER MANAGEMENT # ============================================================================ - def get_all_users(self, db: Session, skip: int = 0, limit: int = 100) -> List[User]: + def get_all_users(self, db: Session, skip: int = 0, limit: int = 100) -> list[User]: """Get paginated list of all users.""" try: return db.query(User).offset(skip).limit(limit).all() @@ -52,7 +56,7 @@ class AdminService: def toggle_user_status( self, db: Session, user_id: int, current_admin_id: int - ) -> Tuple[User, str]: + ) -> tuple[User, str]: """Toggle user active status.""" user = self._get_user_by_id_or_raise(db, user_id) @@ -72,7 +76,7 @@ class AdminService: try: original_status = user.is_active user.is_active = not user.is_active - user.updated_at = datetime.now(timezone.utc) + user.updated_at = datetime.now(UTC) db.commit() db.refresh(user) @@ -98,7 +102,7 @@ class AdminService: def create_vendor_with_owner( self, db: Session, vendor_data: VendorCreate - ) -> Tuple[Vendor, User, str]: + ) -> tuple[Vendor, User, str]: """ Create vendor with owner user account. @@ -222,10 +226,10 @@ class AdminService: db: Session, skip: int = 0, limit: int = 100, - search: Optional[str] = None, - is_active: Optional[bool] = None, - is_verified: Optional[bool] = None, - ) -> Tuple[List[Vendor], int]: + search: str | None = None, + is_active: bool | None = None, + is_verified: bool | None = None, + ) -> tuple[list[Vendor], int]: """Get paginated list of all vendors with filtering.""" try: query = db.query(Vendor) @@ -261,17 +265,17 @@ class AdminService: """Get vendor by ID.""" return self._get_vendor_by_id_or_raise(db, vendor_id) - def verify_vendor(self, db: Session, vendor_id: int) -> Tuple[Vendor, str]: + def verify_vendor(self, db: Session, vendor_id: int) -> tuple[Vendor, str]: """Toggle vendor verification status.""" vendor = self._get_vendor_by_id_or_raise(db, vendor_id) try: original_status = vendor.is_verified vendor.is_verified = not vendor.is_verified - vendor.updated_at = datetime.now(timezone.utc) + vendor.updated_at = datetime.now(UTC) if vendor.is_verified: - vendor.verified_at = datetime.now(timezone.utc) + vendor.verified_at = datetime.now(UTC) db.commit() db.refresh(vendor) @@ -291,14 +295,14 @@ class AdminService: current_verification_status=original_status, ) - def toggle_vendor_status(self, db: Session, vendor_id: int) -> Tuple[Vendor, str]: + def toggle_vendor_status(self, db: Session, vendor_id: int) -> tuple[Vendor, str]: """Toggle vendor active status.""" vendor = self._get_vendor_by_id_or_raise(db, vendor_id) try: original_status = vendor.is_active vendor.is_active = not vendor.is_active - vendor.updated_at = datetime.now(timezone.utc) + vendor.updated_at = datetime.now(UTC) db.commit() db.refresh(vendor) @@ -347,7 +351,10 @@ class AdminService: ) def update_vendor( - self, db: Session, vendor_id: int, vendor_update # VendorUpdate schema + self, + db: Session, + vendor_id: int, + vendor_update, # VendorUpdate schema ) -> Vendor: """ Update vendor information (Admin only). @@ -402,7 +409,7 @@ class AdminService: for field, value in update_data.items(): setattr(vendor, field, value) - vendor.updated_at = datetime.now(timezone.utc) + vendor.updated_at = datetime.now(UTC) db.commit() db.refresh(vendor) @@ -430,7 +437,7 @@ class AdminService: db: Session, vendor_id: int, transfer_data, # VendorTransferOwnership schema - ) -> Tuple[Vendor, User, User]: + ) -> tuple[Vendor, User, User]: """ Transfer vendor ownership to another user. @@ -556,7 +563,7 @@ class AdminService: # Update vendor owner_user_id vendor.owner_user_id = new_owner.id - vendor.updated_at = datetime.now(timezone.utc) + vendor.updated_at = datetime.now(UTC) db.commit() db.refresh(vendor) @@ -593,12 +600,12 @@ class AdminService: def get_marketplace_import_jobs( self, db: Session, - marketplace: Optional[str] = None, - vendor_name: Optional[str] = None, - status: Optional[str] = None, + marketplace: str | None = None, + vendor_name: str | None = None, + status: str | None = None, skip: int = 0, limit: int = 100, - ) -> List[MarketplaceImportJobResponse]: + ) -> list[MarketplaceImportJobResponse]: """Get filtered and paginated marketplace import jobs.""" try: query = db.query(MarketplaceImportJob) @@ -633,7 +640,7 @@ class AdminService: # STATISTICS # ============================================================================ - def get_recent_vendors(self, db: Session, limit: int = 5) -> List[dict]: + def get_recent_vendors(self, db: Session, limit: int = 5) -> list[dict]: """Get recently created vendors.""" try: vendors = ( @@ -656,7 +663,7 @@ class AdminService: logger.error(f"Failed to get recent vendors: {str(e)}") return [] - def get_recent_import_jobs(self, db: Session, limit: int = 10) -> List[dict]: + def get_recent_import_jobs(self, db: Session, limit: int = 10) -> list[dict]: """Get recent marketplace import jobs.""" try: jobs = ( diff --git a/app/services/admin_settings_service.py b/app/services/admin_settings_service.py index fa4565f3..74b75b77 100644 --- a/app/services/admin_settings_service.py +++ b/app/services/admin_settings_service.py @@ -10,17 +10,23 @@ This module provides functions for: import json import logging -from datetime import datetime, timezone -from typing import Any, Dict, List, Optional +from datetime import UTC, datetime +from typing import Any from sqlalchemy import func from sqlalchemy.orm import Session -from app.exceptions import (AdminOperationException, ResourceNotFoundException, - ValidationException) +from app.exceptions import ( + AdminOperationException, + ResourceNotFoundException, + ValidationException, +) from models.database.admin import AdminSetting -from models.schema.admin import (AdminSettingCreate, AdminSettingResponse, - AdminSettingUpdate) +from models.schema.admin import ( + AdminSettingCreate, + AdminSettingResponse, + AdminSettingUpdate, +) logger = logging.getLogger(__name__) @@ -28,7 +34,7 @@ logger = logging.getLogger(__name__) class AdminSettingsService: """Service for managing platform-wide settings.""" - def get_setting_by_key(self, db: Session, key: str) -> Optional[AdminSetting]: + def get_setting_by_key(self, db: Session, key: str) -> AdminSetting | None: """Get setting by key.""" try: return ( @@ -60,14 +66,13 @@ class AdminSettingsService: try: if setting.value_type == "integer": return int(setting.value) - elif setting.value_type == "float": + if setting.value_type == "float": return float(setting.value) - elif setting.value_type == "boolean": + if setting.value_type == "boolean": return setting.value.lower() in ("true", "1", "yes") - elif setting.value_type == "json": + if setting.value_type == "json": return json.loads(setting.value) - else: - return setting.value + return setting.value except Exception as e: logger.error(f"Failed to convert setting {key} value: {str(e)}") return default @@ -75,9 +80,9 @@ class AdminSettingsService: def get_all_settings( self, db: Session, - category: Optional[str] = None, - is_public: Optional[bool] = None, - ) -> List[AdminSettingResponse]: + category: str | None = None, + is_public: bool | None = None, + ) -> list[AdminSettingResponse]: """Get all settings with optional filtering.""" try: query = db.query(AdminSetting) @@ -100,7 +105,7 @@ class AdminSettingsService: operation="get_all_settings", reason="Database query failed" ) - def get_settings_by_category(self, db: Session, category: str) -> Dict[str, Any]: + def get_settings_by_category(self, db: Session, category: str) -> dict[str, Any]: """ Get all settings in a category as a dictionary. @@ -198,7 +203,7 @@ class AdminSettingsService: if update_data.description is not None: setting.description = update_data.description setting.last_modified_by_user_id = admin_user_id - setting.updated_at = datetime.now(timezone.utc) + setting.updated_at = datetime.now(UTC) db.commit() db.refresh(setting) @@ -228,8 +233,7 @@ class AdminSettingsService: value=setting_data.value, description=setting_data.description ) return self.update_setting(db, setting_data.key, update_data, admin_user_id) - else: - return self.create_setting(db, setting_data, admin_user_id) + return self.create_setting(db, setting_data, admin_user_id) def delete_setting(self, db: Session, key: str, admin_user_id: int) -> str: """Delete setting.""" diff --git a/app/services/auth_service.py b/app/services/auth_service.py index f05da054..9a15680b 100644 --- a/app/services/auth_service.py +++ b/app/services/auth_service.py @@ -9,13 +9,17 @@ This module provides classes and functions for: """ import logging -from typing import Any, Dict, Optional +from datetime import UTC +from typing import Any from sqlalchemy.orm import Session -from app.exceptions import (InvalidCredentialsException, - UserAlreadyExistsException, UserNotActiveException, - ValidationException) +from app.exceptions import ( + InvalidCredentialsException, + UserAlreadyExistsException, + UserNotActiveException, + ValidationException, +) from middleware.auth import AuthManager from models.database.user import User from models.schema.auth import UserLogin, UserRegister @@ -82,7 +86,7 @@ class AuthService: logger.error(f"Error registering user: {str(e)}") raise ValidationException("Registration failed") - def login_user(self, db: Session, user_credentials: UserLogin) -> Dict[str, Any]: + def login_user(self, db: Session, user_credentials: UserLogin) -> dict[str, Any]: """ Login user and return JWT token with user data. @@ -120,7 +124,7 @@ class AuthService: logger.error(f"Error during login: {str(e)}") raise InvalidCredentialsException() - def get_user_by_email(self, db: Session, email: str) -> Optional[User]: + def get_user_by_email(self, db: Session, email: str) -> User | None: """Get user by email.""" try: return db.query(User).filter(User.email == email).first() @@ -128,7 +132,7 @@ class AuthService: logger.error(f"Error getting user by email: {str(e)}") return None - def get_user_by_username(self, db: Session, username: str) -> Optional[User]: + def get_user_by_username(self, db: Session, username: str) -> User | None: """Get user by username.""" try: return db.query(User).filter(User.username == username).first() @@ -138,7 +142,7 @@ class AuthService: def authenticate_user( self, db: Session, username: str, password: str - ) -> Optional[User]: + ) -> User | None: """Authenticate user with username/password.""" try: return self.auth_manager.authenticate_user(db, username, password) @@ -146,7 +150,7 @@ class AuthService: logger.error(f"Error authenticating user: {str(e)}") return None - def create_access_token(self, user: User) -> Dict[str, Any]: + def create_access_token(self, user: User) -> dict[str, Any]: """Create access token for user.""" try: return self.auth_manager.create_access_token(user) @@ -182,7 +186,7 @@ class AuthService: Returns: Dictionary with access_token, token_type, and expires_in """ - from datetime import datetime, timedelta, timezone + from datetime import datetime, timedelta from jose import jwt @@ -190,13 +194,13 @@ class AuthService: try: expires_delta = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - expire = datetime.now(timezone.utc) + expires_delta + expire = datetime.now(UTC) + expires_delta # Build payload with provided data payload = { **data, "exp": expire, - "iat": datetime.now(timezone.utc), + "iat": datetime.now(UTC), } token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") diff --git a/app/services/cart_service.py b/app/services/cart_service.py index b3c8ddd1..8d4bb4dc 100644 --- a/app/services/cart_service.py +++ b/app/services/cart_service.py @@ -9,20 +9,18 @@ This module provides: """ import logging -from datetime import datetime, timezone -from typing import Dict, List, Optional from sqlalchemy import and_ from sqlalchemy.orm import Session -from app.exceptions import (CartItemNotFoundException, CartValidationException, - InsufficientInventoryForCartException, - InvalidCartQuantityException, - ProductNotAvailableForCartException, - ProductNotFoundException) +from app.exceptions import ( + CartItemNotFoundException, + InsufficientInventoryForCartException, + InvalidCartQuantityException, + ProductNotFoundException, +) from models.database.cart import CartItem from models.database.product import Product -from models.database.vendor import Vendor logger = logging.getLogger(__name__) @@ -30,7 +28,7 @@ logger = logging.getLogger(__name__) class CartService: """Service for managing shopping carts.""" - def get_cart(self, db: Session, vendor_id: int, session_id: str) -> Dict: + def get_cart(self, db: Session, vendor_id: int, session_id: str) -> dict: """ Get cart contents for a session. @@ -43,7 +41,7 @@ class CartService: Cart data with items and totals """ logger.info( - f"[CART_SERVICE] get_cart called", + "[CART_SERVICE] get_cart called", extra={ "vendor_id": vendor_id, "session_id": session_id, @@ -111,7 +109,7 @@ class CartService: session_id: str, product_id: int, quantity: int = 1, - ) -> Dict: + ) -> dict: """ Add product to cart. @@ -130,7 +128,7 @@ class CartService: InsufficientInventoryException: If not enough inventory """ logger.info( - f"[CART_SERVICE] add_to_cart called", + "[CART_SERVICE] add_to_cart called", extra={ "vendor_id": vendor_id, "session_id": session_id, @@ -154,7 +152,7 @@ class CartService: if not product: logger.error( - f"[CART_SERVICE] Product not found", + "[CART_SERVICE] Product not found", extra={"product_id": product_id, "vendor_id": vendor_id}, ) raise ProductNotFoundException(product_id=product_id, vendor_id=vendor_id) @@ -191,7 +189,7 @@ class CartService: # Check inventory for new total quantity if product.available_inventory < new_quantity: logger.warning( - f"[CART_SERVICE] Insufficient inventory for update", + "[CART_SERVICE] Insufficient inventory for update", extra={ "product_id": product_id, "current_in_cart": existing_item.quantity, @@ -212,7 +210,7 @@ class CartService: db.refresh(existing_item) logger.info( - f"[CART_SERVICE] Updated existing cart item", + "[CART_SERVICE] Updated existing cart item", extra={"cart_item_id": existing_item.id, "new_quantity": new_quantity}, ) @@ -221,50 +219,49 @@ class CartService: "product_id": product_id, "quantity": new_quantity, } - else: - # Check inventory for new item - if product.available_inventory < quantity: - logger.warning( - f"[CART_SERVICE] Insufficient inventory", - extra={ - "product_id": product_id, - "requested": quantity, - "available": product.available_inventory, - }, - ) - raise InsufficientInventoryForCartException( - product_id=product_id, - product_name=product.marketplace_product.title, - requested=quantity, - available=product.available_inventory, - ) - - # Create new cart item - cart_item = CartItem( - vendor_id=vendor_id, - session_id=session_id, - product_id=product_id, - quantity=quantity, - price_at_add=current_price, - ) - db.add(cart_item) - db.commit() - db.refresh(cart_item) - - logger.info( - f"[CART_SERVICE] Created new cart item", + # Check inventory for new item + if product.available_inventory < quantity: + logger.warning( + "[CART_SERVICE] Insufficient inventory", extra={ - "cart_item_id": cart_item.id, - "quantity": quantity, - "price": current_price, + "product_id": product_id, + "requested": quantity, + "available": product.available_inventory, }, ) + raise InsufficientInventoryForCartException( + product_id=product_id, + product_name=product.marketplace_product.title, + requested=quantity, + available=product.available_inventory, + ) - return { - "message": "Product added to cart", - "product_id": product_id, + # Create new cart item + cart_item = CartItem( + vendor_id=vendor_id, + session_id=session_id, + product_id=product_id, + quantity=quantity, + price_at_add=current_price, + ) + db.add(cart_item) + db.commit() + db.refresh(cart_item) + + logger.info( + "[CART_SERVICE] Created new cart item", + extra={ + "cart_item_id": cart_item.id, "quantity": quantity, - } + "price": current_price, + }, + ) + + return { + "message": "Product added to cart", + "product_id": product_id, + "quantity": quantity, + } def update_cart_item( self, @@ -273,7 +270,7 @@ class CartService: session_id: str, product_id: int, quantity: int, - ) -> Dict: + ) -> dict: """ Update quantity of item in cart. @@ -344,7 +341,7 @@ class CartService: db.refresh(cart_item) logger.info( - f"[CART_SERVICE] Updated cart item quantity", + "[CART_SERVICE] Updated cart item quantity", extra={ "cart_item_id": cart_item.id, "product_id": product_id, @@ -360,7 +357,7 @@ class CartService: def remove_from_cart( self, db: Session, vendor_id: int, session_id: str, product_id: int - ) -> Dict: + ) -> dict: """ Remove item from cart. @@ -398,7 +395,7 @@ class CartService: db.commit() logger.info( - f"[CART_SERVICE] Removed item from cart", + "[CART_SERVICE] Removed item from cart", extra={ "cart_item_id": cart_item.id, "product_id": product_id, @@ -408,7 +405,7 @@ class CartService: return {"message": "Item removed from cart", "product_id": product_id} - def clear_cart(self, db: Session, vendor_id: int, session_id: str) -> Dict: + def clear_cart(self, db: Session, vendor_id: int, session_id: str) -> dict: """ Clear all items from cart. @@ -432,7 +429,7 @@ class CartService: db.commit() logger.info( - f"[CART_SERVICE] Cleared cart", + "[CART_SERVICE] Cleared cart", extra={ "session_id": session_id, "vendor_id": vendor_id, diff --git a/app/services/code_quality_service.py b/app/services/code_quality_service.py index ff058e78..6ef18e44 100644 --- a/app/services/code_quality_service.py +++ b/app/services/code_quality_service.py @@ -7,16 +7,16 @@ import json import logging import subprocess from datetime import datetime -from pathlib import Path -from typing import Dict, List, Optional, Tuple from sqlalchemy import desc, func from sqlalchemy.orm import Session -from app.models.architecture_scan import (ArchitectureRule, ArchitectureScan, - ArchitectureViolation, - ViolationAssignment, - ViolationComment) +from app.models.architecture_scan import ( + ArchitectureScan, + ArchitectureViolation, + ViolationAssignment, + ViolationComment, +) logger = logging.getLogger(__name__) @@ -118,7 +118,7 @@ class CodeQualityService: logger.info(f"Scan completed: {scan.total_violations} violations found") return scan - def get_latest_scan(self, db: Session) -> Optional[ArchitectureScan]: + def get_latest_scan(self, db: Session) -> ArchitectureScan | None: """Get the most recent scan""" return ( db.query(ArchitectureScan) @@ -126,11 +126,11 @@ class CodeQualityService: .first() ) - def get_scan_by_id(self, db: Session, scan_id: int) -> Optional[ArchitectureScan]: + def get_scan_by_id(self, db: Session, scan_id: int) -> ArchitectureScan | None: """Get scan by ID""" return db.query(ArchitectureScan).filter(ArchitectureScan.id == scan_id).first() - def get_scan_history(self, db: Session, limit: int = 30) -> List[ArchitectureScan]: + def get_scan_history(self, db: Session, limit: int = 30) -> list[ArchitectureScan]: """ Get scan history for trend graphs @@ -158,7 +158,7 @@ class CodeQualityService: file_path: str = None, limit: int = 100, offset: int = 0, - ) -> Tuple[List[ArchitectureViolation], int]: + ) -> tuple[list[ArchitectureViolation], int]: """ Get violations with filtering and pagination @@ -217,7 +217,7 @@ class CodeQualityService: def get_violation_by_id( self, db: Session, violation_id: int - ) -> Optional[ArchitectureViolation]: + ) -> ArchitectureViolation | None: """Get single violation with details""" return ( db.query(ArchitectureViolation) @@ -348,7 +348,7 @@ class CodeQualityService: logger.info(f"Comment added to violation {violation_id} by user {user_id}") return comment_obj - def get_dashboard_stats(self, db: Session) -> Dict: + def get_dashboard_stats(self, db: Session) -> dict: """ Get statistics for dashboard @@ -507,7 +507,7 @@ class CodeQualityService: score = 100 - (scan.errors * 0.5 + scan.warnings * 0.05) return max(0, min(100, int(score))) # Clamp to 0-100 - def _get_git_commit_hash(self) -> Optional[str]: + def _get_git_commit_hash(self) -> str | None: """Get current git commit hash""" try: result = subprocess.run( diff --git a/app/services/content_page_service.py b/app/services/content_page_service.py index af17bc7d..6394241f 100644 --- a/app/services/content_page_service.py +++ b/app/services/content_page_service.py @@ -17,10 +17,9 @@ This allows: """ import logging -from datetime import datetime, timezone -from typing import List, Optional +from datetime import UTC, datetime -from sqlalchemy import and_, or_ +from sqlalchemy import and_ from sqlalchemy.orm import Session from models.database.content_page import ContentPage @@ -35,9 +34,9 @@ class ContentPageService: def get_page_for_vendor( db: Session, slug: str, - vendor_id: Optional[int] = None, + vendor_id: int | None = None, include_unpublished: bool = False, - ) -> Optional[ContentPage]: + ) -> ContentPage | None: """ Get content page for a vendor with fallback to platform default. @@ -90,11 +89,11 @@ class ContentPageService: @staticmethod def list_pages_for_vendor( db: Session, - vendor_id: Optional[int] = None, + vendor_id: int | None = None, include_unpublished: bool = False, footer_only: bool = False, header_only: bool = False, - ) -> List[ContentPage]: + ) -> list[ContentPage]: """ List all available pages for a vendor (includes vendor overrides + platform defaults). @@ -156,16 +155,16 @@ class ContentPageService: slug: str, title: str, content: str, - vendor_id: Optional[int] = None, + vendor_id: int | None = None, content_format: str = "html", template: str = "default", - meta_description: Optional[str] = None, - meta_keywords: Optional[str] = None, + meta_description: str | None = None, + meta_keywords: str | None = None, is_published: bool = False, show_in_footer: bool = True, show_in_header: bool = False, display_order: int = 0, - created_by: Optional[int] = None, + created_by: int | None = None, ) -> ContentPage: """ Create a new content page. @@ -199,7 +198,7 @@ class ContentPageService: meta_description=meta_description, meta_keywords=meta_keywords, is_published=is_published, - published_at=datetime.now(timezone.utc) if is_published else None, + published_at=datetime.now(UTC) if is_published else None, show_in_footer=show_in_footer, show_in_header=show_in_header, display_order=display_order, @@ -220,18 +219,18 @@ class ContentPageService: def update_page( db: Session, page_id: int, - title: Optional[str] = None, - content: Optional[str] = None, - content_format: Optional[str] = None, - template: Optional[str] = None, - meta_description: Optional[str] = None, - meta_keywords: Optional[str] = None, - is_published: Optional[bool] = None, - show_in_footer: Optional[bool] = None, - show_in_header: Optional[bool] = None, - display_order: Optional[int] = None, - updated_by: Optional[int] = None, - ) -> Optional[ContentPage]: + title: str | None = None, + content: str | None = None, + content_format: str | None = None, + template: str | None = None, + meta_description: str | None = None, + meta_keywords: str | None = None, + is_published: bool | None = None, + show_in_footer: bool | None = None, + show_in_header: bool | None = None, + display_order: int | None = None, + updated_by: int | None = None, + ) -> ContentPage | None: """ Update an existing content page. @@ -275,7 +274,7 @@ class ContentPageService: if is_published is not None: page.is_published = is_published if is_published and not page.published_at: - page.published_at = datetime.now(timezone.utc) + page.published_at = datetime.now(UTC) if show_in_footer is not None: page.show_in_footer = show_in_footer if show_in_header is not None: @@ -316,14 +315,14 @@ class ContentPageService: return True @staticmethod - def get_page_by_id(db: Session, page_id: int) -> Optional[ContentPage]: + def get_page_by_id(db: Session, page_id: int) -> ContentPage | None: """Get content page by ID.""" return db.query(ContentPage).filter(ContentPage.id == page_id).first() @staticmethod def list_all_vendor_pages( db: Session, vendor_id: int, include_unpublished: bool = False - ) -> List[ContentPage]: + ) -> list[ContentPage]: """ List only vendor-specific pages (no platform defaults). @@ -350,7 +349,7 @@ class ContentPageService: @staticmethod def list_all_platform_pages( db: Session, include_unpublished: bool = False - ) -> List[ContentPage]: + ) -> list[ContentPage]: """ List only platform default pages. diff --git a/app/services/customer_service.py b/app/services/customer_service.py index d76078b6..9855fbef 100644 --- a/app/services/customer_service.py +++ b/app/services/customer_service.py @@ -7,22 +7,22 @@ with complete vendor isolation. """ import logging -from datetime import datetime, timedelta -from typing import Any, Dict, Optional +from datetime import UTC, datetime, timedelta +from typing import Any from sqlalchemy import and_ from sqlalchemy.orm import Session -from app.exceptions.customer import (CustomerAlreadyExistsException, - CustomerNotActiveException, - CustomerNotFoundException, - CustomerValidationException, - DuplicateCustomerEmailException, - InvalidCustomerCredentialsException) -from app.exceptions.vendor import (VendorNotActiveException, - VendorNotFoundException) +from app.exceptions.customer import ( + CustomerNotActiveException, + CustomerNotFoundException, + CustomerValidationException, + DuplicateCustomerEmailException, + InvalidCustomerCredentialsException, +) +from app.exceptions.vendor import VendorNotActiveException, VendorNotFoundException from app.services.auth_service import AuthService -from models.database.customer import Customer, CustomerAddress +from models.database.customer import Customer from models.database.vendor import Vendor from models.schema.auth import UserLogin from models.schema.customer import CustomerRegister, CustomerUpdate @@ -128,7 +128,7 @@ class CustomerService: def login_customer( self, db: Session, vendor_id: int, credentials: UserLogin - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Authenticate customer and generate JWT token. @@ -177,13 +177,13 @@ class CustomerService: # Generate JWT token with customer context # Use auth_manager directly since Customer is not a User model - from datetime import datetime, timedelta, timezone + from datetime import datetime from jose import jwt auth_manager = self.auth_service.auth_manager expires_delta = timedelta(minutes=auth_manager.token_expire_minutes) - expire = datetime.now(timezone.utc) + expires_delta + expire = datetime.now(UTC) + expires_delta payload = { "sub": str(customer.id), @@ -191,7 +191,7 @@ class CustomerService: "vendor_id": vendor_id, "type": "customer", "exp": expire, - "iat": datetime.now(timezone.utc), + "iat": datetime.now(UTC), } token = jwt.encode( @@ -239,7 +239,7 @@ class CustomerService: def get_customer_by_email( self, db: Session, vendor_id: int, email: str - ) -> Optional[Customer]: + ) -> Customer | None: """ Get customer by email (vendor-scoped). diff --git a/app/services/inventory_service.py b/app/services/inventory_service.py index b7f945d2..ae194b80 100644 --- a/app/services/inventory_service.py +++ b/app/services/inventory_service.py @@ -1,24 +1,27 @@ # app/services/inventory_service.py import logging -from datetime import datetime, timezone -from typing import List, Optional +from datetime import UTC, datetime from sqlalchemy.orm import Session -from app.exceptions import (InsufficientInventoryException, - InvalidInventoryOperationException, - InvalidQuantityException, - InventoryNotFoundException, - InventoryValidationException, - NegativeInventoryException, - ProductNotFoundException, ValidationException) +from app.exceptions import ( + InsufficientInventoryException, + InvalidQuantityException, + InventoryNotFoundException, + InventoryValidationException, + ProductNotFoundException, + ValidationException, +) from models.database.inventory import Inventory from models.database.product import Product -from models.database.vendor import Vendor -from models.schema.inventory import (InventoryAdjust, InventoryCreate, - InventoryLocationResponse, - InventoryReserve, InventoryUpdate, - ProductInventorySummary) +from models.schema.inventory import ( + InventoryAdjust, + InventoryCreate, + InventoryLocationResponse, + InventoryReserve, + InventoryUpdate, + ProductInventorySummary, +) logger = logging.getLogger(__name__) @@ -58,7 +61,7 @@ class InventoryService: if existing: old_qty = existing.quantity existing.quantity = inventory_data.quantity - existing.updated_at = datetime.now(timezone.utc) + existing.updated_at = datetime.now(UTC) db.commit() db.refresh(existing) @@ -67,24 +70,23 @@ class InventoryService: f"{old_qty} → {inventory_data.quantity}" ) return existing - else: - # Create new inventory entry - new_inventory = Inventory( - product_id=inventory_data.product_id, - vendor_id=vendor_id, - location=location, - quantity=inventory_data.quantity, - gtin=product.marketplace_product.gtin, # Optional reference - ) - db.add(new_inventory) - db.commit() - db.refresh(new_inventory) + # Create new inventory entry + new_inventory = Inventory( + product_id=inventory_data.product_id, + vendor_id=vendor_id, + location=location, + quantity=inventory_data.quantity, + gtin=product.marketplace_product.gtin, # Optional reference + ) + db.add(new_inventory) + db.commit() + db.refresh(new_inventory) - logger.info( - f"Created inventory for product {inventory_data.product_id} at {location}: " - f"{inventory_data.quantity}" - ) - return new_inventory + logger.info( + f"Created inventory for product {inventory_data.product_id} at {location}: " + f"{inventory_data.quantity}" + ) + return new_inventory except ( ProductNotFoundException, @@ -162,7 +164,7 @@ class InventoryService: ) existing.quantity = new_qty - existing.updated_at = datetime.now(timezone.utc) + existing.updated_at = datetime.now(UTC) db.commit() db.refresh(existing) @@ -224,7 +226,7 @@ class InventoryService: # Reserve inventory inventory.reserved_quantity += reserve_data.quantity - inventory.updated_at = datetime.now(timezone.utc) + inventory.updated_at = datetime.now(UTC) db.commit() db.refresh(inventory) @@ -284,7 +286,7 @@ class InventoryService: else: inventory.reserved_quantity -= reserve_data.quantity - inventory.updated_at = datetime.now(timezone.utc) + inventory.updated_at = datetime.now(UTC) db.commit() db.refresh(inventory) @@ -350,7 +352,7 @@ class InventoryService: inventory.reserved_quantity = max( 0, inventory.reserved_quantity - reserve_data.quantity ) - inventory.updated_at = datetime.now(timezone.utc) + inventory.updated_at = datetime.now(UTC) db.commit() db.refresh(inventory) @@ -443,9 +445,9 @@ class InventoryService: vendor_id: int, skip: int = 0, limit: int = 100, - location: Optional[str] = None, - low_stock_threshold: Optional[int] = None, - ) -> List[Inventory]: + location: str | None = None, + low_stock_threshold: int | None = None, + ) -> list[Inventory]: """ Get all inventory for a vendor with filtering. @@ -504,7 +506,7 @@ class InventoryService: if inventory_update.location: inventory.location = self._validate_location(inventory_update.location) - inventory.updated_at = datetime.now(timezone.utc) + inventory.updated_at = datetime.now(UTC) db.commit() db.refresh(inventory) @@ -565,7 +567,7 @@ class InventoryService: def _get_inventory_entry( self, db: Session, product_id: int, location: str - ) -> Optional[Inventory]: + ) -> Inventory | None: """Get inventory entry by product and location.""" return ( db.query(Inventory) diff --git a/app/services/marketplace_import_job_service.py b/app/services/marketplace_import_job_service.py index 2007cd2b..13056353 100644 --- a/app/services/marketplace_import_job_service.py +++ b/app/services/marketplace_import_job_service.py @@ -1,19 +1,20 @@ # app/services/marketplace_import_job_service.py import logging -from datetime import datetime, timezone -from typing import List, Optional from sqlalchemy.orm import Session -from app.exceptions import (ImportJobCannotBeCancelledException, - ImportJobCannotBeDeletedException, - ImportJobNotFoundException, - ImportJobNotOwnedException, ValidationException) +from app.exceptions import ( + ImportJobNotFoundException, + ImportJobNotOwnedException, + ValidationException, +) from models.database.marketplace_import_job import MarketplaceImportJob from models.database.user import User from models.database.vendor import Vendor -from models.schema.marketplace_import_job import (MarketplaceImportJobRequest, - MarketplaceImportJobResponse) +from models.schema.marketplace_import_job import ( + MarketplaceImportJobRequest, + MarketplaceImportJobResponse, +) logger = logging.getLogger(__name__) @@ -98,10 +99,10 @@ class MarketplaceImportJobService: db: Session, vendor: Vendor, # ADDED: Vendor filter user: User, - marketplace: Optional[str] = None, + marketplace: str | None = None, skip: int = 0, limit: int = 50, - ) -> List[MarketplaceImportJob]: + ) -> list[MarketplaceImportJob]: """Get marketplace import jobs for a specific vendor.""" try: query = db.query(MarketplaceImportJob).filter( diff --git a/app/services/marketplace_product_service.py b/app/services/marketplace_product_service.py index fd572b32..9de1100c 100644 --- a/app/services/marketplace_product_service.py +++ b/app/services/marketplace_product_service.py @@ -8,29 +8,31 @@ This module provides classes and functions for: - Inventory information integration - CSV export functionality """ + import csv import logging -from datetime import datetime, timezone +from collections.abc import Generator +from datetime import UTC, datetime from io import StringIO -from typing import Generator, List, Optional, Tuple from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -from app.exceptions import (InvalidMarketplaceProductDataException, - MarketplaceProductAlreadyExistsException, - MarketplaceProductNotFoundException, - MarketplaceProductValidationException, - ValidationException) -from app.services.marketplace_import_job_service import \ - marketplace_import_job_service +from app.exceptions import ( + InvalidMarketplaceProductDataException, + MarketplaceProductAlreadyExistsException, + MarketplaceProductNotFoundException, + MarketplaceProductValidationException, + ValidationException, +) from app.utils.data_processing import GTINProcessor, PriceProcessor from models.database.inventory import Inventory from models.database.marketplace_product import MarketplaceProduct -from models.schema.inventory import (InventoryLocationResponse, - InventorySummaryResponse) -from models.schema.marketplace_product import (MarketplaceProductCreate, - MarketplaceProductUpdate) +from models.schema.inventory import InventoryLocationResponse, InventorySummaryResponse +from models.schema.marketplace_product import ( + MarketplaceProductCreate, + MarketplaceProductUpdate, +) logger = logging.getLogger(__name__) @@ -109,10 +111,9 @@ class MarketplaceProductService: raise MarketplaceProductAlreadyExistsException( product_data.marketplace_product_id ) - else: - raise MarketplaceProductValidationException( - "Data integrity constraint violation" - ) + raise MarketplaceProductValidationException( + "Data integrity constraint violation" + ) except Exception as e: db.rollback() logger.error(f"Error creating product: {str(e)}") @@ -120,7 +121,7 @@ class MarketplaceProductService: def get_product_by_id( self, db: Session, marketplace_product_id: str - ) -> Optional[MarketplaceProduct]: + ) -> MarketplaceProduct | None: """Get a product by its ID.""" try: return ( @@ -160,13 +161,13 @@ class MarketplaceProductService: db: Session, skip: int = 0, limit: int = 100, - brand: Optional[str] = None, - category: Optional[str] = None, - availability: Optional[str] = None, - marketplace: Optional[str] = None, - vendor_name: Optional[str] = None, - search: Optional[str] = None, - ) -> Tuple[List[MarketplaceProduct], int]: + brand: str | None = None, + category: str | None = None, + availability: str | None = None, + marketplace: str | None = None, + vendor_name: str | None = None, + search: str | None = None, + ) -> tuple[list[MarketplaceProduct], int]: """ Get products with filtering and pagination. @@ -269,7 +270,7 @@ class MarketplaceProductService: for key, value in update_data.items(): setattr(product, key, value) - product.updated_at = datetime.now(timezone.utc) + product.updated_at = datetime.now(UTC) db.commit() db.refresh(product) @@ -324,7 +325,7 @@ class MarketplaceProductService: def get_inventory_info( self, db: Session, gtin: str - ) -> Optional[InventorySummaryResponse]: + ) -> InventorySummaryResponse | None: """ Get inventory information for a product by GTIN. @@ -358,15 +359,14 @@ class MarketplaceProductService: import csv from io import StringIO - from typing import Generator, Optional from sqlalchemy.orm import Session def generate_csv_export( self, db: Session, - marketplace: Optional[str] = None, - vendor_name: Optional[str] = None, + marketplace: str | None = None, + vendor_name: str | None = None, ) -> Generator[str, None, None]: """ Generate CSV export with streaming for memory efficiency and proper CSV escaping. diff --git a/app/services/order_service.py b/app/services/order_service.py index 9e1005fd..e55c2983 100644 --- a/app/services/order_service.py +++ b/app/services/order_service.py @@ -11,15 +11,17 @@ This module provides: import logging import random import string -from datetime import datetime, timezone -from typing import List, Optional, Tuple +from datetime import UTC, datetime -from sqlalchemy import and_, or_ +from sqlalchemy import and_ from sqlalchemy.orm import Session -from app.exceptions import (CustomerNotFoundException, - InsufficientInventoryException, - OrderNotFoundException, ValidationException) +from app.exceptions import ( + CustomerNotFoundException, + InsufficientInventoryException, + OrderNotFoundException, + ValidationException, +) from models.database.customer import Customer, CustomerAddress from models.database.order import Order, OrderItem from models.database.product import Product @@ -38,7 +40,7 @@ class OrderService: Format: ORD-{VENDOR_ID}-{TIMESTAMP}-{RANDOM} Example: ORD-1-20250110-A1B2C3 """ - timestamp = datetime.now(timezone.utc).strftime("%Y%m%d") + timestamp = datetime.now(UTC).strftime("%Y%m%d") random_suffix = "".join( random.choices(string.ascii_uppercase + string.digits, k=6) ) @@ -266,9 +268,9 @@ class OrderService: vendor_id: int, skip: int = 0, limit: int = 100, - status: Optional[str] = None, - customer_id: Optional[int] = None, - ) -> Tuple[List[Order], int]: + status: str | None = None, + customer_id: int | None = None, + ) -> tuple[list[Order], int]: """ Get orders for vendor with filtering. @@ -306,7 +308,7 @@ class OrderService: customer_id: int, skip: int = 0, limit: int = 100, - ) -> Tuple[List[Order], int]: + ) -> tuple[list[Order], int]: """Get orders for a specific customer.""" return self.get_vendor_orders( db=db, vendor_id=vendor_id, skip=skip, limit=limit, customer_id=customer_id @@ -335,7 +337,7 @@ class OrderService: order.status = order_update.status # Update timestamp based on status - now = datetime.now(timezone.utc) + now = datetime.now(UTC) if order_update.status == "shipped" and not order.shipped_at: order.shipped_at = now elif order_update.status == "delivered" and not order.delivered_at: @@ -351,7 +353,7 @@ class OrderService: if order_update.internal_notes: order.internal_notes = order_update.internal_notes - order.updated_at = datetime.now(timezone.utc) + order.updated_at = datetime.now(UTC) db.commit() db.refresh(order) diff --git a/app/services/product_service.py b/app/services/product_service.py index da7f46ba..f0af551a 100644 --- a/app/services/product_service.py +++ b/app/services/product_service.py @@ -9,13 +9,15 @@ This module provides: """ import logging -from datetime import datetime, timezone -from typing import List, Optional, Tuple +from datetime import UTC, datetime from sqlalchemy.orm import Session -from app.exceptions import (ProductAlreadyExistsException, - ProductNotFoundException, ValidationException) +from app.exceptions import ( + ProductAlreadyExistsException, + ProductNotFoundException, + ValidationException, +) from models.database.marketplace_product import MarketplaceProduct from models.database.product import Product from models.schema.product import ProductCreate, ProductUpdate @@ -106,7 +108,7 @@ class ProductService: if existing: raise ProductAlreadyExistsException( - f"Product already exists in catalog" + "Product already exists in catalog" ) # Create product @@ -167,7 +169,7 @@ class ProductService: for key, value in update_data.items(): setattr(product, key, value) - product.updated_at = datetime.now(timezone.utc) + product.updated_at = datetime.now(UTC) db.commit() db.refresh(product) @@ -216,9 +218,9 @@ class ProductService: vendor_id: int, skip: int = 0, limit: int = 100, - is_active: Optional[bool] = None, - is_featured: Optional[bool] = None, - ) -> Tuple[List[Product], int]: + is_active: bool | None = None, + is_featured: bool | None = None, + ) -> tuple[list[Product], int]: """ Get products in vendor catalog with filtering. diff --git a/app/services/stats_service.py b/app/services/stats_service.py index ba8f381b..3ca89596 100644 --- a/app/services/stats_service.py +++ b/app/services/stats_service.py @@ -11,7 +11,7 @@ This module provides: import logging from datetime import datetime, timedelta -from typing import Any, Dict, List +from typing import Any from sqlalchemy import func from sqlalchemy.orm import Session @@ -36,7 +36,7 @@ class StatsService: # VENDOR-SPECIFIC STATISTICS # ======================================================================== - def get_vendor_stats(self, db: Session, vendor_id: int) -> Dict[str, Any]: + def get_vendor_stats(self, db: Session, vendor_id: int) -> dict[str, Any]: """ Get statistics for a specific vendor. @@ -177,7 +177,7 @@ class StatsService: def get_vendor_analytics( self, db: Session, vendor_id: int, period: str = "30d" - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Get a specific vendor analytics for a time period. @@ -283,7 +283,7 @@ class StatsService: # SYSTEM-WIDE STATISTICS (ADMIN) # ======================================================================== - def get_comprehensive_stats(self, db: Session) -> Dict[str, Any]: + def get_comprehensive_stats(self, db: Session) -> dict[str, Any]: """ Get comprehensive system statistics for admin dashboard. @@ -333,7 +333,7 @@ class StatsService: reason=f"Database query failed: {str(e)}", ) - def get_marketplace_breakdown_stats(self, db: Session) -> List[Dict[str, Any]]: + def get_marketplace_breakdown_stats(self, db: Session) -> list[dict[str, Any]]: """ Get statistics broken down by marketplace. @@ -382,7 +382,7 @@ class StatsService: reason=f"Database query failed: {str(e)}", ) - def get_user_statistics(self, db: Session) -> Dict[str, Any]: + def get_user_statistics(self, db: Session) -> dict[str, Any]: """ Get user statistics for admin dashboard. @@ -416,7 +416,7 @@ class StatsService: operation="get_user_statistics", reason="Database query failed" ) - def get_import_statistics(self, db: Session) -> Dict[str, Any]: + def get_import_statistics(self, db: Session) -> dict[str, Any]: """ Get import job statistics. @@ -457,7 +457,7 @@ class StatsService: "success_rate": 0, } - def get_order_statistics(self, db: Session) -> Dict[str, Any]: + def get_order_statistics(self, db: Session) -> dict[str, Any]: """ Get order statistics. @@ -472,7 +472,7 @@ class StatsService: """ return {"total_orders": 0, "pending_orders": 0, "completed_orders": 0} - def get_product_statistics(self, db: Session) -> Dict[str, Any]: + def get_product_statistics(self, db: Session) -> dict[str, Any]: """ Get product statistics. @@ -548,7 +548,7 @@ class StatsService: .count() ) - def _get_inventory_statistics(self, db: Session) -> Dict[str, int]: + def _get_inventory_statistics(self, db: Session) -> dict[str, int]: """ Get inventory-related statistics. diff --git a/app/services/team_service.py b/app/services/team_service.py index 84799d22..e850ab82 100644 --- a/app/services/team_service.py +++ b/app/services/team_service.py @@ -9,13 +9,12 @@ This module provides: """ import logging -from datetime import datetime, timezone -from typing import Any, Dict, List +from datetime import UTC, datetime +from typing import Any from sqlalchemy.orm import Session -from app.exceptions import (UnauthorizedVendorAccessException, - ValidationException) +from app.exceptions import ValidationException from models.database.user import User from models.database.vendor import Role, VendorUser @@ -27,7 +26,7 @@ class TeamService: def get_team_members( self, db: Session, vendor_id: int, current_user: User - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """ Get all team members for vendor. @@ -69,7 +68,7 @@ class TeamService: def invite_team_member( self, db: Session, vendor_id: int, invitation_data: dict, current_user: User - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Invite a new team member. @@ -102,7 +101,7 @@ class TeamService: user_id: int, update_data: dict, current_user: User, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Update team member role or status. @@ -135,7 +134,7 @@ class TeamService: if "is_active" in update_data: vendor_user.is_active = update_data["is_active"] - vendor_user.updated_at = datetime.now(timezone.utc) + vendor_user.updated_at = datetime.now(UTC) db.commit() db.refresh(vendor_user) @@ -178,7 +177,7 @@ class TeamService: # Soft delete vendor_user.is_active = False - vendor_user.updated_at = datetime.now(timezone.utc) + vendor_user.updated_at = datetime.now(UTC) db.commit() logger.info(f"Removed user {user_id} from vendor {vendor_id}") @@ -189,7 +188,7 @@ class TeamService: logger.error(f"Error removing team member: {str(e)}") raise ValidationException("Failed to remove team member") - def get_vendor_roles(self, db: Session, vendor_id: int) -> List[Dict[str, Any]]: + def get_vendor_roles(self, db: Session, vendor_id: int) -> list[dict[str, Any]]: """ Get available roles for vendor. diff --git a/app/services/vendor_domain_service.py b/app/services/vendor_domain_service.py index 1d1776fb..0a539a25 100644 --- a/app/services/vendor_domain_service.py +++ b/app/services/vendor_domain_service.py @@ -12,25 +12,23 @@ This module provides classes and functions for: import logging import secrets -from datetime import datetime, timezone -from typing import List, Optional, Tuple +from datetime import UTC, datetime -from sqlalchemy import and_ from sqlalchemy.orm import Session -from app.exceptions import (DNSVerificationException, - DomainAlreadyVerifiedException, - DomainNotVerifiedException, - DomainVerificationFailedException, - InvalidDomainFormatException, - MaxDomainsReachedException, - MultiplePrimaryDomainsException, - ReservedDomainException, - UnauthorizedDomainAccessException, - ValidationException, - VendorDomainAlreadyExistsException, - VendorDomainNotFoundException, - VendorNotFoundException) +from app.exceptions import ( + DNSVerificationException, + DomainAlreadyVerifiedException, + DomainNotVerifiedException, + DomainVerificationFailedException, + InvalidDomainFormatException, + MaxDomainsReachedException, + ReservedDomainException, + ValidationException, + VendorDomainAlreadyExistsException, + VendorDomainNotFoundException, + VendorNotFoundException, +) from models.database.vendor import Vendor from models.database.vendor_domain import VendorDomain from models.schema.vendor_domain import VendorDomainCreate, VendorDomainUpdate @@ -135,7 +133,7 @@ class VendorDomainService: logger.error(f"Error adding domain: {str(e)}") raise ValidationException("Failed to add domain") - def get_vendor_domains(self, db: Session, vendor_id: int) -> List[VendorDomain]: + def get_vendor_domains(self, db: Session, vendor_id: int) -> list[VendorDomain]: """ Get all domains for a vendor. @@ -272,7 +270,7 @@ class VendorDomainService: logger.error(f"Error deleting domain: {str(e)}") raise ValidationException("Failed to delete domain") - def verify_domain(self, db: Session, domain_id: int) -> Tuple[VendorDomain, str]: + def verify_domain(self, db: Session, domain_id: int) -> tuple[VendorDomain, str]: """ Verify domain ownership via DNS TXT record. @@ -313,7 +311,7 @@ class VendorDomainService: if txt_value == domain.verification_token: # Verification successful domain.is_verified = True - domain.verified_at = datetime.now(timezone.utc) + domain.verified_at = datetime.now(UTC) db.commit() db.refresh(domain) @@ -419,7 +417,7 @@ class VendorDomainService: raise ReservedDomainException(domain, first_part) def _unset_primary_domains( - self, db: Session, vendor_id: int, exclude_domain_id: Optional[int] = None + self, db: Session, vendor_id: int, exclude_domain_id: int | None = None ) -> None: """Unset all primary domains for vendor.""" query = db.query(VendorDomain).filter( diff --git a/app/services/vendor_service.py b/app/services/vendor_service.py index d686c201..72dcc8bd 100644 --- a/app/services/vendor_service.py +++ b/app/services/vendor_service.py @@ -10,18 +10,20 @@ This module provides classes and functions for: """ import logging -from typing import List, Optional, Tuple from sqlalchemy import func from sqlalchemy.orm import Session -from app.exceptions import (InvalidVendorDataException, - MarketplaceProductNotFoundException, - MaxVendorsReachedException, - ProductAlreadyExistsException, - UnauthorizedVendorAccessException, - ValidationException, VendorAlreadyExistsException, - VendorNotFoundException) +from app.exceptions import ( + InvalidVendorDataException, + MarketplaceProductNotFoundException, + MaxVendorsReachedException, + ProductAlreadyExistsException, + UnauthorizedVendorAccessException, + ValidationException, + VendorAlreadyExistsException, + VendorNotFoundException, +) from models.database.marketplace_product import MarketplaceProduct from models.database.product import Product from models.database.user import User @@ -108,7 +110,7 @@ class VendorService: limit: int = 100, active_only: bool = True, verified_only: bool = False, - ) -> Tuple[List[Vendor], int]: + ) -> tuple[list[Vendor], int]: """ Get vendors with filtering. @@ -257,7 +259,7 @@ class VendorService: limit: int = 100, active_only: bool = True, featured_only: bool = False, - ) -> Tuple[List[Product], int]: + ) -> tuple[list[Product], int]: """ Get products in vendor catalog with filtering. diff --git a/app/services/vendor_team_service.py b/app/services/vendor_team_service.py index 2ed95c29..98a5cdc9 100644 --- a/app/services/vendor_team_service.py +++ b/app/services/vendor_team_service.py @@ -8,20 +8,23 @@ Handles: - Role assignment - Permission management """ + import logging import secrets from datetime import datetime, timedelta -from typing import Any, Dict, List, Optional +from typing import Any from sqlalchemy.orm import Session from app.core.permissions import get_preset_permissions -from app.exceptions import (CannotRemoveOwnerException, - InvalidInvitationTokenException, - MaxTeamMembersReachedException, - TeamInvitationAlreadyAcceptedException, - TeamMemberAlreadyExistsException, - UserNotFoundException, VendorNotFoundException) +from app.exceptions import ( + CannotRemoveOwnerException, + InvalidInvitationTokenException, + MaxTeamMembersReachedException, + TeamInvitationAlreadyAcceptedException, + TeamMemberAlreadyExistsException, + UserNotFoundException, +) from middleware.auth import AuthManager from models.database.user import User from models.database.vendor import Role, Vendor, VendorUser, VendorUserType @@ -43,8 +46,8 @@ class VendorTeamService: inviter: User, email: str, role_name: str, - custom_permissions: Optional[List[str]] = None, - ) -> Dict[str, Any]: + custom_permissions: list[str] | None = None, + ) -> dict[str, Any]: """ Invite a new team member to a vendor. @@ -196,9 +199,9 @@ class VendorTeamService: db: Session, invitation_token: str, password: str, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - ) -> Dict[str, Any]: + first_name: str | None = None, + last_name: str | None = None, + ) -> dict[str, Any]: """ Accept a team invitation and activate account. @@ -330,7 +333,7 @@ class VendorTeamService: vendor: Vendor, user_id: int, new_role_name: str, - custom_permissions: Optional[List[str]] = None, + custom_permissions: list[str] | None = None, ) -> VendorUser: """ Update a team member's role. @@ -392,7 +395,7 @@ class VendorTeamService: db: Session, vendor: Vendor, include_inactive: bool = False, - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """ Get all team members for a vendor. @@ -445,7 +448,7 @@ class VendorTeamService: db: Session, vendor: Vendor, role_name: str, - custom_permissions: Optional[List[str]] = None, + custom_permissions: list[str] | None = None, ) -> Role: """Get existing role or create new one with preset/custom permissions.""" # Try to find existing role with same name @@ -492,7 +495,6 @@ class VendorTeamService: # - Vendor name # - Inviter name # - Expiry date - pass # Create service instance diff --git a/app/services/vendor_theme_service.py b/app/services/vendor_theme_service.py index 46cd3e3b..c08cd652 100644 --- a/app/services/vendor_theme_service.py +++ b/app/services/vendor_theme_service.py @@ -8,21 +8,24 @@ Handles theme CRUD operations, preset application, and validation. import logging import re -from typing import Dict, List, Optional from sqlalchemy.orm import Session -from app.core.theme_presets import (THEME_PRESETS, apply_preset, - get_available_presets, get_preset_preview) +from app.core.theme_presets import ( + THEME_PRESETS, + apply_preset, + get_available_presets, + get_preset_preview, +) from app.exceptions.vendor import VendorNotFoundException -from app.exceptions.vendor_theme import (InvalidColorFormatException, - InvalidFontFamilyException, - InvalidThemeDataException, - ThemeOperationException, - ThemePresetAlreadyAppliedException, - ThemePresetNotFoundException, - ThemeValidationException, - VendorThemeNotFoundException) +from app.exceptions.vendor_theme import ( + InvalidColorFormatException, + InvalidFontFamilyException, + ThemeOperationException, + ThemePresetNotFoundException, + ThemeValidationException, + VendorThemeNotFoundException, +) from models.database.vendor import Vendor from models.database.vendor_theme import VendorTheme from models.schema.vendor_theme import ThemePresetPreview, VendorThemeUpdate @@ -77,7 +80,7 @@ class VendorThemeService: # THEME RETRIEVAL # ============================================================================ - def get_theme(self, db: Session, vendor_code: str) -> Dict: + def get_theme(self, db: Session, vendor_code: str) -> dict: """ Get theme for vendor. Returns default if no custom theme exists. @@ -107,7 +110,7 @@ class VendorThemeService: return theme.to_dict() - def _get_default_theme(self) -> Dict: + def _get_default_theme(self) -> dict: """ Get default theme configuration. @@ -329,7 +332,7 @@ class VendorThemeService: operation="apply_preset", vendor_code=vendor_code, reason=str(e) ) - def get_available_presets(self) -> List[ThemePresetPreview]: + def get_available_presets(self) -> list[ThemePresetPreview]: """ Get list of available theme presets. @@ -351,7 +354,7 @@ class VendorThemeService: # THEME DELETION # ============================================================================ - def delete_theme(self, db: Session, vendor_code: str) -> Dict: + def delete_theme(self, db: Session, vendor_code: str) -> dict: """ Delete custom theme for vendor (reverts to default). diff --git a/app/tasks/background_tasks.py b/app/tasks/background_tasks.py index 87ca42f4..bf151b92 100644 --- a/app/tasks/background_tasks.py +++ b/app/tasks/background_tasks.py @@ -1,6 +1,6 @@ # app/tasks/background_tasks.py import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from app.core.database import SessionLocal from app.utils.csv_processor import CSVProcessor @@ -39,13 +39,13 @@ async def process_marketplace_import( logger.error(f"Vendor {vendor_id} not found for import job {job_id}") job.status = "failed" job.error_message = f"Vendor {vendor_id} not found" - job.completed_at = datetime.now(timezone.utc) + job.completed_at = datetime.now(UTC) db.commit() return # Update job status job.status = "processing" - job.started_at = datetime.now(timezone.utc) + job.started_at = datetime.now(UTC) db.commit() logger.info( @@ -64,7 +64,7 @@ async def process_marketplace_import( # Update job with results job.status = "completed" - job.completed_at = datetime.now(timezone.utc) + job.completed_at = datetime.now(UTC) job.imported_count = result["imported"] job.updated_count = result["updated"] job.error_count = result.get("errors", 0) @@ -87,13 +87,13 @@ async def process_marketplace_import( try: job.status = "failed" job.error_message = str(e) - job.completed_at = datetime.now(timezone.utc) + job.completed_at = datetime.now(UTC) db.commit() except Exception as commit_error: logger.error(f"Failed to update job status: {commit_error}") db.rollback() finally: - if hasattr(db, "close") and callable(getattr(db, "close")): + if hasattr(db, "close") and callable(db.close): try: db.close() except Exception as close_error: diff --git a/app/utils/csv_processor.py b/app/utils/csv_processor.py index 6fb9b148..8b12eab3 100644 --- a/app/utils/csv_processor.py +++ b/app/utils/csv_processor.py @@ -8,9 +8,9 @@ This module provides classes and functions for: """ import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from io import StringIO -from typing import Any, Dict +from typing import Any import pandas as pd import requests @@ -145,7 +145,7 @@ class CSVProcessor: logger.info(f"Normalized columns: {list(df.columns)}") return df - def _clean_row_data(self, row_data: Dict[str, Any]) -> Dict[str, Any]: + def _clean_row_data(self, row_data: dict[str, Any]) -> dict[str, Any]: """Process a single row with data normalization.""" # Handle NaN values processed_data = {k: (v if pd.notna(v) else None) for k, v in row_data.items()} @@ -188,7 +188,7 @@ class CSVProcessor: async def process_marketplace_csv_from_url( self, url: str, marketplace: str, vendor_name: str, batch_size: int, db: Session - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Process CSV from URL with marketplace and vendor information. @@ -245,7 +245,7 @@ class CSVProcessor: vendor_name: str, db: Session, batch_num: int, - ) -> Dict[str, int]: + ) -> dict[str, int]: """Process a batch of CSV rows with marketplace information.""" imported = 0 updated = 0 @@ -295,7 +295,7 @@ class CSVProcessor: existing_product, key ): setattr(existing_product, key, value) - existing_product.updated_at = datetime.now(timezone.utc) + existing_product.updated_at = datetime.now(UTC) updated += 1 logger.debug( f"Updated product {product_data['marketplace_product_id']} for " diff --git a/app/utils/data_processing.py b/app/utils/data_processing.py index 85168a39..a5508f1a 100644 --- a/app/utils/data_processing.py +++ b/app/utils/data_processing.py @@ -9,7 +9,6 @@ This module provides classes and functions for: import logging import re -from typing import Optional, Tuple import pandas as pd @@ -21,7 +20,7 @@ class GTINProcessor: VALID_LENGTHS = [8, 12, 13, 14] # List of valid GTIN lengths - def normalize(self, gtin_value: any) -> Optional[str]: + def normalize(self, gtin_value: any) -> str | None: """ Normalize GTIN to proper format. @@ -55,11 +54,11 @@ class GTINProcessor: # Standard lengths - pad appropriately if length == 8: return gtin_clean.zfill(8) # EAN-8 - elif length == 12: + if length == 12: return gtin_clean.zfill(12) # UPC-A - elif length == 13: + if length == 13: return gtin_clean.zfill(13) # EAN-13 - elif length == 14: + if length == 14: return gtin_clean.zfill(14) # GTIN-14 elif length > 14: @@ -111,7 +110,7 @@ class PriceProcessor: def parse_price_currency( self, price_str: any - ) -> Tuple[Optional[str], Optional[str]]: + ) -> tuple[str | None, str | None]: """ Parse a price string to extract the numeric value and currency. diff --git a/app/utils/database.py b/app/utils/database.py index 72fa3e58..1a30b71e 100644 --- a/app/utils/database.py +++ b/app/utils/database.py @@ -40,7 +40,7 @@ def get_db_engine(database_url: str): echo=False, ) - logger.info(f"Database engine created for: " f"{database_url.split('@')[0]}@...") + logger.info(f"Database engine created for: {database_url.split('@')[0]}@...") return engine diff --git a/main.py b/main.py index b1af97ab..8545b5f9 100644 --- a/main.py +++ b/main.py @@ -18,7 +18,7 @@ if sys.platform == "win32": sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from fastapi import Depends, FastAPI, HTTPException, Request, Response @@ -35,11 +35,13 @@ from app.core.database import get_db from app.core.lifespan import lifespan from app.exceptions import ServiceUnavailableException from app.exceptions.handler import setup_exception_handlers + # Import page routers from app.routes import admin_pages, shop_pages, vendor_pages from middleware.context import ContextMiddleware from middleware.logging import LoggingMiddleware from middleware.theme_context import ThemeContextMiddleware + # Import REFACTORED class-based middleware from middleware.vendor_context import VendorContextMiddleware @@ -259,9 +261,8 @@ async def vendor_root_path( return templates.TemplateResponse( template_path, get_shop_context(request, db=db, page=landing_page) ) - else: - # No landing page - redirect to shop - return RedirectResponse(url=f"/vendors/{vendor_code}/shop/", status_code=302) + # No landing page - redirect to shop + return RedirectResponse(url=f"/vendors/{vendor_code}/shop/", status_code=302) # ============================================================================ @@ -317,18 +318,17 @@ async def platform_homepage(request: Request, db: Session = Depends(get_db)): "footer_pages": footer_pages, }, ) - else: - # Fallback to default static template - logger.info("[PLATFORM] No CMS homepage found, using default template") + # Fallback to default static template + logger.info("[PLATFORM] No CMS homepage found, using default template") - return templates.TemplateResponse( - "platform/homepage-default.html", - { - "request": request, - "header_pages": header_pages, - "footer_pages": footer_pages, - }, - ) + return templates.TemplateResponse( + "platform/homepage-default.html", + { + "request": request, + "header_pages": header_pages, + "footer_pages": footer_pages, + }, + ) @app.get("/{slug}", response_class=HTMLResponse, include_in_schema=False) @@ -349,7 +349,10 @@ async def platform_content_page( # Load page from CMS page = content_page_service.get_page_for_vendor( - db, slug=slug, vendor_id=None, include_unpublished=False # Platform pages only + db, + slug=slug, + vendor_id=None, + include_unpublished=False, # Platform pages only ) if not page: @@ -430,30 +433,27 @@ async def root(request: Request, db: Session = Depends(get_db)): return templates.TemplateResponse( template_path, get_shop_context(request, db=db, page=landing_page) ) - else: - # No landing page - redirect to shop - vendor_context = getattr(request.state, "vendor_context", None) - access_method = ( - vendor_context.get("detection_method", "unknown") - if vendor_context - else "unknown" - ) + # No landing page - redirect to shop + vendor_context = getattr(request.state, "vendor_context", None) + access_method = ( + vendor_context.get("detection_method", "unknown") + if vendor_context + else "unknown" + ) - if access_method == "path": - full_prefix = ( - vendor_context.get("full_prefix", "/vendor/") - if vendor_context - else "/vendor/" - ) - return RedirectResponse( - url=f"{full_prefix}{vendor.subdomain}/shop/", status_code=302 - ) - else: - # Domain/subdomain - return RedirectResponse(url="/shop/", status_code=302) - else: - # No vendor - platform root - return RedirectResponse(url="/documentation") + if access_method == "path": + full_prefix = ( + vendor_context.get("full_prefix", "/vendor/") + if vendor_context + else "/vendor/" + ) + return RedirectResponse( + url=f"{full_prefix}{vendor.subdomain}/shop/", status_code=302 + ) + # Domain/subdomain + return RedirectResponse(url="/shop/", status_code=302) + # No vendor - platform root + return RedirectResponse(url="/documentation") @app.get("/health") @@ -464,7 +464,7 @@ def health_check(db: Session = Depends(get_db)): db.execute(text("SELECT 1")) return { "status": "healthy", - "timestamp": datetime.now(timezone.utc), + "timestamp": datetime.now(UTC), "message": f"{settings.project_name} v{settings.version}", "docs": { "swagger": "/docs", diff --git a/middleware/auth.py b/middleware/auth.py index d37207ce..521b573e 100644 --- a/middleware/auth.py +++ b/middleware/auth.py @@ -17,8 +17,9 @@ The module uses the following technologies: import logging import os -from datetime import datetime, timedelta, timezone -from typing import Any, Callable, Dict, Optional +from collections.abc import Callable +from datetime import UTC, datetime, timedelta +from typing import Any from fastapi import HTTPException from fastapi.security import HTTPAuthorizationCredentials @@ -26,10 +27,14 @@ from jose import jwt from passlib.context import CryptContext from sqlalchemy.orm import Session -from app.exceptions import (AdminRequiredException, - InsufficientPermissionsException, - InvalidCredentialsException, InvalidTokenException, - TokenExpiredException, UserNotActiveException) +from app.exceptions import ( + AdminRequiredException, + InsufficientPermissionsException, + InvalidCredentialsException, + InvalidTokenException, + TokenExpiredException, + UserNotActiveException, +) from models.database.user import User logger = logging.getLogger(__name__) @@ -97,7 +102,7 @@ class AuthManager: def authenticate_user( self, db: Session, username: str, password: str - ) -> Optional[User]: + ) -> User | None: """Authenticate user credentials against the database. Supports authentication using either username or email address. @@ -129,7 +134,7 @@ class AuthManager: # Authentication successful, return user object return user - def create_access_token(self, user: User) -> Dict[str, Any]: + def create_access_token(self, user: User) -> dict[str, Any]: """Create a JWT access token for an authenticated user. The token includes user identity and role information in the payload. @@ -146,7 +151,7 @@ class AuthManager: """ # Calculate token expiration time expires_delta = timedelta(minutes=self.token_expire_minutes) - expire = datetime.now(timezone.utc) + expires_delta + expire = datetime.now(UTC) + expires_delta # Build JWT payload with user information payload = { @@ -155,7 +160,7 @@ class AuthManager: "email": user.email, # User email address "role": user.role, # User role for authorization "exp": expire, # Expiration time (JWT standard claim) - "iat": datetime.now(timezone.utc), # Issued at time (JWT standard claim) + "iat": datetime.now(UTC), # Issued at time (JWT standard claim) } # Encode the payload into a JWT token @@ -168,7 +173,7 @@ class AuthManager: "expires_in": self.token_expire_minutes * 60, # Convert minutes to seconds } - def verify_token(self, token: str) -> Dict[str, Any]: + def verify_token(self, token: str) -> dict[str, Any]: """Verify and decode a JWT token, returning the user data. Validates the token signature, expiration, and required claims. @@ -199,8 +204,8 @@ class AuthManager: raise InvalidTokenException("Token missing expiration") # Check if token has expired (additional check beyond jwt.decode) - if datetime.now(timezone.utc) > datetime.fromtimestamp( - exp, tz=timezone.utc + if datetime.now(UTC) > datetime.fromtimestamp( + exp, tz=UTC ): raise TokenExpiredException() diff --git a/middleware/context.py b/middleware/context.py index 00b88fbd..ff8252e5 100644 --- a/middleware/context.py +++ b/middleware/context.py @@ -70,7 +70,7 @@ class ContextManager: host = host.split(":")[0] logger.debug( - f"[CONTEXT] Detecting context", + "[CONTEXT] Detecting context", extra={ "original_path": request.url.path, "clean_path": getattr(request.state, "clean_path", "NOT SET"), diff --git a/middleware/logging.py b/middleware/logging.py index ae2d1ecd..9319f2a4 100644 --- a/middleware/logging.py +++ b/middleware/logging.py @@ -9,7 +9,7 @@ This module provides classes and functions for: import logging import time -from typing import Callable +from collections.abc import Callable from fastapi import Request, Response from starlette.middleware.base import BaseHTTPMiddleware diff --git a/middleware/rate_limiter.py b/middleware/rate_limiter.py index f9261885..f7dc00c4 100644 --- a/middleware/rate_limiter.py +++ b/middleware/rate_limiter.py @@ -9,8 +9,7 @@ This module provides classes and functions for: import logging from collections import defaultdict, deque -from datetime import datetime, timedelta, timezone -from typing import Dict +from datetime import UTC, datetime, timedelta logger = logging.getLogger(__name__) @@ -21,9 +20,9 @@ class RateLimiter: def __init__(self): """Class constructor.""" # Dictionary to store request timestamps for each client - self.clients: Dict[str, deque] = defaultdict(lambda: deque()) + self.clients: dict[str, deque] = defaultdict(lambda: deque()) self.cleanup_interval = 3600 # Clean up old entries every hour - self.last_cleanup = datetime.now(timezone.utc) + self.last_cleanup = datetime.now(UTC) def allow_request( self, client_id: str, max_requests: int, window_seconds: int @@ -33,7 +32,7 @@ class RateLimiter: Uses sliding window algorithm """ - now = datetime.now(timezone.utc) + now = datetime.now(UTC) window_start = now - timedelta(seconds=window_seconds) # Clean up old entries periodically @@ -60,7 +59,7 @@ class RateLimiter: def _cleanup_old_entries(self): """Clean up old entries to prevent memory leaks.""" - cutoff_time = datetime.now(timezone.utc) - timedelta(hours=24) + cutoff_time = datetime.now(UTC) - timedelta(hours=24) clients_to_remove = [] for client_id, requests in self.clients.items(): @@ -80,11 +79,11 @@ class RateLimiter: f"Rate limiter cleanup completed. Removed {len(clients_to_remove)} inactive clients" ) - def get_client_stats(self, client_id: str) -> Dict[str, int]: + def get_client_stats(self, client_id: str) -> dict[str, int]: """Get statistics for a specific client.""" client_requests = self.clients.get(client_id, deque()) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) hour_ago = now - timedelta(hours=1) day_ago = now - timedelta(days=1) diff --git a/middleware/theme_context.py b/middleware/theme_context.py index e89e5117..e317c293 100644 --- a/middleware/theme_context.py +++ b/middleware/theme_context.py @@ -114,7 +114,7 @@ class ThemeContextMiddleware(BaseHTTPMiddleware): request.state.theme = theme logger.debug( - f"[THEME] Theme loaded for vendor", + "[THEME] Theme loaded for vendor", extra={ "vendor_id": vendor.id, "vendor_name": vendor.name, diff --git a/middleware/vendor_context.py b/middleware/vendor_context.py index 982afee6..64690216 100644 --- a/middleware/vendor_context.py +++ b/middleware/vendor_context.py @@ -12,7 +12,6 @@ Also extracts clean_path for nested routing patterns. """ import logging -from typing import Optional from fastapi import Request from sqlalchemy import func @@ -31,7 +30,7 @@ class VendorContextManager: """Manages vendor context detection for multi-tenant routing.""" @staticmethod - def detect_vendor_context(request: Request) -> Optional[dict]: + def detect_vendor_context(request: Request) -> dict | None: """ Detect vendor context from request. @@ -106,7 +105,7 @@ class VendorContextManager: return None @staticmethod - def get_vendor_from_context(db: Session, context: dict) -> Optional[Vendor]: + def get_vendor_from_context(db: Session, context: dict) -> Vendor | None: """ Get vendor from database using context information. @@ -142,11 +141,10 @@ class VendorContextManager: f"[OK] Vendor found via custom domain: {domain} → {vendor.name}" ) return vendor - else: - logger.warning( - f"No active vendor found for custom domain: {domain}" - ) - return None + logger.warning( + f"No active vendor found for custom domain: {domain}" + ) + return None # Method 2 & 3: Subdomain or path-based lookup if "subdomain" in context: @@ -169,7 +167,7 @@ class VendorContextManager: return vendor @staticmethod - def extract_clean_path(request: Request, vendor_context: Optional[dict]) -> str: + def extract_clean_path(request: Request, vendor_context: dict | None) -> str: """ Extract clean path without vendor prefix for routing. @@ -217,7 +215,7 @@ class VendorContextManager: return request.url.path.startswith("/api/v1/shop/") @staticmethod - def extract_vendor_from_referer(request: Request) -> Optional[dict]: + def extract_vendor_from_referer(request: Request) -> dict | None: """ Extract vendor context from Referer header. @@ -250,7 +248,7 @@ class VendorContextManager: referer_host = referer_host.split(":")[0] logger.debug( - f"[VENDOR] Extracting vendor from Referer", + "[VENDOR] Extracting vendor from Referer", extra={ "referer": referer, "referer_host": referer_host, @@ -449,7 +447,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware): request.state.clean_path = request.url.path logger.debug( - f"[VENDOR_CONTEXT] Vendor detected from Referer for shop API", + "[VENDOR_CONTEXT] Vendor detected from Referer for shop API", extra={ "vendor_id": vendor.id, "vendor_name": vendor.name, @@ -463,7 +461,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware): ) else: logger.warning( - f"[WARNING] Vendor context from Referer but vendor not found", + "[WARNING] Vendor context from Referer but vendor not found", extra={ "context": vendor_context, "detection_method": vendor_context.get( @@ -479,7 +477,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware): db.close() else: logger.warning( - f"[VENDOR] Shop API request without Referer header", + "[VENDOR] Shop API request without Referer header", extra={"path": request.url.path}, ) request.state.vendor = None @@ -518,7 +516,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware): ) logger.debug( - f"[VENDOR_CONTEXT] Vendor detected", + "[VENDOR_CONTEXT] Vendor detected", extra={ "vendor_id": vendor.id, "vendor_name": vendor.name, @@ -530,7 +528,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware): ) else: logger.warning( - f"[WARNING] Vendor context detected but vendor not found", + "[WARNING] Vendor context detected but vendor not found", extra={ "context": vendor_context, "detection_method": vendor_context.get("detection_method"), @@ -543,7 +541,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware): db.close() else: logger.debug( - f"[VENDOR] No vendor context detected", + "[VENDOR] No vendor context detected", extra={ "path": request.url.path, "host": request.headers.get("host", ""), @@ -557,7 +555,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware): return await call_next(request) -def get_current_vendor(request: Request) -> Optional[Vendor]: +def get_current_vendor(request: Request) -> Vendor | None: """Helper function to get current vendor from request state.""" return getattr(request.state, "vendor", None) diff --git a/models/__init__.py b/models/__init__.py index ed80aa1c..85fac465 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -3,6 +3,7 @@ # API models (Pydantic) - import the modules, not all classes from . import schema + # Database models (SQLAlchemy) from .database.base import Base from .database.inventory import Inventory diff --git a/models/database/__init__.py b/models/database/__init__.py index 374c8e70..29682ce1 100644 --- a/models/database/__init__.py +++ b/models/database/__init__.py @@ -1,7 +1,13 @@ # models/database/__init__.py """Database models package.""" -from .admin import (AdminAuditLog, AdminNotification, AdminSession, - AdminSetting, PlatformAlert) + +from .admin import ( + AdminAuditLog, + AdminNotification, + AdminSession, + AdminSetting, + PlatformAlert, +) from .base import Base from .customer import Customer, CustomerAddress from .inventory import Inventory diff --git a/models/database/admin.py b/models/database/admin.py index 45c04cee..e1858a1a 100644 --- a/models/database/admin.py +++ b/models/database/admin.py @@ -10,8 +10,16 @@ This module provides models for: - Platform alerts (system-wide issues) """ -from sqlalchemy import (JSON, Boolean, Column, DateTime, ForeignKey, Integer, - String, Text) +from sqlalchemy import ( + JSON, + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + String, + Text, +) from sqlalchemy.orm import relationship from app.core.database import Base diff --git a/models/database/base.py b/models/database/base.py index 052c3c31..e48bfd1e 100644 --- a/models/database/base.py +++ b/models/database/base.py @@ -1,17 +1,15 @@ -from datetime import datetime, timezone +from datetime import UTC, 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.now(timezone.utc), nullable=False) + created_at = Column(DateTime, default=datetime.now(UTC), nullable=False) updated_at = Column( DateTime, - default=datetime.now(timezone.utc), - onupdate=datetime.now(timezone.utc), + default=datetime.now(UTC), + onupdate=datetime.now(UTC), nullable=False, ) diff --git a/models/database/cart.py b/models/database/cart.py index c1763eb5..ee538243 100644 --- a/models/database/cart.py +++ b/models/database/cart.py @@ -1,9 +1,16 @@ # models/database/cart.py """Cart item database model.""" -from datetime import datetime -from sqlalchemy import (Column, Float, ForeignKey, Index, Integer, String, - UniqueConstraint) + +from sqlalchemy import ( + Column, + Float, + ForeignKey, + Index, + Integer, + String, + UniqueConstraint, +) from sqlalchemy.orm import relationship from app.core.database import Base diff --git a/models/database/content_page.py b/models/database/content_page.py index 32223bf0..321f058e 100644 --- a/models/database/content_page.py +++ b/models/database/content_page.py @@ -14,10 +14,19 @@ Features: - Version history support """ -from datetime import datetime, timezone +from datetime import UTC, datetime -from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Index, Integer, - String, Text, UniqueConstraint) +from sqlalchemy import ( + Boolean, + Column, + DateTime, + ForeignKey, + Index, + Integer, + String, + Text, + UniqueConstraint, +) from sqlalchemy.orm import relationship from app.core.database import Base @@ -77,13 +86,13 @@ class ContentPage(Base): # Timestamps created_at = Column( DateTime(timezone=True), - default=lambda: datetime.now(timezone.utc), + default=lambda: datetime.now(UTC), nullable=False, ) updated_at = Column( DateTime(timezone=True), - default=lambda: datetime.now(timezone.utc), - onupdate=lambda: datetime.now(timezone.utc), + default=lambda: datetime.now(UTC), + onupdate=lambda: datetime.now(UTC), nullable=False, ) diff --git a/models/database/customer.py b/models/database/customer.py index 8972f806..f8c947c8 100644 --- a/models/database/customer.py +++ b/models/database/customer.py @@ -1,8 +1,14 @@ -from datetime import datetime -from decimal import Decimal -from sqlalchemy import (JSON, Boolean, Column, DateTime, ForeignKey, Integer, - Numeric, String, Text) +from sqlalchemy import ( + JSON, + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + Numeric, + String, +) from sqlalchemy.orm import relationship from app.core.database import Base diff --git a/models/database/inventory.py b/models/database/inventory.py index 9318c0d8..5d06d946 100644 --- a/models/database/inventory.py +++ b/models/database/inventory.py @@ -1,8 +1,6 @@ # models/database/inventory.py -from datetime import datetime -from sqlalchemy import (Column, ForeignKey, Index, Integer, String, - UniqueConstraint) +from sqlalchemy import Column, ForeignKey, Index, Integer, String, UniqueConstraint from sqlalchemy.orm import relationship from app.core.database import Base diff --git a/models/database/marketplace_import_job.py b/models/database/marketplace_import_job.py index c6587d21..85dedd69 100644 --- a/models/database/marketplace_import_job.py +++ b/models/database/marketplace_import_job.py @@ -1,7 +1,5 @@ -from datetime import datetime, timezone -from sqlalchemy import (Column, DateTime, ForeignKey, Index, Integer, String, - Text) +from sqlalchemy import Column, DateTime, ForeignKey, Index, Integer, String, Text from sqlalchemy.orm import relationship from app.core.database import Base diff --git a/models/database/marketplace_product.py b/models/database/marketplace_product.py index e248c81d..3dbdf9ab 100644 --- a/models/database/marketplace_product.py +++ b/models/database/marketplace_product.py @@ -1,7 +1,10 @@ -from datetime import datetime, timezone -from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index, - Integer, String, Text, UniqueConstraint) +from sqlalchemy import ( + Column, + Index, + Integer, + String, +) from sqlalchemy.orm import relationship # Import Base from the central database module instead of creating a new one diff --git a/models/database/order.py b/models/database/order.py index a62febdb..b4550e5e 100644 --- a/models/database/order.py +++ b/models/database/order.py @@ -1,8 +1,15 @@ # models/database/order.py -from datetime import datetime -from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, - String, Text) +from sqlalchemy import ( + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Integer, + String, + Text, +) from sqlalchemy.orm import relationship from app.core.database import Base diff --git a/models/database/product.py b/models/database/product.py index 269a6885..ba1ca9d2 100644 --- a/models/database/product.py +++ b/models/database/product.py @@ -1,8 +1,15 @@ # models/database/product.py -from datetime import datetime -from sqlalchemy import (Boolean, Column, Float, ForeignKey, Index, Integer, - String, UniqueConstraint) +from sqlalchemy import ( + Boolean, + Column, + Float, + ForeignKey, + Index, + Integer, + String, + UniqueConstraint, +) from sqlalchemy.orm import relationship from app.core.database import Base diff --git a/models/database/user.py b/models/database/user.py index dcce9329..14478d8c 100644 --- a/models/database/user.py +++ b/models/database/user.py @@ -10,9 +10,10 @@ ROLE CLARIFICATION: - Vendor-specific roles (manager, staff, etc.) are stored in VendorUser.role - Customers are NOT in the User table - they use the Customer model """ + import enum -from sqlalchemy import Boolean, Column, DateTime, Enum, Integer, String +from sqlalchemy import Boolean, Column, DateTime, Integer, String from sqlalchemy.orm import relationship from app.core.database import Base diff --git a/models/database/vendor.py b/models/database/vendor.py index c962e852..da36ca91 100644 --- a/models/database/vendor.py +++ b/models/database/vendor.py @@ -5,13 +5,23 @@ Vendor model representing entities that sell products or services. This module defines the Vendor model along with its relationships to other models such as User (owner), Product, Customer, Order, and MarketplaceImportJob. """ + import enum -from sqlalchemy import (JSON, Boolean, Column, DateTime, ForeignKey, Integer, - String, Text) +from sqlalchemy import ( + JSON, + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + String, + Text, +) from sqlalchemy.orm import relationship from app.core.config import settings + # Import Base from the central database module instead of creating a new one from app.core.database import Base from models.database.base import TimestampMixin diff --git a/models/database/vendor_domain.py b/models/database/vendor_domain.py index e22512e1..619416df 100644 --- a/models/database/vendor_domain.py +++ b/models/database/vendor_domain.py @@ -2,10 +2,18 @@ """ Vendor Domain Model - Maps custom domains to vendors """ -from datetime import datetime, timezone -from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Index, Integer, - String, UniqueConstraint) + +from sqlalchemy import ( + Boolean, + Column, + DateTime, + ForeignKey, + Index, + Integer, + String, + UniqueConstraint, +) from sqlalchemy.orm import relationship from app.core.database import Base diff --git a/models/database/vendor_theme.py b/models/database/vendor_theme.py index fcb83cb4..1a0dea49 100644 --- a/models/database/vendor_theme.py +++ b/models/database/vendor_theme.py @@ -3,6 +3,7 @@ Vendor Theme Configuration Model Allows each vendor to customize their shop's appearance """ + from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String, Text from sqlalchemy.orm import relationship diff --git a/models/schema/__init__.py b/models/schema/__init__.py index 9358039d..0c1ecc7d 100644 --- a/models/schema/__init__.py +++ b/models/schema/__init__.py @@ -2,8 +2,16 @@ """API models package - Pydantic models for request/response validation.""" # Import API model modules -from . import (auth, base, inventory, marketplace_import_job, - marketplace_product, stats, vendor) +from . import ( + auth, + base, + inventory, + marketplace_import_job, + marketplace_product, + stats, + vendor, +) + # Common imports for convenience from .base import * # Base Pydantic models diff --git a/models/schema/admin.py b/models/schema/admin.py index 03a533b2..9c865426 100644 --- a/models/schema/admin.py +++ b/models/schema/admin.py @@ -12,7 +12,7 @@ This module provides schemas for: """ from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any from pydantic import BaseModel, Field, field_validator @@ -26,14 +26,14 @@ class AdminAuditLogResponse(BaseModel): id: int admin_user_id: int - admin_username: Optional[str] = None + admin_username: str | None = None action: str target_type: str target_id: str - details: Optional[Dict[str, Any]] = None - ip_address: Optional[str] = None - user_agent: Optional[str] = None - request_id: Optional[str] = None + details: dict[str, Any] | None = None + ip_address: str | None = None + user_agent: str | None = None + request_id: str | None = None created_at: datetime model_config = {"from_attributes": True} @@ -42,11 +42,11 @@ 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 - date_from: Optional[datetime] = None - date_to: Optional[datetime] = None + admin_user_id: int | None = None + action: str | None = None + target_type: str | None = None + date_from: datetime | None = None + date_to: datetime | None = None skip: int = Field(0, ge=0) limit: int = Field(100, ge=1, le=1000) @@ -54,7 +54,7 @@ class AdminAuditLogFilters(BaseModel): class AdminAuditLogListResponse(BaseModel): """Paginated list of audit logs.""" - logs: List[AdminAuditLogResponse] + logs: list[AdminAuditLogResponse] total: int skip: int limit: int @@ -73,8 +73,8 @@ class AdminNotificationCreate(BaseModel): title: str = Field(..., max_length=200) message: str = Field(..., description="Notification message") action_required: bool = Field(default=False) - action_url: Optional[str] = Field(None, max_length=500) - metadata: Optional[Dict[str, Any]] = None + action_url: str | None = Field(None, max_length=500) + metadata: dict[str, Any] | None = None @field_validator("priority") @classmethod @@ -94,11 +94,11 @@ class AdminNotificationResponse(BaseModel): title: str message: str is_read: bool - read_at: Optional[datetime] = None - read_by_user_id: Optional[int] = None + read_at: datetime | None = None + read_by_user_id: int | None = None action_required: bool - action_url: Optional[str] = None - metadata: Optional[Dict[str, Any]] = None + action_url: str | None = None + metadata: dict[str, Any] | None = None created_at: datetime model_config = {"from_attributes": True} @@ -113,7 +113,7 @@ class AdminNotificationUpdate(BaseModel): class AdminNotificationListResponse(BaseModel): """Paginated list of notifications.""" - notifications: List[AdminNotificationResponse] + notifications: list[AdminNotificationResponse] total: int unread_count: int skip: int @@ -131,8 +131,8 @@ class AdminSettingCreate(BaseModel): 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") - category: Optional[str] = Field(None, max_length=50) - description: Optional[str] = None + category: str | None = Field(None, max_length=50) + description: str | None = None is_encrypted: bool = Field(default=False) is_public: bool = Field(default=False, description="Can be exposed to frontend") @@ -162,11 +162,11 @@ class AdminSettingResponse(BaseModel): key: str value: str value_type: str - category: Optional[str] = None - description: Optional[str] = None + category: str | None = None + description: str | None = None is_encrypted: bool is_public: bool - last_modified_by_user_id: Optional[int] = None + last_modified_by_user_id: int | None = None updated_at: datetime model_config = {"from_attributes": True} @@ -176,15 +176,15 @@ class AdminSettingUpdate(BaseModel): """Update admin setting value.""" value: str - description: Optional[str] = None + description: str | None = None class AdminSettingListResponse(BaseModel): """List of settings by category.""" - settings: List[AdminSettingResponse] + settings: list[AdminSettingResponse] total: int - category: Optional[str] = None + category: str | None = None # ============================================================================ @@ -198,9 +198,9 @@ class PlatformAlertCreate(BaseModel): alert_type: str = Field(..., max_length=50) severity: str = Field(..., description="Alert severity") title: str = Field(..., max_length=200) - description: Optional[str] = None - affected_vendors: Optional[List[int]] = None - affected_systems: Optional[List[str]] = None + description: str | None = None + affected_vendors: list[int] | None = None + affected_systems: list[str] | None = None auto_generated: bool = Field(default=True) @field_validator("severity") @@ -234,13 +234,13 @@ class PlatformAlertResponse(BaseModel): alert_type: str severity: str title: str - description: Optional[str] = None - affected_vendors: Optional[List[int]] = None - affected_systems: Optional[List[str]] = None + description: str | None = None + affected_vendors: list[int] | None = None + affected_systems: list[str] | None = None is_resolved: bool - resolved_at: Optional[datetime] = None - resolved_by_user_id: Optional[int] = None - resolution_notes: Optional[str] = None + resolved_at: datetime | None = None + resolved_by_user_id: int | None = None + resolution_notes: str | None = None auto_generated: bool occurrence_count: int first_occurred_at: datetime @@ -254,13 +254,13 @@ class PlatformAlertResolve(BaseModel): """Resolve platform alert.""" is_resolved: bool = True - resolution_notes: Optional[str] = None + resolution_notes: str | None = None class PlatformAlertListResponse(BaseModel): """Paginated list of platform alerts.""" - alerts: List[PlatformAlertResponse] + alerts: list[PlatformAlertResponse] total: int active_count: int critical_count: int @@ -276,10 +276,10 @@ class PlatformAlertListResponse(BaseModel): class BulkVendorAction(BaseModel): """Bulk actions on vendors.""" - vendor_ids: List[int] = Field(..., min_length=1, max_length=100) + 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") + reason: str | None = Field(None, description="Reason for bulk action") @field_validator("action") @classmethod @@ -293,8 +293,8 @@ class BulkVendorAction(BaseModel): class BulkVendorActionResponse(BaseModel): """Response for bulk vendor actions.""" - successful: List[int] - failed: Dict[int, str] # vendor_id -> error_message + successful: list[int] + failed: dict[int, str] # vendor_id -> error_message total_processed: int action_performed: str message: str @@ -303,10 +303,10 @@ class BulkVendorActionResponse(BaseModel): class BulkUserAction(BaseModel): """Bulk actions on users.""" - user_ids: List[int] = Field(..., min_length=1, max_length=100) + 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 + reason: str | None = None @field_validator("action") @classmethod @@ -320,8 +320,8 @@ class BulkUserAction(BaseModel): class BulkUserActionResponse(BaseModel): """Response for bulk user actions.""" - successful: List[int] - failed: Dict[int, str] + successful: list[int] + failed: dict[int, str] total_processed: int action_performed: str message: str @@ -335,14 +335,14 @@ class BulkUserActionResponse(BaseModel): class AdminDashboardStats(BaseModel): """Comprehensive admin dashboard statistics.""" - platform: Dict[str, Any] - users: Dict[str, Any] - vendors: Dict[str, Any] - products: Dict[str, Any] - orders: Dict[str, Any] - imports: Dict[str, Any] - recent_vendors: List[Dict[str, Any]] - recent_imports: List[Dict[str, Any]] + platform: dict[str, Any] + users: dict[str, Any] + vendors: dict[str, Any] + products: dict[str, Any] + orders: dict[str, Any] + imports: dict[str, Any] + recent_vendors: list[dict[str, Any]] + recent_imports: list[dict[str, Any]] unread_notifications: int active_alerts: int critical_alerts: int @@ -357,10 +357,10 @@ 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 + response_time_ms: float | None = None + error_message: str | None = None last_checked: datetime - details: Optional[Dict[str, Any]] = None + details: dict[str, Any] | None = None class SystemHealthResponse(BaseModel): @@ -386,14 +386,14 @@ class AdminSessionResponse(BaseModel): id: int admin_user_id: int - admin_username: Optional[str] = None + admin_username: str | None = None ip_address: str - user_agent: Optional[str] = None + user_agent: str | None = None login_at: datetime last_activity_at: datetime - logout_at: Optional[datetime] = None + logout_at: datetime | None = None is_active: bool - logout_reason: Optional[str] = None + logout_reason: str | None = None model_config = {"from_attributes": True} @@ -401,6 +401,6 @@ class AdminSessionResponse(BaseModel): class AdminSessionListResponse(BaseModel): """List of admin sessions.""" - sessions: List[AdminSessionResponse] + sessions: list[AdminSessionResponse] total: int active_count: int diff --git a/models/schema/auth.py b/models/schema/auth.py index 5d57aedc..16d16045 100644 --- a/models/schema/auth.py +++ b/models/schema/auth.py @@ -1,7 +1,6 @@ # auth.py - Keep security-critical validation import re from datetime import datetime -from typing import Optional from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator @@ -33,7 +32,7 @@ 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( + vendor_code: str | None = Field( None, description="Optional vendor code for context" ) @@ -50,7 +49,7 @@ class UserResponse(BaseModel): username: str role: str is_active: bool - last_login: Optional[datetime] = None + last_login: datetime | None = None created_at: datetime updated_at: datetime diff --git a/models/schema/base.py b/models/schema/base.py index 122a1a11..91b50d05 100644 --- a/models/schema/base.py +++ b/models/schema/base.py @@ -1,4 +1,4 @@ -from typing import Generic, List, TypeVar +from typing import Generic, TypeVar from pydantic import BaseModel @@ -8,7 +8,7 @@ T = TypeVar("T") class ListResponse(BaseModel, Generic[T]): """Generic list response model""" - items: List[T] + items: list[T] total: int skip: int limit: int diff --git a/models/schema/cart.py b/models/schema/cart.py index a5c705b7..8a97b36b 100644 --- a/models/schema/cart.py +++ b/models/schema/cart.py @@ -3,8 +3,6 @@ Pydantic schemas for shopping cart operations. """ -from datetime import datetime -from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field @@ -43,7 +41,7 @@ class CartItemResponse(BaseModel): line_total: float = Field( ..., description="Total price for this line (price * quantity)" ) - image_url: Optional[str] = Field(None, description="Product image URL") + image_url: str | None = Field(None, description="Product image URL") class CartResponse(BaseModel): @@ -51,7 +49,7 @@ class CartResponse(BaseModel): vendor_id: int = Field(..., description="Vendor ID") session_id: str = Field(..., description="Shopping session ID") - items: List[CartItemResponse] = Field( + items: list[CartItemResponse] = Field( default_factory=list, description="Cart items" ) subtotal: float = Field(..., description="Subtotal of all items") @@ -82,7 +80,7 @@ class CartOperationResponse(BaseModel): message: str = Field(..., description="Operation result message") product_id: int = Field(..., description="Product ID affected") - quantity: Optional[int] = Field( + quantity: int | None = Field( None, description="New quantity (for add/update operations)" ) diff --git a/models/schema/customer.py b/models/schema/customer.py index 9c25ac28..ad788d84 100644 --- a/models/schema/customer.py +++ b/models/schema/customer.py @@ -5,7 +5,6 @@ Pydantic schema for customer-related operations. from datetime import datetime from decimal import Decimal -from typing import Any, Dict, List, Optional from pydantic import BaseModel, EmailStr, Field, field_validator @@ -23,7 +22,7 @@ class CustomerRegister(BaseModel): ) 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) + phone: str | None = Field(None, max_length=50) marketing_consent: bool = Field(default=False) @field_validator("email") @@ -48,15 +47,15 @@ class CustomerRegister(BaseModel): class CustomerUpdate(BaseModel): """Schema for updating customer profile.""" - email: Optional[EmailStr] = None - first_name: Optional[str] = Field(None, min_length=1, max_length=100) - last_name: Optional[str] = Field(None, min_length=1, max_length=100) - phone: Optional[str] = Field(None, max_length=50) - marketing_consent: Optional[bool] = None + email: EmailStr | None = None + first_name: str | None = Field(None, min_length=1, max_length=100) + last_name: str | None = Field(None, min_length=1, max_length=100) + phone: str | None = Field(None, max_length=50) + marketing_consent: bool | None = None @field_validator("email") @classmethod - def email_lowercase(cls, v: Optional[str]) -> Optional[str]: + def email_lowercase(cls, v: str | None) -> str | None: """Convert email to lowercase.""" return v.lower() if v else None @@ -72,12 +71,12 @@ class CustomerResponse(BaseModel): id: int vendor_id: int email: str - first_name: Optional[str] - last_name: Optional[str] - phone: Optional[str] + first_name: str | None + last_name: str | None + phone: str | None customer_number: str marketing_consent: bool - last_order_date: Optional[datetime] + last_order_date: datetime | None total_orders: int total_spent: Decimal is_active: bool @@ -97,7 +96,7 @@ class CustomerResponse(BaseModel): class CustomerListResponse(BaseModel): """Schema for paginated customer list.""" - customers: List[CustomerResponse] + customers: list[CustomerResponse] total: int page: int per_page: int @@ -115,9 +114,9 @@ class CustomerAddressCreate(BaseModel): address_type: str = Field(..., pattern="^(billing|shipping)$") 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) + company: str | None = Field(None, max_length=200) address_line_1: str = Field(..., min_length=1, max_length=255) - address_line_2: Optional[str] = Field(None, max_length=255) + address_line_2: str | None = Field(None, max_length=255) city: str = Field(..., min_length=1, max_length=100) postal_code: str = Field(..., min_length=1, max_length=20) country: str = Field(..., min_length=2, max_length=100) @@ -127,16 +126,16 @@ class CustomerAddressCreate(BaseModel): class CustomerAddressUpdate(BaseModel): """Schema for updating customer address.""" - address_type: Optional[str] = Field(None, pattern="^(billing|shipping)$") - first_name: Optional[str] = Field(None, min_length=1, max_length=100) - last_name: Optional[str] = Field(None, min_length=1, max_length=100) - company: Optional[str] = Field(None, max_length=200) - address_line_1: Optional[str] = Field(None, min_length=1, max_length=255) - address_line_2: Optional[str] = Field(None, max_length=255) - city: Optional[str] = Field(None, min_length=1, max_length=100) - postal_code: Optional[str] = Field(None, min_length=1, max_length=20) - country: Optional[str] = Field(None, min_length=2, max_length=100) - is_default: Optional[bool] = None + address_type: str | None = Field(None, pattern="^(billing|shipping)$") + first_name: str | None = Field(None, min_length=1, max_length=100) + last_name: str | None = Field(None, min_length=1, max_length=100) + company: str | None = Field(None, max_length=200) + address_line_1: str | None = Field(None, min_length=1, max_length=255) + address_line_2: str | None = Field(None, max_length=255) + city: str | None = Field(None, min_length=1, max_length=100) + postal_code: str | None = Field(None, min_length=1, max_length=20) + country: str | None = Field(None, min_length=2, max_length=100) + is_default: bool | None = None class CustomerAddressResponse(BaseModel): @@ -148,9 +147,9 @@ class CustomerAddressResponse(BaseModel): address_type: str first_name: str last_name: str - company: Optional[str] + company: str | None address_line_1: str - address_line_2: Optional[str] + address_line_2: str | None city: str postal_code: str country: str @@ -169,7 +168,7 @@ class CustomerAddressResponse(BaseModel): class CustomerPreferencesUpdate(BaseModel): """Schema for updating customer preferences.""" - marketing_consent: Optional[bool] = None - language: Optional[str] = Field(None, max_length=10) - currency: Optional[str] = Field(None, max_length=3) - notification_preferences: Optional[Dict[str, bool]] = None + marketing_consent: bool | None = None + language: str | None = Field(None, max_length=10) + currency: str | None = Field(None, max_length=3) + notification_preferences: dict[str, bool] | None = None diff --git a/models/schema/inventory.py b/models/schema/inventory.py index 89b3b584..96f432be 100644 --- a/models/schema/inventory.py +++ b/models/schema/inventory.py @@ -1,6 +1,5 @@ # models/schema/inventory.py from datetime import datetime -from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field @@ -27,9 +26,9 @@ class InventoryAdjust(InventoryBase): 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 + quantity: int | None = Field(None, ge=0) + reserved_quantity: int | None = Field(None, ge=0) + location: str | None = None class InventoryReserve(BaseModel): @@ -49,7 +48,7 @@ class InventoryResponse(BaseModel): location: str quantity: int reserved_quantity: int - gtin: Optional[str] + gtin: str | None created_at: datetime updated_at: datetime @@ -70,16 +69,16 @@ class ProductInventorySummary(BaseModel): product_id: int vendor_id: int - product_sku: Optional[str] + product_sku: str | None product_title: str total_quantity: int total_reserved: int total_available: int - locations: List[InventoryLocationResponse] + locations: list[InventoryLocationResponse] class InventoryListResponse(BaseModel): - inventories: List[InventoryResponse] + inventories: list[InventoryResponse] total: int skip: int limit: int diff --git a/models/schema/marketplace_import_job.py b/models/schema/marketplace_import_job.py index ece440be..ec02ffd9 100644 --- a/models/schema/marketplace_import_job.py +++ b/models/schema/marketplace_import_job.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import Optional from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -12,7 +11,7 @@ class MarketplaceImportJobRequest(BaseModel): source_url: str = Field(..., description="URL to CSV file from marketplace") marketplace: str = Field(default="Letzshop", description="Marketplace name") - batch_size: Optional[int] = Field( + batch_size: int | None = Field( 1000, description="Processing batch size", ge=100, le=10000 ) @@ -50,12 +49,12 @@ class MarketplaceImportJobResponse(BaseModel): error_count: int = 0 # Error details - error_message: Optional[str] = None + error_message: str | None = None # Timestamps created_at: datetime - started_at: Optional[datetime] = None - completed_at: Optional[datetime] = None + started_at: datetime | None = None + completed_at: datetime | None = None class MarketplaceImportJobListResponse(BaseModel): @@ -71,8 +70,8 @@ class MarketplaceImportJobStatusUpdate(BaseModel): """Schema for updating import job status (internal use).""" status: str - imported_count: Optional[int] = None - updated_count: Optional[int] = None - error_count: Optional[int] = None - total_processed: Optional[int] = None - error_message: Optional[str] = None + imported_count: int | None = None + updated_count: int | None = None + error_count: int | None = None + total_processed: int | None = None + error_message: str | None = None diff --git a/models/schema/marketplace_product.py b/models/schema/marketplace_product.py index e459f1a3..cbaa69d0 100644 --- a/models/schema/marketplace_product.py +++ b/models/schema/marketplace_product.py @@ -1,6 +1,5 @@ # models/schema/marketplace_products.py - Simplified validation from datetime import datetime -from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field @@ -8,45 +7,45 @@ from models.schema.inventory import ProductInventorySummary class MarketplaceProductBase(BaseModel): - marketplace_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 - marketplace: Optional[str] = None - vendor_name: Optional[str] = None + marketplace_product_id: str | None = None + title: str | None = None + description: str | None = None + link: str | None = None + image_link: str | None = None + availability: str | None = None + price: str | None = None + brand: str | None = None + gtin: str | None = None + mpn: str | None = None + condition: str | None = None + adult: str | None = None + multipack: int | None = None + is_bundle: str | None = None + age_group: str | None = None + color: str | None = None + gender: str | None = None + material: str | None = None + pattern: str | None = None + size: str | None = None + size_type: str | None = None + size_system: str | None = None + item_group_id: str | None = None + google_product_category: str | None = None + product_type: str | None = None + custom_label_0: str | None = None + custom_label_1: str | None = None + custom_label_2: str | None = None + custom_label_3: str | None = None + custom_label_4: str | None = None + additional_image_link: str | None = None + sale_price: str | None = None + unit_pricing_measure: str | None = None + unit_pricing_base_measure: str | None = None + identifier_exists: str | None = None + shipping: str | None = None + currency: str | None = None + marketplace: str | None = None + vendor_name: str | None = None class MarketplaceProductCreate(MarketplaceProductBase): @@ -70,7 +69,7 @@ class MarketplaceProductResponse(MarketplaceProductBase): class MarketplaceProductListResponse(BaseModel): - products: List[MarketplaceProductResponse] + products: list[MarketplaceProductResponse] total: int skip: int limit: int @@ -78,4 +77,4 @@ class MarketplaceProductListResponse(BaseModel): class MarketplaceProductDetailResponse(BaseModel): product: MarketplaceProductResponse - inventory_info: Optional[ProductInventorySummary] = None + inventory_info: ProductInventorySummary | None = None diff --git a/models/schema/order.py b/models/schema/order.py index a5a1d371..f64db5b2 100644 --- a/models/schema/order.py +++ b/models/schema/order.py @@ -4,8 +4,6 @@ Pydantic schema for order operations. """ from datetime import datetime -from decimal import Decimal -from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field @@ -30,7 +28,7 @@ class OrderItemResponse(BaseModel): order_id: int product_id: int product_name: str - product_sku: Optional[str] + product_sku: str | None quantity: int unit_price: float total_price: float @@ -50,9 +48,9 @@ class OrderAddressCreate(BaseModel): 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) + company: str | None = Field(None, max_length=200) address_line_1: str = Field(..., min_length=1, max_length=255) - address_line_2: Optional[str] = Field(None, max_length=255) + address_line_2: str | None = Field(None, max_length=255) city: str = Field(..., min_length=1, max_length=100) postal_code: str = Field(..., min_length=1, max_length=20) country: str = Field(..., min_length=2, max_length=100) @@ -67,9 +65,9 @@ class OrderAddressResponse(BaseModel): address_type: str first_name: str last_name: str - company: Optional[str] + company: str | None address_line_1: str - address_line_2: Optional[str] + address_line_2: str | None city: str postal_code: str country: str @@ -83,29 +81,29 @@ class OrderAddressResponse(BaseModel): class OrderCreate(BaseModel): """Schema for creating an order.""" - customer_id: Optional[int] = None # Optional for guest checkout - items: List[OrderItemCreate] = Field(..., min_length=1) + customer_id: int | None = None # Optional for guest checkout + items: list[OrderItemCreate] = Field(..., min_length=1) # Addresses shipping_address: OrderAddressCreate - billing_address: Optional[OrderAddressCreate] = None # Use shipping if not provided + billing_address: OrderAddressCreate | None = None # Use shipping if not provided # Optional fields - shipping_method: Optional[str] = None - customer_notes: Optional[str] = Field(None, max_length=1000) + shipping_method: str | None = None + customer_notes: str | None = Field(None, max_length=1000) # Cart/session info - session_id: Optional[str] = None + session_id: str | None = None class OrderUpdate(BaseModel): """Schema for updating order status.""" - status: Optional[str] = Field( + status: str | None = Field( None, pattern="^(pending|processing|shipped|delivered|cancelled|refunded)$" ) - tracking_number: Optional[str] = None - internal_notes: Optional[str] = None + tracking_number: str | None = None + internal_notes: str | None = None # ============================================================================ @@ -133,26 +131,26 @@ class OrderResponse(BaseModel): currency: str # Shipping - shipping_method: Optional[str] - tracking_number: Optional[str] + shipping_method: str | None + tracking_number: str | None # Notes - customer_notes: Optional[str] - internal_notes: Optional[str] + customer_notes: str | None + internal_notes: str | None # Timestamps created_at: datetime updated_at: datetime - paid_at: Optional[datetime] - shipped_at: Optional[datetime] - delivered_at: Optional[datetime] - cancelled_at: Optional[datetime] + paid_at: datetime | None + shipped_at: datetime | None + delivered_at: datetime | None + cancelled_at: datetime | None class OrderDetailResponse(OrderResponse): """Schema for detailed order response with items and addresses.""" - items: List[OrderItemResponse] + items: list[OrderItemResponse] shipping_address: OrderAddressResponse billing_address: OrderAddressResponse @@ -160,7 +158,7 @@ class OrderDetailResponse(OrderResponse): class OrderListResponse(BaseModel): """Schema for paginated order list.""" - orders: List[OrderResponse] + orders: list[OrderResponse] total: int skip: int limit: int diff --git a/models/schema/product.py b/models/schema/product.py index 1844e955..0f87dce2 100644 --- a/models/schema/product.py +++ b/models/schema/product.py @@ -1,6 +1,5 @@ # models/schema/product.py from datetime import datetime -from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field @@ -12,30 +11,30 @@ class ProductCreate(BaseModel): marketplace_product_id: int = Field( ..., description="MarketplaceProduct ID to add to vendor catalog" ) - product_id: Optional[str] = Field( + product_id: str | None = 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 - availability: Optional[str] = None - condition: Optional[str] = None + price: float | None = Field(None, ge=0) + sale_price: float | None = Field(None, ge=0) + currency: str | None = None + availability: str | None = None + condition: str | None = None is_featured: bool = False min_quantity: int = Field(1, ge=1) - max_quantity: Optional[int] = Field(None, ge=1) + max_quantity: int | None = Field(None, ge=1) class ProductUpdate(BaseModel): - product_id: Optional[str] = None - price: Optional[float] = Field(None, ge=0) - sale_price: Optional[float] = Field(None, ge=0) - currency: Optional[str] = None - availability: Optional[str] = None - condition: Optional[str] = None - is_featured: Optional[bool] = None - is_active: Optional[bool] = None - min_quantity: Optional[int] = Field(None, ge=1) - max_quantity: Optional[int] = Field(None, ge=1) + product_id: str | None = None + price: float | None = Field(None, ge=0) + sale_price: float | None = Field(None, ge=0) + currency: str | None = None + availability: str | None = None + condition: str | None = None + is_featured: bool | None = None + is_active: bool | None = None + min_quantity: int | None = Field(None, ge=1) + max_quantity: int | None = Field(None, ge=1) class ProductResponse(BaseModel): @@ -44,33 +43,33 @@ class ProductResponse(BaseModel): id: int vendor_id: int marketplace_product: MarketplaceProductResponse - product_id: Optional[str] - price: Optional[float] - sale_price: Optional[float] - currency: Optional[str] - availability: Optional[str] - condition: Optional[str] + product_id: str | None + price: float | None + sale_price: float | None + currency: str | None + availability: str | None + condition: str | None is_featured: bool is_active: bool display_order: int min_quantity: int - max_quantity: Optional[int] + max_quantity: int | None created_at: datetime updated_at: datetime # Include inventory summary - total_inventory: Optional[int] = None - available_inventory: Optional[int] = None + total_inventory: int | None = None + available_inventory: int | None = None class ProductDetailResponse(ProductResponse): """Product with full inventory details.""" - inventory_locations: List[InventoryLocationResponse] = [] + inventory_locations: list[InventoryLocationResponse] = [] class ProductListResponse(BaseModel): - products: List[ProductResponse] + products: list[ProductResponse] total: int skip: int limit: int diff --git a/models/schema/stats.py b/models/schema/stats.py index 4ef278fd..ff547ead 100644 --- a/models/schema/stats.py +++ b/models/schema/stats.py @@ -1,9 +1,7 @@ -import re from datetime import datetime from decimal import Decimal -from typing import List, Optional -from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator +from pydantic import BaseModel, Field class StatsResponse(BaseModel): @@ -35,8 +33,8 @@ class CustomerStatsResponse(BaseModel): total_orders: int total_spent: Decimal average_order_value: Decimal - last_order_date: Optional[datetime] - first_order_date: Optional[datetime] + last_order_date: datetime | None + first_order_date: datetime | None lifetime_value: Decimal diff --git a/models/schema/team.py b/models/schema/team.py index 5d3f5ca0..78aef465 100644 --- a/models/schema/team.py +++ b/models/schema/team.py @@ -10,7 +10,6 @@ This module defines request/response schemas for: """ from datetime import datetime -from typing import List, Optional from pydantic import BaseModel, EmailStr, Field, field_validator @@ -23,7 +22,7 @@ class RoleBase(BaseModel): """Base role schema.""" name: str = Field(..., min_length=1, max_length=100, description="Role name") - permissions: List[str] = Field( + permissions: list[str] = Field( default_factory=list, description="List of permission strings" ) @@ -31,14 +30,13 @@ class RoleBase(BaseModel): 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 + name: str | None = Field(None, min_length=1, max_length=100) + permissions: list[str] | None = None class RoleResponse(RoleBase): @@ -56,7 +54,7 @@ class RoleResponse(RoleBase): class RoleListResponse(BaseModel): """Schema for role list response.""" - roles: List[RoleResponse] + roles: list[RoleResponse] total: int @@ -69,20 +67,20 @@ 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) + first_name: str | None = Field(None, max_length=100) + last_name: str | None = Field(None, max_length=100) class TeamMemberInvite(TeamMemberBase): """Schema for inviting a team member.""" - role_id: Optional[int] = Field( + role_id: int | None = Field( None, description="Role ID to assign (for preset roles)" ) - role_name: Optional[str] = Field( + role_name: str | None = Field( None, description="Role name (manager, staff, support, etc.)" ) - custom_permissions: Optional[List[str]] = Field( + custom_permissions: list[str] | None = Field( None, description="Custom permissions (overrides role preset)" ) @@ -112,8 +110,8 @@ 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") + role_id: int | None = Field(None, description="New role ID") + is_active: bool | None = Field(None, description="Active status") class TeamMemberResponse(BaseModel): @@ -122,13 +120,13 @@ class TeamMemberResponse(BaseModel): id: int = Field(..., description="User ID") email: EmailStr username: str - first_name: Optional[str] - last_name: Optional[str] + first_name: str | None + last_name: str | None full_name: str user_type: str = Field(..., description="'owner' or 'member'") role_name: str = Field(..., description="Role name") - role_id: Optional[int] - permissions: List[str] = Field( + role_id: int | None + permissions: list[str] = Field( default_factory=list, description="User's permissions" ) is_active: bool @@ -136,8 +134,8 @@ class TeamMemberResponse(BaseModel): invitation_pending: bool = Field( 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( + invited_at: datetime | None = Field(None, description="When invitation was sent") + accepted_at: datetime | None = Field( None, description="When invitation was accepted" ) joined_at: datetime = Field(..., description="When user joined vendor") @@ -149,7 +147,7 @@ class TeamMemberResponse(BaseModel): class TeamMemberListResponse(BaseModel): """Schema for team member list response.""" - members: List[TeamMemberResponse] + members: list[TeamMemberResponse] total: int active_count: int pending_invitations: int @@ -197,7 +195,7 @@ class InvitationResponse(BaseModel): message: str email: EmailStr role: str - invitation_token: Optional[str] = Field( + invitation_token: str | None = Field( None, description="Token (only returned in dev/test environments)" ) invitation_sent: bool = Field(default=True) @@ -239,7 +237,7 @@ class TeamStatistics(BaseModel): class BulkRemoveRequest(BaseModel): """Schema for bulk removing team members.""" - user_ids: List[int] = Field( + user_ids: list[int] = Field( ..., min_items=1, description="List of user IDs to remove" ) @@ -249,7 +247,7 @@ class BulkRemoveResponse(BaseModel): success_count: int failed_count: int - errors: List[dict] = Field(default_factory=list) + errors: list[dict] = Field(default_factory=list) # ============================================================================ @@ -260,7 +258,7 @@ class BulkRemoveResponse(BaseModel): class PermissionCheckRequest(BaseModel): """Schema for checking permissions.""" - permissions: List[str] = Field(..., min_items=1, description="Permissions to check") + permissions: list[str] = Field(..., min_items=1, description="Permissions to check") class PermissionCheckResponse(BaseModel): @@ -268,8 +266,8 @@ class PermissionCheckResponse(BaseModel): 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( + granted: list[str] = Field(default_factory=list, description="Permissions user has") + denied: list[str] = Field( default_factory=list, description="Permissions user lacks" ) @@ -277,10 +275,10 @@ class PermissionCheckResponse(BaseModel): class UserPermissionsResponse(BaseModel): """Schema for user's permissions response.""" - permissions: List[str] = Field(default_factory=list) + permissions: list[str] = Field(default_factory=list) permission_count: int is_owner: bool - role_name: Optional[str] = None + role_name: str | None = None # ============================================================================ @@ -293,4 +291,4 @@ class TeamErrorResponse(BaseModel): error_code: str message: str - details: Optional[dict] = None + details: dict | None = None diff --git a/models/schema/vendor.py b/models/schema/vendor.py index e622f407..a0049c95 100644 --- a/models/schema/vendor.py +++ b/models/schema/vendor.py @@ -15,7 +15,7 @@ Schemas include: import re from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -36,7 +36,7 @@ class VendorCreate(BaseModel): name: str = Field( ..., description="Display name of the vendor", min_length=2, max_length=255 ) - description: Optional[str] = Field(None, description="Vendor description") + description: str | None = Field(None, description="Vendor description") # Owner Information (Creates User Account) owner_email: str = Field( @@ -45,21 +45,21 @@ class VendorCreate(BaseModel): ) # Business Contact Information (Vendor Fields) - contact_email: Optional[str] = Field( + contact_email: str | None = Field( None, 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") + contact_phone: str | None = Field(None, description="Contact phone number") + website: str | None = Field(None, description="Website URL") # Business Details - business_address: Optional[str] = Field(None, description="Business address") - tax_number: Optional[str] = Field(None, description="Tax/VAT number") + business_address: str | None = Field(None, description="Business address") + tax_number: str | None = Field(None, description="Tax/VAT number") # Marketplace URLs (multi-language support) - letzshop_csv_url_fr: Optional[str] = Field(None, description="French CSV URL") - letzshop_csv_url_en: Optional[str] = Field(None, description="English CSV URL") - letzshop_csv_url_de: Optional[str] = Field(None, description="German CSV URL") + letzshop_csv_url_fr: str | None = Field(None, description="French CSV URL") + letzshop_csv_url_en: str | None = Field(None, description="English CSV URL") + letzshop_csv_url_de: str | None = Field(None, description="German CSV URL") @field_validator("owner_email", "contact_email") @classmethod @@ -95,29 +95,29 @@ class VendorUpdate(BaseModel): """ # Basic Information - name: Optional[str] = Field(None, min_length=2, max_length=255) - description: Optional[str] = None - subdomain: Optional[str] = Field(None, min_length=2, max_length=100) + name: str | None = Field(None, min_length=2, max_length=255) + description: str | None = None + subdomain: str | None = Field(None, min_length=2, max_length=100) # Business Contact Information (Vendor Fields) - contact_email: Optional[str] = Field( + contact_email: str | None = Field( None, description="Public business contact email" ) - contact_phone: Optional[str] = None - website: Optional[str] = None + contact_phone: str | None = None + website: str | None = None # Business Details - business_address: Optional[str] = None - tax_number: Optional[str] = None + business_address: str | None = None + tax_number: str | None = None # Marketplace URLs - letzshop_csv_url_fr: Optional[str] = None - letzshop_csv_url_en: Optional[str] = None - letzshop_csv_url_de: Optional[str] = None + letzshop_csv_url_fr: str | None = None + letzshop_csv_url_en: str | None = None + letzshop_csv_url_de: str | None = None # Status (Admin only) - is_active: Optional[bool] = None - is_verified: Optional[bool] = None + is_active: bool | None = None + is_verified: bool | None = None @field_validator("subdomain") @classmethod @@ -145,22 +145,22 @@ class VendorResponse(BaseModel): vendor_code: str subdomain: str name: str - description: Optional[str] + description: str | None owner_user_id: int # Contact Information (Business) - contact_email: Optional[str] - contact_phone: Optional[str] - website: Optional[str] + contact_email: str | None + contact_phone: str | None + website: str | None # Business Information - business_address: Optional[str] - tax_number: Optional[str] + business_address: str | None + tax_number: str | None # Marketplace URLs - letzshop_csv_url_fr: Optional[str] - letzshop_csv_url_en: Optional[str] - letzshop_csv_url_de: Optional[str] + letzshop_csv_url_fr: str | None + letzshop_csv_url_en: str | None + letzshop_csv_url_de: str | None # Status Flags is_active: bool @@ -196,13 +196,13 @@ 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") + login_url: str | None = Field(None, description="URL for vendor owner to login") class VendorListResponse(BaseModel): """Schema for paginated vendor list.""" - vendors: List[VendorResponse] + vendors: list[VendorResponse] total: int skip: int limit: int @@ -237,7 +237,7 @@ class VendorTransferOwnership(BaseModel): ..., description="Must be true to confirm ownership transfer" ) - transfer_reason: Optional[str] = Field( + transfer_reason: str | None = Field( None, max_length=500, description="Reason for ownership transfer (for audit logs)", @@ -260,12 +260,12 @@ class VendorTransferOwnershipResponse(BaseModel): vendor_code: str vendor_name: str - old_owner: Dict[str, Any] = Field( + old_owner: dict[str, Any] = Field( ..., description="Information about the previous owner" ) - new_owner: Dict[str, Any] = Field( + new_owner: dict[str, Any] = Field( ..., description="Information about the new owner" ) transferred_at: datetime - transfer_reason: Optional[str] + transfer_reason: str | None diff --git a/models/schema/vendor_domain.py b/models/schema/vendor_domain.py index a99b47bb..173cc5e8 100644 --- a/models/schema/vendor_domain.py +++ b/models/schema/vendor_domain.py @@ -12,7 +12,6 @@ Schemas include: import re from datetime import datetime -from typing import Dict, List, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -69,8 +68,8 @@ class VendorDomainCreate(BaseModel): class VendorDomainUpdate(BaseModel): """Schema for updating vendor domain settings.""" - is_primary: Optional[bool] = Field(None, description="Set as primary domain") - is_active: Optional[bool] = Field(None, description="Activate or deactivate domain") + is_primary: bool | None = Field(None, description="Set as primary domain") + is_active: bool | None = Field(None, description="Activate or deactivate domain") model_config = ConfigDict(from_attributes=True) @@ -87,9 +86,9 @@ class VendorDomainResponse(BaseModel): is_active: bool is_verified: bool ssl_status: str - verification_token: Optional[str] = None - verified_at: Optional[datetime] = None - ssl_verified_at: Optional[datetime] = None + verification_token: str | None = None + verified_at: datetime | None = None + ssl_verified_at: datetime | None = None created_at: datetime updated_at: datetime @@ -97,7 +96,7 @@ class VendorDomainResponse(BaseModel): class VendorDomainListResponse(BaseModel): """Schema for paginated vendor domain list.""" - domains: List[VendorDomainResponse] + domains: list[VendorDomainResponse] total: int @@ -106,9 +105,9 @@ class DomainVerificationInstructions(BaseModel): domain: str verification_token: str - instructions: Dict[str, str] - txt_record: Dict[str, str] - common_registrars: Dict[str, str] + instructions: dict[str, str] + txt_record: dict[str, str] + common_registrars: dict[str, str] model_config = ConfigDict(from_attributes=True) diff --git a/models/schema/vendor_theme.py b/models/schema/vendor_theme.py index 9ba482fc..90b6943d 100644 --- a/models/schema/vendor_theme.py +++ b/models/schema/vendor_theme.py @@ -3,7 +3,6 @@ Pydantic schemas for vendor theme operations. """ -from typing import Dict, List, Optional from pydantic import BaseModel, Field @@ -11,40 +10,40 @@ 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") - background: Optional[str] = Field(None, description="Background color") - text: Optional[str] = Field(None, description="Text color") - border: Optional[str] = Field(None, description="Border color") + primary: str | None = Field(None, description="Primary brand color") + secondary: str | None = Field(None, description="Secondary color") + accent: str | None = Field(None, description="Accent/CTA color") + background: str | None = Field(None, description="Background color") + text: str | None = Field(None, description="Text color") + border: str | None = Field(None, description="Border color") 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") + heading: str | None = Field(None, description="Font for headings") + body: str | None = 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") - banner: Optional[str] = Field(None, description="Banner image URL") + logo: str | None = Field(None, description="Logo URL") + logo_dark: str | None = Field(None, description="Dark mode logo URL") + favicon: str | None = Field(None, description="Favicon URL") + banner: str | None = Field(None, description="Banner image URL") class VendorThemeLayout(BaseModel): """Layout settings for vendor theme.""" - style: Optional[str] = Field( + style: str | None = Field( None, description="Product layout style (grid, list, masonry)" ) - header: Optional[str] = Field( + header: str | None = Field( None, description="Header style (fixed, static, transparent)" ) - product_card: Optional[str] = Field( + product_card: str | None = Field( None, description="Product card style (modern, classic, minimal)" ) @@ -52,15 +51,15 @@ class VendorThemeLayout(BaseModel): 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( + theme_name: str | None = Field(None, description="Theme preset name") + colors: dict[str, str] | None = Field(None, description="Color scheme") + fonts: dict[str, str] | None = Field(None, description="Font settings") + branding: dict[str, str | None] | None = 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( + layout: dict[str, str] | None = Field(None, description="Layout settings") + custom_css: str | None = Field(None, description="Custom CSS rules") + social_links: dict[str, str] | None = Field( None, description="Social media links" ) @@ -69,15 +68,15 @@ 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( + colors: dict[str, str] = Field(..., description="Color scheme") + fonts: dict[str, str] = Field(..., description="Font settings") + branding: dict[str, str | None] = Field(..., description="Branding assets") + layout: dict[str, str] = Field(..., description="Layout settings") + social_links: dict[str, str] | None = Field( default_factory=dict, description="Social links" ) - custom_css: Optional[str] = Field(None, description="Custom CSS") - css_variables: Optional[Dict[str, str]] = Field( + custom_css: str | None = Field(None, description="Custom CSS") + css_variables: dict[str, str] | None = Field( None, description="CSS custom properties" ) @@ -105,4 +104,4 @@ class ThemePresetResponse(BaseModel): class ThemePresetListResponse(BaseModel): """List of available theme presets.""" - presets: List[ThemePresetPreview] = Field(..., description="Available presets") + presets: list[ThemePresetPreview] = Field(..., description="Available presets") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..5e3f25c3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,159 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "letzshop-product-import" +version = "0.1.0" +description = "Multi-tenant e-commerce marketplace platform" +requires-python = ">=3.11" + +# ============================================================================= +# RUFF - Modern All-in-One Linter & Formatter +# ============================================================================= +[tool.ruff] +line-length = 88 +target-version = "py311" + +# Exclude directories +exclude = [ + ".git", + ".venv", + "venv", + "__pycache__", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + "build", + "dist", + "alembic/versions", +] + +[tool.ruff.lint] +# Enable comprehensive rule sets +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort (import sorting) + "N", # pep8-naming + "UP", # pyupgrade (modern Python syntax) + "B", # flake8-bugbear (common bugs) + "C4", # flake8-comprehensions + "SIM", # flake8-simplify + "PIE", # flake8-pie + "RET", # flake8-return + "Q", # flake8-quotes +] + +# Ignore specific rules +ignore = [ + "E501", # line too long (handled by formatter) + "B008", # do not perform function calls in argument defaults (FastAPI Depends) + "RET504", # unnecessary variable assignment before return + "SIM102", # use a single if statement instead of nested if (sometimes less readable) +] + +# Allow autofix for all rules +fixable = ["ALL"] +unfixable = [] + +# Per-file ignores +[tool.ruff.lint.per-file-ignores] +# Ignore import violations in __init__.py files +"__init__.py" = ["F401", "F403"] +# Ignore specific rules in test files +"tests/**/*.py" = ["S101", "PLR2004"] +# Alembic migrations can have longer lines and specific patterns +"alembic/versions/*.py" = ["E501", "F401"] + +# Import sorting configuration (replaces isort) +[tool.ruff.lint.isort] +known-first-party = ["app", "models", "middleware", "tasks", "storage", "scripts"] +section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] +split-on-trailing-comma = true + +# Formatter configuration (replaces black) +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +# ============================================================================= +# MYPY - Static Type Checker +# ============================================================================= +[tool.mypy] +python_version = "3.11" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +ignore_missing_imports = true +exclude = [ + "^\\.venv/", + "^venv/", + "^build/", + "^dist/", + "^alembic/versions/", +] + +# Per-module options +[[tool.mypy.overrides]] +module = "tests.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "alembic.*" +ignore_errors = true + +# ============================================================================= +# PYTEST - Testing +# ============================================================================= +[tool.pytest.ini_options] +minversion = "7.0" +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "-v", + "--strict-markers", + "--strict-config", + "--color=yes", +] +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Slow running tests", +] +filterwarnings = [ + "ignore::DeprecationWarning", +] + +# ============================================================================= +# COVERAGE +# ============================================================================= +[tool.coverage.run] +source = ["app", "models", "middleware", "tasks", "storage"] +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/venv/*", + "*/.venv/*", + "*/alembic/*", + "*/scripts/*", +] + +[tool.coverage.report] +precision = 2 +show_missing = true +skip_covered = false +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "if typing.TYPE_CHECKING:", +] diff --git a/scripts/backup_database.py b/scripts/backup_database.py index 41385cd5..fce76905 100644 --- a/scripts/backup_database.py +++ b/scripts/backup_database.py @@ -2,12 +2,10 @@ """Database backup utility that uses project configuration.""" import os -import shutil import sqlite3 import sys from datetime import datetime from pathlib import Path -from urllib.parse import urlparse # Add project root to Python path sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) @@ -25,11 +23,10 @@ def get_database_path(): if db_path.startswith("./"): db_path = db_path[2:] # Remove ./ prefix return db_path - else: - # For PostgreSQL or other databases, we can't do file backup - print(f"[INFO] Database type: {db_url.split('://')[0]}") - print("[ERROR] File backup only supported for SQLite databases") - return None + # For PostgreSQL or other databases, we can't do file backup + print(f"[INFO] Database type: {db_url.split('://')[0]}") + print("[ERROR] File backup only supported for SQLite databases") + return None def backup_sqlite_database(): @@ -78,10 +75,9 @@ def backup_database(): if settings.database_url.startswith("sqlite"): return backup_sqlite_database() - else: - print("[INFO] For PostgreSQL databases, use pg_dump:") - print(f"pg_dump {settings.database_url} > backup_$(date +%Y%m%d_%H%M%S).sql") - return True + print("[INFO] For PostgreSQL databases, use pg_dump:") + print(f"pg_dump {settings.database_url} > backup_$(date +%Y%m%d_%H%M%S).sql") + return True if __name__ == "__main__": diff --git a/scripts/create_default_content_pages.py b/scripts/create_default_content_pages.py index 14fc665d..770e213e 100755 --- a/scripts/create_default_content_pages.py +++ b/scripts/create_default_content_pages.py @@ -26,7 +26,7 @@ Usage: """ import sys -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path # Add project root to path @@ -496,12 +496,12 @@ def create_default_pages(db: Session) -> None: meta_description=page_data["meta_description"], meta_keywords=page_data["meta_keywords"], is_published=True, - published_at=datetime.now(timezone.utc), + published_at=datetime.now(UTC), show_in_footer=page_data["show_in_footer"], show_in_header=page_data.get("show_in_header", False), display_order=page_data["display_order"], - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) db.add(page) @@ -511,7 +511,7 @@ def create_default_pages(db: Session) -> None: db.commit() print("\n" + "=" * 70) - print(f"Summary:") + print("Summary:") print(f" Created: {created_count} pages") print(f" Skipped: {skipped_count} pages (already exist)") print(f" Total: {created_count + skipped_count} pages") diff --git a/scripts/create_landing_page.py b/scripts/create_landing_page.py index 203c33f3..219f832e 100755 --- a/scripts/create_landing_page.py +++ b/scripts/create_landing_page.py @@ -12,7 +12,7 @@ from pathlib import Path # Add project root to path sys.path.insert(0, str(Path(__file__).parent.parent)) -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy.orm import Session @@ -65,7 +65,7 @@ def create_landing_page( if content: existing.content = content existing.is_published = True - existing.updated_at = datetime.now(timezone.utc) + existing.updated_at = datetime.now(UTC) db.commit() print(f"āœ… Updated landing page with template: {template}") @@ -78,7 +78,7 @@ def create_landing_page( content=content or f"""

About {vendor.name}

-

{vendor.description or 'Your trusted shopping destination for quality products.'}

+

{vendor.description or "Your trusted shopping destination for quality products."}

Why Choose Us?