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,142 @@
1
+ """Default configuration constants and helper functions."""
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ from wl_cli import __version__
7
+
8
+ # Inlined from .core constants to avoid circular import (core -> defaults -> core)
9
+ ENVIRONMENT_DEVELOPMENT = "development"
10
+ LOG_DIRECTORY = "log"
11
+ LOG_FILE_NAME = "wl_action.log"
12
+ # This is the **single** definition of the ``.wl`` directory name (#119). A second
13
+ # copy used to live in ``core``; the deploy-root path now derives its ``.wl`` from
14
+ # ``Root.wl_dir()`` instead, and ``config_loader`` imports this name for the
15
+ # user-level ``~/.wl/config.json``. It has to stay here rather than in ``core``:
16
+ # this module is the leaf of the ``core -> defaults`` dependency, so only it can
17
+ # be imported by both.
18
+ WL_DIRECTORY = ".wl"
19
+
20
+ DEFAULT_LOG_LEVEL = "INFO"
21
+
22
+ DEFAULT_LOG_FILE = os.path.join(WL_DIRECTORY, LOG_DIRECTORY, LOG_FILE_NAME)
23
+
24
+ DEFAULT_LOG_CONSOLE = False
25
+
26
+ DEFAULT_LOG_CONSOLE_FORMAT = "colored"
27
+
28
+ DEFAULT_LOG_FILE_FORMAT = "human"
29
+
30
+ DEFAULT_LOG_MAX_SIZE = 10 * 1024 * 1024
31
+
32
+ DEFAULT_LOG_MAX_BACKUPS = 5
33
+
34
+ DEFAULT_LOG_ROTATE_DAYS = 7
35
+
36
+ DEFAULT_LANGUAGE = "auto"
37
+
38
+ DEFAULT_ALIASES: dict[str, str] = {}
39
+
40
+ DEFAULT_VERSION = __version__
41
+
42
+ DEFAULT_ENVIRONMENT = ENVIRONMENT_DEVELOPMENT
43
+
44
+ DEFAULT_COVERAGE_FAIL_UNDER = 70.0
45
+ DEFAULT_COVERAGE_CACHE_ENABLED = True
46
+ DEFAULT_COVERAGE_PARALLEL_WORKERS = None
47
+
48
+ # Environment-specific defaults
49
+ ENVIRONMENT_DEFAULTS = {
50
+ "development": {
51
+ "log_level": "DEBUG",
52
+ "log_console": True,
53
+ "log_console_format": "colored",
54
+ "log_file_format": "human",
55
+ },
56
+ "production": {
57
+ "log_level": "INFO",
58
+ "log_console": False,
59
+ "log_console_format": "compact",
60
+ "log_file_format": "json",
61
+ },
62
+ "testing": {
63
+ "log_level": "WARNING",
64
+ "log_console": False,
65
+ "log_file_format": "human",
66
+ },
67
+ }
68
+
69
+
70
+ #: Config keys whose value is *derived from the running environment* at load
71
+ #: time (see ``ConfigLoader.get_environment``) rather than owned by the project.
72
+ #:
73
+ #: ``environment`` looked like a harmless default, but ``load_all`` gives the
74
+ #: file's value precedence over detection -- so writing the derived value into a
75
+ #: brand-new ``config.json`` silently turns a derivation into a permanent
76
+ #: override. A ``wl`` sync that first ran under pytest (``testing``) or in CI
77
+ #: (``production``) froze that value forever, because the version stamp then
78
+ #: certified the file as current and nothing ever re-derived it (#208).
79
+ RUNTIME_DERIVED_KEYS = frozenset({"environment"})
80
+
81
+
82
+ def get_default_config() -> dict[str, Any]:
83
+ """Get default configuration dictionary.
84
+
85
+ This is the shape of the *merged* runtime configuration, so it still names
86
+ ``environment``: readers need a value even when no file exists. Use
87
+ :func:`get_creation_defaults` when writing a new file -- a derived value
88
+ must not be persisted as if it were a project setting (#208).
89
+ """
90
+ return {
91
+ "environment": DEFAULT_ENVIRONMENT,
92
+ "log_level": DEFAULT_LOG_LEVEL,
93
+ "log_file": DEFAULT_LOG_FILE,
94
+ "log_console": DEFAULT_LOG_CONSOLE,
95
+ "log_console_format": DEFAULT_LOG_CONSOLE_FORMAT,
96
+ "log_file_format": DEFAULT_LOG_FILE_FORMAT,
97
+ "log_max_size": DEFAULT_LOG_MAX_SIZE,
98
+ "log_max_backups": DEFAULT_LOG_MAX_BACKUPS,
99
+ "log_rotate_days": DEFAULT_LOG_ROTATE_DAYS,
100
+ "language": DEFAULT_LANGUAGE,
101
+ "aliases": DEFAULT_ALIASES,
102
+ "version": DEFAULT_VERSION,
103
+ "test_speed": {
104
+ "enabled": True,
105
+ "threshold": 5.0,
106
+ "fail_on_timeout": True,
107
+ },
108
+ "coverage": {
109
+ "fail_under": DEFAULT_COVERAGE_FAIL_UNDER,
110
+ "cache_enabled": DEFAULT_COVERAGE_CACHE_ENABLED,
111
+ "parallel_workers": DEFAULT_COVERAGE_PARALLEL_WORKERS,
112
+ },
113
+ }
114
+
115
+
116
+ def get_environment_defaults(environment: str) -> dict[str, Any]:
117
+ """Get default configuration for specific environment."""
118
+ return ENVIRONMENT_DEFAULTS.get(environment, {})
119
+
120
+
121
+ def get_creation_defaults() -> dict[str, Any]:
122
+ """Payload written when a brand-new ``.wl/config.json`` is created (#208).
123
+
124
+ The distinction this draws is between a *creation default* -- a value the
125
+ project owns from the moment the file appears -- and a *runtime-derived*
126
+ value, which belongs to the process that is running, not to the checkout.
127
+ Persisting the latter is what made the force-write of #202 a trap: the file
128
+ was created under pytest with ``environment=testing`` and every later run
129
+ read that back as an explicit setting.
130
+
131
+ Log-related keys are deliberately *kept*: they are static defaults, and
132
+ ``ConfigLoader.load_all`` re-applies the environment's ``log_*`` defaults on
133
+ every load anyway, so they can never pin a run mode.
134
+
135
+ Returns:
136
+ A fresh dict of the JSON payload for a new config file.
137
+ """
138
+ return {
139
+ key: value
140
+ for key, value in get_default_config().items()
141
+ if key not in RUNTIME_DERIVED_KEYS
142
+ }
@@ -0,0 +1,86 @@
1
+ """Environment variable exporter for WL Commands."""
2
+
3
+ import json
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from .defaults import DEFAULT_LANGUAGE, DEFAULT_LOG_FILE, get_environment_defaults
7
+
8
+ if TYPE_CHECKING:
9
+ from .config_loader import ConfigLoader
10
+
11
+
12
+ class EnvExporter:
13
+ """Export configuration as environment variables.
14
+
15
+ EnvExporter handles converting configuration dictionary to properly formatted
16
+ environment variables and generates environment templates for different
17
+ environments.
18
+ """
19
+
20
+ def __init__(self, config_loader: "ConfigLoader") -> None:
21
+ """Initialize environment exporter.
22
+
23
+ Args:
24
+ config_loader: Config loader instance to get environment from.
25
+ """
26
+ self._loader = config_loader
27
+
28
+ def export_environment_variables(
29
+ self, config: dict[str, Any], environment: str | None = None
30
+ ) -> dict[str, str]:
31
+ """Export current configuration as environment variables.
32
+
33
+ Args:
34
+ config: Configuration dictionary to export.
35
+ environment: Target environment. If None, gets from ConfigLoader.
36
+
37
+ Returns:
38
+ Dictionary of environment variables with WL_ prefix.
39
+ """
40
+ if environment is None:
41
+ environment = self._loader.get_environment()
42
+
43
+ env_vars = {}
44
+
45
+ # Convert config to environment variables
46
+ for key, value in config.items():
47
+ if key == "environment":
48
+ continue
49
+
50
+ env_key = f"WL_{key.upper()}"
51
+ # Convert value to string
52
+ if isinstance(value, bool):
53
+ env_vars[env_key] = str(value).lower()
54
+ elif isinstance(value, list | dict):
55
+ env_vars[env_key] = json.dumps(value)
56
+ else:
57
+ env_vars[env_key] = str(value)
58
+
59
+ # Add environment variable
60
+ env_vars["WL_ENVIRONMENT"] = environment
61
+
62
+ return env_vars
63
+
64
+ def get_environment_template(self, environment: str) -> dict[str, str]:
65
+ """Get environment variable template for specific environment.
66
+
67
+ Args:
68
+ environment: Target environment for template.
69
+
70
+ Returns:
71
+ Dictionary of template environment variables with default values.
72
+ """
73
+ env_defaults = get_environment_defaults(environment)
74
+ template = {}
75
+
76
+ # Add environment-specific defaults
77
+ for key, value in env_defaults.items():
78
+ env_key = f"WL_{key.upper()}"
79
+ template[env_key] = str(value)
80
+
81
+ # Add common variables
82
+ template["WL_ENVIRONMENT"] = environment
83
+ template["WL_LOG_FILE"] = DEFAULT_LOG_FILE
84
+ template["WL_LANGUAGE"] = DEFAULT_LANGUAGE
85
+
86
+ return template
@@ -0,0 +1,108 @@
1
+ """Environment variable parsing utilities for configuration."""
2
+
3
+ import json
4
+ import os
5
+ from typing import Any
6
+
7
+
8
+ class EnvConfigParser:
9
+ """Parse environment variables into configuration values."""
10
+
11
+ ENV_PREFIX = "WL_"
12
+
13
+ def parse_env_value(self, value: str) -> Any:
14
+ """Parse environment variable value to appropriate Python type.
15
+
16
+ Handles: bool, int, float, JSON lists, JSON dicts, falls back to string.
17
+
18
+ Args:
19
+ value: Raw string value from environment variable
20
+
21
+ Returns:
22
+ Parsed value with appropriate type
23
+ """
24
+ # Handle boolean values
25
+ lower = value.lower()
26
+ if lower in ("true", "1", "yes", "y", "on"):
27
+ return True
28
+ if lower in ("false", "0", "no", "n", "off"):
29
+ return False
30
+
31
+ # Handle numeric values
32
+ if value.isdigit():
33
+ return int(value)
34
+ if value.replace(".", "", 1).isdigit():
35
+ try:
36
+ return float(value)
37
+ except ValueError:
38
+ pass
39
+
40
+ # Handle lists
41
+ if value.startswith("[") and value.endswith("]"):
42
+ try:
43
+ return json.loads(value)
44
+ except json.JSONDecodeError:
45
+ # Fallback to simple comma-separated parsing
46
+ items = value[1:-1].split(",")
47
+ return [item.strip() for item in items if item.strip()]
48
+
49
+ # Handle dictionaries
50
+ if value.startswith("{") and value.endswith("}"):
51
+ try:
52
+ return json.loads(value)
53
+ except json.JSONDecodeError:
54
+ pass
55
+
56
+ # Return as string if no other type matches
57
+ return value
58
+
59
+ def group_env_variables(self, env_config: dict[str, Any]) -> dict[str, Any]:
60
+ """Group environment variables by prefix for nested configuration.
61
+
62
+ Converts WL_LOG_MAX_SIZE → log.max_size. Only groups when there are
63
+ more than two parts separated by underscore. Keeps original keys for
64
+ backward compatibility.
65
+
66
+ Args:
67
+ env_config: Dictionary with environment parsed configuration
68
+
69
+ Returns:
70
+ Grouped configuration with nested dictionaries
71
+ """
72
+ grouped = env_config.copy()
73
+
74
+ # Group variables with underscore separators (only for nested groups)
75
+ # Keep original keys for backward compatibility
76
+ for key, value in env_config.items():
77
+ if "_" in key:
78
+ parts = key.split("_")
79
+ if len(parts) > 1:
80
+ # Only group if we have more than 2 parts (e.g., log_max_size -> log.max_size)
81
+ if len(parts) > 2:
82
+ group = parts[0]
83
+ subgroup_key = "_".join(parts[1:])
84
+
85
+ if group not in grouped:
86
+ grouped[group] = {}
87
+ grouped[group][subgroup_key] = value
88
+
89
+ return grouped
90
+
91
+ def load_from_env(self) -> dict[str, Any]:
92
+ """Load configuration from environment variables with WL_ prefix.
93
+
94
+ Environment variables starting with WL_ are converted:
95
+ WL_LOG_LEVEL → log_level → log.level after grouping.
96
+
97
+ Returns:
98
+ Configuration dictionary loaded from environment
99
+ """
100
+ env_config: dict[str, Any] = {}
101
+
102
+ for key, value in os.environ.items():
103
+ if key.startswith(self.ENV_PREFIX):
104
+ config_key = key[len(self.ENV_PREFIX) :].lower()
105
+ parsed_value = self.parse_env_value(value)
106
+ env_config[config_key] = parsed_value
107
+
108
+ return self.group_env_variables(env_config)
@@ -0,0 +1,280 @@
1
+ """Configuration validation utilities."""
2
+
3
+ from typing import Any
4
+
5
+ VALID_LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
6
+
7
+ VALID_CONSOLE_FORMATS = ["colored", "json"]
8
+
9
+ VALID_FILE_FORMATS = ["json", "human"]
10
+
11
+ VALID_CACHE_BACKENDS = ["memory", "disk"]
12
+
13
+ VALID_CONFIG_FORMATS = ["json", "toml", "yaml"]
14
+
15
+
16
+ def validate_log_level(log_level: Any) -> str:
17
+ """Validate log level value."""
18
+ if not isinstance(log_level, str):
19
+ return "INFO"
20
+ return log_level.upper() if log_level.upper() in VALID_LOG_LEVELS else "INFO"
21
+
22
+
23
+ def validate_log_max_size(size: Any) -> int:
24
+ """Validate log max size value."""
25
+ return size if isinstance(size, int) and size > 0 else 10 * 1024 * 1024
26
+
27
+
28
+ def validate_log_max_backups(count: Any) -> int:
29
+ """Validate log max backups value."""
30
+ return count if isinstance(count, int) and count >= 0 else 5
31
+
32
+
33
+ def validate_log_rotate_days(days: Any) -> int:
34
+ """Validate log rotate days value."""
35
+ return days if isinstance(days, int) and days > 0 else 7
36
+
37
+
38
+ def validate_log_console(value: Any) -> bool:
39
+ """Validate log console value."""
40
+ return validate_boolean(value)
41
+
42
+
43
+ def validate_log_console_format(fmt: Any) -> str:
44
+ """Validate log console format value."""
45
+ if not isinstance(fmt, str):
46
+ return "colored"
47
+ return fmt.lower() if fmt.lower() in VALID_CONSOLE_FORMATS else "colored"
48
+
49
+
50
+ def validate_log_file_format(fmt: Any) -> str:
51
+ """Validate log file format value."""
52
+ if not isinstance(fmt, str):
53
+ return "human"
54
+ return fmt.lower() if fmt.lower() in VALID_FILE_FORMATS else "human"
55
+
56
+
57
+ def validate_cache_backend(backend: Any) -> str:
58
+ """Validate cache backend value."""
59
+ if not isinstance(backend, str):
60
+ return "memory"
61
+ return backend.lower() if backend.lower() in VALID_CACHE_BACKENDS else "memory"
62
+
63
+
64
+ def validate_cache_ttl(ttl: Any) -> int:
65
+ """Validate cache TTL value."""
66
+ return ttl if isinstance(ttl, int) and ttl > 0 else 3600
67
+
68
+
69
+ def validate_config_format(fmt: Any) -> str:
70
+ """Validate config format value."""
71
+ if not isinstance(fmt, str):
72
+ return "json"
73
+ return fmt.lower() if fmt.lower() in VALID_CONFIG_FORMATS else "json"
74
+
75
+
76
+ def validate_boolean(value: Any) -> bool:
77
+ """Validate boolean value."""
78
+ if isinstance(value, bool):
79
+ return value
80
+ if isinstance(value, str):
81
+ normalized_value = value.lower().strip()
82
+ return normalized_value in ["true", "1", "yes", "y", "on"]
83
+ if isinstance(value, int):
84
+ return value > 0
85
+ return False
86
+
87
+
88
+ def validate_integer(value: Any, min_value: int | None = None) -> int:
89
+ """Validate integer value."""
90
+ if isinstance(value, int):
91
+ if min_value is not None and value < min_value:
92
+ return min_value
93
+ return value
94
+ try:
95
+ int_value = int(value)
96
+ if min_value is not None and int_value < min_value:
97
+ return min_value
98
+ return int_value
99
+ except (ValueError, TypeError):
100
+ return min_value if min_value is not None else 0
101
+
102
+
103
+ def validate_string(value: Any, default: str = "") -> str:
104
+ """Validate string value."""
105
+ return str(value) if value is not None else default
106
+
107
+
108
+ def _validate_top_level_logging(config: dict[str, Any]) -> dict[str, Any]:
109
+ """Validate top-level logging configuration values."""
110
+ validated = config.copy()
111
+
112
+ if "log_level" in validated:
113
+ validated["log_level"] = validate_log_level(validated["log_level"])
114
+ if "log_max_size" in validated:
115
+ validated["log_max_size"] = validate_log_max_size(validated["log_max_size"])
116
+ if "log_max_backups" in validated:
117
+ validated["log_max_backups"] = validate_log_max_backups(
118
+ validated["log_max_backups"]
119
+ )
120
+ if "log_rotate_days" in validated:
121
+ validated["log_rotate_days"] = validate_log_rotate_days(
122
+ validated["log_rotate_days"]
123
+ )
124
+ if "log_console" in validated:
125
+ validated["log_console"] = validate_log_console(validated["log_console"])
126
+ if "log_console_format" in validated:
127
+ validated["log_console_format"] = validate_log_console_format(
128
+ validated["log_console_format"]
129
+ )
130
+ if "log_file_format" in validated:
131
+ validated["log_file_format"] = validate_log_file_format(
132
+ validated["log_file_format"]
133
+ )
134
+
135
+ return validated
136
+
137
+
138
+ def _validate_top_level_cache(config: dict[str, Any]) -> dict[str, Any]:
139
+ """Validate top-level cache configuration values."""
140
+ validated = config.copy()
141
+
142
+ if "cache_backend" in validated:
143
+ validated["cache_backend"] = validate_cache_backend(validated["cache_backend"])
144
+ if "cache_ttl" in validated:
145
+ validated["cache_ttl"] = validate_cache_ttl(validated["cache_ttl"])
146
+
147
+ return validated
148
+
149
+
150
+ def _validate_top_level_general(config: dict[str, Any]) -> dict[str, Any]:
151
+ """Validate top-level general configuration values."""
152
+ validated = config.copy()
153
+
154
+ if "config_format" in validated:
155
+ validated["config_format"] = validate_config_format(validated["config_format"])
156
+
157
+ return validated
158
+
159
+
160
+ def validate_config(config: dict[str, Any]) -> dict[str, Any]:
161
+ """Validate and normalize configuration values."""
162
+ validated = config.copy()
163
+
164
+ # Validate top-level configurations
165
+ validated = _validate_top_level_logging(validated)
166
+ validated = _validate_top_level_cache(validated)
167
+ validated = _validate_top_level_general(validated)
168
+
169
+ # Validate nested configurations
170
+ if "logging" in validated and isinstance(validated["logging"], dict):
171
+ validated["logging"] = validate_logging_config(validated["logging"])
172
+ if "cache" in validated and isinstance(validated["cache"], dict):
173
+ validated["cache"] = validate_cache_config(validated["cache"])
174
+ if "test_speed" in validated and isinstance(validated["test_speed"], dict):
175
+ validated["test_speed"] = validate_test_speed_config(validated["test_speed"])
176
+
177
+ return validated
178
+
179
+
180
+ def validate_logging_config(logging_config: dict[str, Any]) -> dict[str, Any]:
181
+ """Validate logging configuration."""
182
+ validated = logging_config.copy()
183
+
184
+ if "level" in validated:
185
+ validated["level"] = validate_log_level(validated["level"])
186
+ if "console" in validated:
187
+ validated["console"] = validate_boolean(validated["console"])
188
+ if "console_format" in validated:
189
+ validated["console_format"] = validate_log_console_format(
190
+ validated["console_format"]
191
+ )
192
+ if "file_format" in validated:
193
+ validated["file_format"] = validate_log_file_format(validated["file_format"])
194
+ if "max_size" in validated:
195
+ validated["max_size"] = validate_log_max_size(validated["max_size"])
196
+ if "max_backups" in validated:
197
+ validated["max_backups"] = validate_log_max_backups(validated["max_backups"])
198
+ if "rotate_days" in validated:
199
+ validated["rotate_days"] = validate_log_rotate_days(validated["rotate_days"])
200
+
201
+ return validated
202
+
203
+
204
+ def validate_cache_config(cache_config: dict[str, Any]) -> dict[str, Any]:
205
+ """Validate cache configuration."""
206
+ validated = cache_config.copy()
207
+
208
+ if "backend" in validated:
209
+ validated["backend"] = validate_cache_backend(validated["backend"])
210
+ if "ttl" in validated:
211
+ validated["ttl"] = validate_cache_ttl(validated["ttl"])
212
+ if "enabled" in validated:
213
+ validated["enabled"] = validate_boolean(validated["enabled"])
214
+
215
+ return validated
216
+
217
+
218
+ def validate_test_speed_config(test_speed_config: dict[str, Any]) -> dict[str, Any]:
219
+ """Validate test speed configuration."""
220
+ validated = test_speed_config.copy()
221
+
222
+ if "enabled" in validated:
223
+ validated["enabled"] = validate_boolean(validated["enabled"])
224
+ else:
225
+ validated["enabled"] = True
226
+
227
+ if "threshold" in validated:
228
+ if (
229
+ isinstance(validated["threshold"], int | float)
230
+ and validated["threshold"] > 0
231
+ ):
232
+ validated["threshold"] = float(validated["threshold"])
233
+ else:
234
+ validated["threshold"] = 5.0
235
+ else:
236
+ validated["threshold"] = 5.0
237
+
238
+ if "fail_on_timeout" in validated:
239
+ validated["fail_on_timeout"] = validate_boolean(validated["fail_on_timeout"])
240
+ else:
241
+ validated["fail_on_timeout"] = True
242
+
243
+ return validated
244
+
245
+
246
+ def validate_config_schema(
247
+ config: dict[str, Any], schema: dict[str, Any]
248
+ ) -> dict[str, Any]:
249
+ """Validate configuration against a schema."""
250
+ validated = config.copy()
251
+
252
+ for key, schema_info in schema.items():
253
+ if key in validated:
254
+ value = validated[key]
255
+ validator = schema_info.get("validator")
256
+ if validator:
257
+ validated[key] = validator(value)
258
+ else:
259
+ # Set default value if not present
260
+ validated[key] = schema_info.get("default")
261
+
262
+ return validated
263
+
264
+
265
+ # Default configuration schema
266
+ DEFAULT_CONFIG_SCHEMA = {
267
+ "log_level": {"validator": validate_log_level, "default": "INFO"},
268
+ "log_console": {"validator": validate_boolean, "default": True},
269
+ "log_console_format": {
270
+ "validator": validate_log_console_format,
271
+ "default": "colored",
272
+ },
273
+ "log_file_format": {"validator": validate_log_file_format, "default": "human"},
274
+ "log_max_size": {"validator": validate_log_max_size, "default": 10 * 1024 * 1024},
275
+ "log_max_backups": {"validator": validate_log_max_backups, "default": 5},
276
+ "log_rotate_days": {"validator": validate_log_rotate_days, "default": 7},
277
+ "cache_backend": {"validator": validate_cache_backend, "default": "memory"},
278
+ "cache_ttl": {"validator": validate_cache_ttl, "default": 3600},
279
+ "config_format": {"validator": validate_config_format, "default": "json"},
280
+ }