non-building checkpoint 1
This commit is contained in:
76
backend/app/core/config.py
Normal file
76
backend/app/core/config.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Configuration settings for the application.
|
||||
Uses Pydantic settings management with environment variable support.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List, Optional
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import AnyHttpUrl, validator
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings."""
|
||||
|
||||
# API Settings
|
||||
API_V1_STR: str = "/api/v1"
|
||||
PROJECT_NAME: str = "Daily Journal Prompt Generator API"
|
||||
VERSION: str = "1.0.0"
|
||||
DEBUG: bool = False
|
||||
ENVIRONMENT: str = "development"
|
||||
|
||||
# Server Settings
|
||||
HOST: str = "0.0.0.0"
|
||||
PORT: int = 8000
|
||||
|
||||
# CORS Settings
|
||||
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [
|
||||
"http://localhost:3000", # Frontend dev server
|
||||
"http://localhost:80", # Frontend production
|
||||
]
|
||||
|
||||
# API Keys
|
||||
DEEPSEEK_API_KEY: Optional[str] = None
|
||||
OPENAI_API_KEY: Optional[str] = None
|
||||
API_BASE_URL: str = "https://api.deepseek.com"
|
||||
MODEL: str = "deepseek-chat"
|
||||
|
||||
# Application Settings
|
||||
MIN_PROMPT_LENGTH: int = 500
|
||||
MAX_PROMPT_LENGTH: int = 1000
|
||||
NUM_PROMPTS_PER_SESSION: int = 6
|
||||
CACHED_POOL_VOLUME: int = 20
|
||||
HISTORY_BUFFER_SIZE: int = 60
|
||||
FEEDBACK_HISTORY_SIZE: int = 30
|
||||
|
||||
# File Paths (relative to project root)
|
||||
DATA_DIR: str = "data"
|
||||
PROMPT_TEMPLATE_PATH: str = "data/ds_prompt.txt"
|
||||
FEEDBACK_TEMPLATE_PATH: str = "data/ds_feedback.txt"
|
||||
SETTINGS_CONFIG_PATH: str = "data/settings.cfg"
|
||||
|
||||
# Data File Names (relative to DATA_DIR)
|
||||
PROMPTS_HISTORIC_FILE: str = "prompts_historic.json"
|
||||
PROMPTS_POOL_FILE: str = "prompts_pool.json"
|
||||
FEEDBACK_WORDS_FILE: str = "feedback_words.json"
|
||||
FEEDBACK_HISTORIC_FILE: str = "feedback_historic.json"
|
||||
|
||||
@validator("BACKEND_CORS_ORIGINS", pre=True)
|
||||
def assemble_cors_origins(cls, v: str | List[str]) -> List[str] | str:
|
||||
"""Parse CORS origins from string or list."""
|
||||
if isinstance(v, str) and not v.startswith("["):
|
||||
return [i.strip() for i in v.split(",")]
|
||||
elif isinstance(v, (list, str)):
|
||||
return v
|
||||
raise ValueError(v)
|
||||
|
||||
class Config:
|
||||
"""Pydantic configuration."""
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
# Create global settings instance
|
||||
settings = Settings()
|
||||
|
||||
130
backend/app/core/exception_handlers.py
Normal file
130
backend/app/core/exception_handlers.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Exception handlers for the application.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from fastapi import FastAPI, Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
|
||||
from app.core.exceptions import DailyJournalPromptException
|
||||
from app.core.logging import setup_logging
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
def setup_exception_handlers(app: FastAPI) -> None:
|
||||
"""Set up exception handlers for the FastAPI application."""
|
||||
|
||||
@app.exception_handler(DailyJournalPromptException)
|
||||
async def daily_journal_prompt_exception_handler(
|
||||
request: Request,
|
||||
exc: DailyJournalPromptException,
|
||||
) -> JSONResponse:
|
||||
"""Handle DailyJournalPromptException."""
|
||||
logger.error(f"DailyJournalPromptException: {exc.detail}")
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
"error": {
|
||||
"type": exc.__class__.__name__,
|
||||
"message": str(exc.detail),
|
||||
"status_code": exc.status_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def request_validation_exception_handler(
|
||||
request: Request,
|
||||
exc: RequestValidationError,
|
||||
) -> JSONResponse:
|
||||
"""Handle request validation errors."""
|
||||
logger.warning(f"RequestValidationError: {exc.errors()}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={
|
||||
"error": {
|
||||
"type": "ValidationError",
|
||||
"message": "Invalid request data",
|
||||
"details": exc.errors(),
|
||||
"status_code": status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@app.exception_handler(PydanticValidationError)
|
||||
async def pydantic_validation_exception_handler(
|
||||
request: Request,
|
||||
exc: PydanticValidationError,
|
||||
) -> JSONResponse:
|
||||
"""Handle Pydantic validation errors."""
|
||||
logger.warning(f"PydanticValidationError: {exc.errors()}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={
|
||||
"error": {
|
||||
"type": "ValidationError",
|
||||
"message": "Invalid data format",
|
||||
"details": exc.errors(),
|
||||
"status_code": status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def generic_exception_handler(
|
||||
request: Request,
|
||||
exc: Exception,
|
||||
) -> JSONResponse:
|
||||
"""Handle all other exceptions."""
|
||||
logger.exception(f"Unhandled exception: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={
|
||||
"error": {
|
||||
"type": "InternalServerError",
|
||||
"message": "An unexpected error occurred",
|
||||
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@app.exception_handler(404)
|
||||
async def not_found_exception_handler(
|
||||
request: Request,
|
||||
exc: Exception,
|
||||
) -> JSONResponse:
|
||||
"""Handle 404 Not Found errors."""
|
||||
logger.warning(f"404 Not Found: {request.url}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
content={
|
||||
"error": {
|
||||
"type": "NotFoundError",
|
||||
"message": f"Resource not found: {request.url}",
|
||||
"status_code": status.HTTP_404_NOT_FOUND,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@app.exception_handler(405)
|
||||
async def method_not_allowed_exception_handler(
|
||||
request: Request,
|
||||
exc: Exception,
|
||||
) -> JSONResponse:
|
||||
"""Handle 405 Method Not Allowed errors."""
|
||||
logger.warning(f"405 Method Not Allowed: {request.method} {request.url}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
content={
|
||||
"error": {
|
||||
"type": "MethodNotAllowedError",
|
||||
"message": f"Method {request.method} not allowed for {request.url}",
|
||||
"status_code": status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
172
backend/app/core/exceptions.py
Normal file
172
backend/app/core/exceptions.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Custom exceptions for the application.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
|
||||
class DailyJournalPromptException(HTTPException):
|
||||
"""Base exception for Daily Journal Prompt application."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail: Any = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
super().__init__(status_code=status_code, detail=detail, headers=headers)
|
||||
|
||||
|
||||
class ValidationError(DailyJournalPromptException):
|
||||
"""Exception for validation errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detail: Any = "Validation error",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
class NotFoundError(DailyJournalPromptException):
|
||||
"""Exception for resource not found errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detail: Any = "Resource not found",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
class UnauthorizedError(DailyJournalPromptException):
|
||||
"""Exception for unauthorized access errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detail: Any = "Unauthorized access",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
class ForbiddenError(DailyJournalPromptException):
|
||||
"""Exception for forbidden access errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detail: Any = "Forbidden access",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
class AIServiceError(DailyJournalPromptException):
|
||||
"""Exception for AI service errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detail: Any = "AI service error",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
class DataServiceError(DailyJournalPromptException):
|
||||
"""Exception for data service errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detail: Any = "Data service error",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
class ConfigurationError(DailyJournalPromptException):
|
||||
"""Exception for configuration errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detail: Any = "Configuration error",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
class PromptPoolEmptyError(DailyJournalPromptException):
|
||||
"""Exception for empty prompt pool."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detail: Any = "Prompt pool is empty",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
class InsufficientPoolSizeError(DailyJournalPromptException):
|
||||
"""Exception for insufficient pool size."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
current_size: int,
|
||||
requested: int,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
detail = f"Pool only has {current_size} prompts, requested {requested}"
|
||||
super().__init__(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
class TemplateNotFoundError(DailyJournalPromptException):
|
||||
"""Exception for missing template files."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
template_name: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
detail = f"Template not found: {template_name}"
|
||||
super().__init__(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
54
backend/app/core/logging.py
Normal file
54
backend/app/core/logging.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Logging configuration for the application.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
def setup_logging(
|
||||
logger_name: str = "daily_journal_prompt",
|
||||
log_level: Optional[str] = None,
|
||||
) -> logging.Logger:
|
||||
"""
|
||||
Set up logging configuration.
|
||||
|
||||
Args:
|
||||
logger_name: Name of the logger
|
||||
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
|
||||
Returns:
|
||||
Configured logger instance
|
||||
"""
|
||||
if log_level is None:
|
||||
log_level = "DEBUG" if settings.DEBUG else "INFO"
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(getattr(logging, log_level.upper()))
|
||||
|
||||
# Remove existing handlers to avoid duplicates
|
||||
logger.handlers.clear()
|
||||
|
||||
# Create console handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(getattr(logging, log_level.upper()))
|
||||
|
||||
# Create formatter
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# Add handler to logger
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# Prevent propagation to root logger
|
||||
logger.propagate = False
|
||||
|
||||
return logger
|
||||
|
||||
Reference in New Issue
Block a user