mftool 3.4__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.
- mftool/__init__.py +26 -0
- mftool/cache.py +233 -0
- mftool/const.json +20065 -0
- mftool/mftool.py +935 -0
- mftool/utils.py +56 -0
- mftool-3.4.dist-info/METADATA +69 -0
- mftool-3.4.dist-info/RECORD +10 -0
- mftool-3.4.dist-info/WHEEL +5 -0
- mftool-3.4.dist-info/licenses/LICENSE +22 -0
- mftool-3.4.dist-info/top_level.txt +1 -0
mftool/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Sujit Nayakwadi
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
23
|
+
"""
|
|
24
|
+
__VERSION__='3.4'
|
|
25
|
+
from .mftool import Mftool
|
|
26
|
+
|
mftool/cache.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache utility for mftool
|
|
3
|
+
Implements TTL-based caching to reduce redundant API calls
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from threading import Lock
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CacheManager:
|
|
13
|
+
"""
|
|
14
|
+
Thread-safe in-memory cache with TTL support
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self, default_ttl=86400): # 24 hours default
|
|
17
|
+
self._cache = {}
|
|
18
|
+
self._lock = Lock()
|
|
19
|
+
self.default_ttl = default_ttl
|
|
20
|
+
self.enabled = True
|
|
21
|
+
|
|
22
|
+
def get(self, key):
|
|
23
|
+
"""Get value from cache if not expired"""
|
|
24
|
+
if not self.enabled:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
with self._lock:
|
|
28
|
+
if key in self._cache:
|
|
29
|
+
value, expiry = self._cache[key]
|
|
30
|
+
if time.time() < expiry:
|
|
31
|
+
return value
|
|
32
|
+
else:
|
|
33
|
+
# Remove expired entry
|
|
34
|
+
del self._cache[key]
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def set(self, key, value, ttl=None):
|
|
38
|
+
"""Set value in cache with TTL"""
|
|
39
|
+
if not self.enabled:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if ttl is None:
|
|
43
|
+
ttl = self.default_ttl
|
|
44
|
+
|
|
45
|
+
expiry = time.time() + ttl
|
|
46
|
+
with self._lock:
|
|
47
|
+
self._cache[key] = (value, expiry)
|
|
48
|
+
|
|
49
|
+
def clear(self):
|
|
50
|
+
"""Clear all cache entries"""
|
|
51
|
+
with self._lock:
|
|
52
|
+
self._cache.clear()
|
|
53
|
+
|
|
54
|
+
def clear_expired(self):
|
|
55
|
+
"""Remove all expired entries"""
|
|
56
|
+
current_time = time.time()
|
|
57
|
+
with self._lock:
|
|
58
|
+
expired_keys = [k for k, (_, expiry) in self._cache.items() if current_time >= expiry]
|
|
59
|
+
for key in expired_keys:
|
|
60
|
+
del self._cache[key]
|
|
61
|
+
|
|
62
|
+
def disable(self):
|
|
63
|
+
"""Disable caching"""
|
|
64
|
+
self.enabled = False
|
|
65
|
+
|
|
66
|
+
def enable(self):
|
|
67
|
+
"""Enable caching"""
|
|
68
|
+
self.enabled = True
|
|
69
|
+
|
|
70
|
+
def get_stats(self):
|
|
71
|
+
"""Get cache statistics"""
|
|
72
|
+
with self._lock:
|
|
73
|
+
total_entries = len(self._cache)
|
|
74
|
+
current_time = time.time()
|
|
75
|
+
valid_entries = sum(1 for _, expiry in self._cache.values() if current_time < expiry)
|
|
76
|
+
expired_entries = total_entries - valid_entries
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
'total_entries': total_entries,
|
|
80
|
+
'valid_entries': valid_entries,
|
|
81
|
+
'expired_entries': expired_entries,
|
|
82
|
+
'cache_enabled': self.enabled
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DiskCache:
|
|
87
|
+
"""
|
|
88
|
+
Persistent disk-based cache with TTL support
|
|
89
|
+
"""
|
|
90
|
+
def __init__(self, cache_dir=None, default_ttl=86400):
|
|
91
|
+
if cache_dir is None:
|
|
92
|
+
cache_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.cache')
|
|
93
|
+
|
|
94
|
+
self.cache_dir = cache_dir
|
|
95
|
+
self.default_ttl = default_ttl
|
|
96
|
+
self.enabled = True
|
|
97
|
+
self._lock = Lock()
|
|
98
|
+
|
|
99
|
+
# Create cache directory if it doesn't exist
|
|
100
|
+
if not os.path.exists(self.cache_dir):
|
|
101
|
+
os.makedirs(self.cache_dir)
|
|
102
|
+
|
|
103
|
+
def _get_cache_path(self, key):
|
|
104
|
+
"""Generate cache file path for key"""
|
|
105
|
+
# Use hash to create safe filename
|
|
106
|
+
safe_key = str(abs(hash(key)))
|
|
107
|
+
return os.path.join(self.cache_dir, f"{safe_key}.json")
|
|
108
|
+
|
|
109
|
+
def get(self, key):
|
|
110
|
+
"""Get value from disk cache if not expired"""
|
|
111
|
+
if not self.enabled:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
cache_path = self._get_cache_path(key)
|
|
115
|
+
|
|
116
|
+
with self._lock:
|
|
117
|
+
if os.path.exists(cache_path):
|
|
118
|
+
try:
|
|
119
|
+
with open(cache_path, 'r') as f:
|
|
120
|
+
cache_data = json.load(f)
|
|
121
|
+
|
|
122
|
+
if time.time() < cache_data['expiry']:
|
|
123
|
+
return cache_data['value']
|
|
124
|
+
else:
|
|
125
|
+
# Remove expired file
|
|
126
|
+
os.remove(cache_path)
|
|
127
|
+
except (json.JSONDecodeError, KeyError, IOError):
|
|
128
|
+
# Invalid cache file, remove it
|
|
129
|
+
if os.path.exists(cache_path):
|
|
130
|
+
os.remove(cache_path)
|
|
131
|
+
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def set(self, key, value, ttl=None):
|
|
135
|
+
"""Set value in disk cache with TTL"""
|
|
136
|
+
if not self.enabled:
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
if ttl is None:
|
|
140
|
+
ttl = self.default_ttl
|
|
141
|
+
|
|
142
|
+
cache_path = self._get_cache_path(key)
|
|
143
|
+
expiry = time.time() + ttl
|
|
144
|
+
|
|
145
|
+
cache_data = {
|
|
146
|
+
'value': value,
|
|
147
|
+
'expiry': expiry,
|
|
148
|
+
'created': time.time()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
with self._lock:
|
|
152
|
+
try:
|
|
153
|
+
with open(cache_path, 'w') as f:
|
|
154
|
+
json.dump(cache_data, f)
|
|
155
|
+
except IOError:
|
|
156
|
+
pass # Silently fail if can't write cache
|
|
157
|
+
|
|
158
|
+
def clear(self):
|
|
159
|
+
"""Clear all cache files"""
|
|
160
|
+
with self._lock:
|
|
161
|
+
if os.path.exists(self.cache_dir):
|
|
162
|
+
for filename in os.listdir(self.cache_dir):
|
|
163
|
+
file_path = os.path.join(self.cache_dir, filename)
|
|
164
|
+
if os.path.isfile(file_path):
|
|
165
|
+
os.remove(file_path)
|
|
166
|
+
|
|
167
|
+
def clear_expired(self):
|
|
168
|
+
"""Remove all expired cache files"""
|
|
169
|
+
current_time = time.time()
|
|
170
|
+
with self._lock:
|
|
171
|
+
if os.path.exists(self.cache_dir):
|
|
172
|
+
for filename in os.listdir(self.cache_dir):
|
|
173
|
+
file_path = os.path.join(self.cache_dir, filename)
|
|
174
|
+
if os.path.isfile(file_path):
|
|
175
|
+
try:
|
|
176
|
+
with open(file_path, 'r') as f:
|
|
177
|
+
cache_data = json.load(f)
|
|
178
|
+
if current_time >= cache_data['expiry']:
|
|
179
|
+
os.remove(file_path)
|
|
180
|
+
except (json.JSONDecodeError, KeyError, IOError):
|
|
181
|
+
os.remove(file_path)
|
|
182
|
+
|
|
183
|
+
def disable(self):
|
|
184
|
+
"""Disable caching"""
|
|
185
|
+
self.enabled = False
|
|
186
|
+
|
|
187
|
+
def enable(self):
|
|
188
|
+
"""Enable caching"""
|
|
189
|
+
self.enabled = True
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def cached(ttl=86400, cache_type='memory'):
|
|
193
|
+
"""
|
|
194
|
+
Decorator for caching function results
|
|
195
|
+
|
|
196
|
+
:param ttl: Time to live in seconds (default 24 hours)
|
|
197
|
+
:param cache_type: 'memory' or 'disk'
|
|
198
|
+
"""
|
|
199
|
+
def decorator(func):
|
|
200
|
+
# Create cache instance for this function
|
|
201
|
+
if cache_type == 'disk':
|
|
202
|
+
cache = DiskCache(default_ttl=ttl)
|
|
203
|
+
else:
|
|
204
|
+
cache = CacheManager(default_ttl=ttl)
|
|
205
|
+
|
|
206
|
+
@wraps(func)
|
|
207
|
+
def wrapper(*args, **kwargs):
|
|
208
|
+
# Create cache key from function name and arguments
|
|
209
|
+
# Skip 'self' for instance methods
|
|
210
|
+
cache_args = args[1:] if args and hasattr(args[0], func.__name__) else args
|
|
211
|
+
cache_key = f"{func.__name__}:{str(cache_args)}:{str(kwargs)}"
|
|
212
|
+
|
|
213
|
+
# Try to get from cache
|
|
214
|
+
cached_value = cache.get(cache_key)
|
|
215
|
+
if cached_value is not None:
|
|
216
|
+
return cached_value
|
|
217
|
+
|
|
218
|
+
# Call function and cache result
|
|
219
|
+
result = func(*args, **kwargs)
|
|
220
|
+
if result is not None:
|
|
221
|
+
cache.set(cache_key, result, ttl)
|
|
222
|
+
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
# Attach cache management methods
|
|
226
|
+
wrapper.clear_cache = cache.clear
|
|
227
|
+
wrapper.clear_expired = cache.clear_expired
|
|
228
|
+
wrapper.disable_cache = cache.disable
|
|
229
|
+
wrapper.enable_cache = cache.enable
|
|
230
|
+
|
|
231
|
+
return wrapper
|
|
232
|
+
return decorator
|
|
233
|
+
|