vnai 2.1.7__py3-none-any.whl → 2.1.8__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 +240 -178
- vnai/beam/__init__.py +4 -2
- vnai/beam/metrics.py +218 -167
- vnai/beam/pulse.py +108 -79
- vnai/beam/quota.py +486 -333
- vnai/flow/__init__.py +5 -2
- vnai/flow/queue.py +133 -100
- vnai/flow/relay.py +447 -356
- vnai/scope/__init__.py +7 -4
- vnai/scope/profile.py +765 -579
- vnai/scope/promo.py +375 -278
- vnai/scope/state.py +222 -155
- {vnai-2.1.7.dist-info → vnai-2.1.8.dist-info}/METADATA +20 -20
- vnai-2.1.8.dist-info/RECORD +16 -0
- vnai-2.1.7.dist-info/RECORD +0 -16
- {vnai-2.1.7.dist-info → vnai-2.1.8.dist-info}/WHEEL +0 -0
- {vnai-2.1.7.dist-info → vnai-2.1.8.dist-info}/top_level.txt +0 -0
vnai/scope/promo.py
CHANGED
@@ -1,278 +1,375 @@
|
|
1
|
-
import logging
|
2
|
-
import requests
|
3
|
-
from datetime import datetime
|
4
|
-
import random
|
5
|
-
import threading
|
6
|
-
import time
|
7
|
-
import urllib.parse
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
if
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
self.
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
if
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
""
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
""
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
"
|
272
|
-
"
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
1
|
+
import logging
|
2
|
+
import requests
|
3
|
+
from datetime import datetime
|
4
|
+
import random
|
5
|
+
import threading
|
6
|
+
import time
|
7
|
+
import urllib.parse
|
8
|
+
|
9
|
+
_vnii_check_attempted = False
|
10
|
+
|
11
|
+
class AdCategory:
|
12
|
+
FREE = 0
|
13
|
+
MANDATORY = 1
|
14
|
+
ANNOUNCEMENT = 2
|
15
|
+
REFERRAL = 3
|
16
|
+
FEATURE = 4
|
17
|
+
GUIDE = 5
|
18
|
+
SURVEY = 6
|
19
|
+
PROMOTION = 7
|
20
|
+
SECURITY = 8
|
21
|
+
MAINTENANCE = 9
|
22
|
+
WARNING = 10
|
23
|
+
|
24
|
+
# Thêm import kiểm tra license từ vnii
|
25
|
+
try:
|
26
|
+
from vnii import lc_init
|
27
|
+
except ImportError:
|
28
|
+
lc_init = None # Nếu không có vnii, không xác định được trạng thái license
|
29
|
+
|
30
|
+
# Module-level logger setup
|
31
|
+
logger = logging.getLogger(__name__)
|
32
|
+
if not logger.hasHandlers():
|
33
|
+
# Add a simple stream handler that only outputs the message text
|
34
|
+
handler = logging.StreamHandler()
|
35
|
+
handler.setFormatter(logging.Formatter('%(message)s'))
|
36
|
+
logger.addHandler(handler)
|
37
|
+
# Set to ERROR level to minimize output
|
38
|
+
logger.setLevel(logging.ERROR)
|
39
|
+
|
40
|
+
class ContentManager:
|
41
|
+
"""
|
42
|
+
Singleton manager to fetch remote or fallback promotional content and
|
43
|
+
present it in different environments (Jupyter, terminal, other).
|
44
|
+
|
45
|
+
Displays content automatically at randomized intervals via a background thread.
|
46
|
+
"""
|
47
|
+
_instance = None
|
48
|
+
_lock = threading.Lock()
|
49
|
+
|
50
|
+
def __new__(cls):
|
51
|
+
"""
|
52
|
+
Ensure only one instance of ContentManager is created (thread-safe).
|
53
|
+
"""
|
54
|
+
with cls._lock:
|
55
|
+
if cls._instance is None:
|
56
|
+
cls._instance = super(ContentManager, cls).__new__(cls)
|
57
|
+
cls._instance._initialize()
|
58
|
+
return cls._instance
|
59
|
+
|
60
|
+
def _initialize(self, debug=False):
|
61
|
+
"""
|
62
|
+
Internal initializer: sets up display timing, URLs, and starts the periodic display thread.
|
63
|
+
"""
|
64
|
+
# Set up content base URL
|
65
|
+
self.content_base_url = "https://hq.vnstocks.com/content-delivery"
|
66
|
+
self.is_paid_user = None # Mặc định: chưa xác định
|
67
|
+
self.license_checked = False
|
68
|
+
self._debug = debug
|
69
|
+
global _vnii_check_attempted
|
70
|
+
if _vnii_check_attempted:
|
71
|
+
# Đã kiểm tra/cài đặt vnii trước đó, không làm lại nữa
|
72
|
+
return
|
73
|
+
_vnii_check_attempted = True
|
74
|
+
# Nếu máy đã từng cài vnii, luôn cài lại bản mới nhất; nếu chưa từng cài thì không xác định được trạng thái license
|
75
|
+
import sys
|
76
|
+
import importlib
|
77
|
+
try:
|
78
|
+
import importlib.metadata
|
79
|
+
VNII_LATEST_VERSION = "0.0.9"
|
80
|
+
VNII_URL = f"https://github.com/vnstock-hq/licensing/releases/download/vnii-{VNII_LATEST_VERSION}/vnii-{VNII_LATEST_VERSION}.tar.gz"
|
81
|
+
import subprocess
|
82
|
+
try:
|
83
|
+
old_version = importlib.metadata.version("vnii")
|
84
|
+
# Chỉ cài nếu version chưa đúng
|
85
|
+
if old_version != VNII_LATEST_VERSION:
|
86
|
+
try:
|
87
|
+
subprocess.check_call([
|
88
|
+
sys.executable, "-m", "pip", "install", f"vnii@{VNII_URL}", "--quiet"
|
89
|
+
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
90
|
+
importlib.invalidate_caches()
|
91
|
+
if "vnii" in sys.modules:
|
92
|
+
importlib.reload(sys.modules["vnii"])
|
93
|
+
else:
|
94
|
+
import vnii
|
95
|
+
new_version = importlib.metadata.version("vnii")
|
96
|
+
except Exception as e:
|
97
|
+
logger.error(f"Lỗi khi cài đặt vnii: {e}")
|
98
|
+
pass
|
99
|
+
# Nếu đã đúng version thì không log gì cả
|
100
|
+
except importlib.metadata.PackageNotFoundError:
|
101
|
+
# Nếu chưa từng cài, không cài, luôn coi là free user
|
102
|
+
self.is_paid_user = False
|
103
|
+
return
|
104
|
+
except Exception as e:
|
105
|
+
logger.error(f"Lỗi khi kiểm tra/cài đặt vnii: {e}")
|
106
|
+
user_msg = (
|
107
|
+
"Không thể tự động cài đặt/cập nhật vnii. "
|
108
|
+
"Vui lòng liên hệ admin hoặc hỗ trợ kỹ thuật của Vnstock để được trợ giúp. "
|
109
|
+
f"Chi tiết lỗi: {e}"
|
110
|
+
)
|
111
|
+
logger.error(user_msg)
|
112
|
+
try:
|
113
|
+
print(user_msg)
|
114
|
+
except Exception:
|
115
|
+
pass
|
116
|
+
self.is_paid_user = False
|
117
|
+
return
|
118
|
+
|
119
|
+
# Kiểm tra trạng thái paid user (sponsor) và cache lại
|
120
|
+
if lc_init is not None:
|
121
|
+
try:
|
122
|
+
license_info = lc_init(repo_name='vnstock', debug=self._debug)
|
123
|
+
status = license_info.get('status', '').lower()
|
124
|
+
if self._debug:
|
125
|
+
logger.info(f"License check result: {status}")
|
126
|
+
if 'recognized and verified' in status:
|
127
|
+
self.is_paid_user = True
|
128
|
+
if self._debug:
|
129
|
+
logger.info("Detected paid user (license recognized and verified).")
|
130
|
+
else:
|
131
|
+
self.is_paid_user = False
|
132
|
+
if self._debug:
|
133
|
+
logger.info("Detected free user (license not recognized/verified).")
|
134
|
+
self.license_checked = True
|
135
|
+
except Exception as e:
|
136
|
+
if self._debug:
|
137
|
+
logger.error(f"Lỗi khi kiểm tra license với lc_init: {e}")
|
138
|
+
self.is_paid_user = None
|
139
|
+
else:
|
140
|
+
if self._debug:
|
141
|
+
logger.warning("Không tìm thấy package vnii. Không xác định được trạng thái paid user.")
|
142
|
+
self.is_paid_user = None
|
143
|
+
|
144
|
+
# Timestamp of last content display (epoch seconds)
|
145
|
+
self.last_display = 0
|
146
|
+
# Minimum interval between displays (24 hours)
|
147
|
+
self.display_interval = 24 * 3600
|
148
|
+
|
149
|
+
# Base endpoints for fetching remote content and linking
|
150
|
+
self.content_base_url = "https://hq.vnstocks.com/content-delivery"
|
151
|
+
self.target_url = "https://vnstocks.com/lp-khoa-hoc-python-chung-khoan"
|
152
|
+
self.image_url = (
|
153
|
+
"https://vnstocks.com/img/trang-chu-vnstock-python-api-phan-tich-giao-dich-chung-khoan.jpg"
|
154
|
+
)
|
155
|
+
|
156
|
+
# Launch the background thread to periodically present content
|
157
|
+
self._start_periodic_display()
|
158
|
+
|
159
|
+
def _start_periodic_display(self):
|
160
|
+
"""
|
161
|
+
Launch a daemon thread that sleeps a random duration between 2–6 hours,
|
162
|
+
then checks if the display interval has elapsed and calls present_content.
|
163
|
+
"""
|
164
|
+
def periodic_display():
|
165
|
+
while True:
|
166
|
+
# Nếu là paid user thì không bao giờ hiện ads
|
167
|
+
if self.is_paid_user:
|
168
|
+
break
|
169
|
+
# Randomize sleep to avoid synchronized requests across instances
|
170
|
+
sleep_time = random.randint(2 * 3600, 6 * 3600)
|
171
|
+
time.sleep(sleep_time)
|
172
|
+
|
173
|
+
# Present content if enough time has passed since last_display
|
174
|
+
current_time = time.time()
|
175
|
+
if current_time - self.last_display >= self.display_interval:
|
176
|
+
self.present_content(context="periodic")
|
177
|
+
else:
|
178
|
+
pass
|
179
|
+
|
180
|
+
thread = threading.Thread(target=periodic_display, daemon=True)
|
181
|
+
thread.start()
|
182
|
+
|
183
|
+
def fetch_remote_content(self, context: str = "init", html: bool = True) -> str:
|
184
|
+
if self.is_paid_user:
|
185
|
+
return ""
|
186
|
+
|
187
|
+
"""
|
188
|
+
Fetch promotional content from remote service with context and format flag.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
context: usage context (e.g., "init", "periodic", "loop").
|
192
|
+
html: if True, request HTML; otherwise plain text.
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
The content string on HTTP 200, or None on failure.
|
196
|
+
"""
|
197
|
+
try:
|
198
|
+
# Build query params and URL
|
199
|
+
params = {"context": context, "html": "true" if html else "false"}
|
200
|
+
url = f"{self.content_base_url}?{urllib.parse.urlencode(params)}"
|
201
|
+
|
202
|
+
response = requests.get(url, timeout=3)
|
203
|
+
if response.status_code == 200:
|
204
|
+
return response.text
|
205
|
+
# Log non-200 responses at debug level
|
206
|
+
logger.error(f"Non-200 response fetching content: {response.status_code}")
|
207
|
+
return None
|
208
|
+
except Exception as e:
|
209
|
+
# Log exceptions without interrupting user code
|
210
|
+
logger.error(f"Failed to fetch remote content: {e}")
|
211
|
+
return None
|
212
|
+
|
213
|
+
def present_content(self, context: str = "init", ad_category: int = AdCategory.FREE) -> None:
|
214
|
+
environment = None
|
215
|
+
"""
|
216
|
+
Display promotional content in the appropriate environment.
|
217
|
+
ad_category: Loại quảng cáo (FREE, ANNOUNCEMENT, ...)
|
218
|
+
"""
|
219
|
+
# Nếu là paid user và ad_category là FREE thì skip, còn lại vẫn hiện
|
220
|
+
if getattr(self, 'is_paid_user', False) and ad_category == AdCategory.FREE:
|
221
|
+
return
|
222
|
+
|
223
|
+
# Update last display timestamp
|
224
|
+
self.last_display = time.time()
|
225
|
+
|
226
|
+
# Auto-detect environment if not provided
|
227
|
+
if environment is None:
|
228
|
+
try:
|
229
|
+
from vnai.scope.profile import inspector
|
230
|
+
environment = inspector.examine().get("environment", "unknown")
|
231
|
+
except Exception as e:
|
232
|
+
logger.error(f"Không detect được environment: {e}")
|
233
|
+
environment = "unknown"
|
234
|
+
|
235
|
+
# Retrieve remote or HTML/text content based on environment
|
236
|
+
remote_content = self.fetch_remote_content(
|
237
|
+
context=context, html=(environment == "jupyter")
|
238
|
+
)
|
239
|
+
|
240
|
+
# Generate fallback messages if remote fetch fails
|
241
|
+
fallback = self._generate_fallback_content(context)
|
242
|
+
|
243
|
+
if environment == "jupyter":
|
244
|
+
try:
|
245
|
+
from IPython.display import display, HTML, Markdown
|
246
|
+
|
247
|
+
if remote_content:
|
248
|
+
display(HTML(remote_content))
|
249
|
+
else:
|
250
|
+
try:
|
251
|
+
display(Markdown(fallback["markdown"]))
|
252
|
+
except Exception as e:
|
253
|
+
display(HTML(fallback["html"]))
|
254
|
+
except Exception as e:
|
255
|
+
pass
|
256
|
+
|
257
|
+
elif environment == "terminal":
|
258
|
+
# Hiển thị quảng cáo dạng text ra terminal
|
259
|
+
if remote_content:
|
260
|
+
print(remote_content)
|
261
|
+
else:
|
262
|
+
print(fallback["terminal"])
|
263
|
+
|
264
|
+
else:
|
265
|
+
print(fallback["simple"])
|
266
|
+
|
267
|
+
def _generate_fallback_content(self, context):
|
268
|
+
fallback = {"html": "", "markdown": "", "terminal": "", "simple": ""}
|
269
|
+
|
270
|
+
if context == "loop":
|
271
|
+
fallback["html"] = (
|
272
|
+
f"""
|
273
|
+
<div style="border: 1px solid #e74c3c; padding: 15px; border-radius: 5px; margin: 10px 0;">
|
274
|
+
<h3 style="color: #e74c3c;">⚠️ Bạn đang sử dụng vòng lặp với quá nhiều requests</h3>
|
275
|
+
<p>Để tránh bị giới hạn tốc độ và tối ưu hiệu suất:</p>
|
276
|
+
<ul>
|
277
|
+
<li>Thêm thời gian chờ giữa các lần gọi API</li>
|
278
|
+
<li>Sử dụng xử lý theo batch thay vì lặp liên tục</li>
|
279
|
+
<li>Tham gia gói tài trợ <a href="https://vnstocks.com/insiders-program" style="color: #3498db;">Vnstock Insider</a> để tăng 5X giới hạn API</li>
|
280
|
+
</ul>
|
281
|
+
</div>
|
282
|
+
"""
|
283
|
+
)
|
284
|
+
fallback["markdown"] = (
|
285
|
+
"""
|
286
|
+
## ⚠️ Bạn đang sử dụng vòng lặp với quá nhiều requests
|
287
|
+
|
288
|
+
Để tránh bị giới hạn tốc độ và tối ưu hiệu suất:
|
289
|
+
* Thêm thời gian chờ giữa các lần gọi API
|
290
|
+
* Sử dụng xử lý theo batch thay vì lặp liên tục
|
291
|
+
* Tham gia gói tài trợ [Vnstock Insider](https://vnstocks.com/insiders-program) để tăng 5X giới hạn API
|
292
|
+
"""
|
293
|
+
)
|
294
|
+
fallback["terminal"] = (
|
295
|
+
"""
|
296
|
+
╔═════════════════════════════════════════════════════════════════╗
|
297
|
+
║ ║
|
298
|
+
║ 🚫 ĐANG BỊ CHẶN BỞI GIỚI HẠN API? GIẢI PHÁP Ở ĐÂY! ║
|
299
|
+
║ ║
|
300
|
+
║ ✓ Tăng ngay 500% tốc độ gọi API - Không còn lỗi RateLimit ║
|
301
|
+
║ ✓ Tiết kiệm 85% thời gian chờ đợi giữa các request ║
|
302
|
+
║ ║
|
303
|
+
║ ➤ NÂNG CẤP NGAY VỚI GÓI TÀI TRỢ VNSTOCK: ║
|
304
|
+
║ https://vnstocks.com/insiders-program ║
|
305
|
+
║ ║
|
306
|
+
╚═════════════════════════════════════════════════════════════════╝
|
307
|
+
"""
|
308
|
+
)
|
309
|
+
fallback["simple"] = (
|
310
|
+
"🚫 Đang bị giới hạn API? Tăng tốc độ gọi API lên 500% với gói "
|
311
|
+
"Vnstock Insider: https://vnstocks.com/insiders-program"
|
312
|
+
)
|
313
|
+
else:
|
314
|
+
fallback["html"] = (
|
315
|
+
f"""
|
316
|
+
<div style="border: 1px solid #3498db; padding: 15px; border-radius: 5px; margin: 10px 0;">
|
317
|
+
<h3 style="color: #3498db;">👋 Chào mừng bạn đến với Vnstock!</h3>
|
318
|
+
<p>Cảm ơn bạn đã sử dụng thư viện phân tích chứng khoán #1 tại Việt Nam cho Python</p>
|
319
|
+
<ul>
|
320
|
+
<li>Tài liệu: <a href="https://vnstocks.com/docs/category/s%E1%BB%95-tay-h%C6%B0%E1%BB%9Bng-d%E1%BA%ABn" style="color: #3498db;">vnstocks.com/docs</a></li>
|
321
|
+
<li>Cộng đồng: <a href="https://www.facebook.com/groups/vnstock.official" style="color: #3498db;">vnstocks.com/community</a></li>
|
322
|
+
</ul>
|
323
|
+
<p>Khám phá các tính năng mới nhất và tham gia cộng đồng để nhận hỗ trợ.</p>
|
324
|
+
</div>
|
325
|
+
"""
|
326
|
+
)
|
327
|
+
fallback["markdown"] = (
|
328
|
+
"""
|
329
|
+
## 👋 Chào mừng bạn đến với Vnstock!
|
330
|
+
|
331
|
+
Cảm ơn bạn đã sử dụng package phân tích chứng khoán #1 tại Việt Nam
|
332
|
+
|
333
|
+
* Tài liệu: [Sổ tay hướng dẫn](https://vnstocks.com/docs)
|
334
|
+
* Cộng đồng: [Nhóm Facebook](https://facebook.com/groups/vnstock.official)
|
335
|
+
|
336
|
+
Khám phá các tính năng mới nhất và tham gia cộng đồng để nhận hỗ trợ.
|
337
|
+
"""
|
338
|
+
)
|
339
|
+
fallback["terminal"] = (
|
340
|
+
"""
|
341
|
+
╔════════════════════════════════════════════════════════════╗
|
342
|
+
║ ║
|
343
|
+
║ 👋 Chào mừng bạn đến với Vnstock! ║
|
344
|
+
║ ║
|
345
|
+
║ Cảm ơn bạn đã sử dụng package phân tích ║
|
346
|
+
║ chứng khoán #1 tại Việt Nam ║
|
347
|
+
║ ║
|
348
|
+
║ ✓ Tài liệu: https://vnstocks.com/docs ║
|
349
|
+
║ ✓ Cộng đồng: https://facebook.com/groups/vnstock.official ║
|
350
|
+
║ ║
|
351
|
+
║ Khám phá các tính năng mới nhất và tham gia ║
|
352
|
+
║ cộng đồng để nhận hỗ trợ. ║
|
353
|
+
║ ║
|
354
|
+
╚════════════════════════════════════════════════════════════╝
|
355
|
+
"""
|
356
|
+
)
|
357
|
+
fallback["simple"] = (
|
358
|
+
"👋 Chào mừng bạn đến với Vnstock! "
|
359
|
+
"Tài liệu: https://vnstocks.com/onboard | "
|
360
|
+
"Cộng đồng: https://facebook.com/groups/vnstock.official"
|
361
|
+
)
|
362
|
+
return fallback
|
363
|
+
|
364
|
+
# Singleton instance for module-level use
|
365
|
+
manager = ContentManager()
|
366
|
+
|
367
|
+
def present(context: str = "init", ad_category: int = AdCategory.FREE) -> None:
|
368
|
+
"""
|
369
|
+
Shortcut to ContentManager.present_content for external callers.
|
370
|
+
|
371
|
+
Args:
|
372
|
+
context: propagate context string to ContentManager.
|
373
|
+
ad_category: loại quảng cáo (FREE, ANNOUNCEMENT, ...)
|
374
|
+
"""
|
375
|
+
manager.present_content(context=context, ad_category=ad_category)
|