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
« 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
14from sqlalchemy import text
16from app.config import PROJECT_ROOT
17from app.helpers import check_and_generate_build_info, read_build_info
19# during application initialization, make sure the build_info file is current
20check_and_generate_build_info()
23def build_about_info() -> dict:
24 """
25 Generate a dictionary containing detailed information about the application environment,
26 system, Python runtime, libraries, and database.
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.
34 :returns: A dictionary containing detailed build and environment information.
35 :rtype: dict
36 """
37 about_info = read_build_info()
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)
48 return about_info
51def _environment_info() -> dict[str, str]:
52 """
53 Retrieves detailed information about the current execution environment, including
54 system-related and Python-specific details.
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 }
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.
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.
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
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"]
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}
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
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';")
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]
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]
161 connection.close()
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 {}
174__all__ = ["build_about_info"]