mcp-ticketer 0.1.1__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.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

@@ -0,0 +1,5 @@
1
+ """Caching utilities for MCP Ticketer."""
2
+
3
+ from .memory import MemoryCache, cache_decorator
4
+
5
+ __all__ = ["MemoryCache", "cache_decorator"]
@@ -0,0 +1,193 @@
1
+ """In-memory cache implementation with TTL support."""
2
+
3
+ import asyncio
4
+ import hashlib
5
+ import json
6
+ import time
7
+ from functools import wraps
8
+ from typing import Any, Optional, Callable, Dict, Tuple
9
+
10
+
11
+ class CacheEntry:
12
+ """Single cache entry with TTL."""
13
+
14
+ def __init__(self, value: Any, ttl: float):
15
+ """Initialize cache entry.
16
+
17
+ Args:
18
+ value: Cached value
19
+ ttl: Time to live in seconds
20
+ """
21
+ self.value = value
22
+ self.expires_at = time.time() + ttl if ttl > 0 else float("inf")
23
+
24
+ def is_expired(self) -> bool:
25
+ """Check if entry has expired."""
26
+ return time.time() > self.expires_at
27
+
28
+
29
+ class MemoryCache:
30
+ """Simple in-memory cache with TTL support."""
31
+
32
+ def __init__(self, default_ttl: float = 300.0):
33
+ """Initialize cache.
34
+
35
+ Args:
36
+ default_ttl: Default TTL in seconds (5 minutes)
37
+ """
38
+ self._cache: Dict[str, CacheEntry] = {}
39
+ self._default_ttl = default_ttl
40
+ self._lock = asyncio.Lock()
41
+
42
+ async def get(self, key: str) -> Optional[Any]:
43
+ """Get value from cache.
44
+
45
+ Args:
46
+ key: Cache key
47
+
48
+ Returns:
49
+ Cached value or None if not found/expired
50
+ """
51
+ async with self._lock:
52
+ entry = self._cache.get(key)
53
+ if entry and not entry.is_expired():
54
+ return entry.value
55
+ elif entry:
56
+ # Remove expired entry
57
+ del self._cache[key]
58
+ return None
59
+
60
+ async def set(
61
+ self,
62
+ key: str,
63
+ value: Any,
64
+ ttl: Optional[float] = None
65
+ ) -> None:
66
+ """Set value in cache.
67
+
68
+ Args:
69
+ key: Cache key
70
+ value: Value to cache
71
+ ttl: Optional TTL override
72
+ """
73
+ async with self._lock:
74
+ ttl = ttl if ttl is not None else self._default_ttl
75
+ self._cache[key] = CacheEntry(value, ttl)
76
+
77
+ async def delete(self, key: str) -> bool:
78
+ """Delete key from cache.
79
+
80
+ Args:
81
+ key: Cache key
82
+
83
+ Returns:
84
+ True if key was deleted
85
+ """
86
+ async with self._lock:
87
+ if key in self._cache:
88
+ del self._cache[key]
89
+ return True
90
+ return False
91
+
92
+ async def clear(self) -> None:
93
+ """Clear all cache entries."""
94
+ async with self._lock:
95
+ self._cache.clear()
96
+
97
+ async def cleanup_expired(self) -> int:
98
+ """Remove expired entries.
99
+
100
+ Returns:
101
+ Number of entries removed
102
+ """
103
+ async with self._lock:
104
+ expired_keys = [
105
+ key for key, entry in self._cache.items()
106
+ if entry.is_expired()
107
+ ]
108
+ for key in expired_keys:
109
+ del self._cache[key]
110
+ return len(expired_keys)
111
+
112
+ def size(self) -> int:
113
+ """Get number of entries in cache."""
114
+ return len(self._cache)
115
+
116
+ @staticmethod
117
+ def generate_key(*args, **kwargs) -> str:
118
+ """Generate cache key from arguments.
119
+
120
+ Args:
121
+ *args: Positional arguments
122
+ **kwargs: Keyword arguments
123
+
124
+ Returns:
125
+ Hash-based cache key
126
+ """
127
+ # Create string representation of arguments
128
+ key_data = {
129
+ "args": args,
130
+ "kwargs": sorted(kwargs.items())
131
+ }
132
+ key_str = json.dumps(key_data, sort_keys=True, default=str)
133
+
134
+ # Generate hash
135
+ return hashlib.sha256(key_str.encode()).hexdigest()[:16]
136
+
137
+
138
+ def cache_decorator(
139
+ ttl: Optional[float] = None,
140
+ key_prefix: str = "",
141
+ cache_instance: Optional[MemoryCache] = None
142
+ ) -> Callable:
143
+ """Decorator for caching async function results.
144
+
145
+ Args:
146
+ ttl: TTL for cached results
147
+ key_prefix: Prefix for cache keys
148
+ cache_instance: Cache instance to use (creates new if None)
149
+
150
+ Returns:
151
+ Decorated function
152
+ """
153
+ # Use shared cache instance or create new
154
+ cache = cache_instance or MemoryCache()
155
+
156
+ def decorator(func: Callable) -> Callable:
157
+ @wraps(func)
158
+ async def wrapper(*args, **kwargs):
159
+ # Generate cache key
160
+ base_key = MemoryCache.generate_key(*args, **kwargs)
161
+ cache_key = f"{key_prefix}:{func.__name__}:{base_key}"
162
+
163
+ # Try to get from cache
164
+ cached_value = await cache.get(cache_key)
165
+ if cached_value is not None:
166
+ return cached_value
167
+
168
+ # Execute function
169
+ result = await func(*args, **kwargs)
170
+
171
+ # Cache result
172
+ await cache.set(cache_key, result, ttl)
173
+
174
+ return result
175
+
176
+ # Add cache control methods
177
+ wrapper.cache_clear = lambda: cache.clear()
178
+ wrapper.cache_delete = lambda *a, **k: cache.delete(
179
+ f"{key_prefix}:{func.__name__}:{MemoryCache.generate_key(*a, **k)}"
180
+ )
181
+
182
+ return wrapper
183
+
184
+ return decorator
185
+
186
+
187
+ # Global cache instance for shared use
188
+ _global_cache = MemoryCache()
189
+
190
+
191
+ def get_global_cache() -> MemoryCache:
192
+ """Get global cache instance."""
193
+ return _global_cache
@@ -0,0 +1,5 @@
1
+ """Command-line interface for MCP Ticketer."""
2
+
3
+ from .main import app
4
+
5
+ __all__ = ["app"]