vnai 0.1.3__py3-none-any.whl → 2.3.7__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.
- vnai/__init__.py +304 -292
- vnai/beam/__init__.py +26 -0
- vnai/beam/auth.py +312 -0
- vnai/beam/fundamental.py +168 -0
- vnai/beam/metrics.py +167 -0
- vnai/beam/patching.py +223 -0
- vnai/beam/pulse.py +79 -0
- vnai/beam/quota.py +403 -0
- vnai/beam/sync.py +87 -0
- vnai/flow/__init__.py +2 -0
- vnai/flow/queue.py +100 -0
- vnai/flow/relay.py +347 -0
- vnai/scope/__init__.py +11 -0
- vnai/scope/device.py +315 -0
- vnai/scope/lc_integration.py +351 -0
- vnai/scope/license.py +197 -0
- vnai/scope/profile.py +599 -0
- vnai/scope/promo.py +389 -0
- vnai/scope/state.py +155 -0
- vnai-2.3.7.dist-info/METADATA +21 -0
- vnai-2.3.7.dist-info/RECORD +23 -0
- {vnai-0.1.3.dist-info → vnai-2.3.7.dist-info}/WHEEL +1 -1
- vnai-0.1.3.dist-info/METADATA +0 -20
- vnai-0.1.3.dist-info/RECORD +0 -5
- {vnai-0.1.3.dist-info → vnai-2.3.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
def _get_ide_info() -> Dict:
|
|
9
|
+
try:
|
|
10
|
+
from vnai.scope.device import get_current_ide_info
|
|
11
|
+
return get_current_ide_info()
|
|
12
|
+
except Exception as e:
|
|
13
|
+
logger.debug(f"Failed to get IDE info: {e}")
|
|
14
|
+
return {}
|
|
15
|
+
|
|
16
|
+
def _sync_profile_to_api(
|
|
17
|
+
device_info: Dict[str, Any],
|
|
18
|
+
ide_info: Dict[str, Any],
|
|
19
|
+
license_info: Dict[str, Any],
|
|
20
|
+
api_key: Optional[str] = None,
|
|
21
|
+
force: bool = False
|
|
22
|
+
) -> Dict[str, Any]:
|
|
23
|
+
try:
|
|
24
|
+
import requests
|
|
25
|
+
except ImportError:
|
|
26
|
+
logger.debug("requests library not available, skipping API sync")
|
|
27
|
+
return {
|
|
28
|
+
'status':'skipped',
|
|
29
|
+
'reason':'requests_not_available',
|
|
30
|
+
}
|
|
31
|
+
try:
|
|
32
|
+
profile = {
|
|
33
|
+
'timestamp': datetime.now().isoformat(),
|
|
34
|
+
'device': {
|
|
35
|
+
'device_id': device_info.get('machine_id'),
|
|
36
|
+
'os': device_info.get('os_name'),
|
|
37
|
+
'os_platform': device_info.get('platform'),
|
|
38
|
+
'python_version': device_info.get('python_version'),
|
|
39
|
+
'arch': device_info.get('platform','').split('-')[-1] if device_info.get('platform') else'unknown',
|
|
40
|
+
'cpu_count': device_info.get('cpu_count'),
|
|
41
|
+
'memory_gb': device_info.get('memory_gb'),
|
|
42
|
+
'environment': device_info.get('environment'),
|
|
43
|
+
'hosting_service': device_info.get('hosting_service'),
|
|
44
|
+
},
|
|
45
|
+
'ide': {
|
|
46
|
+
'name': ide_info.get('ide_name'),
|
|
47
|
+
'detection_method': ide_info.get('detection_method'),
|
|
48
|
+
'detected_at': ide_info.get('detected_at'),
|
|
49
|
+
'process_chain_depth': ide_info.get('process_chain_depth'),
|
|
50
|
+
'frontend': ide_info.get('frontend'),
|
|
51
|
+
},
|
|
52
|
+
'license': {
|
|
53
|
+
'is_paid': license_info.get('is_paid'),
|
|
54
|
+
'status': license_info.get('status'),
|
|
55
|
+
'tier': license_info.get('tier'),
|
|
56
|
+
} if license_info else None,
|
|
57
|
+
}
|
|
58
|
+
payload = {
|
|
59
|
+
'profile': profile,
|
|
60
|
+
'sync_timestamp': datetime.now().isoformat(),
|
|
61
|
+
'sync_version':'2.0',
|
|
62
|
+
}
|
|
63
|
+
headers = {
|
|
64
|
+
'Content-Type':'application/json',
|
|
65
|
+
'User-Agent':'vnstock-analytics/2.0',
|
|
66
|
+
}
|
|
67
|
+
if api_key:
|
|
68
|
+
headers['Authorization'] =f'Bearer {api_key}'
|
|
69
|
+
endpoint ="https://api.vnstocks.com/v1/user/profile/sync"
|
|
70
|
+
response = requests.post(
|
|
71
|
+
endpoint,
|
|
72
|
+
json=payload,
|
|
73
|
+
headers=headers,
|
|
74
|
+
timeout=10
|
|
75
|
+
)
|
|
76
|
+
if response.status_code == 200:
|
|
77
|
+
logger.info("Profile successfully synced to vnstocks.com API")
|
|
78
|
+
return {
|
|
79
|
+
'status':'success',
|
|
80
|
+
'synced_at': datetime.now().isoformat(),
|
|
81
|
+
'api_response': response.json(),
|
|
82
|
+
}
|
|
83
|
+
else:
|
|
84
|
+
logger.debug(f"API sync failed: {response.status_code}")
|
|
85
|
+
return {
|
|
86
|
+
'status':'failed',
|
|
87
|
+
'error': response.text,
|
|
88
|
+
'status_code': response.status_code,
|
|
89
|
+
}
|
|
90
|
+
except requests.exceptions.RequestException as e:
|
|
91
|
+
logger.debug(f"API request failed: {e}")
|
|
92
|
+
return {
|
|
93
|
+
'status':'failed',
|
|
94
|
+
'error': str(e),
|
|
95
|
+
}
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Error syncing profile to API: {e}")
|
|
98
|
+
return {
|
|
99
|
+
'status':'error',
|
|
100
|
+
'error': str(e),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class APIKeyChecker:
|
|
104
|
+
_instance = None
|
|
105
|
+
_lock = None
|
|
106
|
+
|
|
107
|
+
def __new__(cls):
|
|
108
|
+
import threading
|
|
109
|
+
if cls._lock is None:
|
|
110
|
+
cls._lock = threading.Lock()
|
|
111
|
+
with cls._lock:
|
|
112
|
+
if cls._instance is None:
|
|
113
|
+
cls._instance = super(APIKeyChecker, cls).__new__(cls)
|
|
114
|
+
cls._instance._initialize()
|
|
115
|
+
return cls._instance
|
|
116
|
+
|
|
117
|
+
def _initialize(self) -> None:
|
|
118
|
+
self.checked = False
|
|
119
|
+
self.last_check_time = None
|
|
120
|
+
self.is_paid = None
|
|
121
|
+
self.license_info = None
|
|
122
|
+
|
|
123
|
+
def _get_vnstock_directories(self) -> list[Path]:
|
|
124
|
+
paths = []
|
|
125
|
+
local_path = Path.home() /".vnstock"
|
|
126
|
+
paths.append(local_path)
|
|
127
|
+
colab_drive_path = Path('/content/drive/MyDrive/.vnstock')
|
|
128
|
+
if colab_drive_path.parent.exists():
|
|
129
|
+
paths.append(colab_drive_path)
|
|
130
|
+
try:
|
|
131
|
+
from vnstock.core.config.ggcolab import get_vnstock_directory
|
|
132
|
+
vnstock_dir = get_vnstock_directory()
|
|
133
|
+
if vnstock_dir not in paths:
|
|
134
|
+
paths.append(vnstock_dir)
|
|
135
|
+
except ImportError:
|
|
136
|
+
pass
|
|
137
|
+
return paths
|
|
138
|
+
|
|
139
|
+
def _find_api_key_file(self) -> Optional[Path]:
|
|
140
|
+
for vnstock_dir in self._get_vnstock_directories():
|
|
141
|
+
api_key_path = vnstock_dir /"api_key.json"
|
|
142
|
+
if api_key_path.exists():
|
|
143
|
+
logger.debug(f"Found api_key.json at: {api_key_path}")
|
|
144
|
+
return api_key_path
|
|
145
|
+
logger.debug("api_key.json not found in any vnstock directory")
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def _read_api_key(self, api_key_path: Path) -> Optional[str]:
|
|
149
|
+
try:
|
|
150
|
+
with open(api_key_path,'r', encoding='utf-8') as f:
|
|
151
|
+
data = json.load(f)
|
|
152
|
+
api_key = data.get('api_key')
|
|
153
|
+
if api_key and isinstance(api_key, str):
|
|
154
|
+
return api_key.strip()
|
|
155
|
+
else:
|
|
156
|
+
logger.warning(
|
|
157
|
+
f"Invalid api_key format in {api_key_path}"
|
|
158
|
+
)
|
|
159
|
+
return None
|
|
160
|
+
except json.JSONDecodeError as e:
|
|
161
|
+
logger.error(f"Invalid JSON in {api_key_path}: {e}")
|
|
162
|
+
return None
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"Error reading {api_key_path}: {e}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def check_license_with_vnii(
|
|
168
|
+
self,
|
|
169
|
+
force: bool = False,
|
|
170
|
+
include_ide_info: bool = True
|
|
171
|
+
) -> Dict[str, Any]:
|
|
172
|
+
if self.checked and not force:
|
|
173
|
+
result = {
|
|
174
|
+
'is_paid': self.is_paid,
|
|
175
|
+
'status':'cached',
|
|
176
|
+
'checked_at': self.last_check_time,
|
|
177
|
+
'api_key_found': True,
|
|
178
|
+
'vnii_available': True
|
|
179
|
+
}
|
|
180
|
+
if include_ide_info:
|
|
181
|
+
result['ide_info'] = _get_ide_info()
|
|
182
|
+
return result
|
|
183
|
+
result = {
|
|
184
|
+
'is_paid': False,
|
|
185
|
+
'status':'unknown',
|
|
186
|
+
'checked_at': datetime.now().isoformat(),
|
|
187
|
+
'api_key_found': False,
|
|
188
|
+
'vnii_available': False
|
|
189
|
+
}
|
|
190
|
+
if include_ide_info:
|
|
191
|
+
result['ide_info'] = _get_ide_info()
|
|
192
|
+
try:
|
|
193
|
+
from vnii import lc_init
|
|
194
|
+
result['vnii_available'] = True
|
|
195
|
+
except ImportError:
|
|
196
|
+
logger.debug("vnii package not available")
|
|
197
|
+
result['status'] ='vnii_not_installed'
|
|
198
|
+
return result
|
|
199
|
+
api_key_path = self._find_api_key_file()
|
|
200
|
+
if not api_key_path:
|
|
201
|
+
logger.debug("No api_key.json found")
|
|
202
|
+
result['status'] ='no_api_key_file'
|
|
203
|
+
return result
|
|
204
|
+
api_key = self._read_api_key(api_key_path)
|
|
205
|
+
if not api_key:
|
|
206
|
+
logger.warning("Could not read API key from file")
|
|
207
|
+
result['status'] ='invalid_api_key_file'
|
|
208
|
+
return result
|
|
209
|
+
result['api_key_found'] = True
|
|
210
|
+
try:
|
|
211
|
+
from vnii import lc_init
|
|
212
|
+
import os
|
|
213
|
+
original_env = os.environ.get('VNSTOCK_API_KEY')
|
|
214
|
+
os.environ['VNSTOCK_API_KEY'] = api_key
|
|
215
|
+
try:
|
|
216
|
+
license_info = lc_init(debug=False)
|
|
217
|
+
status = license_info.get('status','').lower()
|
|
218
|
+
tier = license_info.get('tier','').lower()
|
|
219
|
+
is_paid = (
|
|
220
|
+
'recognized and verified' in status or
|
|
221
|
+
tier in ['bronze','silver','golden','gold']
|
|
222
|
+
)
|
|
223
|
+
result['is_paid'] = is_paid
|
|
224
|
+
result['status'] ='verified' if is_paid else'free_user'
|
|
225
|
+
result['license_info'] = license_info
|
|
226
|
+
self.checked = True
|
|
227
|
+
self.last_check_time = result['checked_at']
|
|
228
|
+
self.is_paid = is_paid
|
|
229
|
+
self.license_info = license_info
|
|
230
|
+
logger.info(
|
|
231
|
+
f"License verified via vnii: "
|
|
232
|
+
f"is_paid={is_paid}, tier={tier}"
|
|
233
|
+
)
|
|
234
|
+
finally:
|
|
235
|
+
if original_env is None:
|
|
236
|
+
os.environ.pop('VNSTOCK_API_KEY', None)
|
|
237
|
+
else:
|
|
238
|
+
os.environ['VNSTOCK_API_KEY'] = original_env
|
|
239
|
+
except SystemExit as e:
|
|
240
|
+
error_msg = str(e)
|
|
241
|
+
if'device limit exceeded' in error_msg.lower():
|
|
242
|
+
logger.warning(f"Device limit exceeded but user is paid")
|
|
243
|
+
result['status'] ='device_limit_exceeded'
|
|
244
|
+
result['is_paid'] = True
|
|
245
|
+
result['error'] = error_msg
|
|
246
|
+
self.checked = True
|
|
247
|
+
self.last_check_time = result['checked_at']
|
|
248
|
+
self.is_paid = True
|
|
249
|
+
self.license_info = {'status':'Device limit','tier':'paid'}
|
|
250
|
+
else:
|
|
251
|
+
logger.warning(f"vnii license check failed: {e}")
|
|
252
|
+
result['status'] ='license_check_failed'
|
|
253
|
+
result['error'] = error_msg
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(f"Error calling vnii lc_init: {e}")
|
|
256
|
+
result['status'] ='error'
|
|
257
|
+
result['error'] = str(e)
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
def sync_profile_to_api(
|
|
261
|
+
self,
|
|
262
|
+
device_info: Optional[Dict[str, Any]] = None,
|
|
263
|
+
api_key: Optional[str] = None,
|
|
264
|
+
force: bool = False
|
|
265
|
+
) -> Dict[str, Any]:
|
|
266
|
+
try:
|
|
267
|
+
ide_info = _get_ide_info()
|
|
268
|
+
license_info = {
|
|
269
|
+
'is_paid': self.is_paid,
|
|
270
|
+
'status':'verified' if self.is_paid else'free_user',
|
|
271
|
+
'tier': self.license_info.get('tier') if self.license_info else None,
|
|
272
|
+
} if self.checked else None
|
|
273
|
+
if device_info is None:
|
|
274
|
+
try:
|
|
275
|
+
from vnai.scope.profile import inspector
|
|
276
|
+
device_data = inspector.examine()
|
|
277
|
+
device_info = {
|
|
278
|
+
'machine_id': device_data.get('machine_id'),
|
|
279
|
+
'os_name': device_data.get('os_name'),
|
|
280
|
+
'platform': device_data.get('platform'),
|
|
281
|
+
'python_version': device_data.get('python_version'),
|
|
282
|
+
'cpu_count': device_data.get('cpu_count'),
|
|
283
|
+
'memory_gb': device_data.get('memory_gb'),
|
|
284
|
+
'environment': device_data.get('environment'),
|
|
285
|
+
'hosting_service': device_data.get('hosting_service'),
|
|
286
|
+
}
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.debug(f"Failed to get device info: {e}")
|
|
289
|
+
device_info = {}
|
|
290
|
+
return _sync_profile_to_api(
|
|
291
|
+
device_info=device_info,
|
|
292
|
+
ide_info=ide_info,
|
|
293
|
+
license_info=license_info,
|
|
294
|
+
api_key=api_key,
|
|
295
|
+
force=force
|
|
296
|
+
)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
logger.error(f"Error in sync_profile_to_api: {e}")
|
|
299
|
+
return {
|
|
300
|
+
'status':'error',
|
|
301
|
+
'error': str(e),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
def is_paid_user(self) -> Optional[bool]:
|
|
305
|
+
if not self.checked:
|
|
306
|
+
result = self.check_license_with_vnii()
|
|
307
|
+
return result.get('is_paid')
|
|
308
|
+
return self.is_paid
|
|
309
|
+
|
|
310
|
+
def get_license_info(self) -> Optional[Dict]:
|
|
311
|
+
return self.license_info
|
|
312
|
+
api_key_checker = APIKeyChecker()
|
|
313
|
+
|
|
314
|
+
def check_license_via_api_key(force: bool = False) -> Dict[str, Any]:
|
|
315
|
+
return api_key_checker.check_license_with_vnii(force=force)
|
|
316
|
+
|
|
317
|
+
def is_paid_user_via_api_key() -> Optional[bool]:
|
|
318
|
+
return api_key_checker.is_paid_user()
|
|
319
|
+
|
|
320
|
+
def check_license_status() -> Optional[bool]:
|
|
321
|
+
try:
|
|
322
|
+
is_paid = api_key_checker.is_paid_user()
|
|
323
|
+
return is_paid
|
|
324
|
+
except ImportError:
|
|
325
|
+
logger.warning("API key checker not available")
|
|
326
|
+
return None
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.error(f"Error checking license status: {e}")
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
def update_license_from_vnii() -> bool:
|
|
332
|
+
try:
|
|
333
|
+
result = api_key_checker.check_license_with_vnii(force=True)
|
|
334
|
+
if result.get('status') in ['verified','cached']:
|
|
335
|
+
is_paid = result.get('is_paid', False)
|
|
336
|
+
logger.info(f"License updated via API key: is_paid={is_paid}")
|
|
337
|
+
return True
|
|
338
|
+
else:
|
|
339
|
+
logger.warning(
|
|
340
|
+
f"Failed to update license: {result.get('status')}"
|
|
341
|
+
)
|
|
342
|
+
return False
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logger.error(f"Error updating license from vnii: {e}")
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
def sync_user_profile_to_api(
|
|
348
|
+
api_key: Optional[str] = None,
|
|
349
|
+
force: bool = False
|
|
350
|
+
) -> Dict[str, Any]:
|
|
351
|
+
return api_key_checker.sync_profile_to_api(api_key=api_key, force=force)
|
vnai/scope/license.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional, Dict, Union, Any
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class LicenseCache:
|
|
11
|
+
_instance = None
|
|
12
|
+
_lock = threading.Lock()
|
|
13
|
+
CACHE_TTL = 24 * 3600
|
|
14
|
+
GRACE_PERIOD = 48 * 3600
|
|
15
|
+
|
|
16
|
+
def __new__(cls):
|
|
17
|
+
with cls._lock:
|
|
18
|
+
if cls._instance is None:
|
|
19
|
+
cls._instance = super(LicenseCache, cls).__new__(cls)
|
|
20
|
+
cls._instance._initialize()
|
|
21
|
+
return cls._instance
|
|
22
|
+
|
|
23
|
+
def _initialize(self) -> None:
|
|
24
|
+
try:
|
|
25
|
+
from vnstock.core.config.ggcolab import get_vnstock_directory
|
|
26
|
+
cache_dir = get_vnstock_directory() /"id"
|
|
27
|
+
except ImportError:
|
|
28
|
+
cache_dir = Path.home() /".vnstock" /"id"
|
|
29
|
+
self.cache_dir = cache_dir
|
|
30
|
+
self.cache_file = self.cache_dir /"license_cache.json"
|
|
31
|
+
self.cache = None
|
|
32
|
+
self.lock = threading.Lock()
|
|
33
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
self._load_cache()
|
|
35
|
+
|
|
36
|
+
def _load_cache(self) -> None:
|
|
37
|
+
try:
|
|
38
|
+
if self.cache_file.exists():
|
|
39
|
+
with open(self.cache_file,'r') as f:
|
|
40
|
+
self.cache = json.load(f)
|
|
41
|
+
msg =f"Loaded license cache from {self.cache_file}"
|
|
42
|
+
logger.debug(msg)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.warning(f"Failed to load license cache: {e}")
|
|
45
|
+
self.cache = None
|
|
46
|
+
|
|
47
|
+
def _save_cache(self) -> None:
|
|
48
|
+
try:
|
|
49
|
+
with open(self.cache_file,'w') as f:
|
|
50
|
+
json.dump(self.cache, f, indent=2)
|
|
51
|
+
logger.debug(f"Saved license cache to {self.cache_file}")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Failed to save license cache: {e}")
|
|
54
|
+
|
|
55
|
+
def get(self, user_id: str | None = None) -> dict | None:
|
|
56
|
+
with self.lock:
|
|
57
|
+
if not self.cache:
|
|
58
|
+
return None
|
|
59
|
+
current_time = time.time()
|
|
60
|
+
cache_time = self.cache.get("checked_at")
|
|
61
|
+
if not cache_time:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
cache_time_ts = datetime.fromisoformat(
|
|
65
|
+
cache_time
|
|
66
|
+
).timestamp()
|
|
67
|
+
except (ValueError, TypeError):
|
|
68
|
+
cache_time_ts = 0
|
|
69
|
+
age = current_time - cache_time_ts
|
|
70
|
+
if age > self.CACHE_TTL:
|
|
71
|
+
logger.info(
|
|
72
|
+
f"License cache expired (age: {age:.0f}s, TTL: "
|
|
73
|
+
f"{self.CACHE_TTL}s)"
|
|
74
|
+
)
|
|
75
|
+
return None
|
|
76
|
+
return self.cache
|
|
77
|
+
|
|
78
|
+
def set(self, is_paid: bool, checked_at: str | None = None) -> bool:
|
|
79
|
+
with self.lock:
|
|
80
|
+
try:
|
|
81
|
+
if checked_at is None:
|
|
82
|
+
checked_at = datetime.now().isoformat()
|
|
83
|
+
cache_time = datetime.fromisoformat(
|
|
84
|
+
checked_at
|
|
85
|
+
).timestamp()
|
|
86
|
+
cache_ttl_until = cache_time + self.CACHE_TTL
|
|
87
|
+
grace_until = cache_time + self.GRACE_PERIOD
|
|
88
|
+
self.cache = {
|
|
89
|
+
"is_paid": is_paid,
|
|
90
|
+
"checked_at": checked_at,
|
|
91
|
+
"cache_ttl_until": cache_ttl_until,
|
|
92
|
+
"grace_period_until": grace_until,
|
|
93
|
+
"cache_age_seconds": 0
|
|
94
|
+
}
|
|
95
|
+
self._save_cache()
|
|
96
|
+
logger.info(
|
|
97
|
+
f"License cache updated: is_paid={is_paid}, "
|
|
98
|
+
f"TTL_expires={datetime.fromtimestamp(cache_ttl_until)}"
|
|
99
|
+
)
|
|
100
|
+
return True
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"Failed to set license cache: {e}")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def is_valid(self) -> bool:
|
|
106
|
+
with self.lock:
|
|
107
|
+
if not self.cache:
|
|
108
|
+
return False
|
|
109
|
+
current_time = time.time()
|
|
110
|
+
cache_time = self.cache.get("checked_at")
|
|
111
|
+
if not cache_time:
|
|
112
|
+
return False
|
|
113
|
+
try:
|
|
114
|
+
cache_time_ts = datetime.fromisoformat(
|
|
115
|
+
cache_time
|
|
116
|
+
).timestamp()
|
|
117
|
+
except (ValueError, TypeError):
|
|
118
|
+
return False
|
|
119
|
+
age = current_time - cache_time_ts
|
|
120
|
+
return age <= self.CACHE_TTL
|
|
121
|
+
|
|
122
|
+
def is_in_grace_period(self) -> bool:
|
|
123
|
+
with self.lock:
|
|
124
|
+
if not self.cache:
|
|
125
|
+
return False
|
|
126
|
+
current_time = time.time()
|
|
127
|
+
cache_time = self.cache.get("checked_at")
|
|
128
|
+
if not cache_time:
|
|
129
|
+
return False
|
|
130
|
+
try:
|
|
131
|
+
cache_time_ts = datetime.fromisoformat(
|
|
132
|
+
cache_time
|
|
133
|
+
).timestamp()
|
|
134
|
+
except (ValueError, TypeError):
|
|
135
|
+
return False
|
|
136
|
+
age = current_time - cache_time_ts
|
|
137
|
+
return (age > self.CACHE_TTL and
|
|
138
|
+
age <= self.CACHE_TTL + self.GRACE_PERIOD)
|
|
139
|
+
|
|
140
|
+
def get_is_paid(self) -> bool | None:
|
|
141
|
+
cache = self.get()
|
|
142
|
+
if cache:
|
|
143
|
+
return cache.get("is_paid", False)
|
|
144
|
+
if self.is_in_grace_period():
|
|
145
|
+
if self.cache:
|
|
146
|
+
logger.info(
|
|
147
|
+
"Using cached license status (grace period active)"
|
|
148
|
+
)
|
|
149
|
+
return self.cache.get("is_paid", False)
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def clear(self) -> bool:
|
|
153
|
+
with self.lock:
|
|
154
|
+
try:
|
|
155
|
+
self.cache = None
|
|
156
|
+
if self.cache_file.exists():
|
|
157
|
+
self.cache_file.unlink()
|
|
158
|
+
logger.info("License cache cleared")
|
|
159
|
+
return True
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"Failed to clear license cache: {e}")
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def get_cache_info(self) -> dict:
|
|
165
|
+
with self.lock:
|
|
166
|
+
if not self.cache:
|
|
167
|
+
return {"status":"empty"}
|
|
168
|
+
current_time = time.time()
|
|
169
|
+
cache_time_str = self.cache.get("checked_at")
|
|
170
|
+
if not isinstance(cache_time_str, str):
|
|
171
|
+
return {"status":"invalid"}
|
|
172
|
+
try:
|
|
173
|
+
cache_time = datetime.fromisoformat(cache_time_str)
|
|
174
|
+
cache_time_ts = cache_time.timestamp()
|
|
175
|
+
except (ValueError, TypeError):
|
|
176
|
+
return {"status":"invalid"}
|
|
177
|
+
age = current_time - cache_time_ts
|
|
178
|
+
is_valid = age <= self.CACHE_TTL
|
|
179
|
+
in_grace = (age > self.CACHE_TTL and
|
|
180
|
+
age <= self.CACHE_TTL + self.GRACE_PERIOD)
|
|
181
|
+
return {
|
|
182
|
+
"status":"valid" if is_valid else (
|
|
183
|
+
"grace_period" if in_grace else"expired"
|
|
184
|
+
),
|
|
185
|
+
"is_paid": self.cache.get("is_paid"),
|
|
186
|
+
"checked_at": cache_time_str,
|
|
187
|
+
"age_seconds": int(age),
|
|
188
|
+
"ttl_seconds": self.CACHE_TTL,
|
|
189
|
+
"ttl_remaining_seconds": max(
|
|
190
|
+
0, self.CACHE_TTL - int(age)
|
|
191
|
+
),
|
|
192
|
+
"grace_period_seconds": self.GRACE_PERIOD,
|
|
193
|
+
"grace_remaining_seconds": max(
|
|
194
|
+
0, self.CACHE_TTL + self.GRACE_PERIOD - int(age)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
license_cache = LicenseCache()
|