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
« 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# ///////////////////////////////////////////////////////////////
6"""Central application service — orchestrates config, assets, resources and settings."""
8from __future__ import annotations
10# ///////////////////////////////////////////////////////////////
11# IMPORTS
12# ///////////////////////////////////////////////////////////////
13# Standard library imports
14from pathlib import Path
15from typing import Any
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
27# ///////////////////////////////////////////////////////////////
28# MODULE-LEVEL STATE
29# ///////////////////////////////////////////////////////////////
31# Config names that have been mutated in-memory and need to be flushed to disk.
32_dirty: set[str] = set()
34# Guard against double-registration of the aboutToQuit signal.
35_quit_signal_connected: bool = False
38# ///////////////////////////////////////////////////////////////
39# CLASSES
40# ///////////////////////////////////////////////////////////////
43class AppService:
44 """
45 Central orchestration service for application lifecycle.
47 Aggregates config, asset, resource and settings operations into a
48 single cohesive snake_case API used by widgets, CLI and bootstrap.
49 """
51 # ///////////////////////////////////////////////////////////////
52 # PUBLIC METHODS
53 # ///////////////////////////////////////////////////////////////
55 # ------------------------------------------------
56 # ASSETS
57 # ------------------------------------------------
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.
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 )
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.
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 )
108 # ------------------------------------------------
109 # CONFIGURATION
110 # ------------------------------------------------
112 @staticmethod
113 def set_project_root(project_root: Path) -> None:
114 """
115 Set the project root directory used by the config service.
117 Args:
118 project_root: The Path to the project root.
119 """
120 get_config_service().set_project_root(project_root)
122 @staticmethod
123 def load_config(config_name: str) -> dict[str, Any]:
124 """
125 Load a named configuration from its YAML file.
127 Args:
128 config_name: Logical name of the configuration (e.g. "app").
130 Returns:
131 dict: The loaded configuration data.
132 """
133 return get_config_service().load_config(config_name)
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.
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.
145 Returns:
146 Any: The configuration value or default.
147 """
148 return get_config_service().get_config_value(config_name, key_path, default)
150 @staticmethod
151 def save_config(config_name: str, data: dict[str, Any]) -> None:
152 """
153 Persist a named configuration to its YAML file.
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)
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.
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
173 config_name = keys[0]
174 config_service = get_config_service()
175 config = config_service.load_config(config_name)
177 current = config
178 for key in keys[1:-1]:
179 if key not in current:
180 current[key] = {}
181 current = current[key]
183 current[keys[-1]] = val
184 config_service.save_config(config_name, config)
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.
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
198 global _quit_signal_connected # noqa: PLW0603
200 config_name = keys[0]
201 config_service = get_config_service()
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]
210 current[keys[-1]] = val
211 _dirty.add(config_name)
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
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
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
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)
232 _dirty.clear()
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()
239 # ------------------------------------------------
240 # PACKAGE RESOURCES
241 # ------------------------------------------------
243 @staticmethod
244 def get_package_resource(resource_path: str) -> Path:
245 """
246 Return a Path to an installed package resource.
248 Args:
249 resource_path: Relative path inside the package.
251 Returns:
252 Path: Absolute path to the resource.
253 """
254 return get_package_resource(resource_path)
256 @staticmethod
257 def get_package_resource_content(resource_path: str) -> str:
258 """
259 Return the text content of an installed package resource.
261 Args:
262 resource_path: Relative path inside the package.
264 Returns:
265 str: Content of the resource.
266 """
267 return get_package_resource_content(resource_path)
269 # ------------------------------------------------
270 # RESOURCES (FONTS)
271 # ------------------------------------------------
273 @staticmethod
274 def load_fonts_resources(app: bool = False) -> None:
275 """
276 Load .ttf font files into Qt's font database.
278 Args:
279 app: Whether to also load fonts from the bin/fonts/ directory.
280 """
281 ResourceService.load_fonts_resources(app)
283 # ------------------------------------------------
284 # SETTINGS
285 # ------------------------------------------------
287 @staticmethod
288 def load_app_settings() -> dict[str, Any]:
289 """
290 Load app settings from YAML and apply to SettingsService.
292 Returns:
293 dict: Loaded settings.
294 """
295 return SettingsLoader.load_app_settings()