Coverage for src / dataknobs_bots / api / exceptions.py: 0%
58 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-16 10:13 -0700
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-16 10:13 -0700
1"""Custom exceptions and exception handlers for FastAPI applications.
3This module provides a consistent exception hierarchy and handlers
4for bot-related API errors. The exceptions extend from dataknobs_common
5for consistency across the codebase.
7Example:
8 ```python
9 from fastapi import FastAPI
10 from dataknobs_bots.api.exceptions import (
11 register_exception_handlers,
12 BotNotFoundError,
13 )
15 app = FastAPI()
16 register_exception_handlers(app)
18 @app.get("/bots/{bot_id}")
19 async def get_bot(bot_id: str):
20 bot = await manager.get(bot_id)
21 if not bot:
22 raise BotNotFoundError(bot_id)
23 return {"bot_id": bot_id}
24 ```
25"""
27from __future__ import annotations
29from datetime import datetime, timezone
30from typing import TYPE_CHECKING, Any
32from dataknobs_common.exceptions import (
33 ConfigurationError as CommonConfigurationError,
34)
35from dataknobs_common.exceptions import (
36 DataknobsError,
37)
38from dataknobs_common.exceptions import (
39 NotFoundError as CommonNotFoundError,
40)
41from dataknobs_common.exceptions import (
42 ValidationError as CommonValidationError,
43)
45if TYPE_CHECKING:
46 from fastapi import FastAPI, HTTPException, Request
47 from fastapi.responses import JSONResponse
50class APIError(DataknobsError):
51 """Base exception for API errors.
53 Extends DataknobsError to provide HTTP-specific error handling
54 with status codes and structured error responses.
56 Attributes:
57 message: Error message
58 status_code: HTTP status code
59 detail: Error details (maps to DataknobsError.context)
60 error_code: Machine-readable error code
61 """
63 def __init__(
64 self,
65 message: str,
66 status_code: int = 500,
67 detail: dict[str, Any] | None = None,
68 error_code: str | None = None,
69 ):
70 """Initialize API error.
72 Args:
73 message: Human-readable error message
74 status_code: HTTP status code (default: 500)
75 detail: Optional dictionary with error details
76 error_code: Optional machine-readable error code
77 """
78 # Pass detail as context to DataknobsError
79 super().__init__(message, context=detail)
80 self.status_code = status_code
81 self.error_code = error_code or self.__class__.__name__
83 @property
84 def detail(self) -> dict[str, Any]:
85 """Alias for context to maintain API compatibility."""
86 return self.context
88 def to_dict(self) -> dict[str, Any]:
89 """Convert error to dictionary for JSON response.
91 Returns:
92 Dictionary representation of the error
93 """
94 return {
95 "error": self.error_code,
96 "message": str(self),
97 "detail": self.context,
98 "timestamp": datetime.now(timezone.utc).isoformat(),
99 }
102class BotNotFoundError(APIError, CommonNotFoundError):
103 """Exception raised when bot instance is not found."""
105 def __init__(self, bot_id: str):
106 APIError.__init__(
107 self,
108 message=f"Bot with ID '{bot_id}' not found",
109 status_code=404,
110 detail={"bot_id": bot_id},
111 )
114class BotCreationError(APIError):
115 """Exception raised when bot creation fails."""
117 def __init__(self, bot_id: str, reason: str):
118 super().__init__(
119 message=f"Failed to create bot '{bot_id}': {reason}",
120 status_code=500,
121 detail={"bot_id": bot_id, "reason": reason},
122 )
125class ConversationNotFoundError(APIError, CommonNotFoundError):
126 """Exception raised when conversation is not found."""
128 def __init__(self, conversation_id: str):
129 APIError.__init__(
130 self,
131 message=f"Conversation with ID '{conversation_id}' not found",
132 status_code=404,
133 detail={"conversation_id": conversation_id},
134 )
137class ValidationError(APIError, CommonValidationError):
138 """Exception raised when input validation fails."""
140 def __init__(self, message: str, detail: dict[str, Any] | None = None):
141 APIError.__init__(
142 self,
143 message=message,
144 status_code=422,
145 detail=detail,
146 )
149class ConfigurationError(APIError, CommonConfigurationError):
150 """Exception raised when configuration is invalid."""
152 def __init__(self, message: str, config_key: str | None = None):
153 detail = {}
154 if config_key:
155 detail["config_key"] = config_key
156 APIError.__init__(
157 self,
158 message=message,
159 status_code=500,
160 detail=detail,
161 )
164class RateLimitError(APIError):
165 """Exception raised when rate limit is exceeded."""
167 def __init__(
168 self,
169 message: str = "Rate limit exceeded",
170 retry_after: int | None = None,
171 ):
172 detail = {}
173 if retry_after:
174 detail["retry_after"] = retry_after
175 super().__init__(
176 message=message,
177 status_code=429,
178 detail=detail,
179 )
182# Exception Handlers
183# Note: These use TYPE_CHECKING imports to avoid requiring FastAPI at import time
186async def api_error_handler(
187 request: Request, # type: ignore[name-defined]
188 exc: APIError,
189) -> JSONResponse: # type: ignore[name-defined]
190 """Handle API errors with standardized response format.
192 Args:
193 request: FastAPI request object
194 exc: API error exception
196 Returns:
197 JSON response with error details
198 """
199 from fastapi.responses import JSONResponse
201 return JSONResponse(
202 status_code=exc.status_code,
203 content=exc.to_dict(),
204 )
207async def http_exception_handler(
208 request: Request, # type: ignore[name-defined]
209 exc: HTTPException, # type: ignore[name-defined]
210) -> JSONResponse: # type: ignore[name-defined]
211 """Handle FastAPI HTTP exceptions.
213 Args:
214 request: FastAPI request object
215 exc: HTTP exception
217 Returns:
218 JSON response with error details
219 """
220 from fastapi.responses import JSONResponse
222 return JSONResponse(
223 status_code=exc.status_code,
224 content={
225 "error": "HTTPException",
226 "message": str(exc.detail),
227 "detail": {},
228 "timestamp": datetime.now(timezone.utc).isoformat(),
229 },
230 )
233async def general_exception_handler(
234 request: Request, # type: ignore[name-defined]
235 exc: Exception,
236) -> JSONResponse: # type: ignore[name-defined]
237 """Handle unexpected exceptions.
239 Args:
240 request: FastAPI request object
241 exc: Generic exception
243 Returns:
244 JSON response with error details
246 Note:
247 This handler logs the full exception but returns a generic
248 message to avoid leaking internal details.
249 """
250 import logging
252 from fastapi.responses import JSONResponse
254 logger = logging.getLogger(__name__)
255 logger.exception(f"Unhandled exception: {exc}")
257 return JSONResponse(
258 status_code=500,
259 content={
260 "error": "InternalServerError",
261 "message": "An unexpected error occurred",
262 "detail": {"exception_type": type(exc).__name__},
263 "timestamp": datetime.now(timezone.utc).isoformat(),
264 },
265 )
268def register_exception_handlers(
269 app: FastAPI, # type: ignore[name-defined]
270) -> None:
271 """Register all exception handlers with a FastAPI app.
273 Args:
274 app: FastAPI application instance
276 Example:
277 ```python
278 from fastapi import FastAPI
279 from dataknobs_bots.api.exceptions import register_exception_handlers
281 app = FastAPI()
282 register_exception_handlers(app)
283 ```
284 """
285 from fastapi import HTTPException
287 app.add_exception_handler(APIError, api_error_handler) # type: ignore
288 app.add_exception_handler(HTTPException, http_exception_handler) # type: ignore
289 app.add_exception_handler(Exception, general_exception_handler)