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.
@@ -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
- message =f"Bạn đã gửi quá nhiều request tới {resource_type}. "
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"Vui lòng thử lại sau {round(retry_after)} giây."
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 +="Vui lòng thêm thời gian chờ giữa các lần gửi request."
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.resource_limits["default"] = {"min": 60,"hour": 3000}
36
- self.resource_limits["TCBS"] = {"min": 60,"hour": 3000}
37
- self.resource_limits["VCI"] = {"min": 60,"hour": 3000}
38
- self.resource_limits["MBK"] = {"min": 600,"hour": 36000}
39
- self.resource_limits["MAS.ext"] = {"min": 600,"hour": 36000}
40
- self.resource_limits["VCI.ext"] = {"min": 600,"hour": 36000}
41
- self.resource_limits["FMK.ext"] = {"min": 600,"hour": 36000}
42
- self.resource_limits["VND.ext"] = {"min": 600,"hour": 36000}
43
- self.resource_limits["CAF.ext"] = {"min": 600,"hour": 36000}
44
- self.resource_limits["SPL.ext"] = {"min": 600,"hour": 36000}
45
- self.resource_limits["VDS.ext"] = {"min": 600,"hour": 36000}
46
- self.resource_limits["FAD.ext"] = {"min": 600,"hour": 36000}
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 verify(self, operation_id, resource_type="default"):
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.resource_limits.get(resource_type, self.resource_limits["default"])
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.resource_limits.get(resource_type, self.resource_limits["default"])
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.resource_limits.get(resource_type, self.resource_limits["default"])
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"[OPTIMIZE] Đã phát hiện vòng lặp cho {func.__name__}: {len(call_history)} lần gọi trong {time_window}s")
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"[OPTIMIZE] Số lần phát hiện vòng lặp liên tiếp: {consecutive_loop_detections}/{content_trigger_threshold}")
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"[OPTIMIZE] Đã kích hoạt nội dung cho {func.__name__}")
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
- try:
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"[OPTIMIZE] Lỗi khi hiển thị nội dung: {str(e)}")
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
- try:
265
- from vnai.scope.profile import inspector
266
- environment = inspector.examine().get("environment", None)
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"[OPTIMIZE] Đã đạt giới hạn tốc độ cho {func.__name__}, thử lại sau {wait_time} giây (lần thử {retries}/{max_retries})")
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: