Coverage for src / ezqt_app / services / application / app_service.py: 57.55%

86 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-06 13:12 +0000

1# /////////////////////////////////////////////////////////////// 

2# SERVICES.APPLICATION.APP_SERVICE - Application orchestration service 

3# Project: ezqt_app 

4# /////////////////////////////////////////////////////////////// 

5 

6"""Central application service — orchestrates config, assets, resources and settings.""" 

7 

8from __future__ import annotations 

9 

10# /////////////////////////////////////////////////////////////// 

11# IMPORTS 

12# /////////////////////////////////////////////////////////////// 

13# Standard library imports 

14from pathlib import Path 

15from typing import Any 

16 

17# Local imports 

18from ..config.config_service import ( 

19 get_config_service, 

20 get_package_resource, 

21 get_package_resource_content, 

22) 

23from .assets_service import AssetsService 

24from .resource_service import ResourceService 

25from .settings_loader import SettingsLoader 

26 

27# /////////////////////////////////////////////////////////////// 

28# MODULE-LEVEL STATE 

29# /////////////////////////////////////////////////////////////// 

30 

31# Config names that have been mutated in-memory and need to be flushed to disk. 

32_dirty: set[str] = set() 

33 

34# Guard against double-registration of the aboutToQuit signal. 

35_quit_signal_connected: bool = False 

36 

37 

38# /////////////////////////////////////////////////////////////// 

39# CLASSES 

40# /////////////////////////////////////////////////////////////// 

41 

42 

43class AppService: 

44 """ 

45 Central orchestration service for application lifecycle. 

46 

47 Aggregates config, asset, resource and settings operations into a 

48 single cohesive snake_case API used by widgets, CLI and bootstrap. 

49 """ 

50 

51 # /////////////////////////////////////////////////////////////// 

52 # PUBLIC METHODS 

53 # /////////////////////////////////////////////////////////////// 

54 

55 # ------------------------------------------------ 

56 # ASSETS 

57 # ------------------------------------------------ 

58 

59 @staticmethod 

60 def check_assets_requirements( 

61 base_path: Path | None = None, 

62 bin_path: Path | None = None, 

63 overwrite_policy: str = "ask", 

64 ) -> None: 

65 """ 

66 Generate asset binaries, QRC and RC files at APP_PATH. 

67 

68 Args: 

69 base_path: Optional base path for assets. 

70 bin_path: Optional binary path. 

71 overwrite_policy: Policy for overwriting existing files (default: "ask"). 

72 """ 

73 AssetsService.check_assets_requirements( 

74 base_path=base_path, 

75 bin_path=bin_path, 

76 overwrite_policy=overwrite_policy, 

77 ) 

78 

79 @staticmethod 

80 def make_required_files( 

81 mk_theme: bool = True, 

82 mk_config: bool = True, 

83 mk_translations: bool = True, 

84 base_path: Path | None = None, 

85 bin_path: Path | None = None, 

86 overwrite_policy: str = "ask", 

87 ) -> None: 

88 """ 

89 Copy YAML, QSS theme and translation files into project directories. 

90 

91 Args: 

92 mk_theme: When True also copies the QSS theme file. 

93 mk_config: When True copies configuration files. 

94 mk_translations: When True copies translation files. 

95 base_path: Optional base path. 

96 bin_path: Optional binary path. 

97 overwrite_policy: Policy for overwriting (default: "ask"). 

98 """ 

99 AssetsService.make_required_files( 

100 mk_theme=mk_theme, 

101 mk_config=mk_config, 

102 mk_translations=mk_translations, 

103 base_path=base_path, 

104 bin_path=bin_path, 

105 overwrite_policy=overwrite_policy, 

106 ) 

107 

108 # ------------------------------------------------ 

109 # CONFIGURATION 

110 # ------------------------------------------------ 

111 

112 @staticmethod 

113 def set_project_root(project_root: Path) -> None: 

114 """ 

115 Set the project root directory used by the config service. 

116 

117 Args: 

118 project_root: The Path to the project root. 

119 """ 

120 get_config_service().set_project_root(project_root) 

121 

122 @staticmethod 

123 def load_config(config_name: str) -> dict[str, Any]: 

124 """ 

125 Load a named configuration from its YAML file. 

126 

127 Args: 

128 config_name: Logical name of the configuration (e.g. "app"). 

129 

130 Returns: 

131 dict: The loaded configuration data. 

132 """ 

133 return get_config_service().load_config(config_name) 

134 

135 @staticmethod 

136 def get_config_value(config_name: str, key_path: str, default: Any = None) -> Any: 

137 """ 

138 Get a value from a named configuration using dot-separated path. 

139 

140 Args: 

141 config_name: Logical name of the configuration. 

142 key_path: Dot-separated key path, e.g. "app.name". 

143 default: Value returned when the path is absent. 

144 

145 Returns: 

146 Any: The configuration value or default. 

147 """ 

148 return get_config_service().get_config_value(config_name, key_path, default) 

149 

150 @staticmethod 

151 def save_config(config_name: str, data: dict[str, Any]) -> None: 

152 """ 

153 Persist a named configuration to its YAML file. 

154 

155 Args: 

156 config_name: Logical name of the configuration. 

157 data: Full configuration dict to write. 

158 """ 

159 get_config_service().save_config(config_name, data) 

160 

161 @staticmethod 

162 def write_yaml_config(keys: list[str], val: Any) -> None: 

163 """ 

164 Write a single value into a YAML config using a key list immediately. 

165 

166 Args: 

167 keys: List where keys[0] is config name and keys[1:] is the path. 

168 val: Value to assign at the leaf key. 

169 """ 

170 if not keys: 

171 return 

172 

173 config_name = keys[0] 

174 config_service = get_config_service() 

175 config = config_service.load_config(config_name) 

176 

177 current = config 

178 for key in keys[1:-1]: 

179 if key not in current: 

180 current[key] = {} 

181 current = current[key] 

182 

183 current[keys[-1]] = val 

184 config_service.save_config(config_name, config) 

185 

186 @staticmethod 

187 def stage_config_value(keys: list[str], val: Any) -> None: 

188 """ 

189 Mutate a config value in the shared cache and mark it dirty for flush. 

190 

191 Args: 

192 keys: List where keys[0] is config name and keys[1:] is the path. 

193 val: Value to assign at the leaf key. 

194 """ 

195 if not keys: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true

196 return 

197 

198 global _quit_signal_connected # noqa: PLW0603 

199 

200 config_name = keys[0] 

201 config_service = get_config_service() 

202 

203 config: dict[str, Any] = config_service.load_config(config_name) 

204 current: dict[str, Any] = config 

205 for key in keys[1:-1]: 

206 if key not in current or not isinstance(current[key], dict): 206 ↛ 208line 206 didn't jump to line 208 because the condition on line 206 was always true

207 current[key] = {} 

208 current = current[key] 

209 

210 current[keys[-1]] = val 

211 _dirty.add(config_name) 

212 

213 if not _quit_signal_connected: 213 ↛ exitline 213 didn't return from function 'stage_config_value' because the condition on line 213 was always true

214 from PySide6.QtCore import QCoreApplication 

215 

216 app = QCoreApplication.instance() 

217 if app is not None: 217 ↛ exitline 217 didn't return from function 'stage_config_value' because the condition on line 217 was always true

218 app.aboutToQuit.connect(AppService.flush_all) 

219 _quit_signal_connected = True 

220 

221 @staticmethod 

222 def flush_all() -> None: 

223 """Write all dirty configs to disk and clear the dirty set.""" 

224 if not _dirty: 

225 return 

226 

227 config_service = get_config_service() 

228 for config_name in list(_dirty): 

229 config_data = config_service.load_config(config_name) 

230 config_service.save_config(config_name, config_data) 

231 

232 _dirty.clear() 

233 

234 @staticmethod 

235 def copy_package_configs_to_project() -> None: 

236 """Copy package default configs into the child project directory.""" 

237 get_config_service().copy_package_configs_to_project() 

238 

239 # ------------------------------------------------ 

240 # PACKAGE RESOURCES 

241 # ------------------------------------------------ 

242 

243 @staticmethod 

244 def get_package_resource(resource_path: str) -> Path: 

245 """ 

246 Return a Path to an installed package resource. 

247 

248 Args: 

249 resource_path: Relative path inside the package. 

250 

251 Returns: 

252 Path: Absolute path to the resource. 

253 """ 

254 return get_package_resource(resource_path) 

255 

256 @staticmethod 

257 def get_package_resource_content(resource_path: str) -> str: 

258 """ 

259 Return the text content of an installed package resource. 

260 

261 Args: 

262 resource_path: Relative path inside the package. 

263 

264 Returns: 

265 str: Content of the resource. 

266 """ 

267 return get_package_resource_content(resource_path) 

268 

269 # ------------------------------------------------ 

270 # RESOURCES (FONTS) 

271 # ------------------------------------------------ 

272 

273 @staticmethod 

274 def load_fonts_resources(app: bool = False) -> None: 

275 """ 

276 Load .ttf font files into Qt's font database. 

277 

278 Args: 

279 app: Whether to also load fonts from the bin/fonts/ directory. 

280 """ 

281 ResourceService.load_fonts_resources(app) 

282 

283 # ------------------------------------------------ 

284 # SETTINGS 

285 # ------------------------------------------------ 

286 

287 @staticmethod 

288 def load_app_settings() -> dict[str, Any]: 

289 """ 

290 Load app settings from YAML and apply to SettingsService. 

291 

292 Returns: 

293 dict: Loaded settings. 

294 """ 

295 return SettingsLoader.load_app_settings()