vnai 2.1.9__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 +193 -33
- vnai/beam/__init__.py +26 -3
- vnai/beam/auth.py +312 -0
- vnai/beam/fundamental.py +168 -0
- vnai/beam/patching.py +223 -0
- vnai/beam/quota.py +114 -44
- vnai/beam/sync.py +87 -0
- vnai/flow/relay.py +18 -12
- vnai/scope/__init__.py +8 -1
- vnai/scope/device.py +315 -0
- vnai/scope/lc_integration.py +351 -0
- vnai/scope/license.py +197 -0
- vnai/scope/profile.py +37 -17
- vnai/scope/promo.py +203 -107
- {vnai-2.1.9.dist-info → vnai-2.3.7.dist-info}/METADATA +3 -2
- vnai-2.3.7.dist-info/RECORD +23 -0
- {vnai-2.1.9.dist-info → vnai-2.3.7.dist-info}/WHEEL +1 -1
- vnai-2.1.9.dist-info/RECORD +0 -16
- {vnai-2.1.9.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()
|
vnai/scope/profile.py
CHANGED
|
@@ -3,7 +3,6 @@ import sys
|
|
|
3
3
|
import platform
|
|
4
4
|
import uuid
|
|
5
5
|
import hashlib
|
|
6
|
-
import psutil
|
|
7
6
|
import threading
|
|
8
7
|
import time
|
|
9
8
|
import importlib.metadata
|
|
@@ -32,13 +31,25 @@ class Inspector:
|
|
|
32
31
|
self.machine_id = None
|
|
33
32
|
self._colab_auth_triggered = False
|
|
34
33
|
self.home_dir = Path.home()
|
|
35
|
-
self.project_dir = self.
|
|
36
|
-
self.project_dir.mkdir(exist_ok=True)
|
|
34
|
+
self.project_dir = self._get_project_dir()
|
|
35
|
+
self.project_dir.mkdir(parents=True, exist_ok=True)
|
|
37
36
|
self.id_dir = self.project_dir /'id'
|
|
38
|
-
self.id_dir.mkdir(exist_ok=True)
|
|
37
|
+
self.id_dir.mkdir(parents=True, exist_ok=True)
|
|
39
38
|
self.machine_id_path = self.id_dir /"machine_id.txt"
|
|
40
39
|
self.examine()
|
|
41
40
|
|
|
41
|
+
def _get_home_dir(self) -> Path:
|
|
42
|
+
return Path.home()
|
|
43
|
+
|
|
44
|
+
def _get_project_dir(self) -> Path:
|
|
45
|
+
try:
|
|
46
|
+
from vnstock.core.config.ggcolab import get_vnstock_directory
|
|
47
|
+
return get_vnstock_directory()
|
|
48
|
+
except ImportError:
|
|
49
|
+
if not hasattr(self,'home_dir'):
|
|
50
|
+
self.home_dir = Path.home()
|
|
51
|
+
return self.home_dir /".vnstock"
|
|
52
|
+
|
|
42
53
|
def examine(self, force_refresh=False):
|
|
43
54
|
current_time = time.time()
|
|
44
55
|
if not force_refresh and (current_time - self.last_examination) < self.cache_ttl:
|
|
@@ -75,9 +86,11 @@ class Inspector:
|
|
|
75
86
|
except:
|
|
76
87
|
info["environment"] ="unknown"
|
|
77
88
|
try:
|
|
89
|
+
import psutil
|
|
78
90
|
info["cpu_count"] = os.cpu_count()
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
memory_total = psutil.virtual_memory().total
|
|
92
|
+
info["memory_gb"] = round(memory_total / (1024**3), 1)
|
|
93
|
+
except Exception:
|
|
81
94
|
pass
|
|
82
95
|
is_colab ='google.colab' in sys.modules
|
|
83
96
|
if is_colab:
|
|
@@ -98,24 +111,27 @@ class Inspector:
|
|
|
98
111
|
def fingerprint(self):
|
|
99
112
|
if self.machine_id:
|
|
100
113
|
return self.machine_id
|
|
114
|
+
try:
|
|
115
|
+
from vnai.scope.device import device_registry
|
|
116
|
+
registry = device_registry.get_registry()
|
|
117
|
+
if registry and registry.get('device_id'):
|
|
118
|
+
self.machine_id = registry['device_id']
|
|
119
|
+
return self.machine_id
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
101
122
|
if self.machine_id_path.exists():
|
|
102
123
|
try:
|
|
103
124
|
with open(self.machine_id_path,"r") as f:
|
|
104
125
|
self.machine_id = f.read().strip()
|
|
105
126
|
return self.machine_id
|
|
106
|
-
except:
|
|
127
|
+
except Exception:
|
|
107
128
|
pass
|
|
108
129
|
is_colab = self.detect_colab_with_delayed_auth()
|
|
109
130
|
try:
|
|
110
131
|
system_info = platform.node() + platform.platform() + platform.machine()
|
|
111
132
|
self.machine_id = hashlib.md5(system_info.encode()).hexdigest()
|
|
112
|
-
except:
|
|
133
|
+
except Exception:
|
|
113
134
|
self.machine_id = str(uuid.uuid4())
|
|
114
|
-
try:
|
|
115
|
-
with open(self.machine_id_path,"w") as f:
|
|
116
|
-
f.write(self.machine_id)
|
|
117
|
-
except:
|
|
118
|
-
pass
|
|
119
135
|
return self.machine_id
|
|
120
136
|
|
|
121
137
|
def detect_hosting(self):
|
|
@@ -559,16 +575,20 @@ class Inspector:
|
|
|
559
575
|
|
|
560
576
|
def analyze_dependencies(self):
|
|
561
577
|
try:
|
|
562
|
-
|
|
578
|
+
try:
|
|
579
|
+
from importlib.metadata import distributions
|
|
580
|
+
except ImportError:
|
|
581
|
+
from importlib_metadata import distributions
|
|
563
582
|
enterprise_packages = [
|
|
564
583
|
"snowflake-connector-python","databricks","azure",
|
|
565
584
|
"aws","google-cloud","stripe","atlassian",
|
|
566
585
|
"salesforce","bigquery","tableau","sap"
|
|
567
586
|
]
|
|
568
587
|
commercial_deps = []
|
|
569
|
-
for
|
|
570
|
-
|
|
571
|
-
|
|
588
|
+
for dist in distributions():
|
|
589
|
+
pkg_name = dist.metadata['Name'].lower()
|
|
590
|
+
if any(ent in pkg_name for ent in enterprise_packages):
|
|
591
|
+
commercial_deps.append({"name": pkg_name,"version": dist.version})
|
|
572
592
|
return {
|
|
573
593
|
"has_commercial_deps": len(commercial_deps) > 0,
|
|
574
594
|
"commercial_deps_count": len(commercial_deps),
|