vnai 0.1.3__py3-none-any.whl → 2.3.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vnai/__init__.py +304 -292
- vnai/beam/__init__.py +26 -0
- vnai/beam/auth.py +312 -0
- vnai/beam/fundamental.py +168 -0
- vnai/beam/metrics.py +167 -0
- vnai/beam/patching.py +223 -0
- vnai/beam/pulse.py +79 -0
- vnai/beam/quota.py +403 -0
- vnai/beam/sync.py +87 -0
- vnai/flow/__init__.py +2 -0
- vnai/flow/queue.py +100 -0
- vnai/flow/relay.py +347 -0
- vnai/scope/__init__.py +11 -0
- vnai/scope/device.py +315 -0
- vnai/scope/lc_integration.py +351 -0
- vnai/scope/license.py +197 -0
- vnai/scope/profile.py +599 -0
- vnai/scope/promo.py +389 -0
- vnai/scope/state.py +155 -0
- vnai-2.3.7.dist-info/METADATA +21 -0
- vnai-2.3.7.dist-info/RECORD +23 -0
- {vnai-0.1.3.dist-info → vnai-2.3.7.dist-info}/WHEEL +1 -1
- vnai-0.1.3.dist-info/METADATA +0 -20
- vnai-0.1.3.dist-info/RECORD +0 -5
- {vnai-0.1.3.dist-info → vnai-2.3.7.dist-info}/top_level.txt +0 -0
vnai/beam/quota.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import functools
|
|
3
|
+
import threading
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
class RateLimitExceeded(Exception):
|
|
8
|
+
def __init__(self, resource_type, limit_type="min", current_usage=None, limit_value=None, retry_after=None, tier=None):
|
|
9
|
+
self.resource_type = resource_type
|
|
10
|
+
self.limit_type = limit_type
|
|
11
|
+
self.current_usage = current_usage
|
|
12
|
+
self.limit_value = limit_value
|
|
13
|
+
self.retry_after = retry_after
|
|
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"
|
|
51
|
+
if retry_after:
|
|
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"
|
|
68
|
+
else:
|
|
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
|
|
74
|
+
super().__init__(message)
|
|
75
|
+
|
|
76
|
+
class Guardian:
|
|
77
|
+
_instance = None
|
|
78
|
+
_lock = threading.Lock()
|
|
79
|
+
|
|
80
|
+
def __new__(cls):
|
|
81
|
+
with cls._lock:
|
|
82
|
+
if cls._instance is None:
|
|
83
|
+
cls._instance = super(Guardian, cls).__new__(cls)
|
|
84
|
+
cls._instance._initialize()
|
|
85
|
+
return cls._instance
|
|
86
|
+
|
|
87
|
+
def _initialize(self):
|
|
88
|
+
self.usage_counters = defaultdict(lambda: defaultdict(list))
|
|
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}
|
|
106
|
+
|
|
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):
|
|
115
|
+
current_time = time.time()
|
|
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
|
+
)
|
|
129
|
+
minute_cutoff = current_time - 60
|
|
130
|
+
self.usage_counters[resource_type]["min"] = [
|
|
131
|
+
t for t in self.usage_counters[resource_type]["min"]
|
|
132
|
+
if t > minute_cutoff
|
|
133
|
+
]
|
|
134
|
+
minute_usage = len(self.usage_counters[resource_type]["min"])
|
|
135
|
+
minute_exceeded = minute_usage >= limits["min"]
|
|
136
|
+
if minute_exceeded:
|
|
137
|
+
from vnai.beam.metrics import collector
|
|
138
|
+
collector.record(
|
|
139
|
+
"rate_limit",
|
|
140
|
+
{
|
|
141
|
+
"resource_type": resource_type,
|
|
142
|
+
"limit_type":"min",
|
|
143
|
+
"limit_value": limits["min"],
|
|
144
|
+
"current_usage": minute_usage,
|
|
145
|
+
"is_exceeded": True
|
|
146
|
+
},
|
|
147
|
+
priority="high"
|
|
148
|
+
)
|
|
149
|
+
current_tier = self._get_current_tier()
|
|
150
|
+
raise RateLimitExceeded(
|
|
151
|
+
resource_type=resource_type,
|
|
152
|
+
limit_type="min",
|
|
153
|
+
current_usage=minute_usage,
|
|
154
|
+
limit_value=limits["min"],
|
|
155
|
+
retry_after=60 - (current_time % 60),
|
|
156
|
+
tier=current_tier
|
|
157
|
+
)
|
|
158
|
+
hour_cutoff = current_time - 3600
|
|
159
|
+
self.usage_counters[resource_type]["hour"] = [
|
|
160
|
+
t for t in self.usage_counters[resource_type]["hour"]
|
|
161
|
+
if t > hour_cutoff
|
|
162
|
+
]
|
|
163
|
+
hour_usage = len(self.usage_counters[resource_type]["hour"])
|
|
164
|
+
hour_exceeded = hour_usage >= limits["hour"]
|
|
165
|
+
from vnai.beam.metrics import collector
|
|
166
|
+
collector.record(
|
|
167
|
+
"rate_limit",
|
|
168
|
+
{
|
|
169
|
+
"resource_type": resource_type,
|
|
170
|
+
"limit_type":"hour" if hour_exceeded else"min",
|
|
171
|
+
"limit_value": limits["hour"] if hour_exceeded else limits["min"],
|
|
172
|
+
"current_usage": hour_usage if hour_exceeded else minute_usage,
|
|
173
|
+
"is_exceeded": hour_exceeded
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
if hour_exceeded:
|
|
177
|
+
current_tier = self._get_current_tier()
|
|
178
|
+
raise RateLimitExceeded(
|
|
179
|
+
resource_type=resource_type,
|
|
180
|
+
limit_type="hour",
|
|
181
|
+
current_usage=hour_usage,
|
|
182
|
+
limit_value=limits["hour"],
|
|
183
|
+
retry_after=3600 - (current_time % 3600),
|
|
184
|
+
tier=current_tier
|
|
185
|
+
)
|
|
186
|
+
self.usage_counters[resource_type]["min"].append(current_time)
|
|
187
|
+
self.usage_counters[resource_type]["hour"].append(current_time)
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
def usage(self, resource_type="default"):
|
|
191
|
+
current_time = time.time()
|
|
192
|
+
limits = self._get_tier_limits()
|
|
193
|
+
minute_cutoff = current_time - 60
|
|
194
|
+
hour_cutoff = current_time - 3600
|
|
195
|
+
self.usage_counters[resource_type]["min"] = [
|
|
196
|
+
t for t in self.usage_counters[resource_type]["min"]
|
|
197
|
+
if t > minute_cutoff
|
|
198
|
+
]
|
|
199
|
+
self.usage_counters[resource_type]["hour"] = [
|
|
200
|
+
t for t in self.usage_counters[resource_type]["hour"]
|
|
201
|
+
if t > hour_cutoff
|
|
202
|
+
]
|
|
203
|
+
minute_usage = len(self.usage_counters[resource_type]["min"])
|
|
204
|
+
hour_usage = len(self.usage_counters[resource_type]["hour"])
|
|
205
|
+
minute_percentage = (minute_usage / limits["min"]) * 100 if limits["min"] > 0 else 0
|
|
206
|
+
hour_percentage = (hour_usage / limits["hour"]) * 100 if limits["hour"] > 0 else 0
|
|
207
|
+
return max(minute_percentage, hour_percentage)
|
|
208
|
+
|
|
209
|
+
def get_limit_status(self, resource_type="default"):
|
|
210
|
+
current_time = time.time()
|
|
211
|
+
limits = self._get_tier_limits()
|
|
212
|
+
minute_cutoff = current_time - 60
|
|
213
|
+
hour_cutoff = current_time - 3600
|
|
214
|
+
minute_usage = len([t for t in self.usage_counters[resource_type]["min"] if t > minute_cutoff])
|
|
215
|
+
hour_usage = len([t for t in self.usage_counters[resource_type]["hour"] if t > hour_cutoff])
|
|
216
|
+
return {
|
|
217
|
+
"resource_type": resource_type,
|
|
218
|
+
"minute_limit": {
|
|
219
|
+
"usage": minute_usage,
|
|
220
|
+
"limit": limits["min"],
|
|
221
|
+
"percentage": (minute_usage / limits["min"]) * 100 if limits["min"] > 0 else 0,
|
|
222
|
+
"remaining": max(0, limits["min"] - minute_usage),
|
|
223
|
+
"reset_in_seconds": 60 - (current_time % 60)
|
|
224
|
+
},
|
|
225
|
+
"hour_limit": {
|
|
226
|
+
"usage": hour_usage,
|
|
227
|
+
"limit": limits["hour"],
|
|
228
|
+
"percentage": (hour_usage / limits["hour"]) * 100 if limits["hour"] > 0 else 0,
|
|
229
|
+
"remaining": max(0, limits["hour"] - hour_usage),
|
|
230
|
+
"reset_in_seconds": 3600 - (current_time % 3600)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
guardian = Guardian()
|
|
234
|
+
|
|
235
|
+
class CleanErrorContext:
|
|
236
|
+
_last_message_time = 0
|
|
237
|
+
_message_cooldown = 5
|
|
238
|
+
|
|
239
|
+
def __enter__(self):
|
|
240
|
+
return self
|
|
241
|
+
|
|
242
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
243
|
+
if exc_type is RateLimitExceeded:
|
|
244
|
+
current_time = time.time()
|
|
245
|
+
if current_time - CleanErrorContext._last_message_time >= CleanErrorContext._message_cooldown:
|
|
246
|
+
print(f"\n⚠️ {str(exc_val)}\n")
|
|
247
|
+
CleanErrorContext._last_message_time = current_time
|
|
248
|
+
import sys
|
|
249
|
+
sys.exit(f"Rate limit exceeded. {str(exc_val)} Process terminated.")
|
|
250
|
+
return False
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
def optimize(resource_type='default', loop_threshold=10, time_window=5, ad_cooldown=150, content_trigger_threshold=3,
|
|
254
|
+
max_retries=2, backoff_factor=2, debug=False):
|
|
255
|
+
if callable(resource_type):
|
|
256
|
+
func = resource_type
|
|
257
|
+
return _create_wrapper(func,'default', loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
|
258
|
+
max_retries, backoff_factor, debug)
|
|
259
|
+
if loop_threshold < 2:
|
|
260
|
+
raise ValueError(f"loop_threshold must be at least 2, got {loop_threshold}")
|
|
261
|
+
if time_window <= 0:
|
|
262
|
+
raise ValueError(f"time_window must be positive, got {time_window}")
|
|
263
|
+
if content_trigger_threshold < 1:
|
|
264
|
+
raise ValueError(f"content_trigger_threshold must be at least 1, got {content_trigger_threshold}")
|
|
265
|
+
if max_retries < 0:
|
|
266
|
+
raise ValueError(f"max_retries must be non-negative, got {max_retries}")
|
|
267
|
+
if backoff_factor <= 0:
|
|
268
|
+
raise ValueError(f"backoff_factor must be positive, got {backoff_factor}")
|
|
269
|
+
|
|
270
|
+
def decorator(func):
|
|
271
|
+
return _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
|
272
|
+
max_retries, backoff_factor, debug)
|
|
273
|
+
return decorator
|
|
274
|
+
|
|
275
|
+
def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
|
276
|
+
max_retries, backoff_factor, debug):
|
|
277
|
+
call_history = []
|
|
278
|
+
last_ad_time = 0
|
|
279
|
+
consecutive_loop_detections = 0
|
|
280
|
+
session_displayed = False
|
|
281
|
+
session_start_time = time.time()
|
|
282
|
+
session_timeout = 1800
|
|
283
|
+
@functools.wraps(func)
|
|
284
|
+
|
|
285
|
+
def wrapper(*args, **kwargs):
|
|
286
|
+
nonlocal last_ad_time, consecutive_loop_detections, session_displayed, session_start_time
|
|
287
|
+
current_time = time.time()
|
|
288
|
+
content_triggered = False
|
|
289
|
+
if current_time - session_start_time > session_timeout:
|
|
290
|
+
session_displayed = False
|
|
291
|
+
session_start_time = current_time
|
|
292
|
+
retries = 0
|
|
293
|
+
while True:
|
|
294
|
+
call_history.append(current_time)
|
|
295
|
+
while call_history and current_time - call_history[0] > time_window:
|
|
296
|
+
call_history.pop(0)
|
|
297
|
+
loop_detected = len(call_history) >= loop_threshold
|
|
298
|
+
if debug and loop_detected:
|
|
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")
|
|
300
|
+
if loop_detected:
|
|
301
|
+
consecutive_loop_detections += 1
|
|
302
|
+
if debug:
|
|
303
|
+
print(f"Số lần phát hiện vòng lặp liên tiếp: {consecutive_loop_detections}/{content_trigger_threshold}")
|
|
304
|
+
else:
|
|
305
|
+
consecutive_loop_detections = 0
|
|
306
|
+
should_show_content = (consecutive_loop_detections >= content_trigger_threshold) and (current_time - last_ad_time >= ad_cooldown) and not session_displayed
|
|
307
|
+
if should_show_content:
|
|
308
|
+
last_ad_time = current_time
|
|
309
|
+
consecutive_loop_detections = 0
|
|
310
|
+
content_triggered = True
|
|
311
|
+
session_displayed = True
|
|
312
|
+
if debug:
|
|
313
|
+
print(f"Đã kích hoạt nội dung cho {func.__name__}")
|
|
314
|
+
try:
|
|
315
|
+
from vnai.scope.promo import manager
|
|
316
|
+
manager.present_content(context="loop")
|
|
317
|
+
except ImportError:
|
|
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")
|
|
319
|
+
except Exception as e:
|
|
320
|
+
if debug:
|
|
321
|
+
print(f"Lỗi khi hiển thị nội dung: {str(e)}")
|
|
322
|
+
try:
|
|
323
|
+
with CleanErrorContext():
|
|
324
|
+
guardian.verify(func.__name__, resource_type)
|
|
325
|
+
except RateLimitExceeded as e:
|
|
326
|
+
from vnai.beam.metrics import collector
|
|
327
|
+
collector.record(
|
|
328
|
+
"error",
|
|
329
|
+
{
|
|
330
|
+
"function": func.__name__,
|
|
331
|
+
"error": str(e),
|
|
332
|
+
"context":"resource_verification",
|
|
333
|
+
"resource_type": resource_type,
|
|
334
|
+
"retry_attempt": retries
|
|
335
|
+
},
|
|
336
|
+
priority="high"
|
|
337
|
+
)
|
|
338
|
+
if not session_displayed:
|
|
339
|
+
try:
|
|
340
|
+
from vnai.scope.promo import manager
|
|
341
|
+
manager.present_content(context="loop")
|
|
342
|
+
session_displayed = True
|
|
343
|
+
last_ad_time = current_time
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
if retries < max_retries:
|
|
347
|
+
wait_time = backoff_factor ** retries
|
|
348
|
+
retries += 1
|
|
349
|
+
if hasattr(e,"retry_after") and e.retry_after:
|
|
350
|
+
wait_time = min(wait_time, e.retry_after)
|
|
351
|
+
if debug:
|
|
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})")
|
|
353
|
+
time.sleep(wait_time)
|
|
354
|
+
continue
|
|
355
|
+
else:
|
|
356
|
+
raise
|
|
357
|
+
start_time = time.time()
|
|
358
|
+
success = False
|
|
359
|
+
error = None
|
|
360
|
+
try:
|
|
361
|
+
result = func(*args, **kwargs)
|
|
362
|
+
success = True
|
|
363
|
+
return result
|
|
364
|
+
except Exception as e:
|
|
365
|
+
error = str(e)
|
|
366
|
+
raise
|
|
367
|
+
finally:
|
|
368
|
+
execution_time = time.time() - start_time
|
|
369
|
+
try:
|
|
370
|
+
from vnai.beam.metrics import collector
|
|
371
|
+
collector.record(
|
|
372
|
+
"function",
|
|
373
|
+
{
|
|
374
|
+
"function": func.__name__,
|
|
375
|
+
"resource_type": resource_type,
|
|
376
|
+
"execution_time": execution_time,
|
|
377
|
+
"success": success,
|
|
378
|
+
"error": error,
|
|
379
|
+
"in_loop": loop_detected,
|
|
380
|
+
"loop_depth": len(call_history),
|
|
381
|
+
"content_triggered": content_triggered,
|
|
382
|
+
"timestamp": datetime.now().isoformat(),
|
|
383
|
+
"retry_count": retries if retries > 0 else None
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
if content_triggered:
|
|
387
|
+
collector.record(
|
|
388
|
+
"ad_opportunity",
|
|
389
|
+
{
|
|
390
|
+
"function": func.__name__,
|
|
391
|
+
"resource_type": resource_type,
|
|
392
|
+
"call_frequency": len(call_history) / time_window,
|
|
393
|
+
"consecutive_loops": consecutive_loop_detections,
|
|
394
|
+
"timestamp": datetime.now().isoformat()
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
except ImportError:
|
|
398
|
+
pass
|
|
399
|
+
break
|
|
400
|
+
return wrapper
|
|
401
|
+
|
|
402
|
+
def rate_limit_status(resource_type="default"):
|
|
403
|
+
return guardian.get_limit_status(resource_type)
|
vnai/beam/sync.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend Integration Layer
|
|
3
|
+
Integrates vnai with the backend vnstock API for:
|
|
4
|
+
- Real-time quota verification
|
|
5
|
+
- Device management
|
|
6
|
+
- Add-on package tracking
|
|
7
|
+
- Offline fallback support
|
|
8
|
+
"""
|
|
9
|
+
import logging
|
|
10
|
+
import requests
|
|
11
|
+
from typing import Dict, Any, Optional
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
class BackendIntegration:
|
|
16
|
+
def __init__(self, backend_url: str ="https://vnstocks.com/api/vnstock"):
|
|
17
|
+
self.backend_url = backend_url
|
|
18
|
+
self.timeout = 5
|
|
19
|
+
self.cache = {}
|
|
20
|
+
|
|
21
|
+
def _make_request(self, endpoint: str, api_key: str, method: str ="GET") -> Dict[str, Any]:
|
|
22
|
+
try:
|
|
23
|
+
headers = {
|
|
24
|
+
"Authorization":f"Bearer {api_key}",
|
|
25
|
+
"Content-Type":"application/json"
|
|
26
|
+
}
|
|
27
|
+
url =f"{self.backend_url}{endpoint}"
|
|
28
|
+
if method =="GET":
|
|
29
|
+
response = requests.get(url, headers=headers, timeout=self.timeout)
|
|
30
|
+
else:
|
|
31
|
+
return {"success": False,"error":f"Unsupported method: {method}"}
|
|
32
|
+
if response.status_code == 200:
|
|
33
|
+
return response.json()
|
|
34
|
+
elif response.status_code == 401:
|
|
35
|
+
return {"success": False,"error":"Unauthorized - Invalid API key"}
|
|
36
|
+
elif response.status_code == 404:
|
|
37
|
+
return {"success": False,"error":"Resource not found"}
|
|
38
|
+
else:
|
|
39
|
+
return {"success": False,"error":f"HTTP {response.status_code}"}
|
|
40
|
+
except requests.exceptions.Timeout:
|
|
41
|
+
log.warning(f"Backend API timeout: {endpoint}")
|
|
42
|
+
return {"success": False,"error":"Backend API timeout"}
|
|
43
|
+
except requests.exceptions.ConnectionError:
|
|
44
|
+
log.warning(f"Backend API connection error: {endpoint}")
|
|
45
|
+
return {"success": False,"error":"Backend API connection error"}
|
|
46
|
+
except Exception as e:
|
|
47
|
+
log.warning(f"Backend API error: {e}")
|
|
48
|
+
return {"success": False,"error": str(e)}
|
|
49
|
+
|
|
50
|
+
def get_quota_status(self, api_key: str) -> Dict[str, Any]:
|
|
51
|
+
return self._make_request("/quota/status", api_key)
|
|
52
|
+
|
|
53
|
+
def get_devices(self, api_key: str) -> Dict[str, Any]:
|
|
54
|
+
return self._make_request("/devices", api_key)
|
|
55
|
+
|
|
56
|
+
def get_device_limits(self, api_key: str) -> Dict[str, Any]:
|
|
57
|
+
return self._make_request("/devices/limits", api_key)
|
|
58
|
+
|
|
59
|
+
def get_device(self, api_key: str, device_id: str) -> Dict[str, Any]:
|
|
60
|
+
return self._make_request(f"/devices/{device_id}", api_key)
|
|
61
|
+
|
|
62
|
+
def get_active_addons(self, api_key: str) -> Dict[str, Any]:
|
|
63
|
+
return self._make_request("/addons/active", api_key)
|
|
64
|
+
|
|
65
|
+
def get_complete_metadata(self, api_key: str) -> Dict[str, Any]:
|
|
66
|
+
try:
|
|
67
|
+
quota = self.get_quota_status(api_key)
|
|
68
|
+
devices = self.get_devices(api_key)
|
|
69
|
+
addons = self.get_active_addons(api_key)
|
|
70
|
+
return {
|
|
71
|
+
"success": True,
|
|
72
|
+
"data": {
|
|
73
|
+
"quota": quota.get("data") if quota.get("success") else None,
|
|
74
|
+
"devices": devices.get("data") if devices.get("success") else None,
|
|
75
|
+
"addons": addons.get("data") if addons.get("success") else None,
|
|
76
|
+
"timestamp": datetime.now().isoformat()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
except Exception as e:
|
|
80
|
+
log.warning(f"Failed to get complete metadata: {e}")
|
|
81
|
+
return {"success": False,"error": str(e)}
|
|
82
|
+
backend_integration = BackendIntegration()
|
|
83
|
+
|
|
84
|
+
def get_backend_integration(backend_url: Optional[str] = None) -> BackendIntegration:
|
|
85
|
+
if backend_url:
|
|
86
|
+
return BackendIntegration(backend_url)
|
|
87
|
+
return backend_integration
|
vnai/flow/__init__.py
ADDED
vnai/flow/queue.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
class Buffer:
|
|
8
|
+
_instance = None
|
|
9
|
+
_lock = threading.Lock()
|
|
10
|
+
|
|
11
|
+
def __new__(cls):
|
|
12
|
+
with cls._lock:
|
|
13
|
+
if cls._instance is None:
|
|
14
|
+
cls._instance = super(Buffer, cls).__new__(cls)
|
|
15
|
+
cls._instance._initialize()
|
|
16
|
+
return cls._instance
|
|
17
|
+
|
|
18
|
+
def _initialize(self):
|
|
19
|
+
self.data = []
|
|
20
|
+
self.lock = threading.Lock()
|
|
21
|
+
self.max_size = 1000
|
|
22
|
+
self.backup_interval = 300
|
|
23
|
+
self.home_dir = Path.home()
|
|
24
|
+
self.project_dir = self.home_dir /".vnstock"
|
|
25
|
+
self.project_dir.mkdir(exist_ok=True)
|
|
26
|
+
self.data_dir = self.project_dir /'data'
|
|
27
|
+
self.data_dir.mkdir(exist_ok=True)
|
|
28
|
+
self.backup_path = self.data_dir /"buffer_backup.json"
|
|
29
|
+
self._load_from_backup()
|
|
30
|
+
self._start_backup_thread()
|
|
31
|
+
|
|
32
|
+
def _load_from_backup(self):
|
|
33
|
+
if self.backup_path.exists():
|
|
34
|
+
try:
|
|
35
|
+
with open(self.backup_path,'r') as f:
|
|
36
|
+
backup_data = json.load(f)
|
|
37
|
+
with self.lock:
|
|
38
|
+
self.data = backup_data
|
|
39
|
+
except:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def _save_to_backup(self):
|
|
43
|
+
with self.lock:
|
|
44
|
+
if not self.data:
|
|
45
|
+
return
|
|
46
|
+
try:
|
|
47
|
+
with open(self.backup_path,'w') as f:
|
|
48
|
+
json.dump(self.data, f)
|
|
49
|
+
except:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def _start_backup_thread(self):
|
|
53
|
+
def backup_task():
|
|
54
|
+
while True:
|
|
55
|
+
time.sleep(self.backup_interval)
|
|
56
|
+
self._save_to_backup()
|
|
57
|
+
backup_thread = threading.Thread(target=backup_task, daemon=True)
|
|
58
|
+
backup_thread.start()
|
|
59
|
+
|
|
60
|
+
def add(self, item, category=None):
|
|
61
|
+
with self.lock:
|
|
62
|
+
if isinstance(item, dict):
|
|
63
|
+
if"timestamp" not in item:
|
|
64
|
+
item["timestamp"] = datetime.now().isoformat()
|
|
65
|
+
if category:
|
|
66
|
+
item["category"] = category
|
|
67
|
+
self.data.append(item)
|
|
68
|
+
if len(self.data) > self.max_size:
|
|
69
|
+
self.data = self.data[-self.max_size:]
|
|
70
|
+
if len(self.data) % 100 == 0:
|
|
71
|
+
self._save_to_backup()
|
|
72
|
+
return len(self.data)
|
|
73
|
+
|
|
74
|
+
def get(self, count=None, category=None):
|
|
75
|
+
with self.lock:
|
|
76
|
+
if category:
|
|
77
|
+
filtered_data = [item for item in self.data if item.get("category") == category]
|
|
78
|
+
else:
|
|
79
|
+
filtered_data = self.data.copy()
|
|
80
|
+
if count:
|
|
81
|
+
return filtered_data[:count]
|
|
82
|
+
else:
|
|
83
|
+
return filtered_data
|
|
84
|
+
|
|
85
|
+
def clear(self, category=None):
|
|
86
|
+
with self.lock:
|
|
87
|
+
if category:
|
|
88
|
+
self.data = [item for item in self.data if item.get("category") != category]
|
|
89
|
+
else:
|
|
90
|
+
self.data = []
|
|
91
|
+
self._save_to_backup()
|
|
92
|
+
return len(self.data)
|
|
93
|
+
|
|
94
|
+
def size(self, category=None):
|
|
95
|
+
with self.lock:
|
|
96
|
+
if category:
|
|
97
|
+
return len([item for item in self.data if item.get("category") == category])
|
|
98
|
+
else:
|
|
99
|
+
return len(self.data)
|
|
100
|
+
buffer = Buffer()
|