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
vnai/__init__.py
CHANGED
|
@@ -5,23 +5,16 @@ import time
|
|
|
5
5
|
import threading
|
|
6
6
|
import functools
|
|
7
7
|
from datetime import datetime
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
from vnai.beam.pulse import monitor
|
|
11
|
-
from vnai.flow.relay import conduit
|
|
12
|
-
from vnai.flow.queue import buffer
|
|
13
|
-
from vnai.scope.profile import inspector
|
|
14
|
-
from vnai.scope.state import tracker, record
|
|
15
|
-
import vnai.scope.promo
|
|
16
|
-
from vnai.scope.promo import present
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import pandas as pd
|
|
17
10
|
TC_VAR ="ACCEPT_TC"
|
|
18
11
|
TC_VAL ="tôi đồng ý"
|
|
19
12
|
TC_PATH = pathlib.Path.home() /".vnstock" /"id" /"terms_agreement.txt"
|
|
20
13
|
TERMS_AND_CONDITIONS ="""
|
|
21
14
|
Khi tiếp tục sử dụng Vnstock, bạn xác nhận rằng bạn đã đọc, hiểu và đồng ý với Chính sách quyền riêng tư và Điều khoản, điều kiện về giấy phép sử dụng Vnstock.
|
|
22
15
|
Chi tiết:
|
|
23
|
-
- Giấy phép sử dụng phần mềm: https://vnstocks.com/
|
|
24
|
-
- Chính sách quyền riêng tư: https://vnstocks.com/
|
|
16
|
+
- Giấy phép sử dụng phần mềm: https://vnstocks.com/onboard/giay-phep-su-dung
|
|
17
|
+
- Chính sách quyền riêng tư: https://vnstocks.com/onboard/chinh-sach-quyen-rieng-tu
|
|
25
18
|
"""
|
|
26
19
|
|
|
27
20
|
class Core:
|
|
@@ -45,12 +38,30 @@ class Core:
|
|
|
45
38
|
self._accept_terms()
|
|
46
39
|
from vnai.scope.profile import inspector
|
|
47
40
|
inspector.setup_vnstock_environment()
|
|
41
|
+
try:
|
|
42
|
+
from vnai.scope.device import device_registry
|
|
43
|
+
vnstock_version = getattr(__import__('vnstock'),
|
|
44
|
+
'__version__','0.0.1')
|
|
45
|
+
if device_registry.needs_reregistration(vnstock_version):
|
|
46
|
+
system_info = inspector.examine()
|
|
47
|
+
device_registry.register(system_info, vnstock_version)
|
|
48
|
+
self.system_info = system_info
|
|
49
|
+
else:
|
|
50
|
+
self.system_info = device_registry.get_registry()
|
|
51
|
+
except Exception as e:
|
|
52
|
+
import logging
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
msg =f"Device registration failed: {e}. Using fallback."
|
|
55
|
+
logger.warning(msg)
|
|
56
|
+
self.system_info = inspector.examine()
|
|
48
57
|
from vnai.scope.promo import ContentManager
|
|
49
58
|
manager = ContentManager()
|
|
50
|
-
if manager.is_paid_user is
|
|
59
|
+
if manager.is_paid_user is not True:
|
|
60
|
+
from vnai.scope.promo import present
|
|
51
61
|
present()
|
|
62
|
+
from vnai.scope.state import record
|
|
52
63
|
record("initialization", {"timestamp": datetime.now().isoformat()})
|
|
53
|
-
|
|
64
|
+
from vnai.flow.relay import conduit
|
|
54
65
|
conduit.queue({
|
|
55
66
|
"type":"system_info",
|
|
56
67
|
"data": {
|
|
@@ -59,25 +70,25 @@ class Core:
|
|
|
59
70
|
}
|
|
60
71
|
}, priority="high")
|
|
61
72
|
self.initialized = True
|
|
73
|
+
_trigger_patching_after_init()
|
|
62
74
|
return True
|
|
63
75
|
|
|
64
76
|
def _check_terms(self):
|
|
65
77
|
return os.path.exists(self.terms_file_path)
|
|
66
78
|
|
|
67
79
|
def _accept_terms(self):
|
|
80
|
+
from vnai.scope.profile import inspector
|
|
68
81
|
system_info = inspector.examine()
|
|
69
82
|
if TC_VAR in os.environ and os.environ[TC_VAR] == TC_VAL:
|
|
70
|
-
|
|
83
|
+
os.environ[TC_VAR] = TC_VAL
|
|
71
84
|
else:
|
|
72
|
-
response = TC_VAL
|
|
73
85
|
os.environ[TC_VAR] = TC_VAL
|
|
74
86
|
now = datetime.now()
|
|
87
|
+
machine_id = system_info['machine_id']
|
|
75
88
|
signed_agreement = (
|
|
76
|
-
f"Người dùng có mã nhận dạng {
|
|
77
|
-
f"điều khoản & điều kiện sử dụng Vnstock
|
|
78
|
-
f"
|
|
79
|
-
f"THÔNG TIN THIẾT BỊ: {json.dumps(system_info, indent=2)}\n\n"
|
|
80
|
-
f"Đính kèm bản sao nội dung bạn đã đọc, hiểu rõ và đồng ý dưới đây:\n"
|
|
89
|
+
f"Người dùng có mã nhận dạng {machine_id} "
|
|
90
|
+
f"đã chấp nhận điều khoản & điều kiện sử dụng Vnstock "
|
|
91
|
+
f"lúc {now.isoformat()}\n\n"
|
|
81
92
|
f"{TERMS_AND_CONDITIONS}"
|
|
82
93
|
)
|
|
83
94
|
with open(self.terms_file_path,"w", encoding="utf-8") as f:
|
|
@@ -86,13 +97,15 @@ f"{TERMS_AND_CONDITIONS}"
|
|
|
86
97
|
env_data = {
|
|
87
98
|
"accepted_agreement": True,
|
|
88
99
|
"timestamp": now.isoformat(),
|
|
89
|
-
"machine_id":
|
|
100
|
+
"machine_id": machine_id
|
|
90
101
|
}
|
|
91
102
|
with open(env_file,"w") as f:
|
|
92
103
|
json.dump(env_data, f)
|
|
93
104
|
return True
|
|
94
105
|
|
|
95
106
|
def status(self):
|
|
107
|
+
from vnai.beam.pulse import monitor
|
|
108
|
+
from vnai.scope.state import tracker
|
|
96
109
|
return {
|
|
97
110
|
"initialized": self.initialized,
|
|
98
111
|
"health": monitor.report(),
|
|
@@ -102,32 +115,81 @@ f"{TERMS_AND_CONDITIONS}"
|
|
|
102
115
|
def configure_privacy(self, level="standard"):
|
|
103
116
|
from vnai.scope.state import tracker
|
|
104
117
|
return tracker.setup_privacy(level)
|
|
105
|
-
|
|
118
|
+
_core_instance = None
|
|
119
|
+
_core_lock = threading.Lock()
|
|
120
|
+
|
|
121
|
+
def _get_core():
|
|
122
|
+
global _core_instance
|
|
123
|
+
if _core_instance is None:
|
|
124
|
+
with _core_lock:
|
|
125
|
+
if _core_instance is None:
|
|
126
|
+
_core_instance = Core()
|
|
127
|
+
return _core_instance
|
|
106
128
|
|
|
107
129
|
def tc_init():
|
|
108
|
-
return
|
|
130
|
+
return _get_core().initialize()
|
|
109
131
|
|
|
110
132
|
def setup():
|
|
111
|
-
return
|
|
133
|
+
return _get_core().initialize()
|
|
112
134
|
|
|
113
135
|
def optimize_execution(resource_type="default"):
|
|
114
|
-
|
|
136
|
+
def decorator(func):
|
|
137
|
+
_optimized_func = [None]
|
|
138
|
+
@functools.wraps(func)
|
|
139
|
+
|
|
140
|
+
def wrapper(*args, **kwargs):
|
|
141
|
+
if _optimized_func[0] is None:
|
|
142
|
+
from vnai.beam.quota import optimize
|
|
143
|
+
actual_decorator = optimize(resource_type)
|
|
144
|
+
_optimized_func[0] = actual_decorator(func)
|
|
145
|
+
return _optimized_func[0](*args, **kwargs)
|
|
146
|
+
return wrapper
|
|
147
|
+
return decorator
|
|
115
148
|
|
|
116
149
|
def agg_execution(resource_type="default"):
|
|
117
|
-
|
|
150
|
+
def decorator(func):
|
|
151
|
+
_optimized_func = [None]
|
|
152
|
+
@functools.wraps(func)
|
|
153
|
+
|
|
154
|
+
def wrapper(*args, **kwargs):
|
|
155
|
+
if _optimized_func[0] is None:
|
|
156
|
+
from vnai.beam.quota import optimize
|
|
157
|
+
actual_decorator = optimize(resource_type, ad_cooldown=1500,
|
|
158
|
+
content_trigger_threshold=100000)
|
|
159
|
+
_optimized_func[0] = actual_decorator(func)
|
|
160
|
+
return _optimized_func[0](*args, **kwargs)
|
|
161
|
+
return wrapper
|
|
162
|
+
return decorator
|
|
118
163
|
|
|
119
164
|
def measure_performance(module_type="function"):
|
|
120
|
-
|
|
165
|
+
def decorator(func):
|
|
166
|
+
_captured_func = [None]
|
|
167
|
+
@functools.wraps(func)
|
|
168
|
+
|
|
169
|
+
def wrapper(*args, **kwargs):
|
|
170
|
+
if _captured_func[0] is None:
|
|
171
|
+
from vnai.beam.metrics import capture
|
|
172
|
+
actual_decorator = capture(module_type)
|
|
173
|
+
_captured_func[0] = actual_decorator(func)
|
|
174
|
+
return _captured_func[0](*args, **kwargs)
|
|
175
|
+
return wrapper
|
|
176
|
+
return decorator
|
|
121
177
|
|
|
122
178
|
def accept_license_terms(terms_text=None):
|
|
123
179
|
if terms_text is None:
|
|
124
180
|
terms_text = TERMS_AND_CONDITIONS
|
|
181
|
+
from vnai.scope.profile import inspector
|
|
125
182
|
system_info = inspector.examine()
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
183
|
+
terms_file_path = (
|
|
184
|
+
pathlib.Path.home() /".vnstock" /"id" /
|
|
185
|
+
"terms_agreement.txt"
|
|
186
|
+
)
|
|
187
|
+
os.makedirs(os.path.dirname(terms_file_path), exist_ok=True)
|
|
188
|
+
now = datetime.now()
|
|
189
|
+
machine_id = system_info['machine_id']
|
|
190
|
+
with open(terms_file_path,"w", encoding="utf-8") as f:
|
|
191
|
+
f.write(f"Người dùng có mã nhận dạng {machine_id} "
|
|
192
|
+
f"đã chấp nhận lúc {now.isoformat()}\n\n")
|
|
131
193
|
f.write(terms_text)
|
|
132
194
|
return True
|
|
133
195
|
|
|
@@ -148,6 +210,13 @@ def accept_vnstock_terms():
|
|
|
148
210
|
try:
|
|
149
211
|
with open(env_file,"w") as f:
|
|
150
212
|
json.dump(env_data, f)
|
|
213
|
+
terms_file = id_dir /"terms_agreement.txt"
|
|
214
|
+
now = datetime.now()
|
|
215
|
+
machine_id = system_info['machine_id']
|
|
216
|
+
with open(terms_file,"w", encoding="utf-8") as f:
|
|
217
|
+
f.write(f"Người dùng có mã nhận dạng {machine_id} "
|
|
218
|
+
f"đã chấp nhận lúc {now.isoformat()}\n\n")
|
|
219
|
+
f.write(TERMS_AND_CONDITIONS)
|
|
151
220
|
print("Vnstock terms accepted successfully.")
|
|
152
221
|
return True
|
|
153
222
|
except Exception as e:
|
|
@@ -164,4 +233,95 @@ def check_commercial_usage():
|
|
|
164
233
|
|
|
165
234
|
def authenticate_for_persistence():
|
|
166
235
|
from vnai.scope.profile import inspector
|
|
167
|
-
return inspector.get_or_create_user_id()
|
|
236
|
+
return inspector.get_or_create_user_id()
|
|
237
|
+
|
|
238
|
+
def get_user_tier():
|
|
239
|
+
try:
|
|
240
|
+
from vnai.beam.auth import authenticator
|
|
241
|
+
return authenticator.get_tier_info()
|
|
242
|
+
except Exception as e:
|
|
243
|
+
return {
|
|
244
|
+
"tier":"guest",
|
|
245
|
+
"description":"Khách (không có API key)",
|
|
246
|
+
"limits": {"per_minute": 20,"per_hour": 1200},
|
|
247
|
+
"error": str(e)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
def refresh_tier_cache():
|
|
251
|
+
try:
|
|
252
|
+
from vnai.beam.auth import authenticator
|
|
253
|
+
authenticator.get_tier(force_refresh=True)
|
|
254
|
+
return True
|
|
255
|
+
except Exception:
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def setup_api_key(api_key):
|
|
259
|
+
from vnai.beam.auth import authenticator
|
|
260
|
+
return authenticator.setup_api_key(api_key)
|
|
261
|
+
|
|
262
|
+
def get_api_key():
|
|
263
|
+
from vnai.beam.auth import authenticator
|
|
264
|
+
return authenticator.get_api_key()
|
|
265
|
+
|
|
266
|
+
def remove_api_key():
|
|
267
|
+
from vnai.beam.auth import authenticator
|
|
268
|
+
return authenticator.remove_api_key()
|
|
269
|
+
|
|
270
|
+
def check_api_key_status():
|
|
271
|
+
from vnai.beam.auth import authenticator
|
|
272
|
+
return authenticator.check_api_key_status()
|
|
273
|
+
|
|
274
|
+
def print_api_key_help():
|
|
275
|
+
from vnai.beam.auth import authenticator
|
|
276
|
+
return authenticator.print_help()
|
|
277
|
+
|
|
278
|
+
def get_quota_status(api_key):
|
|
279
|
+
from vnai.beam.quota_endpoint import quota_endpoint
|
|
280
|
+
return quota_endpoint.get_quota_status(api_key)
|
|
281
|
+
|
|
282
|
+
def get_tier_info():
|
|
283
|
+
from vnai.beam.quota_endpoint import quota_endpoint
|
|
284
|
+
return quota_endpoint.get_tier_info()
|
|
285
|
+
|
|
286
|
+
def check_quota_available(api_key):
|
|
287
|
+
from vnai.beam.quota_endpoint import quota_endpoint
|
|
288
|
+
return quota_endpoint.check_quota(api_key)
|
|
289
|
+
|
|
290
|
+
def get_quota_metadata(api_key):
|
|
291
|
+
from vnai.beam.quota_endpoint import quota_endpoint
|
|
292
|
+
return quota_endpoint.get_metadata(api_key)
|
|
293
|
+
|
|
294
|
+
def record_api_usage(api_key, amount=1):
|
|
295
|
+
from vnai.beam.quota_endpoint import quota_endpoint
|
|
296
|
+
return quota_endpoint.record_usage(api_key, amount)
|
|
297
|
+
|
|
298
|
+
def balance_sheet(symbol: str, source: str ='vci', period: str ='year',
|
|
299
|
+
lang: Optional[str] ='en', show_log: bool = False) -> pd.DataFrame:
|
|
300
|
+
_ensure_patches_applied()
|
|
301
|
+
from vnai.beam.fundamental import balance_sheet as get_balance_sheet
|
|
302
|
+
return get_balance_sheet(symbol, source=source, period=period, lang=lang, show_log=show_log)
|
|
303
|
+
|
|
304
|
+
def income_statement(symbol: str, source: str ='vci', period: str ='year',
|
|
305
|
+
lang: Optional[str] ='en', show_log: bool = False) -> pd.DataFrame:
|
|
306
|
+
_ensure_patches_applied()
|
|
307
|
+
from vnai.beam.fundamental import income_statement as get_income_statement
|
|
308
|
+
return get_income_statement(symbol, source=source, period=period, lang=lang, show_log=show_log)
|
|
309
|
+
|
|
310
|
+
def cash_flow(symbol, source='vci', period='year', lang='en', show_log=False):
|
|
311
|
+
_ensure_patches_applied()
|
|
312
|
+
from vnai.beam.fundamental import cash_flow as get_cash_flow
|
|
313
|
+
return get_cash_flow(symbol, source=source, period=period, lang=lang, show_log=show_log)
|
|
314
|
+
_patches_initialized = False
|
|
315
|
+
|
|
316
|
+
def _ensure_patches_applied():
|
|
317
|
+
global _patches_initialized
|
|
318
|
+
if not _patches_initialized:
|
|
319
|
+
try:
|
|
320
|
+
from vnai.beam.patching import apply_all_patches
|
|
321
|
+
apply_all_patches()
|
|
322
|
+
_patches_initialized = True
|
|
323
|
+
except Exception:
|
|
324
|
+
_patches_initialized = True
|
|
325
|
+
|
|
326
|
+
def _trigger_patching_after_init():
|
|
327
|
+
_ensure_patches_applied()
|
vnai/beam/__init__.py
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from vnai.beam.
|
|
1
|
+
def __getattr__(name):
|
|
2
|
+
if name =='guardian':
|
|
3
|
+
from vnai.beam.quota import guardian
|
|
4
|
+
return guardian
|
|
5
|
+
elif name =='optimize':
|
|
6
|
+
from vnai.beam.quota import optimize
|
|
7
|
+
return optimize
|
|
8
|
+
elif name =='collector':
|
|
9
|
+
from vnai.beam.metrics import collector
|
|
10
|
+
return collector
|
|
11
|
+
elif name =='capture':
|
|
12
|
+
from vnai.beam.metrics import capture
|
|
13
|
+
return capture
|
|
14
|
+
elif name =='monitor':
|
|
15
|
+
from vnai.beam.pulse import monitor
|
|
16
|
+
return monitor
|
|
17
|
+
elif name =='get_auth_state_manager':
|
|
18
|
+
from vnai.beam.auth import get_auth_state_manager
|
|
19
|
+
return get_auth_state_manager
|
|
20
|
+
elif name =='AuthStateManager':
|
|
21
|
+
from vnai.beam.auth import AuthStateManager
|
|
22
|
+
return AuthStateManager
|
|
23
|
+
elif name =='authenticator':
|
|
24
|
+
from vnai.beam.auth import authenticator
|
|
25
|
+
return authenticator
|
|
26
|
+
raise AttributeError(f"module 'vnai.beam' has no attribute '{name}'")
|
vnai/beam/auth.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication and tier management.
|
|
3
|
+
Provides tier detection and API key management for rate limiting.
|
|
4
|
+
Includes centralized authentication state manager for deduplication.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Dict, Any
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
import threading
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class AuthStateManager:
|
|
17
|
+
def __init__(self, state_dir: Path):
|
|
18
|
+
self.state_dir = state_dir
|
|
19
|
+
self.state_file = state_dir /"auth_state.json"
|
|
20
|
+
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
self._state = self._load_state()
|
|
22
|
+
|
|
23
|
+
def _load_state(self) -> Dict:
|
|
24
|
+
if self.state_file.exists():
|
|
25
|
+
try:
|
|
26
|
+
with open(self.state_file,'r') as f:
|
|
27
|
+
return json.load(f)
|
|
28
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
29
|
+
log.debug(f"Could not load auth state: {e}")
|
|
30
|
+
return self._default_state()
|
|
31
|
+
return self._default_state()
|
|
32
|
+
|
|
33
|
+
def _default_state(self) -> Dict:
|
|
34
|
+
return {
|
|
35
|
+
"session_id": self._generate_session_id(),
|
|
36
|
+
"authenticated": False,
|
|
37
|
+
"user": None,
|
|
38
|
+
"tier": None,
|
|
39
|
+
"auth_time": None,
|
|
40
|
+
"packages_notified": [],
|
|
41
|
+
"cache_ttl_minutes": 60
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def _generate_session_id(self) -> str:
|
|
45
|
+
return datetime.now().isoformat()
|
|
46
|
+
|
|
47
|
+
def _save_state(self) -> None:
|
|
48
|
+
try:
|
|
49
|
+
with open(self.state_file,'w') as f:
|
|
50
|
+
json.dump(self._state, f, indent=2)
|
|
51
|
+
except IOError as e:
|
|
52
|
+
log.warning(f"Could not save auth state: {e}")
|
|
53
|
+
|
|
54
|
+
def _is_cache_valid(self) -> bool:
|
|
55
|
+
if not self._state.get("auth_time"):
|
|
56
|
+
return False
|
|
57
|
+
auth_time = datetime.fromisoformat(self._state["auth_time"])
|
|
58
|
+
ttl_minutes = self._state.get("cache_ttl_minutes", 60)
|
|
59
|
+
expiry = auth_time + timedelta(minutes=ttl_minutes)
|
|
60
|
+
return datetime.now() < expiry
|
|
61
|
+
|
|
62
|
+
def should_show_message(self, package_name: str) -> bool:
|
|
63
|
+
if not self._state.get("authenticated") or not self._is_cache_valid():
|
|
64
|
+
return True
|
|
65
|
+
packages_notified = self._state.get("packages_notified", [])
|
|
66
|
+
return len(packages_notified) == 0
|
|
67
|
+
|
|
68
|
+
def mark_authenticated(self, license_info: Dict, package_name: str) -> None:
|
|
69
|
+
self._state["authenticated"] = True
|
|
70
|
+
self._state["user"] = license_info.get("user","Unknown")
|
|
71
|
+
self._state["tier"] = license_info.get("tier","free")
|
|
72
|
+
self._state["auth_time"] = datetime.now().isoformat()
|
|
73
|
+
packages_notified = self._state.get("packages_notified", [])
|
|
74
|
+
if package_name not in packages_notified:
|
|
75
|
+
packages_notified.append(package_name)
|
|
76
|
+
self._state["packages_notified"] = packages_notified
|
|
77
|
+
self._save_state()
|
|
78
|
+
|
|
79
|
+
def get_cached_info(self) -> Optional[Dict]:
|
|
80
|
+
if not self._is_cache_valid():
|
|
81
|
+
return None
|
|
82
|
+
return {
|
|
83
|
+
"user": self._state.get("user"),
|
|
84
|
+
"tier": self._state.get("tier"),
|
|
85
|
+
"from_cache": True
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def reset(self) -> None:
|
|
89
|
+
self._state = self._default_state()
|
|
90
|
+
self._save_state()
|
|
91
|
+
|
|
92
|
+
def get_auth_state_manager(project_dir: Path) -> AuthStateManager:
|
|
93
|
+
return AuthStateManager(project_dir)
|
|
94
|
+
|
|
95
|
+
class Authenticator:
|
|
96
|
+
TIER_LIMITS = {
|
|
97
|
+
"guest": {"min": 20,"hour": 1200,"day": 5000},
|
|
98
|
+
"free": {"min": 60,"hour": 3600,"day": 10000},
|
|
99
|
+
"bronze": {"min": 180,"hour": 10800,"day": 50000},
|
|
100
|
+
"silver": {"min": 300,"hour": 18000,"day": 100000},
|
|
101
|
+
"golden": {"min": 600,"hour": 36000,"day": 150000}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def __init__(self):
|
|
105
|
+
self.vnstock_dir = Path.home() /".vnstock"
|
|
106
|
+
self.api_key_file = self.vnstock_dir /"api_key.json"
|
|
107
|
+
self._cached_tier = None
|
|
108
|
+
self._cache_timestamp = 0
|
|
109
|
+
self._cache_ttl = 300
|
|
110
|
+
|
|
111
|
+
def get_tier(self, force_refresh: bool = False) -> str:
|
|
112
|
+
current_time = time.time()
|
|
113
|
+
if not force_refresh and self._cached_tier and (current_time - self._cache_timestamp) < self._cache_ttl:
|
|
114
|
+
return self._cached_tier
|
|
115
|
+
tier = self._detect_tier()
|
|
116
|
+
self._cached_tier = tier
|
|
117
|
+
self._cache_timestamp = current_time
|
|
118
|
+
log.debug(f"Detected tier: {tier}")
|
|
119
|
+
return tier
|
|
120
|
+
|
|
121
|
+
def _detect_tier(self) -> str:
|
|
122
|
+
tier_from_vnii = self._check_vnii_tier()
|
|
123
|
+
if tier_from_vnii:
|
|
124
|
+
return tier_from_vnii
|
|
125
|
+
if self._has_api_key():
|
|
126
|
+
return"free"
|
|
127
|
+
return"guest"
|
|
128
|
+
|
|
129
|
+
def _check_vnii_tier(self) -> Optional[str]:
|
|
130
|
+
try:
|
|
131
|
+
import vnii
|
|
132
|
+
from vnii.auth import authenticate
|
|
133
|
+
license_info = authenticate(self.vnstock_dir)
|
|
134
|
+
tier_string = license_info.get('tier','free')
|
|
135
|
+
log.debug(f"Got tier from vnii: {tier_string}")
|
|
136
|
+
return tier_string
|
|
137
|
+
except ImportError:
|
|
138
|
+
log.debug("vnii not installed")
|
|
139
|
+
return None
|
|
140
|
+
except SystemExit:
|
|
141
|
+
log.debug("vnii authentication failed")
|
|
142
|
+
return None
|
|
143
|
+
except Exception as e:
|
|
144
|
+
log.debug(f"Error checking vnii tier: {e}")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def _has_api_key(self) -> bool:
|
|
148
|
+
if os.getenv('VNSTOCK_API_KEY'):
|
|
149
|
+
return True
|
|
150
|
+
if self.api_key_file.exists():
|
|
151
|
+
try:
|
|
152
|
+
with open(self.api_key_file,'r') as f:
|
|
153
|
+
data = json.load(f)
|
|
154
|
+
api_key = data.get('api_key','').strip()
|
|
155
|
+
return bool(api_key)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
log.debug(f"Failed to read API key: {e}")
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
def get_limits(self, tier: Optional[str] = None) -> Dict[str, int]:
|
|
161
|
+
if tier is None:
|
|
162
|
+
tier = self.get_tier()
|
|
163
|
+
return self.TIER_LIMITS.get(tier, self.TIER_LIMITS["guest"])
|
|
164
|
+
|
|
165
|
+
def get_tier_info(self) -> Dict:
|
|
166
|
+
tier = self.get_tier()
|
|
167
|
+
limits = self.get_limits(tier)
|
|
168
|
+
descriptions = {
|
|
169
|
+
"guest":"Khách (Guest - chưa đăng ký)",
|
|
170
|
+
"free":"Phiên bản cộng đồng (Community - có API key)",
|
|
171
|
+
"bronze":"Thành viên Bronze (Bronze Member)",
|
|
172
|
+
"silver":"Thành viên Silver (Silver Member)",
|
|
173
|
+
"golden":"Thành viên Golden (Golden Member)"
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
"tier": tier,
|
|
177
|
+
"description": descriptions.get(tier,f"Gói {tier.title()}"),
|
|
178
|
+
"limits": {
|
|
179
|
+
"per_minute": limits["min"],
|
|
180
|
+
"per_hour": limits["hour"]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def setup_api_key(self, api_key: str) -> bool:
|
|
185
|
+
try:
|
|
186
|
+
self.vnstock_dir.mkdir(exist_ok=True)
|
|
187
|
+
api_key_data = {"api_key": api_key.strip()}
|
|
188
|
+
with open(self.api_key_file,'w') as f:
|
|
189
|
+
json.dump(api_key_data, f, indent=2)
|
|
190
|
+
print("✓ API key đã được lưu thành công! (API key saved successfully!)")
|
|
191
|
+
print("Bạn đang sử dụng Phiên bản cộng đồng (60 requests/phút)")
|
|
192
|
+
print("(You are using Community version - 60 requests/minute)")
|
|
193
|
+
print("\nĐể tham gia gói thành viên tài trợ (To join sponsor membership):")
|
|
194
|
+
print(" Truy cập: https://vnstocks.com/insiders-program")
|
|
195
|
+
log.info("API key setup completed")
|
|
196
|
+
return True
|
|
197
|
+
except Exception as e:
|
|
198
|
+
print(f"✗ Không thể lưu API key (Failed to save API key): {e}")
|
|
199
|
+
log.error(f"API key setup failed: {e}")
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def get_api_key(self) -> Optional[str]:
|
|
203
|
+
if os.getenv('VNSTOCK_API_KEY'):
|
|
204
|
+
return os.getenv('VNSTOCK_API_KEY')
|
|
205
|
+
if self.api_key_file.exists():
|
|
206
|
+
try:
|
|
207
|
+
with open(self.api_key_file,'r') as f:
|
|
208
|
+
data = json.load(f)
|
|
209
|
+
return data.get('api_key')
|
|
210
|
+
except Exception as e:
|
|
211
|
+
log.debug(f"Failed to read API key: {e}")
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def remove_api_key(self) -> bool:
|
|
215
|
+
try:
|
|
216
|
+
if self.api_key_file.exists():
|
|
217
|
+
self.api_key_file.unlink()
|
|
218
|
+
print("✓ API key đã được xóa (API key removed)")
|
|
219
|
+
print("Bạn đang ở chế độ Khách (20 requests/phút) (You are in Guest mode - 20 requests/minute)")
|
|
220
|
+
else:
|
|
221
|
+
print("Không tìm thấy API key (No API key found)")
|
|
222
|
+
log.info("API key removed")
|
|
223
|
+
return True
|
|
224
|
+
except Exception as e:
|
|
225
|
+
print(f"✗ Không thể xóa API key (Failed to remove API key): {e}")
|
|
226
|
+
log.error(f"API key removal failed: {e}")
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def _register_device_to_api(self, api_key: str) -> None:
|
|
230
|
+
try:
|
|
231
|
+
import requests
|
|
232
|
+
import platform
|
|
233
|
+
from vnai.scope.profile import inspector
|
|
234
|
+
from vnai.scope.device import IDEDetector
|
|
235
|
+
system_info = inspector.examine()
|
|
236
|
+
try:
|
|
237
|
+
ide_name, ide_info = IDEDetector.detect_ide()
|
|
238
|
+
except Exception:
|
|
239
|
+
ide_name ='Unknown'
|
|
240
|
+
ide_info = {}
|
|
241
|
+
payload = {
|
|
242
|
+
'api_key': api_key,
|
|
243
|
+
'device_id': system_info['machine_id'],
|
|
244
|
+
'device_name': system_info.get('platform', platform.node()),
|
|
245
|
+
'os_type': system_info['os_name'].lower(),
|
|
246
|
+
'os_version': system_info.get('platform', platform.release()),
|
|
247
|
+
'machine_info': {
|
|
248
|
+
'platform': system_info.get('platform', platform.platform()),
|
|
249
|
+
'machine': platform.machine(),
|
|
250
|
+
'processor': platform.processor(),
|
|
251
|
+
'system': platform.system(),
|
|
252
|
+
'release': platform.release(),
|
|
253
|
+
'python_version': system_info.get('python_version'),
|
|
254
|
+
'environment': system_info.get('environment','unknown'),
|
|
255
|
+
'ide_name': ide_name,
|
|
256
|
+
'ide_detection_method': ide_info.get('detection_method'),
|
|
257
|
+
'ide_frontend': ide_info.get('frontend')
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
url ='https://vnstocks.com/api/vnstock/auth/device-register'
|
|
261
|
+
try:
|
|
262
|
+
requests.post(url, json=payload, timeout=5)
|
|
263
|
+
log.debug("Device registered successfully to vnstocks.com")
|
|
264
|
+
except Exception as req_e:
|
|
265
|
+
log.debug(f"Device registration request failed: {req_e}")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
log.debug(f"Device registration error: {e}")
|
|
268
|
+
|
|
269
|
+
def check_api_key_status(self) -> dict:
|
|
270
|
+
api_key = self.get_api_key()
|
|
271
|
+
if api_key:
|
|
272
|
+
preview = api_key[:15] +"..." if len(api_key) > 15 else api_key
|
|
273
|
+
tier_info = self.get_tier_info()
|
|
274
|
+
print(f"✓ API key: {preview}")
|
|
275
|
+
print(f"✓ Tier (Gói): {tier_info['tier']}")
|
|
276
|
+
print(f"✓ Giới hạn (Limits): {tier_info['limits']}")
|
|
277
|
+
return {
|
|
278
|
+
'has_api_key': True,
|
|
279
|
+
'api_key_preview': preview,
|
|
280
|
+
'tier': tier_info['tier'],
|
|
281
|
+
'limits': tier_info['limits']
|
|
282
|
+
}
|
|
283
|
+
else:
|
|
284
|
+
return {
|
|
285
|
+
'has_api_key': False,
|
|
286
|
+
'api_key_preview': None,
|
|
287
|
+
'tier': self.get_tier(),
|
|
288
|
+
'limits': self.get_limits()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
def print_help(self):
|
|
292
|
+
print("\n" +"="*60)
|
|
293
|
+
print("VNSTOCK API KEY SETUP (Cài đặt API Key)")
|
|
294
|
+
print("="*60)
|
|
295
|
+
print("\n📋 Các gói sử dụng (Available Tiers):")
|
|
296
|
+
print(" • Khách (Guest): 20 requests/phút. Tối đa 4 kỳ báo cáo tài chính.")
|
|
297
|
+
print(" • Phiên bản cộng đồng (Community): 60 requests/phút. Tối đa 8 kỳ báo cáo tài chính.")
|
|
298
|
+
print(" • Thành viên tài trợ (Sponsor): 180-500 requests/phút.")
|
|
299
|
+
print("\n🔑 Lấy API Key (Get Your API Key):")
|
|
300
|
+
print(" 1. Truy cập: https://vnstocks.com/account")
|
|
301
|
+
print(" 2. Đăng ký hoặc đăng nhập (Sign up or log in)")
|
|
302
|
+
print(" 3. Sao chép API key của bạn (Copy your API key)")
|
|
303
|
+
print("\n💾 Cài đặt API Key (Setup API Key):")
|
|
304
|
+
print(" import vnai")
|
|
305
|
+
print(' vnai.setup_api_key("your_api_key_here")')
|
|
306
|
+
print("\n📊 Kiểm tra trạng thái (Check Status):")
|
|
307
|
+
print(" import vnai")
|
|
308
|
+
print(" vnai.check_api_key_status()")
|
|
309
|
+
print("\n🤝 Tham gia gói thành viên tài trợ (Join Vnstock Sponsor):")
|
|
310
|
+
print(" Truy cập: https://vnstocks.com/insiders-program")
|
|
311
|
+
print("="*60 +"\n")
|
|
312
|
+
authenticator = Authenticator()
|