spring-ready-python 0.1.0__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.
@@ -0,0 +1,123 @@
1
+ """
2
+ Actuator info endpoint.
3
+ Provides application metadata like Spring Boot Actuator's /actuator/info.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import logging
9
+ from typing import Dict, Any, Optional
10
+ from datetime import datetime
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class InfoEndpoint:
16
+ """
17
+ Info endpoint that provides application metadata.
18
+
19
+ Matches Spring Boot Actuator's info endpoint format:
20
+ - app: Application info (name, version, description)
21
+ - build: Build information
22
+ - git: Git commit info (if available)
23
+ - java: Runtime info (adapted for Python)
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ app_name: Optional[str] = None,
29
+ app_version: Optional[str] = None,
30
+ app_description: Optional[str] = None
31
+ ):
32
+ self.app_name = app_name or os.getenv("SPRING_APPLICATION_NAME", "unknown")
33
+ self.app_version = app_version or os.getenv("APP_VERSION", "unknown")
34
+ self.app_description = app_description or os.getenv("APP_DESCRIPTION", "")
35
+ self.custom_info: Dict[str, Any] = {}
36
+
37
+ def add_info(self, key: str, value: Any) -> None:
38
+ """Add custom info field"""
39
+ self.custom_info[key] = value
40
+
41
+ def get_info(self) -> Dict[str, Any]:
42
+ """
43
+ Get application info.
44
+
45
+ Returns:
46
+ Info response matching Spring Boot Actuator format
47
+ """
48
+ info = {
49
+ "app": {
50
+ "name": self.app_name,
51
+ "version": self.app_version,
52
+ }
53
+ }
54
+
55
+ if self.app_description:
56
+ info["app"]["description"] = self.app_description
57
+
58
+ # Python runtime info (equivalent to java info in Spring)
59
+ info["python"] = {
60
+ "version": sys.version,
61
+ "vendor": sys.copyright.split("\n")[0] if sys.copyright else "Python Software Foundation",
62
+ "runtime": {
63
+ "name": "CPython",
64
+ "version": sys.version.split()[0]
65
+ },
66
+ "jvm": { # Keep "jvm" key for compatibility with Spring Admin
67
+ "name": f"Python {sys.version.split()[0]}",
68
+ "vendor": "Python Software Foundation",
69
+ "version": sys.version.split()[0]
70
+ }
71
+ }
72
+
73
+ # Build info from environment (if available)
74
+ if any(os.getenv(k) for k in ["BUILD_NUMBER", "BUILD_TIME", "GIT_COMMIT"]):
75
+ info["build"] = {}
76
+
77
+ if build_number := os.getenv("BUILD_NUMBER"):
78
+ info["build"]["number"] = build_number
79
+
80
+ if build_time := os.getenv("BUILD_TIME"):
81
+ info["build"]["time"] = build_time
82
+
83
+ if git_commit := os.getenv("GIT_COMMIT"):
84
+ info["build"]["commit"] = git_commit
85
+
86
+ # Git info (if available)
87
+ if git_commit := os.getenv("GIT_COMMIT"):
88
+ info["git"] = {
89
+ "commit": {
90
+ "id": git_commit,
91
+ "time": os.getenv("GIT_COMMIT_TIME", "")
92
+ },
93
+ "branch": os.getenv("GIT_BRANCH", "")
94
+ }
95
+
96
+ # Add custom info
97
+ if self.custom_info:
98
+ info.update(self.custom_info)
99
+
100
+ return info
101
+
102
+
103
+ def create_default_info_endpoint(
104
+ app_name: Optional[str] = None,
105
+ app_version: Optional[str] = None,
106
+ app_description: Optional[str] = None
107
+ ) -> InfoEndpoint:
108
+ """
109
+ Create info endpoint with default configuration.
110
+
111
+ Args:
112
+ app_name: Application name (default from env)
113
+ app_version: Application version (default from env)
114
+ app_description: Application description (default from env)
115
+
116
+ Returns:
117
+ InfoEndpoint instance
118
+ """
119
+ return InfoEndpoint(
120
+ app_name=app_name,
121
+ app_version=app_version,
122
+ app_description=app_description
123
+ )
@@ -0,0 +1,201 @@
1
+ """
2
+ Actuator Logfile Endpoint.
3
+ Provides access to application log files.
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ from typing import Optional
9
+ from pathlib import Path
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class LogfileEndpoint:
15
+ """
16
+ Logfile endpoint for Spring Boot Actuator compatibility.
17
+
18
+ Provides access to application log files with support for:
19
+ - Full log file retrieval
20
+ - Partial retrieval with Range header support
21
+ """
22
+
23
+ def __init__(self, log_file_path: Optional[str] = None):
24
+ """
25
+ Args:
26
+ log_file_path: Path to the log file (default: None, which means no log file)
27
+ """
28
+ self.log_file_path = log_file_path
29
+
30
+ # Verify log file exists if path provided
31
+ if self.log_file_path and not os.path.exists(self.log_file_path):
32
+ logger.warning(f"Log file not found: {self.log_file_path}")
33
+
34
+ def get_logfile(self, range_header: Optional[str] = None) -> tuple[Optional[bytes], Optional[str], int]:
35
+ """
36
+ Get log file contents.
37
+
38
+ Args:
39
+ range_header: HTTP Range header value (e.g., "bytes=0-1023")
40
+
41
+ Returns:
42
+ Tuple of (content, content_range_header, status_code)
43
+ - content: Log file bytes or None if not available
44
+ - content_range_header: Content-Range header value or None
45
+ - status_code: HTTP status code (200, 206, or 404)
46
+ """
47
+ # If no log file configured
48
+ if not self.log_file_path:
49
+ logger.warning("No log file configured for logfile endpoint")
50
+ return None, None, 404
51
+
52
+ # If log file doesn't exist
53
+ if not os.path.exists(self.log_file_path):
54
+ logger.warning(f"Log file not found: {self.log_file_path}")
55
+ return None, None, 404
56
+
57
+ try:
58
+ file_size = os.path.getsize(self.log_file_path)
59
+
60
+ # Handle range request
61
+ if range_header:
62
+ # Parse range header (e.g., "bytes=0-1023")
63
+ if range_header.startswith("bytes="):
64
+ range_spec = range_header[6:]
65
+ start, end = self._parse_range(range_spec, file_size)
66
+
67
+ if start is None or end is None:
68
+ # Invalid range
69
+ return None, None, 416 # Range Not Satisfiable
70
+
71
+ # Read the specified range
72
+ with open(self.log_file_path, 'rb') as f:
73
+ f.seek(start)
74
+ content = f.read(end - start + 1)
75
+
76
+ # Build Content-Range header
77
+ content_range = f"bytes {start}-{end}/{file_size}"
78
+ return content, content_range, 206 # Partial Content
79
+
80
+ # Read entire file
81
+ with open(self.log_file_path, 'rb') as f:
82
+ content = f.read()
83
+
84
+ return content, None, 200
85
+
86
+ except Exception as e:
87
+ logger.error(f"Error reading log file: {e}", exc_info=True)
88
+ return None, None, 500
89
+
90
+ def _parse_range(self, range_spec: str, file_size: int) -> tuple[Optional[int], Optional[int]]:
91
+ """
92
+ Parse HTTP Range specification.
93
+
94
+ Args:
95
+ range_spec: Range specification (e.g., "0-1023" or "-1024" or "1024-")
96
+ file_size: Total file size
97
+
98
+ Returns:
99
+ Tuple of (start, end) byte positions, or (None, None) if invalid
100
+ """
101
+ try:
102
+ if '-' not in range_spec:
103
+ return None, None
104
+
105
+ parts = range_spec.split('-', 1)
106
+
107
+ # Handle "-1024" (last 1024 bytes)
108
+ if not parts[0]:
109
+ suffix_length = int(parts[1])
110
+ start = max(0, file_size - suffix_length)
111
+ end = file_size - 1
112
+ return start, end
113
+
114
+ # Handle "1024-" (from byte 1024 to end)
115
+ if not parts[1]:
116
+ start = int(parts[0])
117
+ end = file_size - 1
118
+ return start, end
119
+
120
+ # Handle "0-1023" (bytes 0 to 1023)
121
+ start = int(parts[0])
122
+ end = int(parts[1])
123
+
124
+ # Validate range
125
+ if start < 0 or end >= file_size or start > end:
126
+ return None, None
127
+
128
+ return start, end
129
+
130
+ except (ValueError, IndexError):
131
+ return None, None
132
+
133
+ def is_available(self) -> bool:
134
+ """Check if log file is available"""
135
+ return (
136
+ self.log_file_path is not None
137
+ and os.path.exists(self.log_file_path)
138
+ )
139
+
140
+
141
+ def create_default_logfile_endpoint(log_file_path: Optional[str] = None) -> LogfileEndpoint:
142
+ """
143
+ Create logfile endpoint with default configuration.
144
+
145
+ Automatically detects log file from Python logging handlers if not specified.
146
+ Priority: explicit parameter > LOG_FILE_PATH env var > auto-detected from logging
147
+
148
+ Args:
149
+ log_file_path: Path to log file (default: auto-detect or from env LOG_FILE_PATH)
150
+
151
+ Returns:
152
+ LogfileEndpoint instance
153
+ """
154
+ # Priority 1: Explicit parameter
155
+ if log_file_path is not None:
156
+ return LogfileEndpoint(log_file_path=log_file_path)
157
+
158
+ # Priority 2: Environment variable
159
+ log_file_path = os.getenv("LOG_FILE_PATH")
160
+ if log_file_path is not None:
161
+ return LogfileEndpoint(log_file_path=log_file_path)
162
+
163
+ # Priority 3: Auto-detect from Python logging handlers
164
+ log_file_path = _auto_detect_log_file()
165
+
166
+ return LogfileEndpoint(log_file_path=log_file_path)
167
+
168
+
169
+ def _auto_detect_log_file() -> Optional[str]:
170
+ """
171
+ Auto-detect log file path from Python logging configuration.
172
+
173
+ Scans all logging handlers for FileHandler, RotatingFileHandler,
174
+ TimedRotatingFileHandler and extracts the file path.
175
+
176
+ Returns:
177
+ Detected log file path or None if no file handler found
178
+ """
179
+ try:
180
+ # Check root logger first
181
+ for handler in logging.root.handlers:
182
+ if isinstance(handler, logging.FileHandler):
183
+ log_path = handler.baseFilename
184
+ logger.info(f"Auto-detected log file: {log_path}")
185
+ return log_path
186
+
187
+ # Check all other loggers
188
+ for logger_name in logging.Logger.manager.loggerDict:
189
+ log_instance = logging.getLogger(logger_name)
190
+ if hasattr(log_instance, 'handlers'):
191
+ for handler in log_instance.handlers:
192
+ if isinstance(handler, logging.FileHandler):
193
+ log_path = handler.baseFilename
194
+ logger.info(f"Auto-detected log file from logger '{logger_name}': {log_path}")
195
+ return log_path
196
+
197
+ logger.debug("No log file found in logging configuration")
198
+ return None
199
+ except Exception as e:
200
+ logger.warning(f"Failed to auto-detect log file: {e}")
201
+ return None
@@ -0,0 +1,184 @@
1
+ """
2
+ Actuator Loggers Endpoint.
3
+ Shows and manages logger configurations and levels.
4
+ """
5
+
6
+ import logging
7
+ from typing import Dict, Any, Optional, List
8
+
9
+
10
+ # Valid log levels
11
+ VALID_LEVELS = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL", "OFF"]
12
+
13
+ # Map Spring Boot levels to Python levels
14
+ LEVEL_MAPPING = {
15
+ "TRACE": logging.DEBUG, # Python doesn't have TRACE, use DEBUG
16
+ "DEBUG": logging.DEBUG,
17
+ "INFO": logging.INFO,
18
+ "WARN": logging.WARNING,
19
+ "ERROR": logging.ERROR,
20
+ "CRITICAL": logging.CRITICAL,
21
+ "OFF": logging.CRITICAL + 10, # Effectively disable logging
22
+ }
23
+
24
+ # Reverse mapping for display
25
+ PYTHON_TO_SPRING = {
26
+ logging.DEBUG: "DEBUG",
27
+ logging.INFO: "INFO",
28
+ logging.WARNING: "WARN",
29
+ logging.ERROR: "ERROR",
30
+ logging.CRITICAL: "CRITICAL",
31
+ }
32
+
33
+
34
+ class LoggersEndpoint:
35
+ """
36
+ Loggers endpoint for Spring Boot Actuator compatibility.
37
+
38
+ Provides:
39
+ - List all loggers with their levels
40
+ - Get individual logger configuration
41
+ - Set/update logger levels
42
+ - Clear logger levels (reset to inherited)
43
+ """
44
+
45
+ def __init__(self):
46
+ """Initialize loggers endpoint"""
47
+ pass
48
+
49
+ def _get_logger_level_name(self, level: Optional[int]) -> Optional[str]:
50
+ """
51
+ Convert Python log level to Spring Boot level name.
52
+
53
+ Args:
54
+ level: Python log level integer
55
+
56
+ Returns:
57
+ Spring Boot level name or None
58
+ """
59
+ if level is None:
60
+ return None
61
+ return PYTHON_TO_SPRING.get(level, "DEBUG")
62
+
63
+ def _get_logger_info(self, logger: logging.Logger) -> Dict[str, Any]:
64
+ """
65
+ Get logger information.
66
+
67
+ Args:
68
+ logger: Logger instance
69
+
70
+ Returns:
71
+ Dictionary with configuredLevel and effectiveLevel
72
+ """
73
+ # Get configured level (may be None if inherited)
74
+ configured_level = logger.level if logger.level != logging.NOTSET else None
75
+ configured_level_name = self._get_logger_level_name(configured_level)
76
+
77
+ # Get effective level (always has a value due to inheritance)
78
+ effective_level = logger.getEffectiveLevel()
79
+ effective_level_name = self._get_logger_level_name(effective_level)
80
+
81
+ return {
82
+ "configuredLevel": configured_level_name,
83
+ "effectiveLevel": effective_level_name
84
+ }
85
+
86
+ def get_all_loggers(self) -> Dict[str, Any]:
87
+ """
88
+ Get all loggers.
89
+
90
+ Returns:
91
+ Dictionary with levels list and loggers dict
92
+ """
93
+ loggers_dict = {}
94
+
95
+ # Get root logger
96
+ root_logger = logging.getLogger()
97
+ loggers_dict["ROOT"] = self._get_logger_info(root_logger)
98
+
99
+ # Get all other loggers
100
+ # Access the internal logger dictionary
101
+ logger_dict = logging.Logger.manager.loggerDict
102
+ for name, logger_item in sorted(logger_dict.items()):
103
+ # Skip PlaceHolder objects, only include actual Logger instances
104
+ if isinstance(logger_item, logging.Logger):
105
+ loggers_dict[name] = self._get_logger_info(logger_item)
106
+
107
+ return {
108
+ "levels": VALID_LEVELS,
109
+ "loggers": loggers_dict,
110
+ "groups": {}
111
+ }
112
+
113
+ def get_logger(self, name: str) -> Optional[Dict[str, Any]]:
114
+ """
115
+ Get a single logger by name.
116
+
117
+ Args:
118
+ name: Logger name (use "ROOT" for root logger)
119
+
120
+ Returns:
121
+ Logger info or None if not found
122
+ """
123
+ if name == "ROOT":
124
+ logger = logging.getLogger()
125
+ else:
126
+ # Check if logger exists
127
+ if name not in logging.Logger.manager.loggerDict:
128
+ return None
129
+ logger = logging.getLogger(name)
130
+
131
+ return self._get_logger_info(logger)
132
+
133
+ def set_logger_level(self, name: str, level: Optional[str]) -> bool:
134
+ """
135
+ Set logger level.
136
+
137
+ Args:
138
+ name: Logger name (use "ROOT" for root logger)
139
+ level: Level name (DEBUG, INFO, WARN, ERROR, etc.) or None to clear
140
+
141
+ Returns:
142
+ True if successful, False if logger not found or invalid level
143
+ """
144
+ # Validate level
145
+ if level is not None and level not in LEVEL_MAPPING:
146
+ return False
147
+
148
+ # Get logger
149
+ if name == "ROOT":
150
+ logger = logging.getLogger()
151
+ else:
152
+ logger = logging.getLogger(name)
153
+
154
+ # Set level
155
+ if level is None:
156
+ # Clear level (set to NOTSET to inherit from parent)
157
+ logger.setLevel(logging.NOTSET)
158
+ else:
159
+ python_level = LEVEL_MAPPING[level]
160
+ logger.setLevel(python_level)
161
+
162
+ return True
163
+
164
+ def clear_logger_level(self, name: str) -> bool:
165
+ """
166
+ Clear logger level (reset to inherited).
167
+
168
+ Args:
169
+ name: Logger name
170
+
171
+ Returns:
172
+ True if successful
173
+ """
174
+ return self.set_logger_level(name, None)
175
+
176
+
177
+ def create_default_loggers_endpoint() -> LoggersEndpoint:
178
+ """
179
+ Create loggers endpoint.
180
+
181
+ Returns:
182
+ LoggersEndpoint instance
183
+ """
184
+ return LoggersEndpoint()
@@ -0,0 +1,88 @@
1
+ """
2
+ Actuator Mappings Endpoint.
3
+ Shows all registered request mappings (routes) in the application.
4
+ """
5
+
6
+ from typing import Dict, Any, List
7
+ from fastapi import FastAPI
8
+ from fastapi.routing import APIRoute
9
+
10
+
11
+ class MappingsEndpoint:
12
+ """
13
+ Mappings endpoint for Spring Boot Actuator compatibility.
14
+
15
+ Shows all registered FastAPI routes with their HTTP methods and handlers.
16
+ """
17
+
18
+ def __init__(self, app: FastAPI):
19
+ """
20
+ Args:
21
+ app: FastAPI application instance
22
+ """
23
+ self.app = app
24
+
25
+ def get_mappings(self) -> Dict[str, Any]:
26
+ """
27
+ Get all request mappings.
28
+
29
+ Returns:
30
+ Dictionary with contexts and dispatcher servlet mappings
31
+ """
32
+ mappings = []
33
+
34
+ # Iterate through all routes
35
+ for route in self.app.routes:
36
+ if isinstance(route, APIRoute):
37
+ # Get handler info
38
+ handler_name = route.endpoint.__name__ if hasattr(route, 'endpoint') else "unknown"
39
+ handler_module = route.endpoint.__module__ if hasattr(route, 'endpoint') else "unknown"
40
+
41
+ # Get methods
42
+ methods = list(route.methods) if hasattr(route, 'methods') else []
43
+
44
+ # Build mapping entry
45
+ mapping = {
46
+ "handler": f"{handler_module}.{handler_name}",
47
+ "predicate": f"{{{', '.join(methods)}}} {route.path}",
48
+ "details": {
49
+ "requestMappingConditions": {
50
+ "methods": methods,
51
+ "patterns": [route.path]
52
+ }
53
+ }
54
+ }
55
+
56
+ mappings.append(mapping)
57
+
58
+ return {
59
+ "contexts": {
60
+ "application": {
61
+ "mappings": {
62
+ "dispatcherServlet": {
63
+ "details": {
64
+ "requestMappingConditions": {
65
+ "patterns": []
66
+ }
67
+ },
68
+ "dispatcherHandlers": {
69
+ "webHandler": mappings
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+
78
+ def create_default_mappings_endpoint(app: FastAPI) -> MappingsEndpoint:
79
+ """
80
+ Create mappings endpoint for a FastAPI application.
81
+
82
+ Args:
83
+ app: FastAPI application instance
84
+
85
+ Returns:
86
+ MappingsEndpoint instance
87
+ """
88
+ return MappingsEndpoint(app)