Initial commit: Ebook Translation System with Docker setup

This commit is contained in:
richardtekula
2025-11-11 16:01:34 +01:00
commit e1b95c613d
43 changed files with 13922 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
import os
from dotenv import load_dotenv
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base
from passlib.context import CryptContext
# Load environment variables
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/postgres")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def hash_password(pw: str) -> str:
return pwd_context.hash(pw)
def verify_password(pw: str, hashed: str) -> bool:
return pwd_context.verify(pw, hashed)

View File

@@ -0,0 +1,8 @@
import random
import string
# def generate_coupon(length: int = 6) -> str:
# return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))
def generate_coupon():
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))

View File

@@ -0,0 +1,211 @@
"""
Custom exceptions for the Ebook Coupon Management System
Provides structured error handling with proper error codes and messages.
"""
from typing import Dict, Any, Optional
from fastapi import HTTPException
class APIException(HTTPException):
"""Base API exception with structured error information"""
def __init__(
self,
status_code: int,
detail: str,
error_code: str,
extra_data: Optional[Dict[str, Any]] = None
):
super().__init__(status_code=status_code, detail=detail)
self.error_code = error_code
self.extra_data = extra_data or {}
class AuthenticationError(APIException):
"""Authentication related errors"""
def __init__(self, detail: str = "Authentication failed"):
super().__init__(
status_code=401,
detail=detail,
error_code="AUTHENTICATION_ERROR"
)
class AuthorizationError(APIException):
"""Authorization related errors"""
def __init__(self, detail: str = "Access denied"):
super().__init__(
status_code=403,
detail=detail,
error_code="AUTHORIZATION_ERROR"
)
class NotFoundError(APIException):
"""Resource not found errors"""
def __init__(self, resource: str, detail: Optional[str] = None):
if detail is None:
detail = f"{resource} not found"
super().__init__(
status_code=404,
detail=detail,
error_code="NOT_FOUND_ERROR"
)
class ValidationError(APIException):
"""Validation related errors"""
def __init__(self, detail: str, field: Optional[str] = None):
if field:
detail = f"Validation error in field '{field}': {detail}"
super().__init__(
status_code=422,
detail=detail,
error_code="VALIDATION_ERROR"
)
class ConflictError(APIException):
"""Resource conflict errors"""
def __init__(self, detail: str):
super().__init__(
status_code=409,
detail=detail,
error_code="CONFLICT_ERROR"
)
class RateLimitError(APIException):
"""Rate limiting errors"""
def __init__(self, detail: str = "Rate limit exceeded"):
super().__init__(
status_code=429,
detail=detail,
error_code="RATE_LIMIT_ERROR"
)
class DatabaseError(APIException):
"""Database related errors"""
def __init__(self, detail: str = "Database operation failed"):
super().__init__(
status_code=500,
detail=detail,
error_code="DATABASE_ERROR"
)
class FileUploadError(APIException):
"""File upload related errors"""
def __init__(self, detail: str):
super().__init__(
status_code=400,
detail=detail,
error_code="FILE_UPLOAD_ERROR"
)
class CouponError(APIException):
"""Coupon related errors"""
def __init__(self, detail: str, error_code: str = "COUPON_ERROR"):
super().__init__(
status_code=400,
detail=detail,
error_code=error_code
)
def handle_api_exception(exc: APIException, path: str) -> Dict[str, Any]:
"""
Handle API exception and return structured error response
Args:
exc: API exception instance
path: Request path
Returns:
Structured error response
"""
return {
"success": False,
"error": exc.detail,
"error_code": exc.error_code,
"timestamp": None, # Will be set by exception handler
"path": path,
**exc.extra_data
}
# Coupon specific exceptions
class CouponNotFoundError(NotFoundError):
"""Coupon not found error"""
def __init__(self, code: str):
super().__init__("coupon", f"Coupon code '{code}' not found")
class CouponAlreadyUsedError(CouponError):
"""Coupon already used error"""
def __init__(self, code: str):
super().__init__(
f"Coupon code '{code}' has already been used",
"COUPON_ALREADY_USED"
)
class CouponBlockedError(CouponError):
"""Coupon blocked error"""
def __init__(self, code: str, remaining_minutes: int):
super().__init__(
f"Coupon code '{code}' is blocked. Try again in {remaining_minutes} minutes",
"COUPON_BLOCKED"
)
class CouponLimitExceededError(CouponError):
"""Coupon usage limit exceeded error"""
def __init__(self, code: str, limit: int):
super().__init__(
f"Coupon code '{code}' usage limit ({limit}) exceeded",
"COUPON_LIMIT_EXCEEDED"
)
# File upload specific exceptions
class FileTypeError(FileUploadError):
"""Invalid file type error"""
def __init__(self, allowed_types: list):
super().__init__(
f"Invalid file type. Allowed types: {', '.join(allowed_types)}"
)
class FileSizeError(FileUploadError):
"""File too large error"""
def __init__(self, max_size_mb: int):
super().__init__(
f"File too large. Maximum size: {max_size_mb}MB"
)
class FileExistsError(FileUploadError):
"""File already exists error"""
def __init__(self, filename: str):
super().__init__(
f"File '{filename}' already exists. Please delete it first."
)

View File

@@ -0,0 +1,157 @@
"""
Professional logging utility for the Ebook Coupon Management System
Provides structured logging with proper formatting and log levels.
"""
import logging
import logging.handlers
import os
import sys
from datetime import datetime
from typing import Optional, Any, Dict
import json
class SafeJSONEncoder(json.JSONEncoder):
"""Custom JSON encoder that handles non-serializable objects safely"""
def default(self, obj):
"""Handle non-serializable objects by converting them to strings"""
if hasattr(obj, '__dict__'):
return str(obj)
elif hasattr(obj, '__str__'):
return str(obj)
else:
return f"<{type(obj).__name__} object>"
class StructuredFormatter(logging.Formatter):
"""Custom formatter for structured logging"""
def format(self, record: logging.LogRecord) -> str:
"""Format log record with structured data"""
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"line": record.lineno
}
# Add extra fields if present
if hasattr(record, 'request_id'):
log_entry['request_id'] = record.request_id
if hasattr(record, 'method'):
log_entry['method'] = record.method
if hasattr(record, 'path'):
log_entry['path'] = record.path
if hasattr(record, 'status_code'):
log_entry['status_code'] = record.status_code
if hasattr(record, 'process_time'):
log_entry['process_time'] = record.process_time
if hasattr(record, 'client_ip'):
log_entry['client_ip'] = record.client_ip
if hasattr(record, 'user_agent'):
log_entry['user_agent'] = record.user_agent
if hasattr(record, 'error'):
log_entry['error'] = record.error
if hasattr(record, 'exception_type'):
log_entry['exception_type'] = record.exception_type
if hasattr(record, 'exception_message'):
log_entry['exception_message'] = record.exception_message
if hasattr(record, 'errors'):
# Handle errors list safely
try:
if isinstance(record.errors, list):
log_entry['errors'] = [str(error) if not isinstance(error, (dict, str, int, float, bool)) else error for error in record.errors]
else:
log_entry['errors'] = str(record.errors)
except Exception:
log_entry['errors'] = str(record.errors)
if hasattr(record, 'app_name'):
log_entry['app_name'] = record.app_name
if hasattr(record, 'version'):
log_entry['version'] = record.version
if hasattr(record, 'environment'):
log_entry['environment'] = record.environment
if hasattr(record, 'debug'):
log_entry['debug'] = record.debug
# Add exception info if present
if record.exc_info:
log_entry['exception'] = self.formatException(record.exc_info)
return json.dumps(log_entry, ensure_ascii=False, cls=SafeJSONEncoder)
def setup_logger(name: str, level: Optional[str] = None) -> logging.Logger:
"""
Setup a logger with proper configuration
Args:
name: Logger name
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
Returns:
Configured logger instance
"""
# Get log level from environment or use default
log_level = level or os.getenv("LOG_LEVEL", "INFO").upper()
# Create logger
logger = logging.getLogger(name)
logger.setLevel(getattr(logging, log_level))
# Avoid duplicate handlers
if logger.handlers:
return logger
# Create formatters
structured_formatter = StructuredFormatter()
console_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
# File handler for structured logs
log_dir = "logs"
os.makedirs(log_dir, exist_ok=True)
file_handler = logging.handlers.RotatingFileHandler(
os.path.join(log_dir, "app.log"),
maxBytes=10*1024*1024, # 10MB
backupCount=5
)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(structured_formatter)
logger.addHandler(file_handler)
# Error file handler
error_handler = logging.handlers.RotatingFileHandler(
os.path.join(log_dir, "error.log"),
maxBytes=10*1024*1024, # 10MB
backupCount=5
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(structured_formatter)
logger.addHandler(error_handler)
return logger
def get_logger(name: str) -> logging.Logger:
"""
Get a logger instance
Args:
name: Logger name
Returns:
Logger instance
"""
return logging.getLogger(name)
# Create default logger
default_logger = setup_logger("ebook_coupon_system")

View File

@@ -0,0 +1,8 @@
from fastapi.templating import Jinja2Templates
import os
BASE_DIR = os.path.dirname(__file__)
PARENT_DIR = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
TEMPLATE_DIR = os.path.join(PARENT_DIR, "admin-frontend")
templates = Jinja2Templates(directory=TEMPLATE_DIR)

View File

@@ -0,0 +1,83 @@
"""
Timezone utilities for CEST/CET conversion
"""
from datetime import datetime, timezone
import pytz
def get_cest_timezone():
"""Get CEST/CET timezone (Europe/Berlin)"""
return pytz.timezone('Europe/Berlin')
def get_server_timezone():
"""Get server's local timezone (IST)"""
return pytz.timezone('Asia/Kolkata')
def utc_to_cest(utc_datetime):
"""
Convert UTC datetime to CEST/CET timezone
Args:
utc_datetime: UTC datetime object
Returns:
datetime object in CEST/CET timezone
"""
if utc_datetime is None:
return None
# Ensure the datetime is timezone-aware
if utc_datetime.tzinfo is None:
utc_datetime = utc_datetime.replace(tzinfo=timezone.utc)
cest_tz = get_cest_timezone()
return utc_datetime.astimezone(cest_tz)
def local_to_cest(local_datetime):
"""
Convert local server time (IST) to CEST/CET timezone
Args:
local_datetime: Local datetime object (from server)
Returns:
datetime object in CEST/CET timezone
"""
if local_datetime is None:
return None
# First, make the local datetime timezone-aware
ist_tz = get_server_timezone()
if local_datetime.tzinfo is None:
local_datetime = ist_tz.localize(local_datetime)
# Convert to CEST/CET
cest_tz = get_cest_timezone()
return local_datetime.astimezone(cest_tz)
def format_cest_datetime(utc_datetime, format_str="%Y-%m-%d %H:%M:%S"):
"""
Format UTC datetime to CEST/CET timezone string
Args:
utc_datetime: UTC datetime object
format_str: Format string for datetime
Returns:
Formatted string in CEST/CET timezone
"""
if utc_datetime is None:
return None
# Convert local server time to CEST/CET
cest_datetime = local_to_cest(utc_datetime)
return cest_datetime.strftime(format_str)
def now_cest():
"""
Get current time in CEST/CET timezone
Returns:
datetime object in CEST/CET timezone
"""
cest_tz = get_cest_timezone()
return datetime.now(cest_tz)