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/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/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/quota.py
CHANGED
|
@@ -5,17 +5,72 @@ from collections import defaultdict
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
|
|
7
7
|
class RateLimitExceeded(Exception):
|
|
8
|
-
def __init__(self, resource_type, limit_type="min", current_usage=None, limit_value=None, retry_after=None):
|
|
8
|
+
def __init__(self, resource_type, limit_type="min", current_usage=None, limit_value=None, retry_after=None, tier=None):
|
|
9
9
|
self.resource_type = resource_type
|
|
10
10
|
self.limit_type = limit_type
|
|
11
11
|
self.current_usage = current_usage
|
|
12
12
|
self.limit_value = limit_value
|
|
13
13
|
self.retry_after = retry_after
|
|
14
|
-
|
|
14
|
+
self.tier = tier
|
|
15
|
+
promotional_message =""
|
|
16
|
+
try:
|
|
17
|
+
from vnai.scope.promo import (
|
|
18
|
+
should_show_promotional_for_rate_limit,
|
|
19
|
+
mark_promotional_shown,
|
|
20
|
+
get_promotional_message
|
|
21
|
+
)
|
|
22
|
+
if should_show_promotional_for_rate_limit(tier):
|
|
23
|
+
promotional_message = get_promotional_message() +"\n"
|
|
24
|
+
mark_promotional_shown()
|
|
25
|
+
except Exception as e:
|
|
26
|
+
pass
|
|
27
|
+
message =f"\n{'='*60}\n"
|
|
28
|
+
message +=f"⚠️ GIỚI HẠN API ĐÃ ĐẠT TỐI ĐA (Rate Limit Exceeded)\n"
|
|
29
|
+
message +=f"{'='*60}\n\n"
|
|
30
|
+
scope_names = {
|
|
31
|
+
'min':'phút (minute)',
|
|
32
|
+
'hour':'giờ (hour)',
|
|
33
|
+
'day':'ngày (day)'
|
|
34
|
+
}
|
|
35
|
+
scope_display = scope_names.get(limit_type, limit_type)
|
|
36
|
+
message +=f"📌 Bạn đã đạt giới hạn tối đa số lượt yêu cầu API trong 1 {scope_display}.\n"
|
|
37
|
+
message +=f" (You have reached the maximum API request limit for this period)\n\n"
|
|
38
|
+
message +=f"📊 Chi tiết (Details):\n"
|
|
39
|
+
if tier:
|
|
40
|
+
tier_names = {
|
|
41
|
+
'guest':'Khách (Guest)',
|
|
42
|
+
'free':'Phiên bản cộng đồng (Community)',
|
|
43
|
+
'bronze':'Thành viên Bronze (Bronze Member)',
|
|
44
|
+
'silver':'Thành viên Silver (Silver Member)',
|
|
45
|
+
'golden':'Thành viên Golden (Golden Member)'
|
|
46
|
+
}
|
|
47
|
+
tier_display = tier_names.get(tier,f"Thành viên {tier.title()}")
|
|
48
|
+
message +=f" • Gói hiện tại: {tier_display}\n"
|
|
49
|
+
message +=f" • Giới hạn: {limit_value} requests/{scope_display.split()[0]}\n"
|
|
50
|
+
message +=f" • Đã sử dụng: {current_usage}/{limit_value}\n"
|
|
15
51
|
if retry_after:
|
|
16
|
-
message +=f"
|
|
52
|
+
message +=f" • Chờ {round(retry_after)} giây để tiếp tục (Wait to retry)\n"
|
|
53
|
+
message +=f"\n💡 Giải pháp (Solutions):\n"
|
|
54
|
+
message +=f" 1️⃣ Chờ {round(retry_after) if retry_after else 'một lúc'} giây rồi thử lại\n"
|
|
55
|
+
message +=f" (Wait and retry)\n"
|
|
56
|
+
message +=f" 2️⃣ Tham gia gói thành viên tài trợ để sử dụng không bị gián đoạn\n"
|
|
57
|
+
message +=f" (Join sponsor membership for uninterrupted access)\n"
|
|
58
|
+
if tier =='guest':
|
|
59
|
+
message +=f"\n🚀 Nâng cấp (Upgrade):\n"
|
|
60
|
+
message +=f" • Phiên bản cộng đồng (60 request/phút - Community):\n"
|
|
61
|
+
message +=f" Đăng ký API key miễn phí: https://vnstocks.com/login\n"
|
|
62
|
+
message +=f" • Gói thành viên tài trợ (180-600 request/phút - Sponsor):\n"
|
|
63
|
+
message +=f" Tham gia: https://vnstocks.com/insiders-program\n"
|
|
64
|
+
elif tier =='free':
|
|
65
|
+
message +=f"\n🚀 Nâng cấp (Upgrade):\n"
|
|
66
|
+
message +=f" • Gói thành viên tài trợ (180-600 request/phút - Sponsor):\n"
|
|
67
|
+
message +=f" Tham gia: https://vnstocks.com/insiders-program\n"
|
|
17
68
|
else:
|
|
18
|
-
message +="
|
|
69
|
+
message +=f"\n🚀 Nâng cấp (Upgrade):\n"
|
|
70
|
+
message +=f" • Gói cao hơn (Higher tier): https://vnstocks.com/insiders-program\n"
|
|
71
|
+
message +=f"\n{'='*60}\n"
|
|
72
|
+
if promotional_message:
|
|
73
|
+
message += promotional_message
|
|
19
74
|
super().__init__(message)
|
|
20
75
|
|
|
21
76
|
class Guardian:
|
|
@@ -30,24 +85,47 @@ class Guardian:
|
|
|
30
85
|
return cls._instance
|
|
31
86
|
|
|
32
87
|
def _initialize(self):
|
|
33
|
-
self.resource_limits = defaultdict(lambda: defaultdict(int))
|
|
34
88
|
self.usage_counters = defaultdict(lambda: defaultdict(list))
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
self.
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
self.quota_manager = None
|
|
90
|
+
|
|
91
|
+
def _get_quota_manager(self):
|
|
92
|
+
if self.quota_manager is None:
|
|
93
|
+
try:
|
|
94
|
+
from vnai.beam.quota_manager import quota_manager
|
|
95
|
+
self.quota_manager = quota_manager
|
|
96
|
+
except ImportError:
|
|
97
|
+
return None
|
|
98
|
+
return self.quota_manager
|
|
99
|
+
|
|
100
|
+
def _get_tier_limits(self):
|
|
101
|
+
try:
|
|
102
|
+
from vnai.beam.auth import authenticator
|
|
103
|
+
return authenticator.get_limits()
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return {"min": 20,"hour": 1200,"day": 28800}
|
|
47
106
|
|
|
48
|
-
def
|
|
107
|
+
def _get_current_tier(self):
|
|
108
|
+
try:
|
|
109
|
+
from vnai.beam.auth import authenticator
|
|
110
|
+
return authenticator.get_tier()
|
|
111
|
+
except Exception:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
def verify(self, operation_id, resource_type="default", api_key=None):
|
|
49
115
|
current_time = time.time()
|
|
50
|
-
limits = self.
|
|
116
|
+
limits = self._get_tier_limits()
|
|
117
|
+
if api_key:
|
|
118
|
+
qm = self._get_quota_manager()
|
|
119
|
+
if qm:
|
|
120
|
+
quota_check = qm.check_quota(api_key)
|
|
121
|
+
if not quota_check.get("allowed"):
|
|
122
|
+
raise RateLimitExceeded(
|
|
123
|
+
resource_type=resource_type,
|
|
124
|
+
limit_type=quota_check.get("reason","unknown"),
|
|
125
|
+
current_usage=quota_check.get("usage"),
|
|
126
|
+
limit_value=quota_check.get("limit"),
|
|
127
|
+
retry_after=quota_check.get("reset_in_seconds")
|
|
128
|
+
)
|
|
51
129
|
minute_cutoff = current_time - 60
|
|
52
130
|
self.usage_counters[resource_type]["min"] = [
|
|
53
131
|
t for t in self.usage_counters[resource_type]["min"]
|
|
@@ -68,12 +146,14 @@ class Guardian:
|
|
|
68
146
|
},
|
|
69
147
|
priority="high"
|
|
70
148
|
)
|
|
149
|
+
current_tier = self._get_current_tier()
|
|
71
150
|
raise RateLimitExceeded(
|
|
72
151
|
resource_type=resource_type,
|
|
73
152
|
limit_type="min",
|
|
74
153
|
current_usage=minute_usage,
|
|
75
154
|
limit_value=limits["min"],
|
|
76
|
-
retry_after=60 - (current_time % 60)
|
|
155
|
+
retry_after=60 - (current_time % 60),
|
|
156
|
+
tier=current_tier
|
|
77
157
|
)
|
|
78
158
|
hour_cutoff = current_time - 3600
|
|
79
159
|
self.usage_counters[resource_type]["hour"] = [
|
|
@@ -94,12 +174,14 @@ class Guardian:
|
|
|
94
174
|
}
|
|
95
175
|
)
|
|
96
176
|
if hour_exceeded:
|
|
177
|
+
current_tier = self._get_current_tier()
|
|
97
178
|
raise RateLimitExceeded(
|
|
98
179
|
resource_type=resource_type,
|
|
99
180
|
limit_type="hour",
|
|
100
181
|
current_usage=hour_usage,
|
|
101
182
|
limit_value=limits["hour"],
|
|
102
|
-
retry_after=3600 - (current_time % 3600)
|
|
183
|
+
retry_after=3600 - (current_time % 3600),
|
|
184
|
+
tier=current_tier
|
|
103
185
|
)
|
|
104
186
|
self.usage_counters[resource_type]["min"].append(current_time)
|
|
105
187
|
self.usage_counters[resource_type]["hour"].append(current_time)
|
|
@@ -107,7 +189,7 @@ class Guardian:
|
|
|
107
189
|
|
|
108
190
|
def usage(self, resource_type="default"):
|
|
109
191
|
current_time = time.time()
|
|
110
|
-
limits = self.
|
|
192
|
+
limits = self._get_tier_limits()
|
|
111
193
|
minute_cutoff = current_time - 60
|
|
112
194
|
hour_cutoff = current_time - 3600
|
|
113
195
|
self.usage_counters[resource_type]["min"] = [
|
|
@@ -126,7 +208,7 @@ class Guardian:
|
|
|
126
208
|
|
|
127
209
|
def get_limit_status(self, resource_type="default"):
|
|
128
210
|
current_time = time.time()
|
|
129
|
-
limits = self.
|
|
211
|
+
limits = self._get_tier_limits()
|
|
130
212
|
minute_cutoff = current_time - 60
|
|
131
213
|
hour_cutoff = current_time - 3600
|
|
132
214
|
minute_usage = len([t for t in self.usage_counters[resource_type]["min"] if t > minute_cutoff])
|
|
@@ -214,11 +296,11 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
|
214
296
|
call_history.pop(0)
|
|
215
297
|
loop_detected = len(call_history) >= loop_threshold
|
|
216
298
|
if debug and loop_detected:
|
|
217
|
-
print(f"
|
|
299
|
+
print(f"Đã phát hiện vòng lặp cho {func.__name__}: {len(call_history)} lần gọi trong {time_window}s")
|
|
218
300
|
if loop_detected:
|
|
219
301
|
consecutive_loop_detections += 1
|
|
220
302
|
if debug:
|
|
221
|
-
print(f"
|
|
303
|
+
print(f"Số lần phát hiện vòng lặp liên tiếp: {consecutive_loop_detections}/{content_trigger_threshold}")
|
|
222
304
|
else:
|
|
223
305
|
consecutive_loop_detections = 0
|
|
224
306
|
should_show_content = (consecutive_loop_detections >= content_trigger_threshold) and (current_time - last_ad_time >= ad_cooldown) and not session_displayed
|
|
@@ -228,20 +310,15 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
|
228
310
|
content_triggered = True
|
|
229
311
|
session_displayed = True
|
|
230
312
|
if debug:
|
|
231
|
-
print(f"
|
|
313
|
+
print(f"Đã kích hoạt nội dung cho {func.__name__}")
|
|
232
314
|
try:
|
|
233
315
|
from vnai.scope.promo import manager
|
|
234
|
-
|
|
235
|
-
from vnai.scope.profile import inspector
|
|
236
|
-
environment = inspector.examine().get("environment", None)
|
|
237
|
-
manager.present_content(environment=environment, context="loop")
|
|
238
|
-
except ImportError:
|
|
239
|
-
manager.present_content(context="loop")
|
|
316
|
+
manager.present_content(context="loop")
|
|
240
317
|
except ImportError:
|
|
241
318
|
print(f"Phát hiện vòng lặp: Hàm '{func.__name__}' đang được gọi trong một vòng lặp")
|
|
242
319
|
except Exception as e:
|
|
243
320
|
if debug:
|
|
244
|
-
print(f"
|
|
321
|
+
print(f"Lỗi khi hiển thị nội dung: {str(e)}")
|
|
245
322
|
try:
|
|
246
323
|
with CleanErrorContext():
|
|
247
324
|
guardian.verify(func.__name__, resource_type)
|
|
@@ -261,16 +338,9 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
|
261
338
|
if not session_displayed:
|
|
262
339
|
try:
|
|
263
340
|
from vnai.scope.promo import manager
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
manager.present_content(environment=environment, context="loop")
|
|
268
|
-
session_displayed = True
|
|
269
|
-
last_ad_time = current_time
|
|
270
|
-
except ImportError:
|
|
271
|
-
manager.present_content(context="loop")
|
|
272
|
-
session_displayed = True
|
|
273
|
-
last_ad_time = current_time
|
|
341
|
+
manager.present_content(context="loop")
|
|
342
|
+
session_displayed = True
|
|
343
|
+
last_ad_time = current_time
|
|
274
344
|
except Exception:
|
|
275
345
|
pass
|
|
276
346
|
if retries < max_retries:
|
|
@@ -279,7 +349,7 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
|
279
349
|
if hasattr(e,"retry_after") and e.retry_after:
|
|
280
350
|
wait_time = min(wait_time, e.retry_after)
|
|
281
351
|
if debug:
|
|
282
|
-
print(f"
|
|
352
|
+
print(f"Đã đạt giới hạn tốc độ cho {func.__name__}, thử lại sau {wait_time} giây (lần thử {retries}/{max_retries})")
|
|
283
353
|
time.sleep(wait_time)
|
|
284
354
|
continue
|
|
285
355
|
else:
|