vnai 2.0.3__py3-none-any.whl → 2.0.5__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 +56 -81
- vnai/beam/__init__.py +3 -0
- vnai/beam/metrics.py +47 -67
- vnai/beam/pulse.py +21 -36
- vnai/beam/quota.py +112 -137
- vnai/flow/__init__.py +2 -4
- vnai/flow/queue.py +20 -31
- vnai/flow/relay.py +100 -111
- vnai/scope/__init__.py +2 -4
- vnai/scope/profile.py +110 -206
- vnai/scope/promo.py +222 -51
- vnai/scope/state.py +38 -64
- {vnai-2.0.3.dist-info → vnai-2.0.5.dist-info}/METADATA +4 -17
- vnai-2.0.5.dist-info/RECORD +16 -0
- {vnai-2.0.3.dist-info → vnai-2.0.5.dist-info}/WHEEL +1 -1
- vnai-2.0.3.dist-info/RECORD +0 -16
- {vnai-2.0.3.dist-info → vnai-2.0.5.dist-info}/top_level.txt +0 -0
vnai/scope/promo.py
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
"""
|
2
|
+
Promo module for vnai: fetches and presents promotional content periodically or on demand,
|
3
|
+
using logging instead of printing to avoid polluting stdout for AI tools.
|
4
|
+
"""
|
1
5
|
import logging
|
2
6
|
import requests
|
3
7
|
from datetime import datetime
|
@@ -6,18 +10,49 @@ import threading
|
|
6
10
|
import time
|
7
11
|
import urllib.parse
|
8
12
|
|
13
|
+
# Enum AdCategory (tương thích với vnii)
|
14
|
+
class AdCategory:
|
15
|
+
FREE = 0
|
16
|
+
MANDATORY = 1
|
17
|
+
ANNOUNCEMENT = 2
|
18
|
+
REFERRAL = 3
|
19
|
+
FEATURE = 4
|
20
|
+
GUIDE = 5
|
21
|
+
SURVEY = 6
|
22
|
+
PROMOTION = 7
|
23
|
+
SECURITY = 8
|
24
|
+
MAINTENANCE = 9
|
25
|
+
WARNING = 10
|
26
|
+
|
27
|
+
# Thêm import kiểm tra license từ vnii
|
28
|
+
try:
|
29
|
+
from vnii import lc_init
|
30
|
+
except ImportError:
|
31
|
+
lc_init = None # Nếu không có vnii, luôn coi là free user
|
32
|
+
|
33
|
+
# Module-level logger setup
|
9
34
|
logger = logging.getLogger(__name__)
|
10
35
|
if not logger.hasHandlers():
|
36
|
+
# Add a simple stream handler that only outputs the message text
|
11
37
|
handler = logging.StreamHandler()
|
12
38
|
handler.setFormatter(logging.Formatter('%(message)s'))
|
13
39
|
logger.addHandler(handler)
|
14
40
|
logger.setLevel(logging.INFO)
|
15
41
|
|
16
42
|
class ContentManager:
|
43
|
+
"""
|
44
|
+
Singleton manager to fetch remote or fallback promotional content and
|
45
|
+
present it in different environments (Jupyter, terminal, other).
|
46
|
+
|
47
|
+
Displays content automatically at randomized intervals via a background thread.
|
48
|
+
"""
|
17
49
|
_instance = None
|
18
50
|
_lock = threading.Lock()
|
19
51
|
|
20
52
|
def __new__(cls):
|
53
|
+
"""
|
54
|
+
Ensure only one instance of ContentManager is created (thread-safe).
|
55
|
+
"""
|
21
56
|
with cls._lock:
|
22
57
|
if cls._instance is None:
|
23
58
|
cls._instance = super(ContentManager, cls).__new__(cls)
|
@@ -25,94 +60,218 @@ class ContentManager:
|
|
25
60
|
return cls._instance
|
26
61
|
|
27
62
|
def _initialize(self):
|
63
|
+
"""
|
64
|
+
Internal initializer: sets up display timing, URLs, and starts the periodic display thread.
|
65
|
+
"""
|
66
|
+
global _vnii_check_attempted
|
67
|
+
if _vnii_check_attempted:
|
68
|
+
# Đã kiểm tra/cài đặt vnii trước đó, không làm lại nữa
|
69
|
+
return
|
70
|
+
_vnii_check_attempted = True
|
71
|
+
# 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ì coi là free user
|
72
|
+
import sys
|
73
|
+
import importlib
|
74
|
+
try:
|
75
|
+
import importlib.metadata
|
76
|
+
try:
|
77
|
+
old_version = importlib.metadata.version("vnii")
|
78
|
+
# Nếu đã từng cài, luôn force cài bản mới nhất
|
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
|
+
logger.debug(f"Đã phát hiện vnii version {old_version}. Đang cập nhật lên bản mới nhất...")
|
82
|
+
import subprocess
|
83
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", f"vnii@{VNII_URL}"])
|
84
|
+
importlib.invalidate_caches()
|
85
|
+
if "vnii" in sys.modules:
|
86
|
+
importlib.reload(sys.modules["vnii"])
|
87
|
+
else:
|
88
|
+
import vnii
|
89
|
+
new_version = importlib.metadata.version("vnii")
|
90
|
+
logger.debug(f"Đã cập nhật vnii lên version {new_version}")
|
91
|
+
except importlib.metadata.PackageNotFoundError:
|
92
|
+
# Nếu chưa từng cài, không cài, luôn coi là free user
|
93
|
+
logger.debug("Không phát hiện vnii trên hệ thống. Luôn coi là free user, không kiểm tra license.")
|
94
|
+
self.is_paid_user = False
|
95
|
+
return
|
96
|
+
except Exception as e:
|
97
|
+
logger.warning(f"Lỗi khi kiểm tra/cài đặt vnii: {e}")
|
98
|
+
user_msg = (
|
99
|
+
"Không thể tự động cài đặt/cập nhật vnii. "
|
100
|
+
"Vui lòng liên hệ admin hoặc hỗ trợ kỹ thuật của Vnstock để được trợ giúp. "
|
101
|
+
f"Chi tiết lỗi: {e}"
|
102
|
+
)
|
103
|
+
logger.error(user_msg)
|
104
|
+
try:
|
105
|
+
print(user_msg)
|
106
|
+
except Exception:
|
107
|
+
pass
|
108
|
+
self.is_paid_user = False
|
109
|
+
return
|
110
|
+
|
111
|
+
# Kiểm tra trạng thái paid user (sponsor) và cache lại
|
112
|
+
self.is_paid_user = False
|
113
|
+
logger.debug("[promo] Bắt đầu kiểm tra trạng thái paid user với vnii...")
|
114
|
+
if lc_init is not None:
|
115
|
+
try:
|
116
|
+
license_info = lc_init(repo_name='vnstock')
|
117
|
+
logger.debug(f"[promo] license_info trả về: {license_info}")
|
118
|
+
status = license_info.get('status', '').lower()
|
119
|
+
if 'recognized and verified' in status:
|
120
|
+
self.is_paid_user = True
|
121
|
+
logger.debug("[promo] Đã xác nhận paid user từ vnii. Sẽ không hiện quảng cáo.")
|
122
|
+
else:
|
123
|
+
logger.debug(f"[promo] Không xác nhận được paid user từ vnii. Status: {status}")
|
124
|
+
except Exception as e:
|
125
|
+
logger.warning(f"[promo] Không thể kiểm tra trạng thái sponsor: {e}. Sẽ coi là free user và hiện quảng cáo.")
|
126
|
+
else:
|
127
|
+
logger.debug("[promo] Không tìm thấy module vnii. Luôn coi là free user và hiện quảng cáo.")
|
128
|
+
|
129
|
+
# Timestamp of last content display (epoch seconds)
|
28
130
|
self.last_display = 0
|
131
|
+
# Minimum interval between displays (24 hours)
|
29
132
|
self.display_interval = 24 * 3600
|
30
133
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
self.target_url = (
|
35
|
-
"https://vnstocks.com/lp-khoa-hoc-python-chung-khoan"
|
36
|
-
)
|
134
|
+
# Base endpoints for fetching remote content and linking
|
135
|
+
self.content_base_url = "https://hq.vnstocks.com/static"
|
136
|
+
self.target_url = "https://vnstocks.com/lp-khoa-hoc-python-chung-khoan"
|
37
137
|
self.image_url = (
|
38
138
|
"https://vnstocks.com/img/trang-chu-vnstock-python-api-phan-tich-giao-dich-chung-khoan.jpg"
|
39
139
|
)
|
40
140
|
|
141
|
+
# Launch the background thread to periodically present content
|
142
|
+
logger.debug(f"[promo] is_paid_user = {self.is_paid_user}")
|
41
143
|
self._start_periodic_display()
|
42
144
|
|
43
145
|
def _start_periodic_display(self):
|
146
|
+
"""
|
147
|
+
Launch a daemon thread that sleeps a random duration between 2–6 hours,
|
148
|
+
then checks if the display interval has elapsed and calls present_content.
|
149
|
+
"""
|
150
|
+
logger.debug("[promo] Khởi tạo thread hiển thị quảng cáo định kỳ...")
|
44
151
|
def periodic_display():
|
152
|
+
logger.debug("[promo] Thread quảng cáo bắt đầu chạy.")
|
45
153
|
while True:
|
154
|
+
# Nếu là paid user thì không bao giờ hiện ads
|
155
|
+
if self.is_paid_user:
|
156
|
+
logger.debug("[promo] Đang là paid user trong thread. Không hiện quảng cáo, dừng thread.")
|
157
|
+
break
|
158
|
+
# Randomize sleep to avoid synchronized requests across instances
|
46
159
|
sleep_time = random.randint(2 * 3600, 6 * 3600)
|
160
|
+
logger.debug(f"[promo] Thread quảng cáo sẽ ngủ {sleep_time//3600} giờ...")
|
47
161
|
time.sleep(sleep_time)
|
48
162
|
|
163
|
+
# Present content if enough time has passed since last_display
|
49
164
|
current_time = time.time()
|
165
|
+
logger.debug(f"[promo] Kiểm tra điều kiện hiện quảng cáo: time since last_display = {current_time - self.last_display}s")
|
50
166
|
if current_time - self.last_display >= self.display_interval:
|
167
|
+
logger.debug("[promo] Đã đủ thời gian, sẽ gọi present_content(context='periodic')")
|
51
168
|
self.present_content(context="periodic")
|
169
|
+
else:
|
170
|
+
logger.debug("[promo] Chưa đủ thời gian, chưa hiện quảng cáo.")
|
52
171
|
|
53
172
|
thread = threading.Thread(target=periodic_display, daemon=True)
|
54
173
|
thread.start()
|
55
174
|
|
56
|
-
def fetch_remote_content(self, context="init", html=True):
|
175
|
+
def fetch_remote_content(self, context: str = "init", html: bool = True) -> str:
|
176
|
+
if self.is_paid_user:
|
177
|
+
logger.debug("Paid user detected. Skip fetching remote content (ads).")
|
178
|
+
return ""
|
179
|
+
|
180
|
+
"""
|
181
|
+
Fetch promotional content from remote service with context and format flag.
|
182
|
+
|
183
|
+
Args:
|
184
|
+
context: usage context (e.g., "init", "periodic", "loop").
|
185
|
+
html: if True, request HTML; otherwise plain text.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
The content string on HTTP 200, or None on failure.
|
189
|
+
"""
|
57
190
|
try:
|
191
|
+
# Build query params and URL
|
58
192
|
params = {"context": context, "html": "true" if html else "false"}
|
59
193
|
url = f"{self.content_base_url}?{urllib.parse.urlencode(params)}"
|
60
194
|
|
61
195
|
response = requests.get(url, timeout=3)
|
62
196
|
if response.status_code == 200:
|
63
197
|
return response.text
|
198
|
+
# Log non-200 responses at debug level
|
64
199
|
logger.debug(f"Non-200 response fetching content: {response.status_code}")
|
65
200
|
return None
|
66
201
|
except Exception as e:
|
202
|
+
# Log exceptions without interrupting user code
|
67
203
|
logger.debug(f"Failed to fetch remote content: {e}")
|
68
204
|
return None
|
69
205
|
|
70
|
-
def present_content(self,
|
206
|
+
def present_content(self, context: str = "init", ad_category: int = AdCategory.FREE) -> None:
|
207
|
+
environment = None
|
208
|
+
logger.debug(f"[promo] Gọi present_content(context={context}, ad_category={ad_category}). is_paid_user = {getattr(self, 'is_paid_user', None)}")
|
209
|
+
"""
|
210
|
+
Display promotional content in the appropriate environment.
|
211
|
+
ad_category: Loại quảng cáo (FREE, ANNOUNCEMENT, ...)
|
212
|
+
"""
|
213
|
+
# Nếu là paid user và ad_category là FREE thì skip, còn lại vẫn hiện
|
214
|
+
if getattr(self, 'is_paid_user', False) and ad_category == AdCategory.FREE:
|
215
|
+
logger.debug("[promo] Đang là paid user và ad_category là FREE. Không hiện quảng cáo.")
|
216
|
+
return
|
217
|
+
|
218
|
+
# Chỉ hiện log này nếu debug mode
|
219
|
+
if logger.level <= logging.DEBUG:
|
220
|
+
logger.debug(f"[promo] Sẽ hiển thị quảng cáo với context={context}, ad_category={ad_category}")
|
221
|
+
# Update last display timestamp
|
71
222
|
self.last_display = time.time()
|
72
223
|
|
224
|
+
# Auto-detect environment if not provided
|
73
225
|
if environment is None:
|
74
226
|
try:
|
75
227
|
from vnai.scope.profile import inspector
|
76
|
-
|
77
|
-
environment
|
78
|
-
|
79
|
-
)
|
80
|
-
except Exception:
|
228
|
+
environment = inspector.examine().get("environment", "unknown")
|
229
|
+
logger.debug(f"[promo] Đã detect environment: {environment}")
|
230
|
+
except Exception as e:
|
231
|
+
logger.debug(f"[promo] Không detect được environment: {e}")
|
81
232
|
environment = "unknown"
|
82
233
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
fallback_content = self._generate_fallback_content(context)
|
234
|
+
# Retrieve remote or HTML/text content based on environment
|
235
|
+
remote_content = self.fetch_remote_content(
|
236
|
+
context=context, html=(environment == "jupyter")
|
237
|
+
)
|
238
|
+
logger.debug(f"[promo] remote_content = {bool(remote_content)} (None -> False, có nội dung -> True)")
|
239
|
+
# Generate fallback messages if remote fetch fails
|
240
|
+
fallback = self._generate_fallback_content(context)
|
241
|
+
logger.debug(f"[promo] fallback keys: {list(fallback.keys())}")
|
93
242
|
|
94
243
|
if environment == "jupyter":
|
244
|
+
logger.debug("[promo] Đang ở môi trường Jupyter, sẽ thử display HTML/Markdown.")
|
95
245
|
try:
|
96
246
|
from IPython.display import display, HTML, Markdown
|
97
247
|
|
98
248
|
if remote_content:
|
249
|
+
logger.debug("[promo] Hiển thị quảng cáo bằng HTML từ remote_content.")
|
99
250
|
display(HTML(remote_content))
|
100
251
|
else:
|
252
|
+
logger.debug("[promo] Không có remote_content, thử display fallback Markdown/HTML.")
|
101
253
|
try:
|
102
|
-
display(Markdown(
|
103
|
-
except Exception:
|
104
|
-
display
|
254
|
+
display(Markdown(fallback["markdown"]))
|
255
|
+
except Exception as e:
|
256
|
+
logger.debug(f"[promo] Lỗi khi display Markdown: {e}, fallback HTML.")
|
257
|
+
display(HTML(fallback["html"]))
|
105
258
|
except Exception as e:
|
106
|
-
logger.debug(f"Jupyter display failed: {e}")
|
259
|
+
logger.debug(f"[promo] Jupyter display failed: {e}")
|
107
260
|
|
108
261
|
elif environment == "terminal":
|
262
|
+
logger.debug("[promo] Đang ở môi trường terminal, sẽ log quảng cáo ra logger.")
|
263
|
+
# Log terminal-friendly or raw content via logger
|
109
264
|
if remote_content:
|
110
|
-
logger.
|
265
|
+
logger.debug("[promo] Hiển thị quảng cáo bằng remote_content cho terminal.")
|
266
|
+
logger.debug(remote_content)
|
111
267
|
else:
|
112
|
-
logger.
|
268
|
+
logger.debug("[promo] Không có remote_content, hiển thị fallback terminal.")
|
269
|
+
logger.debug(fallback["terminal"])
|
113
270
|
|
114
271
|
else:
|
115
|
-
logger.
|
272
|
+
logger.debug(f"[promo] Môi trường khác ({environment}), hiển thị fallback simple.")
|
273
|
+
# Generic simple message for other environments
|
274
|
+
logger.debug(fallback["simple"])
|
116
275
|
|
117
276
|
def _generate_fallback_content(self, context):
|
118
277
|
fallback = {"html": "", "markdown": "", "terminal": "", "simple": ""}
|
@@ -180,39 +339,51 @@ class ContentManager:
|
|
180
339
|
|
181
340
|
Cảm ơn bạn đã sử dụng package phân tích chứng khoán #1 tại Việt Nam
|
182
341
|
|
183
|
-
* Tài liệu: [Sổ tay hướng dẫn](https://vnstocks.com/docs
|
184
|
-
* Cộng đồng: [Nhóm Facebook](https://
|
342
|
+
* Tài liệu: [Sổ tay hướng dẫn](https://vnstocks.com/docs)
|
343
|
+
* Cộng đồng: [Nhóm Facebook](https://facebook.com/groups/vnstock.official)
|
185
344
|
|
186
345
|
Khám phá các tính năng mới nhất và tham gia cộng đồng để nhận hỗ trợ.
|
187
346
|
"""
|
188
347
|
)
|
189
348
|
fallback["terminal"] = (
|
190
349
|
"""
|
191
|
-
|
192
|
-
║
|
193
|
-
║ 👋 Chào mừng bạn đến với Vnstock!
|
194
|
-
║
|
195
|
-
║ Cảm ơn bạn đã sử dụng package phân tích
|
196
|
-
║ chứng khoán #1 tại Việt Nam
|
197
|
-
║
|
198
|
-
║ ✓ Tài liệu: https://vnstocks.com/docs
|
199
|
-
║ ✓ Cộng đồng: https://
|
200
|
-
║
|
201
|
-
║ Khám phá các tính năng mới nhất và tham gia
|
202
|
-
║ cộng đồng để nhận hỗ trợ.
|
203
|
-
║
|
204
|
-
|
350
|
+
╔════════════════════════════════════════════════════════════╗
|
351
|
+
║ ║
|
352
|
+
║ 👋 Chào mừng bạn đến với Vnstock! ║
|
353
|
+
║ ║
|
354
|
+
║ Cảm ơn bạn đã sử dụng package phân tích ║
|
355
|
+
║ chứng khoán #1 tại Việt Nam ║
|
356
|
+
║ ║
|
357
|
+
║ ✓ Tài liệu: https://vnstocks.com/docs ║
|
358
|
+
║ ✓ Cộng đồng: https://facebook.com/groups/vnstock.official ║
|
359
|
+
║ ║
|
360
|
+
║ Khám phá các tính năng mới nhất và tham gia ║
|
361
|
+
║ cộng đồng để nhận hỗ trợ. ║
|
362
|
+
║ ║
|
363
|
+
╚════════════════════════════════════════════════════════════╝
|
205
364
|
"""
|
206
365
|
)
|
207
366
|
fallback["simple"] = (
|
208
367
|
"👋 Chào mừng bạn đến với Vnstock! "
|
209
|
-
"Tài liệu: https://vnstocks.com/
|
210
|
-
"Cộng đồng: https://
|
368
|
+
"Tài liệu: https://vnstocks.com/onboard | "
|
369
|
+
"Cộng đồng: https://facebook.com/groups/vnstock.official"
|
211
370
|
)
|
212
371
|
return fallback
|
213
372
|
|
214
|
-
# Singleton instance
|
373
|
+
# Singleton instance for module-level use
|
215
374
|
manager = ContentManager()
|
216
375
|
|
217
|
-
|
218
|
-
|
376
|
+
# Ép buộc hiện quảng cáo ngay khi import nếu là free user
|
377
|
+
if not getattr(manager, 'is_paid_user', False):
|
378
|
+
manager.present_content(context="auto_import")
|
379
|
+
|
380
|
+
|
381
|
+
def present(context: str = "init", ad_category: int = AdCategory.FREE) -> None:
|
382
|
+
"""
|
383
|
+
Shortcut to ContentManager.present_content for external callers.
|
384
|
+
|
385
|
+
Args:
|
386
|
+
context: propagate context string to ContentManager.
|
387
|
+
ad_category: loại quảng cáo (FREE, ANNOUNCEMENT, ...)
|
388
|
+
"""
|
389
|
+
manager.present_content(context=context, ad_category=ad_category)
|
vnai/scope/state.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
##
|
4
|
-
|
1
|
+
# vnai/scope/state.py
|
2
|
+
# System state tracking
|
5
3
|
|
6
4
|
import time
|
7
5
|
import threading
|
@@ -11,7 +9,7 @@ from datetime import datetime
|
|
11
9
|
from pathlib import Path
|
12
10
|
|
13
11
|
class Tracker:
|
14
|
-
|
12
|
+
"""Tracks system state and performance metrics"""
|
15
13
|
|
16
14
|
_instance = None
|
17
15
|
_lock = threading.Lock()
|
@@ -24,7 +22,7 @@ class Tracker:
|
|
24
22
|
return cls._instance
|
25
23
|
|
26
24
|
def _initialize(self):
|
27
|
-
|
25
|
+
"""Initialize tracker"""
|
28
26
|
self.metrics = {
|
29
27
|
"startup_time": datetime.now().isoformat(),
|
30
28
|
"function_calls": 0,
|
@@ -41,8 +39,7 @@ class Tracker:
|
|
41
39
|
|
42
40
|
self.privacy_level = "standard"
|
43
41
|
|
44
|
-
|
45
|
-
|
42
|
+
# Setup data directory
|
46
43
|
self.home_dir = Path.home()
|
47
44
|
self.project_dir = self.home_dir / ".vnstock"
|
48
45
|
self.project_dir.mkdir(exist_ok=True)
|
@@ -51,31 +48,26 @@ class Tracker:
|
|
51
48
|
self.metrics_path = self.data_dir / "usage_metrics.json"
|
52
49
|
self.privacy_config_path = self.project_dir / 'config' / "privacy.json"
|
53
50
|
|
54
|
-
|
55
|
-
|
51
|
+
# Create config directory if it doesn't exist
|
56
52
|
os.makedirs(os.path.dirname(self.privacy_config_path), exist_ok=True)
|
57
53
|
|
58
|
-
|
59
|
-
|
54
|
+
# Load existing metrics
|
60
55
|
self._load_metrics()
|
61
56
|
|
62
|
-
|
63
|
-
|
57
|
+
# Load privacy settings
|
64
58
|
self._load_privacy_settings()
|
65
59
|
|
66
|
-
|
67
|
-
|
60
|
+
# Start background metrics collector
|
68
61
|
self._start_background_collector()
|
69
62
|
|
70
63
|
def _load_metrics(self):
|
71
|
-
|
64
|
+
"""Load metrics from file"""
|
72
65
|
if self.metrics_path.exists():
|
73
66
|
try:
|
74
67
|
with open(self.metrics_path, 'r') as f:
|
75
68
|
stored_metrics = json.load(f)
|
76
69
|
|
77
|
-
|
78
|
-
|
70
|
+
# Update metrics with stored values
|
79
71
|
for key, value in stored_metrics.items():
|
80
72
|
if key in self.metrics:
|
81
73
|
self.metrics[key] = value
|
@@ -83,7 +75,7 @@ class Tracker:
|
|
83
75
|
pass
|
84
76
|
|
85
77
|
def _save_metrics(self):
|
86
|
-
|
78
|
+
"""Save metrics to file"""
|
87
79
|
try:
|
88
80
|
with open(self.metrics_path, 'w') as f:
|
89
81
|
json.dump(self.metrics, f)
|
@@ -91,7 +83,7 @@ class Tracker:
|
|
91
83
|
pass
|
92
84
|
|
93
85
|
def _load_privacy_settings(self):
|
94
|
-
|
86
|
+
"""Load privacy settings"""
|
95
87
|
if self.privacy_config_path.exists():
|
96
88
|
try:
|
97
89
|
with open(self.privacy_config_path, 'r') as f:
|
@@ -101,7 +93,7 @@ class Tracker:
|
|
101
93
|
pass
|
102
94
|
|
103
95
|
def setup_privacy(self, level=None):
|
104
|
-
|
96
|
+
"""Configure privacy level for data collection"""
|
105
97
|
privacy_levels = {
|
106
98
|
"minimal": "Essential system data only",
|
107
99
|
"standard": "Performance metrics and errors",
|
@@ -109,107 +101,90 @@ class Tracker:
|
|
109
101
|
}
|
110
102
|
|
111
103
|
if level is None:
|
112
|
-
|
113
|
-
|
104
|
+
# Default level
|
114
105
|
level = "standard"
|
115
106
|
|
116
107
|
if level not in privacy_levels:
|
117
108
|
raise ValueError(f"Invalid privacy level: {level}. Choose from {', '.join(privacy_levels.keys())}")
|
118
109
|
|
119
|
-
|
120
|
-
|
110
|
+
# Store preference
|
121
111
|
self.privacy_level = level
|
122
112
|
|
123
|
-
|
124
|
-
|
113
|
+
# Store in configuration file
|
125
114
|
with open(self.privacy_config_path, "w") as f:
|
126
115
|
json.dump({"level": level}, f)
|
127
116
|
|
128
117
|
return level
|
129
118
|
|
130
119
|
def get_privacy_level(self):
|
131
|
-
|
120
|
+
"""Get current privacy level"""
|
132
121
|
return self.privacy_level
|
133
122
|
|
134
123
|
def _start_background_collector(self):
|
135
|
-
|
124
|
+
"""Start background metrics collection"""
|
136
125
|
def collect_metrics():
|
137
126
|
while True:
|
138
127
|
try:
|
139
128
|
import psutil
|
140
129
|
|
141
|
-
|
142
|
-
|
130
|
+
# Update peak memory
|
143
131
|
current_process = psutil.Process()
|
144
132
|
memory_info = current_process.memory_info()
|
145
|
-
memory_usage = memory_info.rss / (1024 * 1024)
|
146
|
-
|
133
|
+
memory_usage = memory_info.rss / (1024 * 1024) # MB
|
147
134
|
|
148
135
|
if memory_usage > self.performance_metrics["peak_memory"]:
|
149
136
|
self.performance_metrics["peak_memory"] = memory_usage
|
150
137
|
|
151
|
-
|
152
|
-
|
138
|
+
# Save metrics periodically
|
153
139
|
self._save_metrics()
|
154
140
|
|
155
141
|
except:
|
156
142
|
pass
|
157
143
|
|
158
|
-
time.sleep(300)
|
159
|
-
|
144
|
+
time.sleep(300) # Run every 5 minutes
|
160
145
|
|
161
|
-
|
162
|
-
|
146
|
+
# Start thread
|
163
147
|
thread = threading.Thread(target=collect_metrics, daemon=True)
|
164
148
|
thread.start()
|
165
149
|
|
166
150
|
def record(self, event_type, data=None):
|
167
|
-
|
168
|
-
|
169
|
-
|
151
|
+
"""Record an event"""
|
152
|
+
# Check privacy level
|
170
153
|
if self.privacy_level == "minimal" and event_type != "errors":
|
171
|
-
|
172
|
-
|
154
|
+
# In minimal mode, only track errors
|
173
155
|
return True
|
174
156
|
|
175
|
-
|
176
|
-
|
157
|
+
# Update counts
|
177
158
|
if event_type in self.metrics:
|
178
159
|
self.metrics[event_type] += 1
|
179
160
|
else:
|
180
161
|
self.metrics[event_type] = 1
|
181
162
|
|
182
|
-
|
183
|
-
|
163
|
+
# Special handling for errors
|
184
164
|
if event_type == "errors":
|
185
165
|
self.performance_metrics["last_error_time"] = datetime.now().isoformat()
|
186
166
|
|
187
|
-
|
188
|
-
|
167
|
+
# Special handling for function calls with timing data
|
189
168
|
if event_type == "function_calls" and data and "execution_time" in data:
|
190
|
-
|
191
|
-
|
169
|
+
# Keep up to 100 latest execution times
|
192
170
|
self.performance_metrics["execution_times"].append(data["execution_time"])
|
193
171
|
if len(self.performance_metrics["execution_times"]) > 100:
|
194
172
|
self.performance_metrics["execution_times"] = self.performance_metrics["execution_times"][-100:]
|
195
173
|
|
196
|
-
|
197
|
-
|
174
|
+
# Save if metrics change significantly
|
198
175
|
if self.metrics["function_calls"] % 100 == 0 or event_type == "errors":
|
199
176
|
self._save_metrics()
|
200
177
|
|
201
178
|
return True
|
202
179
|
|
203
180
|
def get_metrics(self):
|
204
|
-
|
205
|
-
|
206
|
-
|
181
|
+
"""Get current metrics"""
|
182
|
+
# Calculate derived metrics
|
207
183
|
avg_execution_time = 0
|
208
184
|
if self.performance_metrics["execution_times"]:
|
209
185
|
avg_execution_time = sum(self.performance_metrics["execution_times"]) / len(self.performance_metrics["execution_times"])
|
210
186
|
|
211
|
-
|
212
|
-
|
187
|
+
# Add derived metrics to output
|
213
188
|
output = self.metrics.copy()
|
214
189
|
output.update({
|
215
190
|
"avg_execution_time": avg_execution_time,
|
@@ -221,7 +196,7 @@ class Tracker:
|
|
221
196
|
return output
|
222
197
|
|
223
198
|
def reset(self):
|
224
|
-
|
199
|
+
"""Reset metrics"""
|
225
200
|
self.metrics = {
|
226
201
|
"startup_time": datetime.now().isoformat(),
|
227
202
|
"function_calls": 0,
|
@@ -239,11 +214,10 @@ class Tracker:
|
|
239
214
|
self._save_metrics()
|
240
215
|
return True
|
241
216
|
|
242
|
-
|
243
|
-
|
217
|
+
# Create singleton instance
|
244
218
|
tracker = Tracker()
|
245
219
|
|
246
220
|
|
247
221
|
def record(event_type, data=None):
|
248
|
-
|
222
|
+
"""Record an event"""
|
249
223
|
return tracker.record(event_type, data)
|
@@ -1,11 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: vnai
|
3
|
-
Version: 2.0.
|
3
|
+
Version: 2.0.5
|
4
4
|
Summary: System optimization and resource management toolkit
|
5
|
-
|
6
|
-
Author: Your Name
|
7
|
-
Author-email: your.email@example.com
|
5
|
+
Author-email: Vnstock HQ <support@vnstocks.com>
|
8
6
|
License: MIT
|
7
|
+
Project-URL: Homepage, https://vnstocks.com
|
9
8
|
Classifier: Programming Language :: Python :: 3
|
10
9
|
Classifier: Operating System :: OS Independent
|
11
10
|
Classifier: Development Status :: 4 - Beta
|
@@ -17,17 +16,5 @@ Requires-Dist: requests>=2.25.0
|
|
17
16
|
Requires-Dist: psutil>=5.8.0
|
18
17
|
Provides-Extra: dev
|
19
18
|
Requires-Dist: pytest>=6.0.0; extra == "dev"
|
20
|
-
Requires-Dist: black>=21.5b2; extra == "dev"
|
21
|
-
Dynamic: author
|
22
|
-
Dynamic: author-email
|
23
|
-
Dynamic: classifier
|
24
|
-
Dynamic: description
|
25
|
-
Dynamic: description-content-type
|
26
|
-
Dynamic: home-page
|
27
|
-
Dynamic: license
|
28
|
-
Dynamic: provides-extra
|
29
|
-
Dynamic: requires-dist
|
30
|
-
Dynamic: requires-python
|
31
|
-
Dynamic: summary
|
32
19
|
|
33
|
-
|
20
|
+
# VnAI
|