non-building checkpoint 1

This commit is contained in:
2026-01-03 11:18:56 -07:00
parent 9c64cb0c2f
commit 81ea22eae9
37 changed files with 4804 additions and 275 deletions

View 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()

View 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,
}
},
)

View 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,
)

View 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