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/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()
@@ -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