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