py-wlcli 0.8.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.
Files changed (320) hide show
  1. py_wlcli-0.8.0.dist-info/METADATA +139 -0
  2. py_wlcli-0.8.0.dist-info/RECORD +320 -0
  3. py_wlcli-0.8.0.dist-info/WHEEL +4 -0
  4. py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
  5. py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
  6. wl_cli/__init__.py +27 -0
  7. wl_cli/__main__.py +130 -0
  8. wl_cli/capabilities.py +79 -0
  9. wl_cli/cc/__init__.py +5 -0
  10. wl_cli/cc/checks.py +1019 -0
  11. wl_cli/cc/cli.py +370 -0
  12. wl_cli/cc/mutate.py +1350 -0
  13. wl_cli/cli_click.py +156 -0
  14. wl_cli/commands/__init__.py +84 -0
  15. wl_cli/commands/base.py +80 -0
  16. wl_cli/commands/buildcommands/__init__.py +1 -0
  17. wl_cli/commands/buildcommands/build.py +71 -0
  18. wl_cli/commands/buildcommands/build_utils.py +47 -0
  19. wl_cli/commands/buildcommands/builders/__init__.py +18 -0
  20. wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
  21. wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
  22. wl_cli/commands/buildcommands/builders/environment.py +81 -0
  23. wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
  24. wl_cli/commands/buildcommands/builders/stubs.py +58 -0
  25. wl_cli/commands/buildcommands/dist.py +326 -0
  26. wl_cli/commands/buildcommands/test.py +89 -0
  27. wl_cli/commands/clean/__init__.py +1 -0
  28. wl_cli/commands/clean/build.py +151 -0
  29. wl_cli/commands/clean/clean.py +294 -0
  30. wl_cli/commands/clean/clean_utils.py +77 -0
  31. wl_cli/commands/clean/core.py +236 -0
  32. wl_cli/commands/clean/dead_code/__init__.py +0 -0
  33. wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
  34. wl_cli/commands/clean/dead_code/detector.py +161 -0
  35. wl_cli/commands/clean/dead_code/fixer.py +176 -0
  36. wl_cli/commands/clean/dead_code/reporter.py +295 -0
  37. wl_cli/commands/clean/dead_code/utils.py +0 -0
  38. wl_cli/commands/clean/lfs.py +215 -0
  39. wl_cli/commands/clean/python.py +81 -0
  40. wl_cli/commands/clean/rust.py +91 -0
  41. wl_cli/commands/clean/utils/__init__.py +34 -0
  42. wl_cli/commands/clean/venv.py +91 -0
  43. wl_cli/commands/cleanup.py +73 -0
  44. wl_cli/commands/click_base.py +79 -0
  45. wl_cli/commands/config/__init__.py +1 -0
  46. wl_cli/commands/config/config.py +291 -0
  47. wl_cli/commands/debug/__init__.py +6 -0
  48. wl_cli/commands/debug/debug.py +309 -0
  49. wl_cli/commands/debug/debug_cache.py +86 -0
  50. wl_cli/commands/debug/debug_subagent.py +156 -0
  51. wl_cli/commands/debug/output_formatter.py +165 -0
  52. wl_cli/commands/debug/source_hasher.py +92 -0
  53. wl_cli/commands/format/__init__.py +13 -0
  54. wl_cli/commands/format/format_cache_adapter.py +125 -0
  55. wl_cli/commands/format/format_command.py +197 -0
  56. wl_cli/commands/format/format_coordinator.py +191 -0
  57. wl_cli/commands/format/format_executor.py +470 -0
  58. wl_cli/commands/format/format_outcome.py +44 -0
  59. wl_cli/commands/format/format_path_handler.py +1016 -0
  60. wl_cli/commands/format/format_progress.py +62 -0
  61. wl_cli/commands/format/format_reporter.py +31 -0
  62. wl_cli/commands/format/python_formatter.py +506 -0
  63. wl_cli/commands/format/rust_formatter.py +106 -0
  64. wl_cli/commands/gate/__init__.py +1 -0
  65. wl_cli/commands/gate/gate_command.py +862 -0
  66. wl_cli/commands/gate/preflight.py +445 -0
  67. wl_cli/commands/git/__init__.py +1 -0
  68. wl_cli/commands/git/config.py +169 -0
  69. wl_cli/commands/git/git_facts.py +60 -0
  70. wl_cli/commands/git/git_flow.py +86 -0
  71. wl_cli/commands/git/hook.py +112 -0
  72. wl_cli/commands/git/policy.py +216 -0
  73. wl_cli/commands/git/push_update.py +82 -0
  74. wl_cli/commands/initenv/__init__.py +1 -0
  75. wl_cli/commands/initenv/common/__init__.py +23 -0
  76. wl_cli/commands/initenv/common/check_git_bash.py +78 -0
  77. wl_cli/commands/initenv/common/error_handler.py +68 -0
  78. wl_cli/commands/initenv/common/exceptions.py +52 -0
  79. wl_cli/commands/initenv/components/__init__.py +4 -0
  80. wl_cli/commands/initenv/components/cargo_sync.py +240 -0
  81. wl_cli/commands/initenv/components/config_manager.py +106 -0
  82. wl_cli/commands/initenv/components/directory_utils.py +71 -0
  83. wl_cli/commands/initenv/components/git/__init__.py +9 -0
  84. wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
  85. wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
  86. wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
  87. wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
  88. wl_cli/commands/initenv/components/git/git_utility.py +254 -0
  89. wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
  90. wl_cli/commands/initenv/components/git_initializer.py +212 -0
  91. wl_cli/commands/initenv/components/i18n_manager.py +104 -0
  92. wl_cli/commands/initenv/components/log_manager.py +241 -0
  93. wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
  94. wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
  95. wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
  96. wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
  97. wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
  98. wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
  99. wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
  100. wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
  101. wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
  102. wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
  103. wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
  104. wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
  105. wl_cli/commands/initenv/components/rust_initializer.py +145 -0
  106. wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
  107. wl_cli/commands/initenv/components/symlink_manager.py +52 -0
  108. wl_cli/commands/initenv/components/venv_manager.py +118 -0
  109. wl_cli/commands/initenv/initenv.py +169 -0
  110. wl_cli/commands/initenv/services/__init__.py +11 -0
  111. wl_cli/commands/initenv/services/environment_initializer.py +288 -0
  112. wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
  113. wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
  114. wl_cli/commands/lfs/README.md +221 -0
  115. wl_cli/commands/lfs/__init__.py +5 -0
  116. wl_cli/commands/lfs/cli.py +106 -0
  117. wl_cli/commands/lfs/config.py +164 -0
  118. wl_cli/commands/lfs/hook.py +61 -0
  119. wl_cli/commands/lfs/initializer.py +531 -0
  120. wl_cli/commands/lfs/manager.py +95 -0
  121. wl_cli/commands/lfs/policy.py +165 -0
  122. wl_cli/commands/lfs/watcher.py +294 -0
  123. wl_cli/commands/lint/__init__.py +1 -0
  124. wl_cli/commands/lint/lint_command.py +297 -0
  125. wl_cli/commands/lint/lint_executor.py +409 -0
  126. wl_cli/commands/lint/lint_reporter.py +579 -0
  127. wl_cli/commands/lint/lint_run.py +258 -0
  128. wl_cli/commands/lint/mypy_config.py +90 -0
  129. wl_cli/commands/registry.py +103 -0
  130. wl_cli/commands/self/__init__.py +7 -0
  131. wl_cli/commands/self/common/__init__.py +8 -0
  132. wl_cli/commands/self/common/capabilities.py +30 -0
  133. wl_cli/commands/self/common/config.py +27 -0
  134. wl_cli/commands/self/common/version_cache.py +85 -0
  135. wl_cli/commands/self/handlers/__init__.py +19 -0
  136. wl_cli/commands/self/handlers/base_handler.py +28 -0
  137. wl_cli/commands/self/handlers/install_handler.py +85 -0
  138. wl_cli/commands/self/handlers/status_handler.py +66 -0
  139. wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
  140. wl_cli/commands/self/handlers/update_handler.py +102 -0
  141. wl_cli/commands/self/handlers/version_handler.py +16 -0
  142. wl_cli/commands/self/self_command.py +237 -0
  143. wl_cli/commands/self/services/__init__.py +15 -0
  144. wl_cli/commands/self/services/installation_manager.py +187 -0
  145. wl_cli/commands/self/services/local_installer.py +165 -0
  146. wl_cli/commands/self/services/pypi_updater.py +278 -0
  147. wl_cli/commands/self/services/windows_update.py +174 -0
  148. wl_cli/commands/testcommands/__init__.py +8 -0
  149. wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
  150. wl_cli/commands/testcommands/batch_processor.py +278 -0
  151. wl_cli/commands/testcommands/cache_manager.py +1156 -0
  152. wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
  153. wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
  154. wl_cli/commands/testcommands/progress_manager.py +79 -0
  155. wl_cli/commands/testcommands/python_env_detector.py +207 -0
  156. wl_cli/commands/testcommands/report_generator.py +101 -0
  157. wl_cli/commands/testcommands/single_test_executor.py +250 -0
  158. wl_cli/commands/testcommands/speed_checker.py +118 -0
  159. wl_cli/commands/testcommands/test_command_builder.py +165 -0
  160. wl_cli/commands/testcommands/test_commands.py +786 -0
  161. wl_cli/commands/testcommands/test_reporter.py +181 -0
  162. wl_cli/commands/testcommands/test_result_handler.py +96 -0
  163. wl_cli/commands/testcommands/test_runner.py +1343 -0
  164. wl_cli/commands/testcommands/test_utils.py +197 -0
  165. wl_cli/commands/testcommands/utils/__init__.py +17 -0
  166. wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
  167. wl_cli/commands/testcommands/utils/env_info.py +36 -0
  168. wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
  169. wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
  170. wl_cli/commands/testcommands/utils/project_root.py +61 -0
  171. wl_cli/commands/update/__init__.py +9 -0
  172. wl_cli/commands/update/services/__init__.py +21 -0
  173. wl_cli/commands/update/services/backup_manager.py +207 -0
  174. wl_cli/commands/update/services/component_manager.py +83 -0
  175. wl_cli/commands/update/services/conflict_resolver.py +189 -0
  176. wl_cli/commands/update/services/vendors_updater.py +85 -0
  177. wl_cli/commands/update/services/version_tracker.py +127 -0
  178. wl_cli/commands/update/update_command.py +270 -0
  179. wl_cli/commands/update/utils/__init__.py +3 -0
  180. wl_cli/commands/version/__init__.py +9 -0
  181. wl_cli/commands/version/version_command.py +155 -0
  182. wl_cli/commands/workflow.py +477 -0
  183. wl_cli/env/__init__.py +5 -0
  184. wl_cli/env/cli.py +167 -0
  185. wl_cli/env_status.py +157 -0
  186. wl_cli/envelope.py +91 -0
  187. wl_cli/exceptions.py +85 -0
  188. wl_cli/lib/__init__.py +3 -0
  189. wl_cli/lib/rust_utils.py +35 -0
  190. wl_cli/mcp/__init__.py +6 -0
  191. wl_cli/mcp/capabilities.py +19 -0
  192. wl_cli/mcp/cli.py +22 -0
  193. wl_cli/mcp/gitlab.py +553 -0
  194. wl_cli/mcp/k8s.py +344 -0
  195. wl_cli/mcp/protocol.py +159 -0
  196. wl_cli/platform.py +139 -0
  197. wl_cli/quality/__init__.py +18 -0
  198. wl_cli/utils/__init__.py +7 -0
  199. wl_cli/utils/cache.py +491 -0
  200. wl_cli/utils/cache_ledger.py +355 -0
  201. wl_cli/utils/cache_store.py +546 -0
  202. wl_cli/utils/config/__init__.py +27 -0
  203. wl_cli/utils/config/config_loader.py +163 -0
  204. wl_cli/utils/config/core.py +862 -0
  205. wl_cli/utils/config/defaults.py +142 -0
  206. wl_cli/utils/config/env_exporter.py +86 -0
  207. wl_cli/utils/config/env_parser.py +108 -0
  208. wl_cli/utils/config/validation.py +280 -0
  209. wl_cli/utils/error_handler.py +165 -0
  210. wl_cli/utils/file_copier.py +56 -0
  211. wl_cli/utils/file_operations.py +506 -0
  212. wl_cli/utils/language_utils.py +122 -0
  213. wl_cli/utils/lazy_import.py +240 -0
  214. wl_cli/utils/log_levels.py +35 -0
  215. wl_cli/utils/log_rotators.py +74 -0
  216. wl_cli/utils/logging.py +458 -0
  217. wl_cli/utils/output_context.py +142 -0
  218. wl_cli/utils/performance/__init__.py +1 -0
  219. wl_cli/utils/performance/decorators.py +111 -0
  220. wl_cli/utils/performance/exporter.py +38 -0
  221. wl_cli/utils/performance/models.py +16 -0
  222. wl_cli/utils/performance/monitor.py +161 -0
  223. wl_cli/utils/performance_monitor.py +32 -0
  224. wl_cli/utils/platform_adapter.py +131 -0
  225. wl_cli/utils/pre_commit.py +266 -0
  226. wl_cli/utils/pre_commit_config.py +72 -0
  227. wl_cli/utils/root.py +521 -0
  228. wl_cli/utils/root_fallback.py +256 -0
  229. wl_cli/utils/run_evidence.py +236 -0
  230. wl_cli/utils/structured_logger.py +18 -0
  231. wl_cli/utils/structured_logging/__init__.py +19 -0
  232. wl_cli/utils/structured_logging/config.py +337 -0
  233. wl_cli/utils/structured_logging/core.py +177 -0
  234. wl_cli/utils/structured_logging/env.py +56 -0
  235. wl_cli/utils/structured_logging/filtering.py +75 -0
  236. wl_cli/utils/structured_logging/filters.py +55 -0
  237. wl_cli/utils/structured_logging/formatters.py +251 -0
  238. wl_cli/utils/structured_logging/handlers.py +308 -0
  239. wl_cli/utils/structured_logging/logger.py +354 -0
  240. wl_cli/utils/subprocess.py +71 -0
  241. wl_cli/utils/subprocess_utils.py +260 -0
  242. wl_cli/utils/test_utils/__init__.py +5 -0
  243. wl_cli/utils/test_utils/test_filter.py +36 -0
  244. wl_cli/utils/uv_run.py +79 -0
  245. wl_cli/utils/uv_tool.py +117 -0
  246. wl_cli/utils/uv_tree.py +71 -0
  247. wl_cli/utils/uv_workspace.py +174 -0
  248. wl_cli/utils/version/__init__.py +15 -0
  249. wl_cli/utils/version/pypi_version_checker.py +95 -0
  250. wl_cli/utils/version/version_comparator.py +133 -0
  251. wl_cli/utils/version/version_detectors.py +312 -0
  252. wl_cli/utils/version/version_service.py +131 -0
  253. wl_cli/utils/version/version_updater.py +198 -0
  254. wl_cli/utils/wl_dir_updater.py +1440 -0
  255. wl_cli/utils/workspace_detector/__init__.py +28 -0
  256. wl_cli/utils/workspace_detector/core.py +161 -0
  257. wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
  258. wl_cli/utils/workspace_detector/exceptions.py +5 -0
  259. wl_cli/utils/workspace_detector/types.py +17 -0
  260. wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
  261. wl_cli/vendors/config/.codespellrc +2 -0
  262. wl_cli/vendors/config/dependencies.toml +10 -0
  263. wl_cli/vendors/config/mypy.ini +62 -0
  264. wl_cli/vendors/config/pyproject_defaults.toml +28 -0
  265. wl_cli/vendors/config/template_config.toml +24 -0
  266. wl_cli/vendors/config/tool_configs.toml +76 -0
  267. wl_cli/vendors/git/.gitignore +122 -0
  268. wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
  269. wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
  270. wl_cli/vendors/hooks/commit-msg.sample +24 -0
  271. wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
  272. wl_cli/vendors/hooks/post-update.sample +8 -0
  273. wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
  274. wl_cli/vendors/hooks/pre-commit +45 -0
  275. wl_cli/vendors/hooks/pre-commit.legacy +31 -0
  276. wl_cli/vendors/hooks/pre-commit.sample +49 -0
  277. wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
  278. wl_cli/vendors/hooks/pre-push.sample +60 -0
  279. wl_cli/vendors/hooks/pre-rebase.sample +169 -0
  280. wl_cli/vendors/hooks/pre-receive.sample +24 -0
  281. wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
  282. wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
  283. wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
  284. wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
  285. wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
  286. wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
  287. wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
  288. wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
  289. wl_cli/vendors/hooks/unix/post-update.sample +8 -0
  290. wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
  291. wl_cli/vendors/hooks/unix/pre-commit +190 -0
  292. wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
  293. wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
  294. wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
  295. wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
  296. wl_cli/vendors/hooks/unix/pre-push +417 -0
  297. wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
  298. wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
  299. wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
  300. wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
  301. wl_cli/vendors/hooks/update.sample +128 -0
  302. wl_cli/vendors/hooks/win/work.bat +63 -0
  303. wl_cli/vendors/project/.gitignore +90 -0
  304. wl_cli/vendors/project/.python-version +1 -0
  305. wl_cli/vendors/project/LICENSE +21 -0
  306. wl_cli/vendors/project/README.md +0 -0
  307. wl_cli/vendors/project/pyproject.toml +27 -0
  308. wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
  309. wl_cli/vendors/project/pyproject_workspace.toml +14 -0
  310. wl_cli/vendors/readme/README.md +120 -0
  311. wl_cli/vendors/rust/Cargo.toml +14 -0
  312. wl_cli/vendors/rust/lib/__init__.py +3 -0
  313. wl_cli/vendors/rust/lib/rust_utils.py +31 -0
  314. wl_cli/vendors/rust/src/lib.rs +13 -0
  315. wl_cli/vendors/rust/src/main.rs +3 -0
  316. wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
  317. wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
  318. wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
  319. wl_cli/vendors/sop_doc/python-sop.md +342 -0
  320. wl_cli/vendors/sop_doc/rust-readme.md +219 -0
@@ -0,0 +1,251 @@
1
+ """Log formatters for WL Commands."""
2
+
3
+ import datetime
4
+ import json
5
+ import sys
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+
10
+ class LogFormatter:
11
+ """Handles formatting of log messages."""
12
+
13
+ # ANSI color codes for console output
14
+ _COLORS = {
15
+ "DEBUG": "\033[34m", # Blue
16
+ "INFO": "\033[32m", # Green
17
+ "WARNING": "\033[33m", # Yellow
18
+ "ERROR": "\033[31m", # Red
19
+ "CRITICAL": "\033[91m", # Bright Red
20
+ "RESET": "\033[0m", # Reset color
21
+ "TIMESTAMP": "\033[90m", # Gray for timestamp
22
+ "LOGGER": "\033[36m", # Cyan for logger name
23
+ "MESSAGE": "\033[37m", # White for message
24
+ "CONTEXT": "\033[35m", # Magenta for context
25
+ "MODULE": "\033[33m", # Yellow for module
26
+ "FUNCTION": "\033[32m", # Green for function
27
+ "LINE": "\033[34m", # Blue for line number
28
+ }
29
+
30
+ def __init__(
31
+ self, time_format: str = "%Y-%m-%d %H:%M:%S", enable_colors: bool = True
32
+ ):
33
+ """
34
+ Initialize log formatter.
35
+
36
+ Args:
37
+ time_format: Time format string for timestamp formatting.
38
+ enable_colors: Whether to enable colors in console output.
39
+ """
40
+ self.time_format = time_format
41
+ self.enable_colors = enable_colors # Always enable colors for testing
42
+ self.custom_formatters: dict[str, Callable[[dict[str, Any]], str]] = {}
43
+
44
+ def format_human_log(self, record: dict[str, Any]) -> str:
45
+ """Format log record for human-readable file output without colors."""
46
+ # Extract log fields
47
+ level_name = record.get("level_name", "UNKNOWN")
48
+ logger_name = record.get("logger_name", "unknown")
49
+ message = record.get("message", "")
50
+ timestamp = record.get("timestamp", "")
51
+
52
+ # Format timestamp for better readability
53
+ formatted_time = self._format_timestamp(timestamp)
54
+
55
+ # Build base log message
56
+ log_parts = [f"[{formatted_time}] {level_name} {logger_name} - {message}"]
57
+
58
+ # Add context information if available and log level is DEBUG
59
+ context = record.get("context", {})
60
+ if context and record.get("level") == 10: # Only show context in DEBUG mode
61
+ log_parts.append(f" Context: {self._format_context(context)}")
62
+
63
+ # Add module information if available and log level is DEBUG
64
+ if (
65
+ "module" in record and record.get("level") == 10
66
+ ): # Only show module info in DEBUG mode
67
+ module_info = f" Module: {record['module']}"
68
+ if "function" in record:
69
+ module_info += f".{record['function']}"
70
+ if "line" in record:
71
+ module_info += f":{record['line']}"
72
+ log_parts.append(module_info)
73
+
74
+ return "\n".join(log_parts)
75
+
76
+ def format_console_log(self, record: dict[str, Any]) -> str:
77
+ """Format log record for console output with colors and readable format."""
78
+ # Extract log fields
79
+ level_name = record.get("level_name", "UNKNOWN")
80
+ logger_name = record.get("logger_name", "unknown")
81
+ message = record.get("message", "")
82
+ timestamp = record.get("timestamp", "")
83
+
84
+ # Format timestamp
85
+ formatted_time = self._format_timestamp(timestamp)
86
+
87
+ # Get color codes if enabled
88
+ if self.enable_colors:
89
+ color = self._COLORS.get(level_name, self._COLORS["RESET"])
90
+ reset = self._COLORS["RESET"]
91
+ timestamp_color = self._COLORS["TIMESTAMP"]
92
+ logger_color = self._COLORS["LOGGER"]
93
+ message_color = self._COLORS["MESSAGE"]
94
+ else:
95
+ color = reset = timestamp_color = logger_color = message_color = ""
96
+
97
+ # Build base console log message
98
+ console_parts = [
99
+ f"{timestamp_color}[{formatted_time}]{reset} {color}{level_name}{reset} {logger_color}{logger_name}{reset} - {message_color}{message}{reset}"
100
+ ]
101
+
102
+ # Add context information if available and log level is DEBUG
103
+ context = record.get("context", {})
104
+ if context and record.get("level") == 10: # Only show context in DEBUG mode
105
+ if self.enable_colors:
106
+ context_color = self._COLORS["CONTEXT"]
107
+ console_parts.append(
108
+ f" {context_color}Context:{reset} {self._format_context(context)}"
109
+ )
110
+ else:
111
+ console_parts.append(f" Context: {self._format_context(context)}")
112
+
113
+ # Add module information if available and log level is DEBUG
114
+ if (
115
+ "module" in record and record.get("level") == 10
116
+ ): # Only show module info in DEBUG mode
117
+ if self.enable_colors:
118
+ module_color = self._COLORS["MODULE"]
119
+ function_color = self._COLORS["FUNCTION"]
120
+ line_color = self._COLORS["LINE"]
121
+ module_info = f" {module_color}Module:{reset} {record['module']}"
122
+ if "function" in record:
123
+ module_info += f".{function_color}{record['function']}{reset}"
124
+ if "line" in record:
125
+ module_info += f":{line_color}{record['line']}{reset}"
126
+ else:
127
+ module_info = f" Module: {record['module']}"
128
+ if "function" in record:
129
+ module_info += f".{record['function']}"
130
+ if "line" in record:
131
+ module_info += f":{record['line']}"
132
+ console_parts.append(module_info)
133
+
134
+ return "\n".join(console_parts)
135
+
136
+ def format_console_log_compact(self, record: dict[str, Any]) -> str:
137
+ """Format log record for console output with compact format for production environments."""
138
+ # Extract log fields
139
+ level_name = record.get("level_name", "UNKNOWN")
140
+ message = record.get("message", "")
141
+ timestamp = record.get("timestamp", "")
142
+
143
+ # Format timestamp with compact format
144
+ formatted_time = self._format_timestamp(timestamp)
145
+
146
+ # Get color codes if enabled
147
+ if self.enable_colors:
148
+ color = self._COLORS.get(level_name, self._COLORS["RESET"])
149
+ reset = self._COLORS["RESET"]
150
+ timestamp_color = self._COLORS["TIMESTAMP"]
151
+ message_color = self._COLORS["MESSAGE"]
152
+ else:
153
+ color = reset = timestamp_color = message_color = ""
154
+
155
+ # Build compact console log message
156
+ # Only include timestamp, level, and message for production
157
+ return f"{timestamp_color}[{formatted_time}]{reset} {color}{level_name[:4]}{reset} - {message_color}{message}{reset}"
158
+
159
+ def format_json_log(self, record: dict[str, Any]) -> str:
160
+ """Format log record as JSON."""
161
+ try:
162
+ # First try without default to detect non-serializable fields
163
+ return json.dumps(record)
164
+ except (TypeError, ValueError):
165
+ # Fallback if JSON serialization fails
166
+ simple_record = {
167
+ "logger_name": record.get("logger_name", "unknown"),
168
+ "level": record.get("level", 0),
169
+ "level_name": record.get("level_name", "UNKNOWN"),
170
+ "timestamp": record.get("timestamp", ""),
171
+ "message": record.get("message", ""),
172
+ }
173
+ return json.dumps(simple_record)
174
+
175
+ def _format_timestamp(self, timestamp: str) -> str:
176
+ """Format timestamp for better readability."""
177
+ if not timestamp:
178
+ return ""
179
+
180
+ try:
181
+ dt = datetime.datetime.fromisoformat(timestamp)
182
+ return dt.strftime(self.time_format)
183
+ except ValueError:
184
+ return timestamp
185
+
186
+ def _format_context(self, context: dict[str, Any]) -> str:
187
+ """Format context information for human-readable output."""
188
+ if not context:
189
+ return ""
190
+
191
+ context_items = []
192
+ for key, value in context.items():
193
+ if isinstance(value, dict | list):
194
+ try:
195
+ value_str = json.dumps(value, ensure_ascii=False, default=str)
196
+ except Exception:
197
+ value_str = str(value)
198
+ else:
199
+ value_str = str(value)
200
+ context_items.append(f"{key}={value_str}")
201
+
202
+ return ", ".join(context_items)
203
+
204
+ def _json_default(self, obj: Any) -> Any:
205
+ """Default JSON encoder for complex objects."""
206
+ if isinstance(obj, datetime.datetime):
207
+ return obj.isoformat()
208
+ elif isinstance(obj, set | frozenset):
209
+ return list(obj)
210
+ else:
211
+ try:
212
+ if hasattr(obj, "__dict__"):
213
+ return {
214
+ k: v for k, v in obj.__dict__.items() if not k.startswith("_")
215
+ }
216
+ else:
217
+ return str(obj)
218
+ except Exception:
219
+ return str(obj)
220
+
221
+ def register_formatter(
222
+ self, name: str, formatter: Callable[[dict[str, Any]], str]
223
+ ) -> None:
224
+ """Register a custom formatter."""
225
+ self.custom_formatters[name] = formatter
226
+
227
+ def format(self, record: dict[str, Any], format_type: str) -> str:
228
+ """Format log record using specified format type."""
229
+ if format_type in self.custom_formatters:
230
+ return self.custom_formatters[format_type](record)
231
+ elif format_type == "human":
232
+ return self.format_human_log(record)
233
+ elif format_type == "console":
234
+ return self.format_console_log(record)
235
+ elif format_type == "json":
236
+ return self.format_json_log(record)
237
+ else:
238
+ # Default to JSON format
239
+ return self.format_json_log(record)
240
+
241
+ def set_time_format(self, time_format: str) -> None:
242
+ """Set custom time format."""
243
+ self.time_format = time_format
244
+
245
+ def set_colors(self, enable: bool) -> None:
246
+ """Enable or disable colors."""
247
+ self.enable_colors = enable and sys.stdout.isatty()
248
+
249
+ def get_color_code(self, color_name: str) -> str:
250
+ """Get color code by name."""
251
+ return self._COLORS.get(color_name, "")
@@ -0,0 +1,308 @@
1
+ """Log handlers for WL Commands."""
2
+
3
+ import sys
4
+ import threading
5
+ import time
6
+ from typing import Any
7
+
8
+ from ..log_rotators import LogRotator
9
+ from ..platform_adapter import PlatformAdapter
10
+ from .env import is_pytest, is_test_environment
11
+ from .formatters import LogFormatter
12
+
13
+
14
+ class LogHandler:
15
+ """Handles writing log messages to various outputs."""
16
+
17
+ def __init__(
18
+ self,
19
+ log_file: str | None,
20
+ log_rotator: LogRotator | None,
21
+ enable_console: bool = True,
22
+ console_format: str = "colored",
23
+ log_file_format: str = "json",
24
+ buffer_size: int = 100,
25
+ async_write: bool = False,
26
+ flush_interval: float = 5.0,
27
+ ) -> None:
28
+ """
29
+ Initialize log handler.
30
+
31
+ Args:
32
+ log_file (Optional[str]): Path to log file.
33
+ log_rotator (Optional[LogRotator]): Log rotator instance.
34
+ enable_console (bool): Whether to enable console output. Defaults to True.
35
+ console_format (str): Console output format ('colored' or 'json'). Defaults to "colored".
36
+ log_file_format (str): Log file format ('human' or 'json'). Defaults to "json".
37
+ buffer_size (int): Log buffer size for batch writing. Defaults to 100.
38
+ async_write (bool): Whether to write logs asynchronously. Defaults to False.
39
+ flush_interval (float): Interval in seconds to flush buffer for async writing. Defaults to 5.0.
40
+ """
41
+ self.log_file = log_file
42
+ self.log_rotator = log_rotator
43
+ self.console_format = console_format
44
+ self.log_file_format = log_file_format
45
+ self.formatter = LogFormatter()
46
+ self.buffer_size = buffer_size
47
+
48
+ # Disable async write in test environment to ensure immediate file writing
49
+ # - When running through wl test, NON_INTERACTIVE is always set
50
+ # - Disable for any pytest environment, including pytest-xdist workers
51
+ if is_test_environment():
52
+ self.async_write = False
53
+ else:
54
+ self.async_write = async_write
55
+
56
+ # Keep enable_console as provided, even in test environment
57
+ self.enable_console = enable_console
58
+
59
+ self.flush_interval = flush_interval
60
+ self.buffer: list[dict[str, Any]] = []
61
+ self.buffer_lock = threading.Lock()
62
+ self.last_flush_time = time.time()
63
+ self._shutdown = False
64
+
65
+ # Start background flush thread if async write is enabled
66
+ if self.async_write:
67
+ self.flush_thread = threading.Thread(
68
+ target=self._background_flush, daemon=True
69
+ )
70
+ self.flush_thread.start()
71
+
72
+ def write_log(self, record: dict[str, Any]) -> None:
73
+ """Write log record to appropriate outputs."""
74
+ if self.async_write:
75
+ # Add to buffer for asynchronous processing
76
+ with self.buffer_lock:
77
+ self.buffer.append(record)
78
+ current_time = time.time()
79
+ # Flush buffer if it's full or if it's time to flush
80
+ if (
81
+ len(self.buffer) >= self.buffer_size
82
+ or current_time - self.last_flush_time >= self.flush_interval
83
+ ):
84
+ self._flush_buffer()
85
+ self.last_flush_time = current_time
86
+ else:
87
+ # Process synchronously
88
+ self._process_log(record)
89
+
90
+ def _background_flush(self) -> None:
91
+ """Background thread for flushing buffer at regular intervals."""
92
+ while not self._shutdown:
93
+ try:
94
+ time.sleep(self.flush_interval)
95
+ self._flush_buffer()
96
+ except Exception:
97
+ # Ignore exceptions to keep the thread running
98
+ pass
99
+
100
+ def _process_log(self, record: dict[str, Any]) -> None:
101
+ """Process log record synchronously."""
102
+ # Write to file if specified
103
+ if self.log_file:
104
+ self._write_to_file(record)
105
+
106
+ # Write to console only if enabled
107
+ # If enable_console is True, always write - respect the configured setting
108
+ # Non-interactive skipping is only handled when enable_console is not explicitly set
109
+ if self.enable_console:
110
+ # Skip console writing if we're in a non-interactive environment
111
+ # but not in test environment (tests need console output for assertions)
112
+ import os
113
+
114
+ if not os.environ.get("NON_INTERACTIVE") or is_pytest():
115
+ self._write_to_console(record)
116
+
117
+ def _write_to_file(self, record: dict[str, Any]) -> None:
118
+ """Write log record to file."""
119
+ try:
120
+ import os
121
+ from pathlib import Path
122
+
123
+ # Format message based on file format
124
+ if self.log_file_format == "human":
125
+ # For human format, we need to handle JSON errors for tests
126
+ try:
127
+ # Try to format as human log directly
128
+ message = self.formatter.format_human_log(record)
129
+ except Exception:
130
+ # If formatting fails, use JSON format as fallback
131
+ message = self.formatter.format_json_log(record)
132
+ else:
133
+ message = self.formatter.format_json_log(record)
134
+
135
+ # Create parent directory if it doesn't exist
136
+ if self.log_file:
137
+ log_path = Path(self.log_file)
138
+ log_path.parent.mkdir(parents=True, exist_ok=True)
139
+
140
+ # Handle log rotation
141
+ if self.log_rotator and self.log_rotator.should_rotate():
142
+ self.log_rotator.do_rotate()
143
+
144
+ # Write to file with Windows file locking handling
145
+ if PlatformAdapter.is_windows() and self.log_file:
146
+ # Handle Windows file locking issues
147
+ import time
148
+
149
+ max_attempts = 3
150
+ for attempt in range(max_attempts):
151
+ try:
152
+ with open(
153
+ self.log_file, "a", encoding="utf-8", newline=""
154
+ ) as f:
155
+ f.write(message + "\n")
156
+ f.flush() # Ensure immediate write
157
+ os.fsync(f.fileno()) # Ensure file is written to disk
158
+ break
159
+ except PermissionError:
160
+ if attempt < max_attempts - 1:
161
+ time.sleep(0.5)
162
+ continue
163
+ else:
164
+ raise
165
+ elif self.log_file:
166
+ # Write to file on non-Windows platforms
167
+ with open(self.log_file, "a", encoding="utf-8") as f:
168
+ f.write(message + "\n")
169
+ f.flush() # Ensure immediate write
170
+ os.fsync(f.fileno()) # Ensure file is written to disk
171
+ except Exception as e:
172
+ # For debugging, print the exception
173
+ import sys
174
+
175
+ print(f"File write error: {e}", file=sys.stderr)
176
+ # Ignore file write errors
177
+ pass
178
+
179
+ def _write_to_console(self, record: dict[str, Any]) -> None:
180
+ """Write log record to console."""
181
+ try:
182
+ import os
183
+
184
+ # Check if we're in test environment
185
+ is_test = is_pytest()
186
+
187
+ # In test environment with json format, still use json to preserve test behavior
188
+ if is_test and self.console_format == "json":
189
+ message = self.formatter.format_json_log(record)
190
+ elif is_test:
191
+ # In test environment with other formats, use compact format to improve performance
192
+ message = self.formatter.format_console_log_compact(record)
193
+ else:
194
+ # Check if we're in production environment
195
+ is_production = False
196
+ # Use environment variable as fast path
197
+ env = os.environ.get("WL_ENVIRONMENT", "development")
198
+ is_production = env == "production"
199
+ # Fallback to config only if environment variable not set
200
+ if not os.environ.get("WL_ENVIRONMENT"):
201
+ try:
202
+ from wl_cli.utils.config import get_config
203
+
204
+ env = get_config("environment", "development")
205
+ is_production = env == "production"
206
+ except Exception:
207
+ pass
208
+
209
+ # Force compact format for production
210
+ if is_production:
211
+ message = self.formatter.format_console_log_compact(record)
212
+ else:
213
+ # Format message based on console format
214
+ if self.console_format == "colored":
215
+ # For colored format, we need to handle JSON errors for tests
216
+ try:
217
+ # Try to format as colored log directly
218
+ message = self.formatter.format_console_log(record)
219
+ except Exception:
220
+ # If formatting fails, use JSON format as fallback
221
+ message = self.formatter.format_json_log(record)
222
+ elif self.console_format == "compact":
223
+ message = self.formatter.format_console_log_compact(record)
224
+ else:
225
+ message = self.formatter.format_json_log(record)
226
+
227
+ # Determine output stream based on log level
228
+ level = record.get("level", 0)
229
+ level_name = record.get("level_name", "").upper()
230
+ # Use level_name for determining output stream if level is not available
231
+ if level_name in ["ERROR", "CRITICAL"]:
232
+ target = sys.stderr
233
+ else:
234
+ target = sys.stdout if level < 40 else sys.stderr # < ERROR level
235
+
236
+ # Write to console
237
+ print(message, file=target)
238
+ except Exception:
239
+ # Ignore console write errors
240
+ pass
241
+
242
+ def _flush_buffer(self) -> None:
243
+ """Flush log buffer to outputs."""
244
+ with self.buffer_lock:
245
+ if not self.buffer:
246
+ return
247
+
248
+ # Copy buffer and clear it
249
+ records = self.buffer.copy()
250
+ self.buffer.clear()
251
+
252
+ # Process records in a separate thread
253
+ def process_records():
254
+ for record in records:
255
+ self._process_log(record)
256
+
257
+ # Start processing thread
258
+ thread = threading.Thread(target=process_records)
259
+ thread.daemon = True
260
+ thread.start()
261
+
262
+ def flush(self) -> None:
263
+ """Flush any buffered logs."""
264
+ if self.async_write:
265
+ self._flush_buffer()
266
+
267
+ def close(self) -> None:
268
+ """Close log handler and flush any buffered logs."""
269
+ self._shutdown = True
270
+ # Flush any remaining logs
271
+ self.flush()
272
+ # Wait for background thread to finish if it exists
273
+ if hasattr(self, "flush_thread") and self.flush_thread.is_alive():
274
+ self.flush_thread.join(timeout=2.0)
275
+
276
+ def set_console_format(self, format_type: str) -> None:
277
+ """Set console output format.
278
+
279
+ Args:
280
+ format_type (str): Console output format ('colored', 'compact', or 'json').
281
+ """
282
+ self.console_format = format_type
283
+
284
+ def set_file_format(self, format_type: str) -> None:
285
+ """Set file output format.
286
+
287
+ Args:
288
+ format_type (str): File output format ('human' or 'json').
289
+ """
290
+ self.log_file_format = format_type
291
+
292
+ def set_buffer_size(self, size: int) -> None:
293
+ """Set log buffer size.
294
+
295
+ Args:
296
+ size (int): Buffer size for batch writing.
297
+ """
298
+ self.buffer_size = size
299
+
300
+ def set_async_write(self, async_write: bool) -> None:
301
+ """Set asynchronous write mode.
302
+
303
+ Args:
304
+ async_write (bool): Whether to write logs asynchronously.
305
+ """
306
+ self.async_write = async_write
307
+ if not async_write:
308
+ self.flush()