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/patching.py ADDED
@@ -0,0 +1,223 @@
1
+ """
2
+ Monkey-patch vnstock source methods to apply tier-based period limiting.
3
+ This module patches VCI and KBS Finance methods to automatically apply
4
+ period limits based on user tier, so end users get limited periods
5
+ even when calling vnstock methods directly.
6
+ """
7
+ from typing import Optional, Callable
8
+ from functools import wraps
9
+ import pandas as pd
10
+
11
+ def get_max_periods() -> Optional[int]:
12
+ from vnai.beam.auth import authenticator
13
+ PERIOD_LIMITS = {
14
+ 'guest': 4,
15
+ 'free': 8,
16
+ 'bronze': None,
17
+ 'silver': None,
18
+ 'golden': None,
19
+ }
20
+ tier = authenticator.get_tier()
21
+ return PERIOD_LIMITS.get(tier)
22
+
23
+ def limit_vci_periods(df: pd.DataFrame, max_periods: Optional[int] = None) -> pd.DataFrame:
24
+ if max_periods is None or df.empty or'yearReport' not in df.columns:
25
+ return df
26
+ df_limited = df.head(max_periods).copy()
27
+ df_limited = df_limited.reset_index(drop=True)
28
+ return df_limited
29
+
30
+ def get_period_limit_notice() -> Optional[str]:
31
+ from vnai.beam.auth import authenticator
32
+ tier = authenticator.get_tier()
33
+ if tier =='guest':
34
+ return (
35
+ "ℹ️ Phiên bản cộng đồng: Báo cáo tài chính được giới hạn tối đa 4 kỳ để minh hoạ thuật toán. "
36
+ "Để truy cập đầy đủ tất cả các kỳ báo cáo, vui lòng tham gia gói thành viên tài trợ dự án: "
37
+ "https://vnstocks.com/insiders-program"
38
+ )
39
+ elif tier =='free':
40
+ return (
41
+ "ℹ️ Phiên bản cộng đồng: Báo cáo tài chính được giới hạn tối đa 8 kỳ để minh hoạ thuật toán. "
42
+ "Để truy cập đầy đủ tất cả các kỳ báo cáo, vui lòng tham gia gói thành viên tài trợ dự án: "
43
+ "https://vnstocks.com/insiders-program"
44
+ )
45
+ return None
46
+
47
+ def get_period_limit_notice_html() -> Optional[str]:
48
+ from vnai.beam.auth import authenticator
49
+ tier = authenticator.get_tier()
50
+ if tier =='guest':
51
+ return (
52
+ "<div style='background-color: #e3f2fd; border-left: 4px solid #2196f3; "
53
+ "padding: 12px 16px; margin: 12px 0; border-radius: 4px; font-size: 13px;'>"
54
+ "<strong>ℹ️ Phiên bản cộng đồng</strong><br>"
55
+ "Báo cáo tài chính được giới hạn tối đa <strong>4 kỳ</strong> để minh hoạ thuật toán. "
56
+ "Để truy cập đầy đủ tất cả các kỳ báo cáo, vui lòng "
57
+ "<a href='https://vnstocks.com/insiders-program' target='_blank'>tham gia gói thành viên tài trợ dự án</a>."
58
+ "</div>"
59
+ )
60
+ elif tier =='free':
61
+ return (
62
+ "<div style='background-color: #e3f2fd; border-left: 4px solid #2196f3; "
63
+ "padding: 12px 16px; margin: 12px 0; border-radius: 4px; font-size: 13px;'>"
64
+ "<strong>ℹ️ Phiên bản cộng đồng</strong><br>"
65
+ "Báo cáo tài chính được giới hạn tối đa <strong>8 kỳ</strong> để minh hoạ thuật toán. "
66
+ "Để truy cập đầy đủ tất cả các kỳ báo cáo, vui lòng "
67
+ "<a href='https://vnstocks.com/insiders-program' target='_blank'>tham gia gói thành viên tài trợ dự án</a>."
68
+ "</div>"
69
+ )
70
+ return None
71
+
72
+ def display_period_limit_notice_jupyter() -> None:
73
+ try:
74
+ from IPython.display import HTML, display
75
+ notice_html = get_period_limit_notice_html()
76
+ if notice_html:
77
+ display(HTML(notice_html))
78
+ except ImportError:
79
+ notice = get_period_limit_notice()
80
+ if notice:
81
+ print(f"\n{notice}\n")
82
+
83
+ def should_show_notice() -> bool:
84
+ from vnai.beam.auth import authenticator
85
+ tier = authenticator.get_tier()
86
+ return tier in ('guest','free')
87
+
88
+ def limit_periods_by_columns(df: pd.DataFrame, max_periods: Optional[int] = None) -> pd.DataFrame:
89
+ if max_periods is None:
90
+ return df
91
+ metadata_cols = [
92
+ 'ticker','yearReport','lengthReport',
93
+ 'item','item_en','item_id','unit','levels','row_number'
94
+ ]
95
+ period_cols = []
96
+ for col in df.columns:
97
+ if isinstance(col, str) and col not in metadata_cols:
98
+ if col.isdigit() or (len(col) > 4 and'-Q' in col):
99
+ period_cols.append(col)
100
+ if not period_cols:
101
+ return df
102
+
103
+ def parse_period(p):
104
+ if'-Q' in str(p):
105
+ year, quarter = str(p).split('-Q')
106
+ return (int(year), int(quarter))
107
+ else:
108
+ return (int(p), 5)
109
+ period_cols_sorted = sorted(period_cols, key=parse_period, reverse=True)
110
+ keep_periods = period_cols_sorted[:max_periods]
111
+ metadata_cols_present = [col for col in metadata_cols if col in df.columns]
112
+ financial_cols = [col for col in df.columns if col not in metadata_cols and col not in period_cols]
113
+ final_cols = metadata_cols_present + financial_cols + keep_periods
114
+ return df[final_cols]
115
+
116
+ def patch_vci_finance():
117
+ try:
118
+ import sys
119
+ if'vnstock.explorer.vci.financial' not in sys.modules:
120
+ return False
121
+ from vnstock.explorer.vci.financial import Finance as VCI_Finance
122
+ original_balance_sheet = VCI_Finance.balance_sheet
123
+ original_income_statement = VCI_Finance.income_statement
124
+ original_cash_flow = VCI_Finance.cash_flow
125
+ _notice_shown = {'balance_sheet': False,'income_statement': False,'cash_flow': False}
126
+ @wraps(original_balance_sheet)
127
+
128
+ def balance_sheet_with_limit(self, period: Optional[str] = None, lang: Optional[str] ='en',
129
+ dropna: Optional[bool] = True, show_log: Optional[bool] = False) -> pd.DataFrame:
130
+ df = original_balance_sheet(self, period=period, lang=lang, dropna=dropna, show_log=show_log)
131
+ df_limited = limit_vci_periods(df, max_periods=get_max_periods())
132
+ if should_show_notice() and not _notice_shown['balance_sheet']:
133
+ _notice_shown['balance_sheet'] = True
134
+ display_period_limit_notice_jupyter()
135
+ return df_limited
136
+ @wraps(original_income_statement)
137
+
138
+ def income_statement_with_limit(self, period: Optional[str] = None, lang: Optional[str] ='en',
139
+ dropna: Optional[bool] = True, show_log: Optional[bool] = False) -> pd.DataFrame:
140
+ df = original_income_statement(self, period=period, lang=lang, dropna=dropna, show_log=show_log)
141
+ df_limited = limit_vci_periods(df, max_periods=get_max_periods())
142
+ if should_show_notice() and not _notice_shown['income_statement']:
143
+ _notice_shown['income_statement'] = True
144
+ display_period_limit_notice_jupyter()
145
+ return df_limited
146
+ @wraps(original_cash_flow)
147
+
148
+ def cash_flow_with_limit(self, period: Optional[str] = None, lang: Optional[str] ='en',
149
+ dropna: Optional[bool] = True, show_log: Optional[bool] = False) -> pd.DataFrame:
150
+ df = original_cash_flow(self, period=period, lang=lang, dropna=dropna, show_log=show_log)
151
+ df_limited = limit_vci_periods(df, max_periods=get_max_periods())
152
+ if should_show_notice() and not _notice_shown['cash_flow']:
153
+ _notice_shown['cash_flow'] = True
154
+ display_period_limit_notice_jupyter()
155
+ return df_limited
156
+ VCI_Finance.balance_sheet = balance_sheet_with_limit
157
+ VCI_Finance.income_statement = income_statement_with_limit
158
+ VCI_Finance.cash_flow = cash_flow_with_limit
159
+ return True
160
+ except Exception as e:
161
+ print(f"Warning: Could not patch VCI Finance: {e}")
162
+ return False
163
+
164
+ def patch_kbs_finance():
165
+ try:
166
+ from vnstock.explorer.kbs.financial import Finance as KBS_Finance
167
+ from vnai.beam.fundamental import limit_periods_by_columns
168
+ original_balance_sheet = KBS_Finance.balance_sheet
169
+ original_income_statement = KBS_Finance.income_statement
170
+ original_cash_flow = KBS_Finance.cash_flow
171
+ _notice_shown = {'balance_sheet': False,'income_statement': False,'cash_flow': False}
172
+ @wraps(original_balance_sheet)
173
+
174
+ def balance_sheet_with_limit(self, period: Optional[str] = None, show_log: Optional[bool] = False) -> pd.DataFrame:
175
+ df = original_balance_sheet(self, period=period, show_log=show_log)
176
+ df_limited = limit_periods_by_columns(df, max_periods=get_max_periods())
177
+ if should_show_notice() and not _notice_shown['balance_sheet']:
178
+ _notice_shown['balance_sheet'] = True
179
+ display_period_limit_notice_jupyter()
180
+ return df_limited
181
+ @wraps(original_income_statement)
182
+
183
+ def income_statement_with_limit(self, period: Optional[str] = None, show_log: Optional[bool] = False) -> pd.DataFrame:
184
+ df = original_income_statement(self, period=period, show_log=show_log)
185
+ df_limited = limit_periods_by_columns(df, max_periods=get_max_periods())
186
+ if should_show_notice() and not _notice_shown['income_statement']:
187
+ _notice_shown['income_statement'] = True
188
+ display_period_limit_notice_jupyter()
189
+ return df_limited
190
+ @wraps(original_cash_flow)
191
+
192
+ def cash_flow_with_limit(self, period: Optional[str] = None, show_log: Optional[bool] = False) -> pd.DataFrame:
193
+ df = original_cash_flow(self, period=period, show_log=show_log)
194
+ df_limited = limit_periods_by_columns(df, max_periods=get_max_periods())
195
+ if should_show_notice() and not _notice_shown['cash_flow']:
196
+ _notice_shown['cash_flow'] = True
197
+ display_period_limit_notice_jupyter()
198
+ return df_limited
199
+ KBS_Finance.balance_sheet = balance_sheet_with_limit
200
+ KBS_Finance.income_statement = income_statement_with_limit
201
+ KBS_Finance.cash_flow = cash_flow_with_limit
202
+ return True
203
+ except Exception as e:
204
+ print(f"Warning: Could not patch KBS Finance: {e}")
205
+ return False
206
+ _patches_applied = False
207
+ _patches_lock = __import__('threading').Lock()
208
+
209
+ def apply_all_patches():
210
+ global _patches_applied
211
+ with _patches_lock:
212
+ if _patches_applied:
213
+ return {'vci': True,'kbs': True}
214
+ try:
215
+ vci_patched = patch_vci_finance()
216
+ kbs_patched = patch_kbs_finance()
217
+ _patches_applied = True
218
+ return {
219
+ 'vci': vci_patched,
220
+ 'kbs': kbs_patched,
221
+ }
222
+ except Exception as e:
223
+ return {'vci': False,'kbs': False}
vnai/beam/pulse.py ADDED
@@ -0,0 +1,79 @@
1
+ import threading
2
+ import time
3
+ from datetime import datetime
4
+
5
+ class Monitor:
6
+ _instance = None
7
+ _lock = threading.Lock()
8
+
9
+ def __new__(cls):
10
+ with cls._lock:
11
+ if cls._instance is None:
12
+ cls._instance = super(Monitor, cls).__new__(cls)
13
+ cls._instance._initialize()
14
+ return cls._instance
15
+
16
+ def _initialize(self):
17
+ self.health_status ="healthy"
18
+ self.last_check = time.time()
19
+ self.check_interval = 300
20
+ self.error_count = 0
21
+ self.warning_count = 0
22
+ self.status_history = []
23
+ self._start_background_check()
24
+
25
+ def _start_background_check(self):
26
+ def check_health():
27
+ while True:
28
+ try:
29
+ self.check_health()
30
+ except:
31
+ pass
32
+ time.sleep(self.check_interval)
33
+ thread = threading.Thread(target=check_health, daemon=True)
34
+ thread.start()
35
+
36
+ def check_health(self):
37
+ from vnai.beam.metrics import collector
38
+ from vnai.beam.quota import guardian
39
+ self.last_check = time.time()
40
+ metrics_summary = collector.get_metrics_summary()
41
+ has_errors = metrics_summary.get("error", 0) > 0
42
+ resource_usage = guardian.usage()
43
+ high_usage = resource_usage > 80
44
+ if has_errors and high_usage:
45
+ self.health_status ="critical"
46
+ self.error_count += 1
47
+ elif has_errors or high_usage:
48
+ self.health_status ="warning"
49
+ self.warning_count += 1
50
+ else:
51
+ self.health_status ="healthy"
52
+ self.status_history.append({
53
+ "timestamp": datetime.now().isoformat(),
54
+ "status": self.health_status,
55
+ "metrics": metrics_summary,
56
+ "resource_usage": resource_usage
57
+ })
58
+ if len(self.status_history) > 10:
59
+ self.status_history = self.status_history[-10:]
60
+ return self.health_status
61
+
62
+ def report(self):
63
+ if time.time() - self.last_check > self.check_interval:
64
+ self.check_health()
65
+ return {
66
+ "status": self.health_status,
67
+ "last_check": datetime.fromtimestamp(self.last_check).isoformat(),
68
+ "error_count": self.error_count,
69
+ "warning_count": self.warning_count,
70
+ "history": self.status_history[-3:],
71
+ }
72
+
73
+ def reset(self):
74
+ self.health_status ="healthy"
75
+ self.error_count = 0
76
+ self.warning_count = 0
77
+ self.status_history = []
78
+ self.last_check = time.time()
79
+ monitor = Monitor()