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.
- py_wlcli-0.8.0.dist-info/METADATA +139 -0
- py_wlcli-0.8.0.dist-info/RECORD +320 -0
- py_wlcli-0.8.0.dist-info/WHEEL +4 -0
- py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
- py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
- wl_cli/__init__.py +27 -0
- wl_cli/__main__.py +130 -0
- wl_cli/capabilities.py +79 -0
- wl_cli/cc/__init__.py +5 -0
- wl_cli/cc/checks.py +1019 -0
- wl_cli/cc/cli.py +370 -0
- wl_cli/cc/mutate.py +1350 -0
- wl_cli/cli_click.py +156 -0
- wl_cli/commands/__init__.py +84 -0
- wl_cli/commands/base.py +80 -0
- wl_cli/commands/buildcommands/__init__.py +1 -0
- wl_cli/commands/buildcommands/build.py +71 -0
- wl_cli/commands/buildcommands/build_utils.py +47 -0
- wl_cli/commands/buildcommands/builders/__init__.py +18 -0
- wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
- wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
- wl_cli/commands/buildcommands/builders/environment.py +81 -0
- wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
- wl_cli/commands/buildcommands/builders/stubs.py +58 -0
- wl_cli/commands/buildcommands/dist.py +326 -0
- wl_cli/commands/buildcommands/test.py +89 -0
- wl_cli/commands/clean/__init__.py +1 -0
- wl_cli/commands/clean/build.py +151 -0
- wl_cli/commands/clean/clean.py +294 -0
- wl_cli/commands/clean/clean_utils.py +77 -0
- wl_cli/commands/clean/core.py +236 -0
- wl_cli/commands/clean/dead_code/__init__.py +0 -0
- wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
- wl_cli/commands/clean/dead_code/detector.py +161 -0
- wl_cli/commands/clean/dead_code/fixer.py +176 -0
- wl_cli/commands/clean/dead_code/reporter.py +295 -0
- wl_cli/commands/clean/dead_code/utils.py +0 -0
- wl_cli/commands/clean/lfs.py +215 -0
- wl_cli/commands/clean/python.py +81 -0
- wl_cli/commands/clean/rust.py +91 -0
- wl_cli/commands/clean/utils/__init__.py +34 -0
- wl_cli/commands/clean/venv.py +91 -0
- wl_cli/commands/cleanup.py +73 -0
- wl_cli/commands/click_base.py +79 -0
- wl_cli/commands/config/__init__.py +1 -0
- wl_cli/commands/config/config.py +291 -0
- wl_cli/commands/debug/__init__.py +6 -0
- wl_cli/commands/debug/debug.py +309 -0
- wl_cli/commands/debug/debug_cache.py +86 -0
- wl_cli/commands/debug/debug_subagent.py +156 -0
- wl_cli/commands/debug/output_formatter.py +165 -0
- wl_cli/commands/debug/source_hasher.py +92 -0
- wl_cli/commands/format/__init__.py +13 -0
- wl_cli/commands/format/format_cache_adapter.py +125 -0
- wl_cli/commands/format/format_command.py +197 -0
- wl_cli/commands/format/format_coordinator.py +191 -0
- wl_cli/commands/format/format_executor.py +470 -0
- wl_cli/commands/format/format_outcome.py +44 -0
- wl_cli/commands/format/format_path_handler.py +1016 -0
- wl_cli/commands/format/format_progress.py +62 -0
- wl_cli/commands/format/format_reporter.py +31 -0
- wl_cli/commands/format/python_formatter.py +506 -0
- wl_cli/commands/format/rust_formatter.py +106 -0
- wl_cli/commands/gate/__init__.py +1 -0
- wl_cli/commands/gate/gate_command.py +862 -0
- wl_cli/commands/gate/preflight.py +445 -0
- wl_cli/commands/git/__init__.py +1 -0
- wl_cli/commands/git/config.py +169 -0
- wl_cli/commands/git/git_facts.py +60 -0
- wl_cli/commands/git/git_flow.py +86 -0
- wl_cli/commands/git/hook.py +112 -0
- wl_cli/commands/git/policy.py +216 -0
- wl_cli/commands/git/push_update.py +82 -0
- wl_cli/commands/initenv/__init__.py +1 -0
- wl_cli/commands/initenv/common/__init__.py +23 -0
- wl_cli/commands/initenv/common/check_git_bash.py +78 -0
- wl_cli/commands/initenv/common/error_handler.py +68 -0
- wl_cli/commands/initenv/common/exceptions.py +52 -0
- wl_cli/commands/initenv/components/__init__.py +4 -0
- wl_cli/commands/initenv/components/cargo_sync.py +240 -0
- wl_cli/commands/initenv/components/config_manager.py +106 -0
- wl_cli/commands/initenv/components/directory_utils.py +71 -0
- wl_cli/commands/initenv/components/git/__init__.py +9 -0
- wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
- wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
- wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
- wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
- wl_cli/commands/initenv/components/git/git_utility.py +254 -0
- wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
- wl_cli/commands/initenv/components/git_initializer.py +212 -0
- wl_cli/commands/initenv/components/i18n_manager.py +104 -0
- wl_cli/commands/initenv/components/log_manager.py +241 -0
- wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
- wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
- wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
- wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
- wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
- wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
- wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
- wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
- wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
- wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
- wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
- wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
- wl_cli/commands/initenv/components/rust_initializer.py +145 -0
- wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
- wl_cli/commands/initenv/components/symlink_manager.py +52 -0
- wl_cli/commands/initenv/components/venv_manager.py +118 -0
- wl_cli/commands/initenv/initenv.py +169 -0
- wl_cli/commands/initenv/services/__init__.py +11 -0
- wl_cli/commands/initenv/services/environment_initializer.py +288 -0
- wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
- wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
- wl_cli/commands/lfs/README.md +221 -0
- wl_cli/commands/lfs/__init__.py +5 -0
- wl_cli/commands/lfs/cli.py +106 -0
- wl_cli/commands/lfs/config.py +164 -0
- wl_cli/commands/lfs/hook.py +61 -0
- wl_cli/commands/lfs/initializer.py +531 -0
- wl_cli/commands/lfs/manager.py +95 -0
- wl_cli/commands/lfs/policy.py +165 -0
- wl_cli/commands/lfs/watcher.py +294 -0
- wl_cli/commands/lint/__init__.py +1 -0
- wl_cli/commands/lint/lint_command.py +297 -0
- wl_cli/commands/lint/lint_executor.py +409 -0
- wl_cli/commands/lint/lint_reporter.py +579 -0
- wl_cli/commands/lint/lint_run.py +258 -0
- wl_cli/commands/lint/mypy_config.py +90 -0
- wl_cli/commands/registry.py +103 -0
- wl_cli/commands/self/__init__.py +7 -0
- wl_cli/commands/self/common/__init__.py +8 -0
- wl_cli/commands/self/common/capabilities.py +30 -0
- wl_cli/commands/self/common/config.py +27 -0
- wl_cli/commands/self/common/version_cache.py +85 -0
- wl_cli/commands/self/handlers/__init__.py +19 -0
- wl_cli/commands/self/handlers/base_handler.py +28 -0
- wl_cli/commands/self/handlers/install_handler.py +85 -0
- wl_cli/commands/self/handlers/status_handler.py +66 -0
- wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
- wl_cli/commands/self/handlers/update_handler.py +102 -0
- wl_cli/commands/self/handlers/version_handler.py +16 -0
- wl_cli/commands/self/self_command.py +237 -0
- wl_cli/commands/self/services/__init__.py +15 -0
- wl_cli/commands/self/services/installation_manager.py +187 -0
- wl_cli/commands/self/services/local_installer.py +165 -0
- wl_cli/commands/self/services/pypi_updater.py +278 -0
- wl_cli/commands/self/services/windows_update.py +174 -0
- wl_cli/commands/testcommands/__init__.py +8 -0
- wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
- wl_cli/commands/testcommands/batch_processor.py +278 -0
- wl_cli/commands/testcommands/cache_manager.py +1156 -0
- wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
- wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
- wl_cli/commands/testcommands/progress_manager.py +79 -0
- wl_cli/commands/testcommands/python_env_detector.py +207 -0
- wl_cli/commands/testcommands/report_generator.py +101 -0
- wl_cli/commands/testcommands/single_test_executor.py +250 -0
- wl_cli/commands/testcommands/speed_checker.py +118 -0
- wl_cli/commands/testcommands/test_command_builder.py +165 -0
- wl_cli/commands/testcommands/test_commands.py +786 -0
- wl_cli/commands/testcommands/test_reporter.py +181 -0
- wl_cli/commands/testcommands/test_result_handler.py +96 -0
- wl_cli/commands/testcommands/test_runner.py +1343 -0
- wl_cli/commands/testcommands/test_utils.py +197 -0
- wl_cli/commands/testcommands/utils/__init__.py +17 -0
- wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
- wl_cli/commands/testcommands/utils/env_info.py +36 -0
- wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
- wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
- wl_cli/commands/testcommands/utils/project_root.py +61 -0
- wl_cli/commands/update/__init__.py +9 -0
- wl_cli/commands/update/services/__init__.py +21 -0
- wl_cli/commands/update/services/backup_manager.py +207 -0
- wl_cli/commands/update/services/component_manager.py +83 -0
- wl_cli/commands/update/services/conflict_resolver.py +189 -0
- wl_cli/commands/update/services/vendors_updater.py +85 -0
- wl_cli/commands/update/services/version_tracker.py +127 -0
- wl_cli/commands/update/update_command.py +270 -0
- wl_cli/commands/update/utils/__init__.py +3 -0
- wl_cli/commands/version/__init__.py +9 -0
- wl_cli/commands/version/version_command.py +155 -0
- wl_cli/commands/workflow.py +477 -0
- wl_cli/env/__init__.py +5 -0
- wl_cli/env/cli.py +167 -0
- wl_cli/env_status.py +157 -0
- wl_cli/envelope.py +91 -0
- wl_cli/exceptions.py +85 -0
- wl_cli/lib/__init__.py +3 -0
- wl_cli/lib/rust_utils.py +35 -0
- wl_cli/mcp/__init__.py +6 -0
- wl_cli/mcp/capabilities.py +19 -0
- wl_cli/mcp/cli.py +22 -0
- wl_cli/mcp/gitlab.py +553 -0
- wl_cli/mcp/k8s.py +344 -0
- wl_cli/mcp/protocol.py +159 -0
- wl_cli/platform.py +139 -0
- wl_cli/quality/__init__.py +18 -0
- wl_cli/utils/__init__.py +7 -0
- wl_cli/utils/cache.py +491 -0
- wl_cli/utils/cache_ledger.py +355 -0
- wl_cli/utils/cache_store.py +546 -0
- wl_cli/utils/config/__init__.py +27 -0
- wl_cli/utils/config/config_loader.py +163 -0
- wl_cli/utils/config/core.py +862 -0
- wl_cli/utils/config/defaults.py +142 -0
- wl_cli/utils/config/env_exporter.py +86 -0
- wl_cli/utils/config/env_parser.py +108 -0
- wl_cli/utils/config/validation.py +280 -0
- wl_cli/utils/error_handler.py +165 -0
- wl_cli/utils/file_copier.py +56 -0
- wl_cli/utils/file_operations.py +506 -0
- wl_cli/utils/language_utils.py +122 -0
- wl_cli/utils/lazy_import.py +240 -0
- wl_cli/utils/log_levels.py +35 -0
- wl_cli/utils/log_rotators.py +74 -0
- wl_cli/utils/logging.py +458 -0
- wl_cli/utils/output_context.py +142 -0
- wl_cli/utils/performance/__init__.py +1 -0
- wl_cli/utils/performance/decorators.py +111 -0
- wl_cli/utils/performance/exporter.py +38 -0
- wl_cli/utils/performance/models.py +16 -0
- wl_cli/utils/performance/monitor.py +161 -0
- wl_cli/utils/performance_monitor.py +32 -0
- wl_cli/utils/platform_adapter.py +131 -0
- wl_cli/utils/pre_commit.py +266 -0
- wl_cli/utils/pre_commit_config.py +72 -0
- wl_cli/utils/root.py +521 -0
- wl_cli/utils/root_fallback.py +256 -0
- wl_cli/utils/run_evidence.py +236 -0
- wl_cli/utils/structured_logger.py +18 -0
- wl_cli/utils/structured_logging/__init__.py +19 -0
- wl_cli/utils/structured_logging/config.py +337 -0
- wl_cli/utils/structured_logging/core.py +177 -0
- wl_cli/utils/structured_logging/env.py +56 -0
- wl_cli/utils/structured_logging/filtering.py +75 -0
- wl_cli/utils/structured_logging/filters.py +55 -0
- wl_cli/utils/structured_logging/formatters.py +251 -0
- wl_cli/utils/structured_logging/handlers.py +308 -0
- wl_cli/utils/structured_logging/logger.py +354 -0
- wl_cli/utils/subprocess.py +71 -0
- wl_cli/utils/subprocess_utils.py +260 -0
- wl_cli/utils/test_utils/__init__.py +5 -0
- wl_cli/utils/test_utils/test_filter.py +36 -0
- wl_cli/utils/uv_run.py +79 -0
- wl_cli/utils/uv_tool.py +117 -0
- wl_cli/utils/uv_tree.py +71 -0
- wl_cli/utils/uv_workspace.py +174 -0
- wl_cli/utils/version/__init__.py +15 -0
- wl_cli/utils/version/pypi_version_checker.py +95 -0
- wl_cli/utils/version/version_comparator.py +133 -0
- wl_cli/utils/version/version_detectors.py +312 -0
- wl_cli/utils/version/version_service.py +131 -0
- wl_cli/utils/version/version_updater.py +198 -0
- wl_cli/utils/wl_dir_updater.py +1440 -0
- wl_cli/utils/workspace_detector/__init__.py +28 -0
- wl_cli/utils/workspace_detector/core.py +161 -0
- wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
- wl_cli/utils/workspace_detector/exceptions.py +5 -0
- wl_cli/utils/workspace_detector/types.py +17 -0
- wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
- wl_cli/vendors/config/.codespellrc +2 -0
- wl_cli/vendors/config/dependencies.toml +10 -0
- wl_cli/vendors/config/mypy.ini +62 -0
- wl_cli/vendors/config/pyproject_defaults.toml +28 -0
- wl_cli/vendors/config/template_config.toml +24 -0
- wl_cli/vendors/config/tool_configs.toml +76 -0
- wl_cli/vendors/git/.gitignore +122 -0
- wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
- wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/post-update.sample +8 -0
- wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/pre-commit +45 -0
- wl_cli/vendors/hooks/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/pre-push.sample +60 -0
- wl_cli/vendors/hooks/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
- wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
- wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
- wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
- wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/unix/post-update.sample +8 -0
- wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/unix/pre-commit +190 -0
- wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
- wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/unix/pre-push +417 -0
- wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
- wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/update.sample +128 -0
- wl_cli/vendors/hooks/win/work.bat +63 -0
- wl_cli/vendors/project/.gitignore +90 -0
- wl_cli/vendors/project/.python-version +1 -0
- wl_cli/vendors/project/LICENSE +21 -0
- wl_cli/vendors/project/README.md +0 -0
- wl_cli/vendors/project/pyproject.toml +27 -0
- wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
- wl_cli/vendors/project/pyproject_workspace.toml +14 -0
- wl_cli/vendors/readme/README.md +120 -0
- wl_cli/vendors/rust/Cargo.toml +14 -0
- wl_cli/vendors/rust/lib/__init__.py +3 -0
- wl_cli/vendors/rust/lib/rust_utils.py +31 -0
- wl_cli/vendors/rust/src/lib.rs +13 -0
- wl_cli/vendors/rust/src/main.rs +3 -0
- wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
- wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
- wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
- wl_cli/vendors/sop_doc/python-sop.md +342 -0
- wl_cli/vendors/sop_doc/rust-readme.md +219 -0
wl_cli/utils/cache.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""Generic in-memory/on-disk cache framework -- **debug-only** (#213, ADR-0011).
|
|
2
|
+
|
|
3
|
+
This module has exactly one production consumer,
|
|
4
|
+
:mod:`wl_cli.commands.debug.debug_cache`, in the subsystem scheduled for
|
|
5
|
+
retirement. The three live caches -- test results, coverage, format results --
|
|
6
|
+
go through :mod:`wl_cli.utils.cache_ledger`; this framework is *not* their seam
|
|
7
|
+
and must not grow into a second one.
|
|
8
|
+
|
|
9
|
+
It is isolated rather than migrated: a subsystem already judged dead does not
|
|
10
|
+
get ported. ``tests/test_utils_cache_is_debug_only.py`` fails if anything
|
|
11
|
+
outside ``commands/debug/`` imports it, and the module goes away with
|
|
12
|
+
``wl debug``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import pickle
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CacheKeyGenerator:
|
|
24
|
+
"""Cache key generator for creating unique cache keys."""
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def generate_key(*args, **kwargs) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Generate a unique cache key based on arguments.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
*args: Positional arguments
|
|
33
|
+
**kwargs: Keyword arguments
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A unique cache key as a string
|
|
37
|
+
"""
|
|
38
|
+
# Create a hashable representation of the arguments
|
|
39
|
+
key_components = []
|
|
40
|
+
|
|
41
|
+
# Process positional arguments
|
|
42
|
+
for arg in args:
|
|
43
|
+
if isinstance(arg, str | int | float | bool | type(None)):
|
|
44
|
+
key_components.append(str(arg))
|
|
45
|
+
elif isinstance(arg, list | tuple):
|
|
46
|
+
key_components.append(
|
|
47
|
+
str([CacheKeyGenerator._hash_object(item) for item in arg])
|
|
48
|
+
)
|
|
49
|
+
elif isinstance(arg, dict):
|
|
50
|
+
sorted_items = sorted(arg.items())
|
|
51
|
+
key_components.append(
|
|
52
|
+
str(
|
|
53
|
+
[
|
|
54
|
+
(k, CacheKeyGenerator._hash_object(v))
|
|
55
|
+
for k, v in sorted_items
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
key_components.append(CacheKeyGenerator._hash_object(arg))
|
|
61
|
+
|
|
62
|
+
# Process keyword arguments
|
|
63
|
+
sorted_kwargs = sorted(kwargs.items())
|
|
64
|
+
for key, value in sorted_kwargs:
|
|
65
|
+
key_components.append(f"{key}:{CacheKeyGenerator._hash_object(value)}")
|
|
66
|
+
|
|
67
|
+
# Combine and hash
|
|
68
|
+
combined = "|".join(key_components)
|
|
69
|
+
return hashlib.md5(combined.encode()).hexdigest()
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _hash_object(obj: Any) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Hash an object to a string representation.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
obj: The object to hash
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A hash string representation of the object
|
|
81
|
+
"""
|
|
82
|
+
if hasattr(obj, "__dict__"):
|
|
83
|
+
# For objects with __dict__
|
|
84
|
+
sorted_attrs = sorted(obj.__dict__.items())
|
|
85
|
+
return hashlib.md5(str(sorted_attrs).encode()).hexdigest()
|
|
86
|
+
elif callable(obj):
|
|
87
|
+
# For callables
|
|
88
|
+
try:
|
|
89
|
+
return hashlib.md5(
|
|
90
|
+
(obj.__name__ + (obj.__module__ or "")).encode()
|
|
91
|
+
).hexdigest()
|
|
92
|
+
except AttributeError:
|
|
93
|
+
return hashlib.md5(str(obj).encode()).hexdigest()
|
|
94
|
+
else:
|
|
95
|
+
# For other types
|
|
96
|
+
return hashlib.md5(str(obj).encode()).hexdigest()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class CacheItem:
|
|
100
|
+
"""Cache item with metadata."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, value: Any, expiry: float | None = None):
|
|
103
|
+
"""
|
|
104
|
+
Initialize a cache item.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
value: The value to cache
|
|
108
|
+
expiry: Optional expiry time in seconds since epoch
|
|
109
|
+
"""
|
|
110
|
+
self.value = value
|
|
111
|
+
self.created_at = time.time()
|
|
112
|
+
self.expiry = expiry
|
|
113
|
+
|
|
114
|
+
def is_expired(self) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Check if the cache item is expired.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if expired, False otherwise
|
|
120
|
+
"""
|
|
121
|
+
if self.expiry is None:
|
|
122
|
+
return False
|
|
123
|
+
return time.time() > self.expiry
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class BaseCache:
|
|
127
|
+
"""Base cache implementation."""
|
|
128
|
+
|
|
129
|
+
def get(self, key: str) -> Any | None:
|
|
130
|
+
"""
|
|
131
|
+
Get a value from the cache.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
key: The cache key
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The cached value or None if not found or expired
|
|
138
|
+
"""
|
|
139
|
+
raise NotImplementedError
|
|
140
|
+
|
|
141
|
+
def set(self, key: str, value: Any, ttl: int | None = None) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Set a value in the cache.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
key: The cache key
|
|
147
|
+
value: The value to cache
|
|
148
|
+
ttl: Optional time-to-live in seconds
|
|
149
|
+
"""
|
|
150
|
+
raise NotImplementedError
|
|
151
|
+
|
|
152
|
+
def delete(self, key: str) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Delete a value from the cache.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
key: The cache key
|
|
158
|
+
"""
|
|
159
|
+
raise NotImplementedError
|
|
160
|
+
|
|
161
|
+
def clear(self) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Clear the entire cache.
|
|
164
|
+
"""
|
|
165
|
+
raise NotImplementedError
|
|
166
|
+
|
|
167
|
+
def has(self, key: str) -> bool:
|
|
168
|
+
"""
|
|
169
|
+
Check if a key exists in the cache and is not expired.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
key: The cache key
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
True if the key exists and is not expired, False otherwise
|
|
176
|
+
"""
|
|
177
|
+
raise NotImplementedError
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class MemoryCache(BaseCache):
|
|
181
|
+
"""In-memory cache implementation."""
|
|
182
|
+
|
|
183
|
+
def __init__(self):
|
|
184
|
+
"""Initialize the memory cache."""
|
|
185
|
+
self._cache: dict[str, CacheItem] = {}
|
|
186
|
+
self._lock = threading.RLock()
|
|
187
|
+
|
|
188
|
+
def get(self, key: str) -> Any | None:
|
|
189
|
+
"""
|
|
190
|
+
Get a value from the cache.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
key: The cache key
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
The cached value or None if not found or expired
|
|
197
|
+
"""
|
|
198
|
+
with self._lock:
|
|
199
|
+
if key not in self._cache:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
item = self._cache[key]
|
|
203
|
+
if item.is_expired():
|
|
204
|
+
del self._cache[key]
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
return item.value
|
|
208
|
+
|
|
209
|
+
def set(self, key: str, value: Any, ttl: int | None = None) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Set a value in the cache.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
key: The cache key
|
|
215
|
+
value: The value to cache
|
|
216
|
+
ttl: Optional time-to-live in seconds
|
|
217
|
+
"""
|
|
218
|
+
with self._lock:
|
|
219
|
+
expiry = time.time() + ttl if ttl is not None else None
|
|
220
|
+
self._cache[key] = CacheItem(value, expiry)
|
|
221
|
+
|
|
222
|
+
def delete(self, key: str) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Delete a value from the cache.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
key: The cache key
|
|
228
|
+
"""
|
|
229
|
+
with self._lock:
|
|
230
|
+
if key in self._cache:
|
|
231
|
+
del self._cache[key]
|
|
232
|
+
|
|
233
|
+
def clear(self) -> None:
|
|
234
|
+
"""
|
|
235
|
+
Clear the entire cache.
|
|
236
|
+
"""
|
|
237
|
+
with self._lock:
|
|
238
|
+
self._cache.clear()
|
|
239
|
+
|
|
240
|
+
def has(self, key: str) -> bool:
|
|
241
|
+
"""
|
|
242
|
+
Check if a key exists in the cache and is not expired.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
key: The cache key
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
True if the key exists and is not expired, False otherwise
|
|
249
|
+
"""
|
|
250
|
+
with self._lock:
|
|
251
|
+
if key not in self._cache:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
item = self._cache[key]
|
|
255
|
+
if item.is_expired():
|
|
256
|
+
del self._cache[key]
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
def get_size(self) -> int:
|
|
262
|
+
"""
|
|
263
|
+
Get the size of the cache.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
The number of items in the cache
|
|
267
|
+
"""
|
|
268
|
+
with self._lock:
|
|
269
|
+
# Clean up expired items first
|
|
270
|
+
expired_keys = [
|
|
271
|
+
key for key, item in self._cache.items() if item.is_expired()
|
|
272
|
+
]
|
|
273
|
+
for key in expired_keys:
|
|
274
|
+
del self._cache[key]
|
|
275
|
+
|
|
276
|
+
return len(self._cache)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class DiskCache(BaseCache):
|
|
280
|
+
"""Disk-based cache implementation."""
|
|
281
|
+
|
|
282
|
+
def __init__(self, cache_dir: str = ".wl_cache"):
|
|
283
|
+
"""
|
|
284
|
+
Initialize the disk cache.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
cache_dir: The directory to store cache files
|
|
288
|
+
"""
|
|
289
|
+
import os
|
|
290
|
+
|
|
291
|
+
self.cache_dir = cache_dir
|
|
292
|
+
self._lock = threading.RLock()
|
|
293
|
+
|
|
294
|
+
# Create cache directory if it doesn't exist
|
|
295
|
+
os.makedirs(self.cache_dir, exist_ok=True)
|
|
296
|
+
|
|
297
|
+
def get(self, key: str) -> Any | None:
|
|
298
|
+
"""
|
|
299
|
+
Get a value from the disk cache.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
key: The cache key
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
The cached value or None if not found or expired
|
|
306
|
+
"""
|
|
307
|
+
import os
|
|
308
|
+
|
|
309
|
+
# Sanitize key to handle platform-specific path separators
|
|
310
|
+
sanitized_key = key.replace(os.sep, "_")
|
|
311
|
+
cache_file = os.path.join(self.cache_dir, sanitized_key)
|
|
312
|
+
|
|
313
|
+
with self._lock:
|
|
314
|
+
if not os.path.exists(cache_file):
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
with open(cache_file, "rb") as f:
|
|
319
|
+
item = pickle.load(f)
|
|
320
|
+
|
|
321
|
+
if item.is_expired():
|
|
322
|
+
os.remove(cache_file)
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
return item.value
|
|
326
|
+
except (OSError, pickle.UnpicklingError):
|
|
327
|
+
# If there's an error reading the cache, remove the file
|
|
328
|
+
if os.path.exists(cache_file):
|
|
329
|
+
os.remove(cache_file)
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
def set(self, key: str, value: Any, ttl: int | None = None) -> None:
|
|
333
|
+
"""
|
|
334
|
+
Set a value in the disk cache.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
key: The cache key
|
|
338
|
+
value: The value to cache
|
|
339
|
+
ttl: Optional time-to-live in seconds
|
|
340
|
+
"""
|
|
341
|
+
import os
|
|
342
|
+
|
|
343
|
+
# Sanitize key to handle platform-specific path separators
|
|
344
|
+
sanitized_key = key.replace(os.sep, "_")
|
|
345
|
+
cache_file = os.path.join(self.cache_dir, sanitized_key)
|
|
346
|
+
|
|
347
|
+
with self._lock:
|
|
348
|
+
try:
|
|
349
|
+
expiry = time.time() + ttl if ttl is not None else None
|
|
350
|
+
item = CacheItem(value, expiry)
|
|
351
|
+
|
|
352
|
+
# Write to a temporary file first
|
|
353
|
+
import tempfile
|
|
354
|
+
|
|
355
|
+
with tempfile.NamedTemporaryFile(
|
|
356
|
+
dir=self.cache_dir, delete=False
|
|
357
|
+
) as temp_f:
|
|
358
|
+
pickle.dump(item, temp_f)
|
|
359
|
+
temp_file_name = temp_f.name
|
|
360
|
+
|
|
361
|
+
# Atomically rename the temporary file to the final cache file
|
|
362
|
+
import os
|
|
363
|
+
|
|
364
|
+
os.replace(temp_file_name, cache_file)
|
|
365
|
+
except (OSError, pickle.PickleError, TypeError):
|
|
366
|
+
# Ignore IO, pickle, and type errors when writing to cache
|
|
367
|
+
# Clean up any temporary file that might have been created
|
|
368
|
+
if "temp_file_name" in locals() and os.path.exists(temp_file_name):
|
|
369
|
+
try:
|
|
370
|
+
os.remove(temp_file_name)
|
|
371
|
+
except OSError:
|
|
372
|
+
pass
|
|
373
|
+
pass
|
|
374
|
+
|
|
375
|
+
def delete(self, key: str) -> None:
|
|
376
|
+
"""
|
|
377
|
+
Delete a value from the disk cache.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
key: The cache key
|
|
381
|
+
"""
|
|
382
|
+
import os
|
|
383
|
+
|
|
384
|
+
# Sanitize key to handle platform-specific path separators
|
|
385
|
+
sanitized_key = key.replace(os.sep, "_")
|
|
386
|
+
cache_file = os.path.join(self.cache_dir, sanitized_key)
|
|
387
|
+
|
|
388
|
+
with self._lock:
|
|
389
|
+
if os.path.exists(cache_file):
|
|
390
|
+
try:
|
|
391
|
+
os.remove(cache_file)
|
|
392
|
+
except OSError:
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
def clear(self) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Clear the entire disk cache.
|
|
398
|
+
"""
|
|
399
|
+
import os
|
|
400
|
+
import shutil
|
|
401
|
+
|
|
402
|
+
with self._lock:
|
|
403
|
+
if os.path.exists(self.cache_dir):
|
|
404
|
+
try:
|
|
405
|
+
shutil.rmtree(self.cache_dir)
|
|
406
|
+
os.makedirs(self.cache_dir, exist_ok=True)
|
|
407
|
+
except OSError:
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
def has(self, key: str) -> bool:
|
|
411
|
+
"""
|
|
412
|
+
Check if a key exists in the disk cache and is not expired.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
key: The cache key
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
True if the key exists and is not expired, False otherwise
|
|
419
|
+
"""
|
|
420
|
+
return self.get(key) is not None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# Global cache instances
|
|
424
|
+
_memory_cache = MemoryCache()
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def get_cache() -> MemoryCache:
|
|
428
|
+
"""
|
|
429
|
+
Get the global memory cache instance.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
The global memory cache instance
|
|
433
|
+
"""
|
|
434
|
+
return _memory_cache
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def cached(ttl: int | None = 3600):
|
|
438
|
+
"""
|
|
439
|
+
Decorator to cache function results.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
ttl: Time-to-live in seconds
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
A decorator that caches function results
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
def decorator(func: Callable) -> Callable:
|
|
449
|
+
# Precompute function identifier to avoid repeated attribute access
|
|
450
|
+
func_id = f"{func.__module__ or ''}:{func.__name__}"
|
|
451
|
+
cache = get_cache()
|
|
452
|
+
|
|
453
|
+
def wrapper(*args, **kwargs):
|
|
454
|
+
# Generate cache key
|
|
455
|
+
cache_key = f"{func_id}:{CacheKeyGenerator.generate_key(*args, **kwargs)}"
|
|
456
|
+
|
|
457
|
+
# Try to get from cache
|
|
458
|
+
cached_value = cache.get(cache_key)
|
|
459
|
+
if cached_value is not None:
|
|
460
|
+
return cached_value
|
|
461
|
+
|
|
462
|
+
# Call the function
|
|
463
|
+
result = func(*args, **kwargs)
|
|
464
|
+
|
|
465
|
+
# Cache the result
|
|
466
|
+
cache.set(cache_key, result, ttl)
|
|
467
|
+
|
|
468
|
+
return result
|
|
469
|
+
|
|
470
|
+
# Preserve function metadata
|
|
471
|
+
wrapper.__name__ = func.__name__
|
|
472
|
+
wrapper.__doc__ = func.__doc__
|
|
473
|
+
wrapper.__module__ = func.__module__
|
|
474
|
+
|
|
475
|
+
return wrapper
|
|
476
|
+
|
|
477
|
+
return decorator
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# File system cache
|
|
481
|
+
_file_system_cache = MemoryCache()
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def get_file_system_cache() -> MemoryCache:
|
|
485
|
+
"""
|
|
486
|
+
Get the global file system cache instance.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
The global file system cache instance
|
|
490
|
+
"""
|
|
491
|
+
return _file_system_cache
|