htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MemorySharedCache - In-memory singleton cache with LRU eviction and TTL.
|
|
3
|
+
|
|
4
|
+
Thread-safe implementation using RLock for concurrent access.
|
|
5
|
+
Provides O(1) operations for get/set/delete with pattern-based invalidation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import fnmatch
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from .shared_cache import (
|
|
15
|
+
CacheCapacityError,
|
|
16
|
+
CacheKeyError,
|
|
17
|
+
SharedCache,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MemorySharedCache(SharedCache):
|
|
22
|
+
"""
|
|
23
|
+
In-memory cache with LRU eviction and TTL support.
|
|
24
|
+
|
|
25
|
+
Features:
|
|
26
|
+
- Thread-safe singleton pattern
|
|
27
|
+
- O(1) get/set/delete operations
|
|
28
|
+
- LRU eviction when max_size exceeded
|
|
29
|
+
- TTL-based automatic expiration
|
|
30
|
+
- Pattern-based invalidation (prefix matching)
|
|
31
|
+
- Comprehensive statistics tracking
|
|
32
|
+
|
|
33
|
+
Performance:
|
|
34
|
+
- get(key): O(1) average
|
|
35
|
+
- set(key, value, ttl): O(1) average
|
|
36
|
+
- delete(key): O(1)
|
|
37
|
+
- delete_pattern(pattern): O(n) where n = total keys
|
|
38
|
+
- clear(): O(n)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_instance: Optional["MemorySharedCache"] = None
|
|
42
|
+
_lock_class = threading.RLock()
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
max_size: int = 1000,
|
|
47
|
+
default_ttl: int = 3600,
|
|
48
|
+
metrics_enabled: bool = True,
|
|
49
|
+
):
|
|
50
|
+
"""Initialize cache with configuration."""
|
|
51
|
+
self._cache: dict[str, tuple[Any, float | None]] = {}
|
|
52
|
+
self._access_times: dict[str, float] = {}
|
|
53
|
+
self._max_size = max_size
|
|
54
|
+
self._default_ttl = default_ttl
|
|
55
|
+
self._metrics_enabled = metrics_enabled
|
|
56
|
+
self._lock = threading.RLock()
|
|
57
|
+
|
|
58
|
+
# Statistics
|
|
59
|
+
self._stats = {
|
|
60
|
+
"hits": 0,
|
|
61
|
+
"misses": 0,
|
|
62
|
+
"evictions": 0,
|
|
63
|
+
"total_load_time_ms": 0.0,
|
|
64
|
+
"load_count": 0,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# ===== SINGLETON MANAGEMENT =====
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def initialize(
|
|
71
|
+
cls, max_size: int = 1000, default_ttl: int = 3600, metrics_enabled: bool = True
|
|
72
|
+
) -> "MemorySharedCache":
|
|
73
|
+
"""Initialize singleton cache instance."""
|
|
74
|
+
with cls._lock_class:
|
|
75
|
+
if cls._instance is None:
|
|
76
|
+
cls._instance = cls(max_size, default_ttl, metrics_enabled)
|
|
77
|
+
return cls._instance
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def get_instance(cls) -> "MemorySharedCache":
|
|
81
|
+
"""Get singleton instance."""
|
|
82
|
+
with cls._lock_class:
|
|
83
|
+
if cls._instance is None:
|
|
84
|
+
raise RuntimeError("Cache not initialized. Call initialize() first.")
|
|
85
|
+
return cls._instance
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def reset_instance(cls) -> None:
|
|
89
|
+
"""Reset singleton (for testing only)."""
|
|
90
|
+
with cls._lock_class:
|
|
91
|
+
cls._instance = None
|
|
92
|
+
|
|
93
|
+
# ===== GET OPERATIONS =====
|
|
94
|
+
|
|
95
|
+
def get(self, key: str) -> Any | None:
|
|
96
|
+
"""Retrieve cached value by key."""
|
|
97
|
+
if not key:
|
|
98
|
+
raise CacheKeyError("Cache key cannot be empty")
|
|
99
|
+
|
|
100
|
+
with self._lock:
|
|
101
|
+
if key not in self._cache:
|
|
102
|
+
if self._metrics_enabled:
|
|
103
|
+
self._stats["misses"] += 1
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
value, ttl_expiry = self._cache[key]
|
|
107
|
+
|
|
108
|
+
# Check TTL expiration
|
|
109
|
+
if ttl_expiry is not None and ttl_expiry < time.time():
|
|
110
|
+
del self._cache[key]
|
|
111
|
+
del self._access_times[key]
|
|
112
|
+
if self._metrics_enabled:
|
|
113
|
+
self._stats["misses"] += 1
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
# Update LRU tracking
|
|
117
|
+
self._access_times[key] = time.time()
|
|
118
|
+
|
|
119
|
+
if self._metrics_enabled:
|
|
120
|
+
self._stats["hits"] += 1
|
|
121
|
+
|
|
122
|
+
return value
|
|
123
|
+
|
|
124
|
+
def get_or_compute(
|
|
125
|
+
self, key: str, compute_fn: Callable[[], Any], ttl: int | None = None
|
|
126
|
+
) -> Any:
|
|
127
|
+
"""Get cached value or compute and cache if missing."""
|
|
128
|
+
value = self.get(key)
|
|
129
|
+
if value is not None:
|
|
130
|
+
return value
|
|
131
|
+
|
|
132
|
+
# Compute and cache
|
|
133
|
+
start_time = time.time()
|
|
134
|
+
value = compute_fn()
|
|
135
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
136
|
+
|
|
137
|
+
if self._metrics_enabled:
|
|
138
|
+
self._stats["total_load_time_ms"] += elapsed_ms
|
|
139
|
+
self._stats["load_count"] += 1
|
|
140
|
+
|
|
141
|
+
self.set(key, value, ttl)
|
|
142
|
+
return value
|
|
143
|
+
|
|
144
|
+
def exists(self, key: str) -> bool:
|
|
145
|
+
"""Check if key exists in cache."""
|
|
146
|
+
with self._lock:
|
|
147
|
+
if key not in self._cache:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
value, ttl_expiry = self._cache[key]
|
|
151
|
+
|
|
152
|
+
# Check TTL expiration
|
|
153
|
+
if ttl_expiry is not None and ttl_expiry < time.time():
|
|
154
|
+
del self._cache[key]
|
|
155
|
+
del self._access_times[key]
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
# ===== SET OPERATIONS =====
|
|
161
|
+
|
|
162
|
+
def set(self, key: str, value: Any, ttl: int | None = None) -> None:
|
|
163
|
+
"""Cache a value with optional time-to-live."""
|
|
164
|
+
if not key:
|
|
165
|
+
raise CacheKeyError("Cache key cannot be empty")
|
|
166
|
+
|
|
167
|
+
with self._lock:
|
|
168
|
+
# Evict LRU if needed and key doesn't already exist
|
|
169
|
+
if len(self._cache) >= self._max_size and key not in self._cache:
|
|
170
|
+
if not self._access_times:
|
|
171
|
+
raise CacheCapacityError("Cache at capacity and can't evict")
|
|
172
|
+
|
|
173
|
+
# Find and evict LRU item
|
|
174
|
+
lru_key = min(self._access_times, key=lambda k: self._access_times[k])
|
|
175
|
+
del self._cache[lru_key]
|
|
176
|
+
del self._access_times[lru_key]
|
|
177
|
+
|
|
178
|
+
if self._metrics_enabled:
|
|
179
|
+
self._stats["evictions"] += 1
|
|
180
|
+
|
|
181
|
+
# Calculate TTL expiry
|
|
182
|
+
ttl_seconds = ttl if ttl is not None else self._default_ttl
|
|
183
|
+
ttl_expiry = time.time() + ttl_seconds if ttl_seconds else None
|
|
184
|
+
|
|
185
|
+
# Store value and update access time
|
|
186
|
+
self._cache[key] = (value, ttl_expiry)
|
|
187
|
+
self._access_times[key] = time.time()
|
|
188
|
+
|
|
189
|
+
def set_many(self, items: dict[str, Any], ttl: int | None = None) -> None:
|
|
190
|
+
"""Cache multiple key-value pairs at once."""
|
|
191
|
+
for key, value in items.items():
|
|
192
|
+
self.set(key, value, ttl)
|
|
193
|
+
|
|
194
|
+
# ===== DELETE OPERATIONS =====
|
|
195
|
+
|
|
196
|
+
def delete(self, key: str) -> bool:
|
|
197
|
+
"""Delete single cached value."""
|
|
198
|
+
with self._lock:
|
|
199
|
+
if key in self._cache:
|
|
200
|
+
del self._cache[key]
|
|
201
|
+
del self._access_times[key]
|
|
202
|
+
return True
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
def delete_pattern(self, pattern: str) -> int:
|
|
206
|
+
"""Delete all cached values matching pattern."""
|
|
207
|
+
with self._lock:
|
|
208
|
+
# Convert pattern to prefix if using wildcard syntax
|
|
209
|
+
if pattern.endswith("*"):
|
|
210
|
+
prefix = pattern[:-1]
|
|
211
|
+
matching_keys = [k for k in self._cache if k.startswith(prefix)]
|
|
212
|
+
else:
|
|
213
|
+
matching_keys = [k for k in self._cache if fnmatch.fnmatch(k, pattern)]
|
|
214
|
+
|
|
215
|
+
for key in matching_keys:
|
|
216
|
+
del self._cache[key]
|
|
217
|
+
del self._access_times[key]
|
|
218
|
+
|
|
219
|
+
return len(matching_keys)
|
|
220
|
+
|
|
221
|
+
def clear(self) -> int:
|
|
222
|
+
"""Clear all cached values."""
|
|
223
|
+
with self._lock:
|
|
224
|
+
count = len(self._cache)
|
|
225
|
+
self._cache.clear()
|
|
226
|
+
self._access_times.clear()
|
|
227
|
+
return count
|
|
228
|
+
|
|
229
|
+
# ===== BATCH OPERATIONS =====
|
|
230
|
+
|
|
231
|
+
def get_many(self, keys: list[str]) -> dict[str, Any]:
|
|
232
|
+
"""Retrieve multiple cached values at once."""
|
|
233
|
+
result = {}
|
|
234
|
+
for key in keys:
|
|
235
|
+
value = self.get(key)
|
|
236
|
+
if value is not None:
|
|
237
|
+
result[key] = value
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
def delete_many(self, keys: list[str]) -> int:
|
|
241
|
+
"""Delete multiple cached values at once."""
|
|
242
|
+
count = 0
|
|
243
|
+
for key in keys:
|
|
244
|
+
if self.delete(key):
|
|
245
|
+
count += 1
|
|
246
|
+
return count
|
|
247
|
+
|
|
248
|
+
# ===== INVALIDATION HELPERS =====
|
|
249
|
+
|
|
250
|
+
def invalidate_feature(self, feature_id: str) -> None:
|
|
251
|
+
"""Invalidate all caches related to a feature."""
|
|
252
|
+
with self._lock:
|
|
253
|
+
self.delete(f"feature:{feature_id}")
|
|
254
|
+
self.delete_pattern("feature:list:*")
|
|
255
|
+
self.delete(f"dependency:{feature_id}")
|
|
256
|
+
self.delete_pattern(f"dependency:*:blocking_for_{feature_id}")
|
|
257
|
+
self.delete(f"priority:{feature_id}")
|
|
258
|
+
self.delete_pattern("recommendation:*")
|
|
259
|
+
|
|
260
|
+
def invalidate_track(self, track_id: str) -> None:
|
|
261
|
+
"""Invalidate all caches related to a track."""
|
|
262
|
+
with self._lock:
|
|
263
|
+
self.delete(f"track:{track_id}")
|
|
264
|
+
self.delete(f"track:{track_id}:features")
|
|
265
|
+
self.delete_pattern("track:list:*")
|
|
266
|
+
# Tracks can affect features, so invalidate feature analytics
|
|
267
|
+
self.delete_pattern("recommendation:*")
|
|
268
|
+
|
|
269
|
+
def invalidate_analytics(self) -> None:
|
|
270
|
+
"""Invalidate all analytics caches."""
|
|
271
|
+
with self._lock:
|
|
272
|
+
self.delete_pattern("dependency:*")
|
|
273
|
+
self.delete_pattern("priority:*")
|
|
274
|
+
self.delete_pattern("recommendation:*")
|
|
275
|
+
self.delete_pattern("critical_path:*")
|
|
276
|
+
self.delete_pattern("blocking:*")
|
|
277
|
+
|
|
278
|
+
# ===== OBSERVABILITY =====
|
|
279
|
+
|
|
280
|
+
def size(self) -> int:
|
|
281
|
+
"""Get current number of cached items."""
|
|
282
|
+
with self._lock:
|
|
283
|
+
return len(self._cache)
|
|
284
|
+
|
|
285
|
+
def stats(self) -> dict[str, Any]:
|
|
286
|
+
"""Get detailed cache statistics."""
|
|
287
|
+
with self._lock:
|
|
288
|
+
total_requests = self._stats["hits"] + self._stats["misses"]
|
|
289
|
+
hit_rate = (
|
|
290
|
+
self._stats["hits"] / total_requests if total_requests > 0 else 0.0
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
avg_load_ms = (
|
|
294
|
+
self._stats["total_load_time_ms"] / self._stats["load_count"]
|
|
295
|
+
if self._stats["load_count"] > 0
|
|
296
|
+
else 0.0
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Estimate memory usage (rough approximation)
|
|
300
|
+
# Assume average 1KB per cached item
|
|
301
|
+
memory_bytes = len(self._cache) * 1024
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"hits": self._stats["hits"],
|
|
305
|
+
"misses": self._stats["misses"],
|
|
306
|
+
"hit_rate": hit_rate,
|
|
307
|
+
"evictions": self._stats["evictions"],
|
|
308
|
+
"size": len(self._cache),
|
|
309
|
+
"capacity": self._max_size,
|
|
310
|
+
"memory_bytes": memory_bytes,
|
|
311
|
+
"avg_load_ms": avg_load_ms,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
def reset_stats(self) -> None:
|
|
315
|
+
"""Reset cache statistics to zero."""
|
|
316
|
+
with self._lock:
|
|
317
|
+
self._stats = {
|
|
318
|
+
"hits": 0,
|
|
319
|
+
"misses": 0,
|
|
320
|
+
"evictions": 0,
|
|
321
|
+
"total_load_time_ms": 0.0,
|
|
322
|
+
"load_count": 0,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# ===== CONFIGURATION =====
|
|
326
|
+
|
|
327
|
+
def configure(
|
|
328
|
+
self,
|
|
329
|
+
max_size: int | None = None,
|
|
330
|
+
default_ttl: int | None = None,
|
|
331
|
+
metrics_enabled: bool | None = None,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Configure cache behavior."""
|
|
334
|
+
with self._lock:
|
|
335
|
+
if max_size is not None:
|
|
336
|
+
self._max_size = max_size
|
|
337
|
+
if default_ttl is not None:
|
|
338
|
+
self._default_ttl = default_ttl
|
|
339
|
+
if metrics_enabled is not None:
|
|
340
|
+
self._metrics_enabled = metrics_enabled
|
|
341
|
+
|
|
342
|
+
def is_configured(self) -> bool:
|
|
343
|
+
"""Check if cache is properly configured."""
|
|
344
|
+
return self._max_size > 0 and self._default_ttl >= 0 and self._cache is not None
|
|
345
|
+
|
|
346
|
+
# ===== DEBUG / UTILITY =====
|
|
347
|
+
|
|
348
|
+
def debug_info(self) -> dict[str, Any]:
|
|
349
|
+
"""Get detailed debug information."""
|
|
350
|
+
with self._lock:
|
|
351
|
+
keys_info = {}
|
|
352
|
+
for key in self._cache:
|
|
353
|
+
value, ttl_expiry = self._cache[key]
|
|
354
|
+
keys_info[key] = {
|
|
355
|
+
"ttl_remaining": (
|
|
356
|
+
ttl_expiry - time.time() if ttl_expiry is not None else None
|
|
357
|
+
),
|
|
358
|
+
"access_time": self._access_times.get(key),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Sort by LRU order (oldest first)
|
|
362
|
+
lru_order = sorted(
|
|
363
|
+
self._access_times.keys(), key=lambda k: self._access_times[k]
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
"keys": list(self._cache.keys()),
|
|
368
|
+
"key_info": keys_info,
|
|
369
|
+
"lru_order": lru_order,
|
|
370
|
+
"size": len(self._cache),
|
|
371
|
+
"capacity": self._max_size,
|
|
372
|
+
"stats": self.stats(),
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
def validate_integrity(self) -> bool:
|
|
376
|
+
"""Validate cache internal consistency."""
|
|
377
|
+
with self._lock:
|
|
378
|
+
try:
|
|
379
|
+
# Check no expired items
|
|
380
|
+
current_time = time.time()
|
|
381
|
+
for key, (value, ttl_expiry) in self._cache.items():
|
|
382
|
+
if ttl_expiry is not None and ttl_expiry < current_time:
|
|
383
|
+
return False # Expired item found
|
|
384
|
+
|
|
385
|
+
# Check all cache keys have access times
|
|
386
|
+
if set(self._cache.keys()) != set(self._access_times.keys()):
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
# Check size tracking
|
|
390
|
+
if len(self._cache) > self._max_size:
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
return True
|
|
394
|
+
except Exception:
|
|
395
|
+
return False
|