Coverage for app / services / about_service.py: 96%

72 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-06 04:49 +0000

1""" 

2This module provides functionality to gather and expose metadata about the application,  

3its runtime environment, and its database status. It is responsible for reading build  

4information, inspecting installed libraries, and fetching database details (e.g., type,  

5version, and tables). The module ensures important environment variables are securely  

6exposed and includes necessary initialization steps. 

7""" 

8import logging 

9import os 

10import platform 

11import sys 

12from importlib.metadata import distributions 

13 

14from sqlalchemy import text 

15 

16from app.config import PROJECT_ROOT 

17from app.helpers import check_and_generate_build_info, read_build_info 

18 

19# during application initialization, make sure the build_info file is current 

20check_and_generate_build_info() 

21 

22 

23def build_about_info() -> dict: 

24 """ 

25 Generate a dictionary containing detailed information about the application environment, 

26 system, Python runtime, libraries, and database. 

27 

28 This function collects a comprehensive set of data about the runtime environment 

29 of the application. This includes system details, Python interpreter information, 

30 installed libraries with their respective metadata, safe environment variables, 

31 and database statistics. The collected data is consolidated into a dictionary, which 

32 can be used for debugging, logging, or display purposes. 

33 

34 :returns: A dictionary containing detailed build and environment information. 

35 :rtype: dict 

36 """ 

37 about_info = read_build_info() 

38 

39 about_info["python_version"] = sys.version 

40 about_info["platform"] = sys.platform 

41 about_info["app_filepath"] = os.path.abspath(PROJECT_ROOT) 

42 about_info["installed_libraries"] = _installed_libs() 

43 about_info["environment"] = _environment_info() 

44 about_info["vars"] = _get_safe_environment_variables() 

45 from app import db # pylint: disable=import-outside-toplevel 

46 about_info["database"] = _database_info(db) 

47 

48 return about_info 

49 

50 

51def _environment_info() -> dict[str, str]: 

52 """ 

53 Retrieves detailed information about the current execution environment, including 

54 system-related and Python-specific details. 

55 

56 :return: A dictionary containing environment information such as the 

57 system, release, version, machine architecture, processor, Python 

58 implementation, and compiler details. 

59 :rtype: dict 

60 """ 

61 return { 

62 "system": platform.system(), 

63 "release": platform.release(), 

64 "version": platform.version(), 

65 "machine": platform.machine(), 

66 "processor": platform.processor(), 

67 "python_implementation": platform.python_implementation(), 

68 "python_compiler": platform.python_compiler(), 

69 } 

70 

71 

72def _installed_libs() -> list[dict[str, str]]: 

73 """ 

74 Retrieves a list of installed libraries and their metadata, including name, version, 

75 and homepage. Libraries are sorted case-insensitively by their names. 

76 

77 The function gathers metadata for each installed library available through the 

78 `distributions()` function, generating a list of dictionaries. Each dictionary 

79 contains details about the library's name, version, and homepage, depending on 

80 the availability of such attributes. 

81 

82 :return: A sorted list of dictionaries, where each dictionary contains 

83 metadata about an installed library, such as the library's name, version, 

84 and homepage. Libraries are sorted in ascending, case-insensitive order by 

85 their names. 

86 :rtype: list[dict[str, str]] 

87 """ 

88 seen = set() 

89 libs = [] 

90 for dist in distributions(): 

91 name = dist.metadata["Name"] 

92 version = dist.version 

93 lib_key = (name.lower(), version) 

94 # noinspection PyProtectedMember 

95 path = dist._path # pylint: disable=protected-access 

96 if lib_key not in seen: 96 ↛ 90line 96 didn't jump to line 90 because the condition on line 96 was always true

97 seen.add(lib_key) 

98 libs.append({ 

99 "name": name, 

100 "version": version, 

101 "homepage": dist.metadata["Home-Page"] if "Home-Page" in dist.metadata else "N/A", 

102 "path": path, 

103 }) 

104 # sort the list of libs by name, using case-insensitive ordering 

105 libs = sorted(libs, key=lambda x: x["name"].lower()) 

106 return libs 

107 

108 

109def _get_safe_environment_variables(): 

110 # List of environment variables to expose 

111 allowed_env_vars = ["FLASK_ENV", "RDS_HOSTNAME", "RDS_PORT", "RDS_DB_NAME", 

112 "MAIL_SERVER", "MAIL_PORT", "SECURITY_EMAIL_SENDER", 

113 "COOKIE_DOMAIN", "MAIL_USE_TLS"] 

114 

115 # Filter and return only the allowed variables 

116 return {key: os.environ.get(key) for key in allowed_env_vars if key in os.environ} 

117 

118 

119def _database_info(db): 

120 """Expose basic database info such as type and version.""" 

121 try: 

122 # Establish a raw connection to extract database info 

123 connection = db.engine.connect() 

124 server_version = connection.dialect.server_version_info 

125 database_type = connection.engine.name 

126 

127 query = None 

128 table_query = None 

129 # Define database-specific queries 

130 if database_type == "postgresql": 

131 query = text("SELECT version();") 

132 table_query = text(""" 

133 SELECT table_schema, table_name  

134 FROM information_schema.tables  

135 WHERE table_type = 'BASE TABLE'  

136 AND table_schema NOT IN ('pg_catalog', 'information_schema'); 

137 """) 

138 elif database_type == "mysql": 

139 query = text("SHOW VARIABLES LIKE '%os%';") 

140 table_query = text("SHOW TABLES;") 

141 elif database_type == "sqlite": 141 ↛ 145line 141 didn't jump to line 145 because the condition on line 141 was always true

142 # noinspection SqlResolve 

143 table_query = text("SELECT name FROM sqlite_master WHERE type='table';") 

144 

145 db_platform_info = None 

146 if query is not None: 

147 result = connection.execute(query).fetchall() 

148 db_platform_info = [str(row) for row in result] 

149 

150 db_table_info = None 

151 if table_query is not None: 151 ↛ 161line 151 didn't jump to line 161 because the condition on line 151 was always true

152 result = connection.execute(table_query).fetchall() 

153 # Convert rows properly depending on their structure 

154 if database_type == "postgresql": 

155 db_table_info = [{"table_schema": row[0], "table_name": row[1]} for row in result] 

156 elif database_type == "mysql": 

157 db_table_info = [{"table_name": row[0]} for row in result] 

158 elif database_type == "sqlite": 158 ↛ 161line 158 didn't jump to line 161 because the condition on line 158 was always true

159 db_table_info = [{"table_name": row[0]} for row in result] 

160 

161 connection.close() 

162 

163 return { 

164 "database_type": database_type, 

165 "server_version": f"{server_version}", 

166 "db_platform_info": db_platform_info if db_platform_info else "", 

167 "db_table_info": db_table_info if db_table_info else "" 

168 } 

169 except Exception as e: # pylint: disable=broad-except 

170 logging.error("Could not retrieve database info: %s", e, exc_info=True) 

171 return {} 

172 

173 

174__all__ = ["build_about_info"]