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
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()
|
vnai/beam/fundamental.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fundamental financial reports with tier-based period limiting.
|
|
3
|
+
Provides unified interface for financial reports from multiple sources (VCI, KBS, TCBS)
|
|
4
|
+
with automatic tier-based period limiting:
|
|
5
|
+
- Guest tier: 4 periods max
|
|
6
|
+
- Free tier: 8 periods max
|
|
7
|
+
- Paid tier: Unlimited
|
|
8
|
+
Applies limits intelligently based on data structure:
|
|
9
|
+
- VCI/KBS: Periods in columns (slice columns)
|
|
10
|
+
- TCBS: Periods in index (slice rows)
|
|
11
|
+
"""
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from vnai.beam.auth import authenticator
|
|
15
|
+
PERIOD_LIMITS = {
|
|
16
|
+
'guest': 4,
|
|
17
|
+
'free': 8,
|
|
18
|
+
'bronze': None,
|
|
19
|
+
'silver': None,
|
|
20
|
+
'golden': None,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def get_max_periods() -> Optional[int]:
|
|
24
|
+
tier = authenticator.get_tier()
|
|
25
|
+
return PERIOD_LIMITS.get(tier)
|
|
26
|
+
|
|
27
|
+
def parse_period(period_str: str) -> tuple:
|
|
28
|
+
if isinstance(period_str, str):
|
|
29
|
+
if'-Q' in period_str:
|
|
30
|
+
year, quarter = period_str.split('-Q')
|
|
31
|
+
return (int(year), int(quarter))
|
|
32
|
+
elif period_str.isdigit():
|
|
33
|
+
return (int(period_str), 5)
|
|
34
|
+
return (0, 0)
|
|
35
|
+
|
|
36
|
+
def limit_periods_by_columns(df: pd.DataFrame, max_periods: Optional[int] = None) -> pd.DataFrame:
|
|
37
|
+
if max_periods is None:
|
|
38
|
+
return df
|
|
39
|
+
metadata_cols = [
|
|
40
|
+
'ticker','yearReport','lengthReport',
|
|
41
|
+
'item','item_en','item_id','unit','levels','row_number'
|
|
42
|
+
]
|
|
43
|
+
period_cols = []
|
|
44
|
+
for col in df.columns:
|
|
45
|
+
if isinstance(col, str) and col not in metadata_cols:
|
|
46
|
+
if col.isdigit() or (len(col) > 4 and'-Q' in col):
|
|
47
|
+
period_cols.append(col)
|
|
48
|
+
if not period_cols:
|
|
49
|
+
return df
|
|
50
|
+
period_cols_sorted = sorted(period_cols, key=parse_period, reverse=True)
|
|
51
|
+
keep_periods = period_cols_sorted[:max_periods]
|
|
52
|
+
metadata_cols_present = [col for col in metadata_cols if col in df.columns]
|
|
53
|
+
financial_cols = [col for col in df.columns if col not in metadata_cols and col not in period_cols]
|
|
54
|
+
final_cols = metadata_cols_present + financial_cols + keep_periods
|
|
55
|
+
return df[final_cols]
|
|
56
|
+
|
|
57
|
+
def limit_periods_by_index(df: pd.DataFrame, max_periods: Optional[int] = None) -> pd.DataFrame:
|
|
58
|
+
if max_periods is None:
|
|
59
|
+
return df
|
|
60
|
+
periods = df.index.tolist()
|
|
61
|
+
periods_sorted = sorted(periods, key=parse_period, reverse=True)
|
|
62
|
+
keep_periods = periods_sorted[:max_periods]
|
|
63
|
+
return df.loc[keep_periods]
|
|
64
|
+
|
|
65
|
+
def limit_vci_periods(df: pd.DataFrame, max_periods: Optional[int] = None) -> pd.DataFrame:
|
|
66
|
+
if max_periods is None or df.empty or'yearReport' not in df.columns:
|
|
67
|
+
return df
|
|
68
|
+
df_limited = df.head(max_periods).copy()
|
|
69
|
+
df_limited = df_limited.reset_index(drop=True)
|
|
70
|
+
return df_limited
|
|
71
|
+
|
|
72
|
+
def apply_period_limit(df: pd.DataFrame, by_index: bool = False, source: str ='default') -> pd.DataFrame:
|
|
73
|
+
max_periods = get_max_periods()
|
|
74
|
+
if source =='vci' and'yearReport' in df.columns:
|
|
75
|
+
return limit_vci_periods(df, max_periods)
|
|
76
|
+
elif by_index:
|
|
77
|
+
return limit_periods_by_index(df, max_periods)
|
|
78
|
+
else:
|
|
79
|
+
return limit_periods_by_columns(df, max_periods)
|
|
80
|
+
|
|
81
|
+
class FinancialReports:
|
|
82
|
+
def __init__(self, symbol: str, source: str ='vci', show_log: bool = False):
|
|
83
|
+
self.symbol = symbol
|
|
84
|
+
self.source = source.lower()
|
|
85
|
+
self.show_log = show_log
|
|
86
|
+
if self.source =='vci':
|
|
87
|
+
from vnstock.explorer.vci import Finance as VCI_Finance
|
|
88
|
+
self.finance = VCI_Finance(symbol=symbol, show_log=show_log)
|
|
89
|
+
self.by_index = False
|
|
90
|
+
elif self.source =='kbs':
|
|
91
|
+
from vnstock.explorer.kbs import Finance as KBS_Finance
|
|
92
|
+
self.finance = KBS_Finance(symbol=symbol, show_log=show_log)
|
|
93
|
+
self.by_index = False
|
|
94
|
+
elif self.source =='tcbs':
|
|
95
|
+
from vnstock.explorer.tcbs import Finance as TCBS_Finance
|
|
96
|
+
self.finance = TCBS_Finance(symbol=symbol, show_log=show_log)
|
|
97
|
+
self.by_index = True
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(f"Unsupported source: {source}. Must be 'vci', 'kbs', or 'tcbs'.")
|
|
100
|
+
|
|
101
|
+
def balance_sheet(self, period: str ='year', lang: Optional[str] ='en',
|
|
102
|
+
show_log: Optional[bool] = False) -> pd.DataFrame:
|
|
103
|
+
if self.source =='vci':
|
|
104
|
+
df = self.finance.balance_sheet(period=period, lang=lang, show_log=show_log)
|
|
105
|
+
elif self.source =='kbs':
|
|
106
|
+
df = self.finance.balance_sheet(period=period, show_log=show_log)
|
|
107
|
+
elif self.source =='tcbs':
|
|
108
|
+
df = self.finance.balance_sheet(period=period, show_log=show_log)
|
|
109
|
+
df = apply_period_limit(df, by_index=self.by_index, source=self.source)
|
|
110
|
+
if not hasattr(self,'_notice_shown'):
|
|
111
|
+
self._notice_shown = False
|
|
112
|
+
if not self._notice_shown:
|
|
113
|
+
from vnai.beam.patching import should_show_notice, display_period_limit_notice_jupyter
|
|
114
|
+
if should_show_notice():
|
|
115
|
+
self._notice_shown = True
|
|
116
|
+
display_period_limit_notice_jupyter()
|
|
117
|
+
return df
|
|
118
|
+
|
|
119
|
+
def income_statement(self, period: str ='year', lang: Optional[str] ='en',
|
|
120
|
+
show_log: Optional[bool] = False) -> pd.DataFrame:
|
|
121
|
+
if self.source =='vci':
|
|
122
|
+
df = self.finance.income_statement(period=period, lang=lang, show_log=show_log)
|
|
123
|
+
elif self.source =='kbs':
|
|
124
|
+
df = self.finance.income_statement(period=period, show_log=show_log)
|
|
125
|
+
elif self.source =='tcbs':
|
|
126
|
+
df = self.finance.income_statement(period=period, show_log=show_log)
|
|
127
|
+
df = apply_period_limit(df, by_index=self.by_index, source=self.source)
|
|
128
|
+
if not hasattr(self,'_notice_shown'):
|
|
129
|
+
self._notice_shown = False
|
|
130
|
+
if not self._notice_shown:
|
|
131
|
+
from vnai.beam.patching import should_show_notice, display_period_limit_notice_jupyter
|
|
132
|
+
if should_show_notice():
|
|
133
|
+
self._notice_shown = True
|
|
134
|
+
display_period_limit_notice_jupyter()
|
|
135
|
+
return df
|
|
136
|
+
|
|
137
|
+
def cash_flow(self, period: str ='year', lang: Optional[str] ='en',
|
|
138
|
+
show_log: Optional[bool] = False) -> pd.DataFrame:
|
|
139
|
+
if self.source =='vci':
|
|
140
|
+
df = self.finance.cash_flow(period=period, lang=lang, show_log=show_log)
|
|
141
|
+
elif self.source =='kbs':
|
|
142
|
+
df = self.finance.cash_flow(period=period, show_log=show_log)
|
|
143
|
+
elif self.source =='tcbs':
|
|
144
|
+
df = self.finance.cash_flow(period=period, show_log=show_log)
|
|
145
|
+
df = apply_period_limit(df, by_index=self.by_index, source=self.source)
|
|
146
|
+
if not hasattr(self,'_notice_shown'):
|
|
147
|
+
self._notice_shown = False
|
|
148
|
+
if not self._notice_shown:
|
|
149
|
+
from vnai.beam.patching import should_show_notice, display_period_limit_notice_jupyter
|
|
150
|
+
if should_show_notice():
|
|
151
|
+
self._notice_shown = True
|
|
152
|
+
display_period_limit_notice_jupyter()
|
|
153
|
+
return df
|
|
154
|
+
|
|
155
|
+
def balance_sheet(symbol: str, source: str ='vci', period: str ='year',
|
|
156
|
+
lang: Optional[str] ='en', show_log: bool = False) -> pd.DataFrame:
|
|
157
|
+
reports = FinancialReports(symbol=symbol, source=source, show_log=show_log)
|
|
158
|
+
return reports.balance_sheet(period=period, lang=lang, show_log=show_log)
|
|
159
|
+
|
|
160
|
+
def income_statement(symbol: str, source: str ='vci', period: str ='year',
|
|
161
|
+
lang: Optional[str] ='en', show_log: bool = False) -> pd.DataFrame:
|
|
162
|
+
reports = FinancialReports(symbol=symbol, source=source, show_log=show_log)
|
|
163
|
+
return reports.income_statement(period=period, lang=lang, show_log=show_log)
|
|
164
|
+
|
|
165
|
+
def cash_flow(symbol: str, source: str ='vci', period: str ='year',
|
|
166
|
+
lang: Optional[str] ='en', show_log: bool = False) -> pd.DataFrame:
|
|
167
|
+
reports = FinancialReports(symbol=symbol, source=source, show_log=show_log)
|
|
168
|
+
return reports.cash_flow(period=period, lang=lang, show_log=show_log)
|
vnai/beam/metrics.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
class Collector:
|
|
9
|
+
_instance = None
|
|
10
|
+
_lock = threading.Lock()
|
|
11
|
+
|
|
12
|
+
def __new__(cls):
|
|
13
|
+
with cls._lock:
|
|
14
|
+
if cls._instance is None:
|
|
15
|
+
cls._instance = super(Collector, cls).__new__(cls)
|
|
16
|
+
cls._instance._initialize()
|
|
17
|
+
return cls._instance
|
|
18
|
+
|
|
19
|
+
def _initialize(self):
|
|
20
|
+
self.metrics = {
|
|
21
|
+
"function": [],
|
|
22
|
+
"rate_limit": [],
|
|
23
|
+
"request": [],
|
|
24
|
+
"error": []
|
|
25
|
+
}
|
|
26
|
+
self.thresholds = {
|
|
27
|
+
"buffer_size": 50,
|
|
28
|
+
"error_threshold": 0.1,
|
|
29
|
+
"performance_threshold": 5.0
|
|
30
|
+
}
|
|
31
|
+
self.function_count = 0
|
|
32
|
+
self.colab_auth_triggered = False
|
|
33
|
+
self.max_metric_length = 200
|
|
34
|
+
self._last_record_time = {}
|
|
35
|
+
self.min_interval_per_type = 0.5
|
|
36
|
+
self._recent_hashes = []
|
|
37
|
+
self._sending_metrics = False
|
|
38
|
+
|
|
39
|
+
def record(self, metric_type, data, priority=None):
|
|
40
|
+
if not isinstance(data, dict):
|
|
41
|
+
data = {"value": str(data)}
|
|
42
|
+
if"timestamp" not in data:
|
|
43
|
+
data["timestamp"] = datetime.now().isoformat()
|
|
44
|
+
if metric_type !="system_info":
|
|
45
|
+
data.pop("system", None)
|
|
46
|
+
from vnai.scope.profile import inspector
|
|
47
|
+
data["machine_id"] = inspector.fingerprint()
|
|
48
|
+
now = time.time()
|
|
49
|
+
last_time = self._last_record_time.get(metric_type, 0)
|
|
50
|
+
if now - last_time < self.min_interval_per_type and priority !="high":
|
|
51
|
+
return
|
|
52
|
+
self._last_record_time[metric_type] = now
|
|
53
|
+
data_hash = hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest()
|
|
54
|
+
if data_hash in self._recent_hashes and priority !="high":
|
|
55
|
+
return
|
|
56
|
+
self._recent_hashes.append(data_hash)
|
|
57
|
+
if metric_type in self.metrics:
|
|
58
|
+
self.metrics[metric_type].append(data)
|
|
59
|
+
if len(self.metrics[metric_type]) > self.max_metric_length:
|
|
60
|
+
self.metrics[metric_type] = self.metrics[metric_type][-self.max_metric_length:]
|
|
61
|
+
else:
|
|
62
|
+
self.metrics["function"].append(data)
|
|
63
|
+
if metric_type =="function":
|
|
64
|
+
self.function_count += 1
|
|
65
|
+
if self.function_count > 10 and not self.colab_auth_triggered and'google.colab' in sys.modules:
|
|
66
|
+
self.colab_auth_triggered = True
|
|
67
|
+
threading.Thread(target=self._trigger_colab_auth, daemon=True).start()
|
|
68
|
+
if sum(len(metric_list) for metric_list in self.metrics.values()) >= self.thresholds["buffer_size"]:
|
|
69
|
+
self._send_metrics()
|
|
70
|
+
if priority =="high" or metric_type =="error":
|
|
71
|
+
self._send_metrics()
|
|
72
|
+
|
|
73
|
+
def _trigger_colab_auth(self):
|
|
74
|
+
try:
|
|
75
|
+
from vnai.scope.profile import inspector
|
|
76
|
+
inspector.get_or_create_user_id()
|
|
77
|
+
except:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def _send_metrics(self):
|
|
81
|
+
if self._sending_metrics:
|
|
82
|
+
return
|
|
83
|
+
self._sending_metrics = True
|
|
84
|
+
try:
|
|
85
|
+
from vnai.flow.relay import track_function_call, track_rate_limit, track_api_request
|
|
86
|
+
except ImportError:
|
|
87
|
+
for metric_type in self.metrics:
|
|
88
|
+
self.metrics[metric_type] = []
|
|
89
|
+
self._sending_metrics = False
|
|
90
|
+
return
|
|
91
|
+
for metric_type, data_list in self.metrics.items():
|
|
92
|
+
if not data_list:
|
|
93
|
+
continue
|
|
94
|
+
for data in data_list:
|
|
95
|
+
try:
|
|
96
|
+
if metric_type =="function":
|
|
97
|
+
track_function_call(
|
|
98
|
+
function_name=data.get("function","unknown"),
|
|
99
|
+
source=data.get("source","vnai"),
|
|
100
|
+
execution_time=data.get("execution_time", 0),
|
|
101
|
+
success=data.get("success", True),
|
|
102
|
+
error=data.get("error"),
|
|
103
|
+
args=data.get("args")
|
|
104
|
+
)
|
|
105
|
+
elif metric_type =="rate_limit":
|
|
106
|
+
track_rate_limit(
|
|
107
|
+
source=data.get("source","vnai"),
|
|
108
|
+
limit_type=data.get("limit_type","unknown"),
|
|
109
|
+
limit_value=data.get("limit_value", 0),
|
|
110
|
+
current_usage=data.get("current_usage", 0),
|
|
111
|
+
is_exceeded=data.get("is_exceeded", False)
|
|
112
|
+
)
|
|
113
|
+
elif metric_type =="request":
|
|
114
|
+
track_api_request(
|
|
115
|
+
endpoint=data.get("endpoint","unknown"),
|
|
116
|
+
source=data.get("source","vnai"),
|
|
117
|
+
method=data.get("method","GET"),
|
|
118
|
+
status_code=data.get("status_code", 200),
|
|
119
|
+
execution_time=data.get("execution_time", 0),
|
|
120
|
+
request_size=data.get("request_size", 0),
|
|
121
|
+
response_size=data.get("response_size", 0)
|
|
122
|
+
)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
continue
|
|
125
|
+
self.metrics[metric_type] = []
|
|
126
|
+
self._sending_metrics = False
|
|
127
|
+
|
|
128
|
+
def get_metrics_summary(self):
|
|
129
|
+
return {
|
|
130
|
+
metric_type: len(data_list)
|
|
131
|
+
for metric_type, data_list in self.metrics.items()
|
|
132
|
+
}
|
|
133
|
+
collector = Collector()
|
|
134
|
+
|
|
135
|
+
def capture(module_type="function"):
|
|
136
|
+
def decorator(func):
|
|
137
|
+
def wrapper(*args, **kwargs):
|
|
138
|
+
start_time = time.time()
|
|
139
|
+
success = False
|
|
140
|
+
error = None
|
|
141
|
+
try:
|
|
142
|
+
result = func(*args, **kwargs)
|
|
143
|
+
success = True
|
|
144
|
+
return result
|
|
145
|
+
except Exception as e:
|
|
146
|
+
error = str(e)
|
|
147
|
+
collector.record("error", {
|
|
148
|
+
"function": func.__name__,
|
|
149
|
+
"error": error,
|
|
150
|
+
"args": str(args)[:100] if args else None
|
|
151
|
+
})
|
|
152
|
+
raise
|
|
153
|
+
finally:
|
|
154
|
+
execution_time = time.time() - start_time
|
|
155
|
+
collector.record(
|
|
156
|
+
module_type,
|
|
157
|
+
{
|
|
158
|
+
"function": func.__name__,
|
|
159
|
+
"execution_time": execution_time,
|
|
160
|
+
"success": success,
|
|
161
|
+
"error": error,
|
|
162
|
+
"timestamp": datetime.now().isoformat(),
|
|
163
|
+
"args": str(args)[:100] if args else None
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
return wrapper
|
|
167
|
+
return decorator
|