mcp-proxy-adapter 3.0.0__py3-none-any.whl → 3.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. examples/basic_example/README.md +123 -9
  2. examples/basic_example/config.json +4 -0
  3. examples/basic_example/docs/EN/README.md +46 -5
  4. examples/basic_example/docs/RU/README.md +46 -5
  5. examples/basic_example/server.py +127 -21
  6. examples/complete_example/commands/system_command.py +1 -0
  7. examples/complete_example/server.py +65 -11
  8. examples/minimal_example/README.md +20 -6
  9. examples/minimal_example/config.json +7 -14
  10. examples/minimal_example/main.py +109 -40
  11. examples/minimal_example/simple_server.py +53 -14
  12. examples/minimal_example/tests/conftest.py +1 -1
  13. examples/minimal_example/tests/test_integration.py +8 -10
  14. examples/simple_server.py +12 -21
  15. examples/test_server.py +22 -14
  16. examples/tool_description_example.py +82 -0
  17. mcp_proxy_adapter/api/__init__.py +0 -0
  18. mcp_proxy_adapter/api/app.py +391 -0
  19. mcp_proxy_adapter/api/handlers.py +229 -0
  20. mcp_proxy_adapter/api/middleware/__init__.py +49 -0
  21. mcp_proxy_adapter/api/middleware/auth.py +146 -0
  22. mcp_proxy_adapter/api/middleware/base.py +79 -0
  23. mcp_proxy_adapter/api/middleware/error_handling.py +198 -0
  24. mcp_proxy_adapter/api/middleware/logging.py +96 -0
  25. mcp_proxy_adapter/api/middleware/performance.py +83 -0
  26. mcp_proxy_adapter/api/middleware/rate_limit.py +152 -0
  27. mcp_proxy_adapter/api/schemas.py +305 -0
  28. mcp_proxy_adapter/api/tool_integration.py +223 -0
  29. mcp_proxy_adapter/api/tools.py +198 -0
  30. mcp_proxy_adapter/commands/__init__.py +19 -0
  31. mcp_proxy_adapter/commands/base.py +301 -0
  32. mcp_proxy_adapter/commands/command_registry.py +231 -0
  33. mcp_proxy_adapter/commands/config_command.py +113 -0
  34. mcp_proxy_adapter/commands/health_command.py +136 -0
  35. mcp_proxy_adapter/commands/help_command.py +193 -0
  36. mcp_proxy_adapter/commands/result.py +215 -0
  37. mcp_proxy_adapter/config.py +9 -0
  38. mcp_proxy_adapter/core/__init__.py +0 -0
  39. mcp_proxy_adapter/core/errors.py +173 -0
  40. mcp_proxy_adapter/core/logging.py +205 -0
  41. mcp_proxy_adapter/core/utils.py +138 -0
  42. mcp_proxy_adapter/py.typed +0 -0
  43. mcp_proxy_adapter/schemas/base_schema.json +114 -0
  44. mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
  45. mcp_proxy_adapter/tests/__init__.py +0 -0
  46. mcp_proxy_adapter/tests/api/__init__.py +3 -0
  47. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +115 -0
  48. mcp_proxy_adapter/tests/api/test_middleware.py +336 -0
  49. mcp_proxy_adapter/tests/commands/__init__.py +3 -0
  50. mcp_proxy_adapter/tests/commands/test_config_command.py +211 -0
  51. mcp_proxy_adapter/tests/commands/test_echo_command.py +127 -0
  52. mcp_proxy_adapter/tests/commands/test_help_command.py +133 -0
  53. mcp_proxy_adapter/tests/conftest.py +131 -0
  54. mcp_proxy_adapter/tests/functional/__init__.py +3 -0
  55. mcp_proxy_adapter/tests/functional/test_api.py +235 -0
  56. mcp_proxy_adapter/tests/integration/__init__.py +3 -0
  57. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +130 -0
  58. mcp_proxy_adapter/tests/integration/test_integration.py +255 -0
  59. mcp_proxy_adapter/tests/performance/__init__.py +3 -0
  60. mcp_proxy_adapter/tests/performance/test_performance.py +189 -0
  61. mcp_proxy_adapter/tests/stubs/__init__.py +10 -0
  62. mcp_proxy_adapter/tests/stubs/echo_command.py +104 -0
  63. mcp_proxy_adapter/tests/test_api_endpoints.py +271 -0
  64. mcp_proxy_adapter/tests/test_api_handlers.py +289 -0
  65. mcp_proxy_adapter/tests/test_base_command.py +123 -0
  66. mcp_proxy_adapter/tests/test_batch_requests.py +117 -0
  67. mcp_proxy_adapter/tests/test_command_registry.py +245 -0
  68. mcp_proxy_adapter/tests/test_config.py +127 -0
  69. mcp_proxy_adapter/tests/test_utils.py +65 -0
  70. mcp_proxy_adapter/tests/unit/__init__.py +3 -0
  71. mcp_proxy_adapter/tests/unit/test_base_command.py +130 -0
  72. mcp_proxy_adapter/tests/unit/test_config.py +217 -0
  73. mcp_proxy_adapter/version.py +1 -1
  74. {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/METADATA +1 -1
  75. mcp_proxy_adapter-3.0.1.dist-info/RECORD +109 -0
  76. examples/basic_example/config.yaml +0 -20
  77. examples/basic_example/main.py +0 -50
  78. examples/complete_example/main.py +0 -67
  79. examples/minimal_example/config.yaml +0 -26
  80. mcp_proxy_adapter/framework.py +0 -109
  81. mcp_proxy_adapter-3.0.0.dist-info/RECORD +0 -58
  82. {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/WHEEL +0 -0
  83. {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/licenses/LICENSE +0 -0
  84. {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,215 @@
1
+ """
2
+ Module with base classes for command results.
3
+ """
4
+
5
+ import json
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Dict, List, Optional, Type, TypeVar, Union
8
+
9
+ T = TypeVar("T", bound="CommandResult")
10
+
11
+
12
+ class CommandResult(ABC):
13
+ """
14
+ Base abstract class for command execution results.
15
+ """
16
+
17
+ @abstractmethod
18
+ def to_dict(self) -> Dict[str, Any]:
19
+ """
20
+ Converts result to dictionary for serialization.
21
+
22
+ Returns:
23
+ Dictionary with result data.
24
+ """
25
+ pass
26
+
27
+ @classmethod
28
+ @abstractmethod
29
+ def get_schema(cls) -> Dict[str, Any]:
30
+ """
31
+ Returns JSON schema for result validation.
32
+
33
+ Returns:
34
+ Dictionary with JSON schema.
35
+ """
36
+ pass
37
+
38
+ def to_json(self, indent: Optional[int] = None) -> str:
39
+ """
40
+ Converts result to JSON string.
41
+
42
+ Args:
43
+ indent: Indentation for JSON formatting.
44
+
45
+ Returns:
46
+ JSON string with result.
47
+ """
48
+ return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent)
49
+
50
+ @classmethod
51
+ def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
52
+ """
53
+ Creates result instance from dictionary.
54
+ This method must be overridden in subclasses.
55
+
56
+ Args:
57
+ data: Dictionary with result data.
58
+
59
+ Returns:
60
+ Result instance.
61
+ """
62
+ raise NotImplementedError("Method from_dict must be implemented in subclasses")
63
+
64
+
65
+ class SuccessResult(CommandResult):
66
+ """
67
+ Base class for successful command results.
68
+ """
69
+
70
+ def __init__(self, data: Optional[Dict[str, Any]] = None, message: Optional[str] = None):
71
+ """
72
+ Initialize successful result.
73
+
74
+ Args:
75
+ data: Result data.
76
+ message: Result message.
77
+ """
78
+ self.data = data or {}
79
+ self.message = message
80
+
81
+ def to_dict(self) -> Dict[str, Any]:
82
+ """
83
+ Converts result to dictionary for serialization.
84
+
85
+ Returns:
86
+ Dictionary with result data.
87
+ """
88
+ result = {"success": True}
89
+ if self.data:
90
+ result["data"] = self.data
91
+ if self.message:
92
+ result["message"] = self.message
93
+ return result
94
+
95
+ @classmethod
96
+ def get_schema(cls) -> Dict[str, Any]:
97
+ """
98
+ Returns JSON schema for result validation.
99
+
100
+ Returns:
101
+ Dictionary with JSON schema.
102
+ """
103
+ return {
104
+ "type": "object",
105
+ "properties": {
106
+ "success": {"type": "boolean"},
107
+ "data": {"type": "object"},
108
+ "message": {"type": "string"}
109
+ },
110
+ "required": ["success"]
111
+ }
112
+
113
+ @classmethod
114
+ def from_dict(cls, data: Dict[str, Any]) -> "SuccessResult":
115
+ """
116
+ Creates successful result instance from dictionary.
117
+
118
+ Args:
119
+ data: Dictionary with result data.
120
+
121
+ Returns:
122
+ Successful result instance.
123
+ """
124
+ return cls(
125
+ data=data.get("data"),
126
+ message=data.get("message")
127
+ )
128
+
129
+
130
+ class ErrorResult(CommandResult):
131
+ """
132
+ Base class for command results with error.
133
+
134
+ This class follows the JSON-RPC 2.0 error object format:
135
+ https://www.jsonrpc.org/specification#error_object
136
+ """
137
+
138
+ def __init__(
139
+ self,
140
+ message: str,
141
+ code: int = -32000,
142
+ details: Optional[Dict[str, Any]] = None
143
+ ):
144
+ """
145
+ Initialize error result.
146
+
147
+ Args:
148
+ message: Error message.
149
+ code: Error code (following JSON-RPC 2.0 spec).
150
+ details: Additional error details.
151
+ """
152
+ self.message = message
153
+ self.code = code
154
+ self.details = details or {}
155
+
156
+ def to_dict(self) -> Dict[str, Any]:
157
+ """
158
+ Converts result to dictionary for serialization.
159
+
160
+ Returns:
161
+ Dictionary with result data in JSON-RPC 2.0 error format.
162
+ """
163
+ result = {
164
+ "success": False,
165
+ "error": {
166
+ "code": self.code,
167
+ "message": self.message
168
+ }
169
+ }
170
+ if self.details:
171
+ result["error"]["data"] = self.details
172
+ return result
173
+
174
+ @classmethod
175
+ def get_schema(cls) -> Dict[str, Any]:
176
+ """
177
+ Returns JSON schema for result validation.
178
+
179
+ Returns:
180
+ Dictionary with JSON schema.
181
+ """
182
+ return {
183
+ "type": "object",
184
+ "properties": {
185
+ "success": {"type": "boolean"},
186
+ "error": {
187
+ "type": "object",
188
+ "properties": {
189
+ "code": {"type": "integer"},
190
+ "message": {"type": "string"},
191
+ "data": {"type": "object"}
192
+ },
193
+ "required": ["code", "message"]
194
+ }
195
+ },
196
+ "required": ["success", "error"]
197
+ }
198
+
199
+ @classmethod
200
+ def from_dict(cls, data: Dict[str, Any]) -> "ErrorResult":
201
+ """
202
+ Creates error result instance from dictionary.
203
+
204
+ Args:
205
+ data: Dictionary with result data.
206
+
207
+ Returns:
208
+ Error result instance.
209
+ """
210
+ error = data.get("error", {})
211
+ return cls(
212
+ message=error.get("message", "Unknown error"),
213
+ code=error.get("code", -32000),
214
+ details=error.get("data")
215
+ )
@@ -126,6 +126,15 @@ class Config:
126
126
 
127
127
  return value
128
128
 
129
+ def get_all(self) -> Dict[str, Any]:
130
+ """
131
+ Get all configuration values.
132
+
133
+ Returns:
134
+ Dictionary with all configuration values
135
+ """
136
+ return self.config_data.copy()
137
+
129
138
  def set(self, key: str, value: Any) -> None:
130
139
  """
131
140
  Set configuration value for key.
File without changes
@@ -0,0 +1,173 @@
1
+ """
2
+ Module for defining errors and exceptions for the microservice.
3
+ """
4
+
5
+ from typing import Any, Dict, List, Optional, Union
6
+
7
+
8
+ class MicroserviceError(Exception):
9
+ """
10
+ Base class for all microservice exceptions.
11
+
12
+ Attributes:
13
+ message: Error message.
14
+ code: Error code.
15
+ data: Additional error data.
16
+ """
17
+ def __init__(self, message: str, code: int = -32000, data: Optional[Dict[str, Any]] = None):
18
+ """
19
+ Initialize the error.
20
+
21
+ Args:
22
+ message: Error message.
23
+ code: Error code according to JSON-RPC standard.
24
+ data: Additional error data.
25
+ """
26
+ self.message = message
27
+ self.code = code
28
+ self.data = data or {}
29
+ super().__init__(message)
30
+
31
+ def to_dict(self) -> Dict[str, Any]:
32
+ """
33
+ Converts the error to a dictionary for JSON-RPC response.
34
+
35
+ Returns:
36
+ Dictionary with error information.
37
+ """
38
+ result = {
39
+ "code": self.code,
40
+ "message": self.message
41
+ }
42
+
43
+ if self.data:
44
+ result["data"] = self.data
45
+
46
+ return result
47
+
48
+
49
+ class ParseError(MicroserviceError):
50
+ """
51
+ Error while parsing JSON request.
52
+ JSON-RPC Error code: -32700
53
+ """
54
+ def __init__(self, message: str = "Parse error", data: Optional[Dict[str, Any]] = None):
55
+ super().__init__(message, code=-32700, data=data)
56
+
57
+
58
+ class InvalidRequestError(MicroserviceError):
59
+ """
60
+ Invalid JSON-RPC request format.
61
+ JSON-RPC Error code: -32600
62
+ """
63
+ def __init__(self, message: str = "Invalid Request", data: Optional[Dict[str, Any]] = None):
64
+ super().__init__(message, code=-32600, data=data)
65
+
66
+
67
+ class MethodNotFoundError(MicroserviceError):
68
+ """
69
+ Method not found error.
70
+ JSON-RPC Error code: -32601
71
+ """
72
+ def __init__(self, message: str = "Method not found", data: Optional[Dict[str, Any]] = None):
73
+ super().__init__(message, code=-32601, data=data)
74
+
75
+
76
+ class InvalidParamsError(MicroserviceError):
77
+ """
78
+ Invalid method parameters.
79
+ JSON-RPC Error code: -32602
80
+ """
81
+ def __init__(self, message: str = "Invalid params", data: Optional[Dict[str, Any]] = None):
82
+ super().__init__(message, code=-32602, data=data)
83
+
84
+
85
+ class InternalError(MicroserviceError):
86
+ """
87
+ Internal server error.
88
+ JSON-RPC Error code: -32603
89
+ """
90
+ def __init__(self, message: str = "Internal error", data: Optional[Dict[str, Any]] = None):
91
+ super().__init__(message, code=-32603, data=data)
92
+
93
+
94
+ class ValidationError(MicroserviceError):
95
+ """
96
+ Input data validation error.
97
+ JSON-RPC Error code: -32602 (using Invalid params code)
98
+ """
99
+ def __init__(self, message: str = "Validation error", data: Optional[Dict[str, Any]] = None):
100
+ super().__init__(message, code=-32602, data=data)
101
+
102
+
103
+ class CommandError(MicroserviceError):
104
+ """
105
+ Command execution error.
106
+ JSON-RPC Error code: -32000 (server error)
107
+ """
108
+ def __init__(self, message: str = "Command execution error", data: Optional[Dict[str, Any]] = None):
109
+ super().__init__(message, code=-32000, data=data)
110
+
111
+
112
+ class NotFoundError(MicroserviceError):
113
+ """
114
+ "Not found" error.
115
+ JSON-RPC Error code: -32601 (using Method not found code)
116
+ """
117
+ def __init__(self, message: str = "Resource not found", data: Optional[Dict[str, Any]] = None):
118
+ super().__init__(message, code=-32601, data=data)
119
+
120
+
121
+ class ConfigurationError(MicroserviceError):
122
+ """
123
+ Configuration error.
124
+ JSON-RPC Error code: -32603 (using Internal error code)
125
+ """
126
+ def __init__(self, message: str = "Configuration error", data: Optional[Dict[str, Any]] = None):
127
+ super().__init__(message, code=-32603, data=data)
128
+
129
+
130
+ class AuthenticationError(MicroserviceError):
131
+ """
132
+ Authentication error.
133
+ JSON-RPC Error code: -32001 (server error)
134
+ """
135
+ def __init__(self, message: str = "Authentication error", data: Optional[Dict[str, Any]] = None):
136
+ super().__init__(message, code=-32001, data=data)
137
+
138
+
139
+ class AuthorizationError(MicroserviceError):
140
+ """
141
+ Authorization error.
142
+ JSON-RPC Error code: -32002 (server error)
143
+ """
144
+ def __init__(self, message: str = "Authorization error", data: Optional[Dict[str, Any]] = None):
145
+ super().__init__(message, code=-32002, data=data)
146
+
147
+
148
+ class TimeoutError(MicroserviceError):
149
+ """
150
+ Timeout error.
151
+ JSON-RPC Error code: -32003 (server error)
152
+ """
153
+ def __init__(self, message: str = "Timeout error", data: Optional[Dict[str, Any]] = None):
154
+ super().__init__(message, code=-32003, data=data)
155
+
156
+
157
+ def format_validation_errors(errors: List[Dict[str, Any]]) -> Dict[str, Any]:
158
+ """
159
+ Formats validation errors into a standard format.
160
+
161
+ Args:
162
+ errors: List of validation errors.
163
+
164
+ Returns:
165
+ Formatted validation errors.
166
+ """
167
+ formatted_errors = {}
168
+ for error in errors:
169
+ loc = error.get("loc", [])
170
+ field = ".".join(str(item) for item in loc)
171
+ msg = error.get("msg", "Validation error")
172
+ formatted_errors[field] = msg
173
+ return formatted_errors
@@ -0,0 +1,205 @@
1
+ """
2
+ Module for configuring logging in the microservice.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import sys
8
+ import uuid
9
+ from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
10
+ from typing import Dict, Optional, Any
11
+
12
+ from mcp_proxy_adapter.config import config
13
+
14
+
15
+ class CustomFormatter(logging.Formatter):
16
+ """
17
+ Custom formatter for logs with colored output in console.
18
+ """
19
+ grey = "\x1b[38;20m"
20
+ yellow = "\x1b[33;20m"
21
+ red = "\x1b[31;20m"
22
+ bold_red = "\x1b[31;1m"
23
+ reset = "\x1b[0m"
24
+ format_str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
25
+
26
+ FORMATS = {
27
+ logging.DEBUG: grey + format_str + reset,
28
+ logging.INFO: grey + format_str + reset,
29
+ logging.WARNING: yellow + format_str + reset,
30
+ logging.ERROR: red + format_str + reset,
31
+ logging.CRITICAL: bold_red + format_str + reset
32
+ }
33
+
34
+ def format(self, record):
35
+ log_fmt = self.FORMATS.get(record.levelno)
36
+ formatter = logging.Formatter(log_fmt)
37
+ return formatter.format(record)
38
+
39
+
40
+ class RequestContextFilter(logging.Filter):
41
+ """
42
+ Filter for adding request context to logs.
43
+ """
44
+
45
+ def __init__(self, request_id: Optional[str] = None):
46
+ super().__init__()
47
+ self.request_id = request_id
48
+
49
+ def filter(self, record):
50
+ # Add request_id attribute to the record
51
+ record.request_id = self.request_id or "no-request-id"
52
+ return True
53
+
54
+
55
+ class RequestLogger:
56
+ """
57
+ Logger class for logging requests with context.
58
+ """
59
+
60
+ def __init__(self, logger_name: str, request_id: Optional[str] = None):
61
+ """
62
+ Initialize request logger.
63
+
64
+ Args:
65
+ logger_name: Logger name.
66
+ request_id: Request identifier.
67
+ """
68
+ self.logger = logging.getLogger(logger_name)
69
+ self.request_id = request_id or str(uuid.uuid4())
70
+ self.filter = RequestContextFilter(self.request_id)
71
+ self.logger.addFilter(self.filter)
72
+
73
+ def debug(self, msg: str, *args, **kwargs):
74
+ """Log message with DEBUG level."""
75
+ self.logger.debug(f"[{self.request_id}] {msg}", *args, **kwargs)
76
+
77
+ def info(self, msg: str, *args, **kwargs):
78
+ """Log message with INFO level."""
79
+ self.logger.info(f"[{self.request_id}] {msg}", *args, **kwargs)
80
+
81
+ def warning(self, msg: str, *args, **kwargs):
82
+ """Log message with WARNING level."""
83
+ self.logger.warning(f"[{self.request_id}] {msg}", *args, **kwargs)
84
+
85
+ def error(self, msg: str, *args, **kwargs):
86
+ """Log message with ERROR level."""
87
+ self.logger.error(f"[{self.request_id}] {msg}", *args, **kwargs)
88
+
89
+ def exception(self, msg: str, *args, **kwargs):
90
+ """Log exception with traceback."""
91
+ self.logger.exception(f"[{self.request_id}] {msg}", *args, **kwargs)
92
+
93
+ def critical(self, msg: str, *args, **kwargs):
94
+ """Log message with CRITICAL level."""
95
+ self.logger.critical(f"[{self.request_id}] {msg}", *args, **kwargs)
96
+
97
+
98
+ def setup_logging(
99
+ level: Optional[str] = None,
100
+ log_file: Optional[str] = None,
101
+ max_bytes: Optional[int] = None,
102
+ backup_count: Optional[int] = None,
103
+ rotation_type: Optional[str] = None,
104
+ rotation_when: Optional[str] = None,
105
+ rotation_interval: Optional[int] = None
106
+ ) -> logging.Logger:
107
+ """
108
+ Configure logging for the microservice.
109
+
110
+ Args:
111
+ level: Logging level. By default, taken from configuration.
112
+ log_file: Path to log file. By default, taken from configuration.
113
+ max_bytes: Maximum log file size in bytes. By default, taken from configuration.
114
+ backup_count: Number of rotation files. By default, taken from configuration.
115
+ rotation_type: Type of log rotation ('size' or 'time'). By default, taken from configuration.
116
+ rotation_when: Time unit for rotation (D, H, M, S). By default, taken from configuration.
117
+ rotation_interval: Interval for rotation. By default, taken from configuration.
118
+
119
+ Returns:
120
+ Configured logger.
121
+ """
122
+ # Get parameters from configuration if not explicitly specified
123
+ level = level or config.get("logging.level", "INFO")
124
+ log_file = log_file or config.get("logging.file")
125
+ rotation_type = rotation_type or config.get("logging.rotation.type", "size")
126
+
127
+ # Size-based rotation parameters
128
+ max_bytes = max_bytes or config.get("logging.rotation.max_bytes", 10 * 1024 * 1024) # 10 MB by default
129
+ backup_count = backup_count or config.get("logging.rotation.backup_count", 5)
130
+
131
+ # Time-based rotation parameters
132
+ rotation_when = rotation_when or config.get("logging.rotation.when", "D") # Daily by default
133
+ rotation_interval = rotation_interval or config.get("logging.rotation.interval", 1)
134
+
135
+ # Convert string logging level to constant
136
+ numeric_level = getattr(logging, level.upper(), None)
137
+ if not isinstance(numeric_level, int):
138
+ numeric_level = logging.INFO
139
+
140
+ # Create root logger
141
+ logger = logging.getLogger("mcp_proxy_adapter")
142
+ logger.setLevel(numeric_level)
143
+ logger.handlers = [] # Clear handlers in case of repeated call
144
+
145
+ # Create console handler
146
+ console_handler = logging.StreamHandler(sys.stdout)
147
+ console_handler.setLevel(numeric_level)
148
+ console_handler.setFormatter(CustomFormatter())
149
+ logger.addHandler(console_handler)
150
+
151
+ # Create file handler if file specified
152
+ if log_file:
153
+ # Create directory for log file if it doesn't exist
154
+ log_dir = os.path.dirname(log_file)
155
+ if log_dir and not os.path.exists(log_dir):
156
+ os.makedirs(log_dir, exist_ok=True)
157
+
158
+ # Choose rotation type
159
+ if rotation_type.lower() == "time":
160
+ file_handler = TimedRotatingFileHandler(
161
+ log_file,
162
+ when=rotation_when,
163
+ interval=rotation_interval,
164
+ backupCount=backup_count,
165
+ encoding="utf-8"
166
+ )
167
+ else: # Default to size-based rotation
168
+ file_handler = RotatingFileHandler(
169
+ log_file,
170
+ maxBytes=max_bytes,
171
+ backupCount=backup_count,
172
+ encoding="utf-8"
173
+ )
174
+
175
+ file_handler.setLevel(numeric_level)
176
+ file_formatter = logging.Formatter(
177
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
178
+ )
179
+ file_handler.setFormatter(file_formatter)
180
+ logger.addHandler(file_handler)
181
+
182
+ # Configure loggers for external libraries
183
+ log_levels = config.get("logging.levels", {})
184
+ for logger_name, logger_level in log_levels.items():
185
+ lib_logger = logging.getLogger(logger_name)
186
+ lib_logger.setLevel(getattr(logging, logger_level.upper(), logging.INFO))
187
+
188
+ return logger
189
+
190
+
191
+ def get_logger(name: str) -> logging.Logger:
192
+ """
193
+ Get a logger with the specified name.
194
+
195
+ Args:
196
+ name: Logger name.
197
+
198
+ Returns:
199
+ Configured logger instance.
200
+ """
201
+ return logging.getLogger(name)
202
+
203
+
204
+ # Global logger for use throughout the application
205
+ logger = setup_logging()