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.
- mcp_ticketer/__init__.py +27 -0
- mcp_ticketer/__version__.py +40 -0
- mcp_ticketer/adapters/__init__.py +8 -0
- mcp_ticketer/adapters/aitrackdown.py +396 -0
- mcp_ticketer/adapters/github.py +974 -0
- mcp_ticketer/adapters/jira.py +831 -0
- mcp_ticketer/adapters/linear.py +1355 -0
- mcp_ticketer/cache/__init__.py +5 -0
- mcp_ticketer/cache/memory.py +193 -0
- mcp_ticketer/cli/__init__.py +5 -0
- mcp_ticketer/cli/main.py +812 -0
- mcp_ticketer/cli/queue_commands.py +285 -0
- mcp_ticketer/cli/utils.py +523 -0
- mcp_ticketer/core/__init__.py +15 -0
- mcp_ticketer/core/adapter.py +211 -0
- mcp_ticketer/core/config.py +403 -0
- mcp_ticketer/core/http_client.py +430 -0
- mcp_ticketer/core/mappers.py +492 -0
- mcp_ticketer/core/models.py +111 -0
- mcp_ticketer/core/registry.py +128 -0
- mcp_ticketer/mcp/__init__.py +5 -0
- mcp_ticketer/mcp/server.py +459 -0
- mcp_ticketer/py.typed +0 -0
- mcp_ticketer/queue/__init__.py +7 -0
- mcp_ticketer/queue/__main__.py +6 -0
- mcp_ticketer/queue/manager.py +261 -0
- mcp_ticketer/queue/queue.py +357 -0
- mcp_ticketer/queue/run_worker.py +38 -0
- mcp_ticketer/queue/worker.py +425 -0
- mcp_ticketer-0.1.1.dist-info/METADATA +362 -0
- mcp_ticketer-0.1.1.dist-info/RECORD +35 -0
- mcp_ticketer-0.1.1.dist-info/WHEEL +5 -0
- mcp_ticketer-0.1.1.dist-info/entry_points.txt +3 -0
- mcp_ticketer-0.1.1.dist-info/licenses/LICENSE +21 -0
- mcp_ticketer-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -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
|