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/beam/quota.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
##
|
4
|
-
|
1
|
+
# vnai/beam/quota.py
|
2
|
+
# Resource allocation and management (formerly rate_limiter)
|
5
3
|
|
6
4
|
import time
|
7
5
|
import functools
|
@@ -10,7 +8,7 @@ from collections import defaultdict
|
|
10
8
|
from datetime import datetime
|
11
9
|
|
12
10
|
class RateLimitExceeded(Exception):
|
13
|
-
|
11
|
+
"""Custom exception for rate limit violations."""
|
14
12
|
def __init__(self, resource_type, limit_type="min", current_usage=None, limit_value=None, retry_after=None):
|
15
13
|
self.resource_type = resource_type
|
16
14
|
self.limit_type = limit_type
|
@@ -18,8 +16,7 @@ class RateLimitExceeded(Exception):
|
|
18
16
|
self.limit_value = limit_value
|
19
17
|
self.retry_after = retry_after
|
20
18
|
|
21
|
-
|
22
|
-
|
19
|
+
# Create a user-friendly message
|
23
20
|
message = f"Bạn đã gửi quá nhiều request tới {resource_type}. "
|
24
21
|
if retry_after:
|
25
22
|
message += f"Vui lòng thử lại sau {round(retry_after)} giây."
|
@@ -29,7 +26,7 @@ class RateLimitExceeded(Exception):
|
|
29
26
|
super().__init__(message)
|
30
27
|
|
31
28
|
class Guardian:
|
32
|
-
|
29
|
+
"""Ensures optimal resource allocation"""
|
33
30
|
|
34
31
|
_instance = None
|
35
32
|
_lock = threading.Lock()
|
@@ -42,16 +39,18 @@ class Guardian:
|
|
42
39
|
return cls._instance
|
43
40
|
|
44
41
|
def _initialize(self):
|
45
|
-
|
42
|
+
"""Initialize guardian"""
|
46
43
|
self.resource_limits = defaultdict(lambda: defaultdict(int))
|
47
44
|
self.usage_counters = defaultdict(lambda: defaultdict(list))
|
48
45
|
|
49
|
-
|
50
|
-
|
46
|
+
# Define resource limits
|
51
47
|
self.resource_limits["default"] = {"min": 60, "hour": 3000}
|
52
48
|
self.resource_limits["TCBS"] = {"min": 60, "hour": 3000}
|
53
49
|
self.resource_limits["VCI"] = {"min": 60, "hour": 3000}
|
50
|
+
self.resource_limits["MBK"] = {"min": 600, "hour": 36000}
|
51
|
+
self.resource_limits["MAS.ext"] = {"min": 600, "hour": 36000}
|
54
52
|
self.resource_limits["VCI.ext"] = {"min": 600, "hour": 36000}
|
53
|
+
self.resource_limits["FMK.ext"] = {"min": 600, "hour": 36000}
|
55
54
|
self.resource_limits["VND.ext"] = {"min": 600, "hour": 36000}
|
56
55
|
self.resource_limits["CAF.ext"] = {"min": 600, "hour": 36000}
|
57
56
|
self.resource_limits["SPL.ext"] = {"min": 600, "hour": 36000}
|
@@ -59,15 +58,13 @@ class Guardian:
|
|
59
58
|
self.resource_limits["FAD.ext"] = {"min": 600, "hour": 36000}
|
60
59
|
|
61
60
|
def verify(self, operation_id, resource_type="default"):
|
62
|
-
|
61
|
+
"""Verify resource availability before operation"""
|
63
62
|
current_time = time.time()
|
64
63
|
|
65
|
-
|
66
|
-
|
64
|
+
# Get limits for this resource type (or use default)
|
67
65
|
limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
|
68
66
|
|
69
|
-
|
70
|
-
|
67
|
+
# Check minute limit
|
71
68
|
minute_cutoff = current_time - 60
|
72
69
|
self.usage_counters[resource_type]["min"] = [
|
73
70
|
t for t in self.usage_counters[resource_type]["min"]
|
@@ -78,8 +75,7 @@ class Guardian:
|
|
78
75
|
minute_exceeded = minute_usage >= limits["min"]
|
79
76
|
|
80
77
|
if minute_exceeded:
|
81
|
-
|
82
|
-
|
78
|
+
# Track limit check through metrics module
|
83
79
|
from vnai.beam.metrics import collector
|
84
80
|
collector.record(
|
85
81
|
"rate_limit",
|
@@ -92,19 +88,16 @@ class Guardian:
|
|
92
88
|
},
|
93
89
|
priority="high"
|
94
90
|
)
|
95
|
-
|
96
|
-
|
91
|
+
# Raise custom exception with retry information
|
97
92
|
raise RateLimitExceeded(
|
98
93
|
resource_type=resource_type,
|
99
94
|
limit_type="min",
|
100
95
|
current_usage=minute_usage,
|
101
96
|
limit_value=limits["min"],
|
102
|
-
retry_after=60 - (current_time % 60)
|
103
|
-
|
97
|
+
retry_after=60 - (current_time % 60) # Seconds until the minute rolls over
|
104
98
|
)
|
105
99
|
|
106
|
-
|
107
|
-
|
100
|
+
# Check hour limit
|
108
101
|
hour_cutoff = current_time - 3600
|
109
102
|
self.usage_counters[resource_type]["hour"] = [
|
110
103
|
t for t in self.usage_counters[resource_type]["hour"]
|
@@ -114,8 +107,7 @@ class Guardian:
|
|
114
107
|
hour_usage = len(self.usage_counters[resource_type]["hour"])
|
115
108
|
hour_exceeded = hour_usage >= limits["hour"]
|
116
109
|
|
117
|
-
|
118
|
-
|
110
|
+
# Track rate limit check
|
119
111
|
from vnai.beam.metrics import collector
|
120
112
|
collector.record(
|
121
113
|
"rate_limit",
|
@@ -129,30 +121,26 @@ class Guardian:
|
|
129
121
|
)
|
130
122
|
|
131
123
|
if hour_exceeded:
|
132
|
-
|
133
|
-
|
124
|
+
# Raise custom exception with retry information
|
134
125
|
raise RateLimitExceeded(
|
135
126
|
resource_type=resource_type,
|
136
127
|
limit_type="hour",
|
137
128
|
current_usage=hour_usage,
|
138
129
|
limit_value=limits["hour"],
|
139
|
-
retry_after=3600 - (current_time % 3600)
|
140
|
-
|
130
|
+
retry_after=3600 - (current_time % 3600) # Seconds until the hour rolls over
|
141
131
|
)
|
142
132
|
|
143
|
-
|
144
|
-
|
133
|
+
# Record this request
|
145
134
|
self.usage_counters[resource_type]["min"].append(current_time)
|
146
135
|
self.usage_counters[resource_type]["hour"].append(current_time)
|
147
136
|
return True
|
148
137
|
|
149
138
|
def usage(self, resource_type="default"):
|
150
|
-
|
139
|
+
"""Get current usage percentage for resource limits"""
|
151
140
|
current_time = time.time()
|
152
141
|
limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
|
153
142
|
|
154
|
-
|
155
|
-
|
143
|
+
# Clean old timestamps
|
156
144
|
minute_cutoff = current_time - 60
|
157
145
|
hour_cutoff = current_time - 3600
|
158
146
|
|
@@ -166,25 +154,22 @@ class Guardian:
|
|
166
154
|
if t > hour_cutoff
|
167
155
|
]
|
168
156
|
|
169
|
-
|
170
|
-
|
157
|
+
# Calculate percentages
|
171
158
|
minute_usage = len(self.usage_counters[resource_type]["min"])
|
172
159
|
hour_usage = len(self.usage_counters[resource_type]["hour"])
|
173
160
|
|
174
161
|
minute_percentage = (minute_usage / limits["min"]) * 100 if limits["min"] > 0 else 0
|
175
162
|
hour_percentage = (hour_usage / limits["hour"]) * 100 if limits["hour"] > 0 else 0
|
176
163
|
|
177
|
-
|
178
|
-
|
164
|
+
# Return the higher percentage
|
179
165
|
return max(minute_percentage, hour_percentage)
|
180
166
|
|
181
167
|
def get_limit_status(self, resource_type="default"):
|
182
|
-
|
168
|
+
"""Get detailed information about current limit status"""
|
183
169
|
current_time = time.time()
|
184
170
|
limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
|
185
171
|
|
186
|
-
|
187
|
-
|
172
|
+
# Clean old timestamps
|
188
173
|
minute_cutoff = current_time - 60
|
189
174
|
hour_cutoff = current_time - 3600
|
190
175
|
|
@@ -209,17 +194,14 @@ class Guardian:
|
|
209
194
|
}
|
210
195
|
}
|
211
196
|
|
212
|
-
|
213
|
-
|
197
|
+
# Create singleton instance
|
214
198
|
guardian = Guardian()
|
215
199
|
|
216
200
|
class CleanErrorContext:
|
217
|
-
|
218
|
-
|
219
|
-
|
201
|
+
"""Context manager to clean up tracebacks for rate limits"""
|
202
|
+
# Class variable to track if a message has been displayed recently
|
220
203
|
_last_message_time = 0
|
221
|
-
_message_cooldown = 5
|
222
|
-
|
204
|
+
_message_cooldown = 5 # Only show a message every 5 seconds
|
223
205
|
|
224
206
|
def __enter__(self):
|
225
207
|
return self
|
@@ -228,37 +210,64 @@ class CleanErrorContext:
|
|
228
210
|
if exc_type is RateLimitExceeded:
|
229
211
|
current_time = time.time()
|
230
212
|
|
231
|
-
|
232
|
-
|
213
|
+
# Only print the message if enough time has passed since the last one
|
233
214
|
if current_time - CleanErrorContext._last_message_time >= CleanErrorContext._message_cooldown:
|
234
215
|
print(f"\n⚠️ {str(exc_val)}\n")
|
235
216
|
CleanErrorContext._last_message_time = current_time
|
236
217
|
|
237
|
-
|
238
|
-
|
239
|
-
##
|
240
|
-
|
218
|
+
# Re-raise the exception more forcefully to ensure it propagates
|
219
|
+
# This will bypass any try/except blocks that might be catching RateLimitExceeded
|
241
220
|
import sys
|
242
221
|
sys.exit(f"Rate limit exceeded. {str(exc_val)} Process terminated.")
|
243
222
|
|
244
|
-
|
245
|
-
|
223
|
+
# The line below won't be reached, but we keep it for clarity
|
246
224
|
return False
|
247
225
|
return False
|
248
226
|
|
249
227
|
|
250
228
|
def optimize(resource_type='default', loop_threshold=10, time_window=5, ad_cooldown=150, content_trigger_threshold=3,
|
251
229
|
max_retries=2, backoff_factor=2, debug=False):
|
252
|
-
|
253
|
-
|
254
|
-
|
230
|
+
"""
|
231
|
+
Decorator that optimizes function execution, tracks metrics, and detects loop patterns for ad opportunities.
|
232
|
+
|
233
|
+
Features:
|
234
|
+
- Resource verification
|
235
|
+
- Performance metrics collection
|
236
|
+
- Loop detection for ad/content opportunities
|
237
|
+
- Automatic retry with exponential backoff for rate limit errors
|
238
|
+
|
239
|
+
Args:
|
240
|
+
resource_type: Type of resource used by function ("network", "database", "cpu", "memory", "io", "default")
|
241
|
+
loop_threshold: Number of calls within time_window to consider as a loop (min: 2)
|
242
|
+
time_window: Time period in seconds to consider for loop detection
|
243
|
+
ad_cooldown: Minimum seconds between showing ads for the same function
|
244
|
+
content_trigger_threshold: Number of consecutive loop detections before triggering content (min: 1)
|
245
|
+
max_retries: Maximum number of times to retry when rate limits are hit
|
246
|
+
backoff_factor: Base factor for exponential backoff (wait time = backoff_factor^retry_count)
|
247
|
+
debug: When True, prints diagnostic information about loop detection
|
248
|
+
|
249
|
+
Examples:
|
250
|
+
@optimize
|
251
|
+
def simple_function():
|
252
|
+
return "result"
|
253
|
+
|
254
|
+
@optimize("network")
|
255
|
+
def fetch_stock_data(symbol):
|
256
|
+
# Makes network calls
|
257
|
+
return data
|
258
|
+
|
259
|
+
@optimize("database", loop_threshold=4, time_window=10)
|
260
|
+
def query_financial_data(params):
|
261
|
+
# Database queries
|
262
|
+
return results
|
263
|
+
"""
|
264
|
+
# Handle case where decorator is used without arguments: @optimize
|
255
265
|
if callable(resource_type):
|
256
266
|
func = resource_type
|
257
267
|
return _create_wrapper(func, 'default', loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
258
268
|
max_retries, backoff_factor, debug)
|
259
269
|
|
260
|
-
|
261
|
-
|
270
|
+
# Basic validation
|
262
271
|
if loop_threshold < 2:
|
263
272
|
raise ValueError(f"loop_threshold must be at least 2, got {loop_threshold}")
|
264
273
|
if time_window <= 0:
|
@@ -270,8 +279,7 @@ def optimize(resource_type='default', loop_threshold=10, time_window=5, ad_coold
|
|
270
279
|
if backoff_factor <= 0:
|
271
280
|
raise ValueError(f"backoff_factor must be positive, got {backoff_factor}")
|
272
281
|
|
273
|
-
|
274
|
-
|
282
|
+
# Return the actual decorator
|
275
283
|
def decorator(func):
|
276
284
|
return _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
277
285
|
max_retries, backoff_factor, debug)
|
@@ -279,17 +287,14 @@ def optimize(resource_type='default', loop_threshold=10, time_window=5, ad_coold
|
|
279
287
|
|
280
288
|
def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
281
289
|
max_retries, backoff_factor, debug):
|
282
|
-
|
283
|
-
|
284
|
-
|
290
|
+
"""Creates the function wrapper with call tracking for loop detection"""
|
291
|
+
# Static storage for each decorated function instance
|
285
292
|
call_history = []
|
286
293
|
last_ad_time = 0
|
287
294
|
consecutive_loop_detections = 0
|
288
|
-
session_displayed = False
|
289
|
-
|
295
|
+
session_displayed = False # Track if we've displayed an ad in this session
|
290
296
|
session_start_time = time.time()
|
291
|
-
session_timeout = 1800
|
292
|
-
|
297
|
+
session_timeout = 1800 # 30 minutes for session expiration
|
293
298
|
|
294
299
|
@functools.wraps(func)
|
295
300
|
def wrapper(*args, **kwargs):
|
@@ -297,36 +302,29 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
297
302
|
current_time = time.time()
|
298
303
|
content_triggered = False
|
299
304
|
|
300
|
-
|
301
|
-
|
305
|
+
# Reset session if it has expired
|
302
306
|
if current_time - session_start_time > session_timeout:
|
303
307
|
session_displayed = False
|
304
308
|
session_start_time = current_time
|
305
309
|
|
306
|
-
|
307
|
-
|
310
|
+
# For automatic retries with rate limits
|
308
311
|
retries = 0
|
309
312
|
while True:
|
310
|
-
|
311
|
-
|
312
|
-
##
|
313
|
-
|
313
|
+
# ===== LOOP DETECTION LOGIC =====
|
314
|
+
# Add current call to history
|
314
315
|
call_history.append(current_time)
|
315
316
|
|
316
|
-
|
317
|
-
|
317
|
+
# Prune old calls outside the time window
|
318
318
|
while call_history and current_time - call_history[0] > time_window:
|
319
319
|
call_history.pop(0)
|
320
320
|
|
321
|
-
|
322
|
-
|
321
|
+
# Check if we're in a loop pattern
|
323
322
|
loop_detected = len(call_history) >= loop_threshold
|
324
323
|
|
325
324
|
if debug and loop_detected:
|
326
325
|
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")
|
327
326
|
|
328
|
-
|
329
|
-
|
327
|
+
# Handle loop detection
|
330
328
|
if loop_detected:
|
331
329
|
consecutive_loop_detections += 1
|
332
330
|
if debug:
|
@@ -334,29 +332,26 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
334
332
|
else:
|
335
333
|
consecutive_loop_detections = 0
|
336
334
|
|
337
|
-
|
338
|
-
|
339
|
-
|
335
|
+
# Determine if we should show content - add session_displayed check
|
336
|
+
should_show_content = (consecutive_loop_detections >= content_trigger_threshold) and \
|
337
|
+
(current_time - last_ad_time >= ad_cooldown) and \
|
338
|
+
not session_displayed
|
340
339
|
|
341
|
-
|
342
|
-
|
340
|
+
# Handle content opportunity
|
343
341
|
if should_show_content:
|
344
342
|
last_ad_time = current_time
|
345
343
|
consecutive_loop_detections = 0
|
346
344
|
content_triggered = True
|
347
|
-
session_displayed = True
|
348
|
-
|
345
|
+
session_displayed = True # Mark that we've displayed in this session
|
349
346
|
|
350
347
|
if debug:
|
351
348
|
print(f"[OPTIMIZE] Đã kích hoạt nội dung cho {func.__name__}")
|
352
349
|
|
353
|
-
|
354
|
-
|
350
|
+
# Trigger content display using promo manager with "loop" context
|
355
351
|
try:
|
356
352
|
from vnai.scope.promo import manager
|
357
353
|
|
358
|
-
|
359
|
-
|
354
|
+
# Get environment if available
|
360
355
|
try:
|
361
356
|
from vnai.scope.profile import inspector
|
362
357
|
environment = inspector.examine().get("environment", None)
|
@@ -365,26 +360,21 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
365
360
|
manager.present_content(context="loop")
|
366
361
|
|
367
362
|
except ImportError:
|
368
|
-
|
369
|
-
|
363
|
+
# Fallback if content manager is not available
|
370
364
|
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")
|
371
365
|
except Exception as e:
|
372
|
-
|
373
|
-
|
366
|
+
# Don't let content errors affect the main function
|
374
367
|
if debug:
|
375
368
|
print(f"[OPTIMIZE] Lỗi khi hiển thị nội dung: {str(e)}")
|
376
369
|
|
377
|
-
|
378
|
-
|
370
|
+
# ===== RESOURCE VERIFICATION =====
|
379
371
|
try:
|
380
|
-
|
381
|
-
|
372
|
+
# Use a context manager to clean up the traceback
|
382
373
|
with CleanErrorContext():
|
383
374
|
guardian.verify(func.__name__, resource_type)
|
384
375
|
|
385
376
|
except RateLimitExceeded as e:
|
386
|
-
|
387
|
-
|
377
|
+
# Record the rate limit error
|
388
378
|
from vnai.beam.metrics import collector
|
389
379
|
collector.record(
|
390
380
|
"error",
|
@@ -398,8 +388,7 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
398
388
|
priority="high"
|
399
389
|
)
|
400
390
|
|
401
|
-
|
402
|
-
|
391
|
+
# Display rate limit content ONLY if we haven't shown any content this session
|
403
392
|
if not session_displayed:
|
404
393
|
try:
|
405
394
|
from vnai.scope.promo import manager
|
@@ -407,25 +396,21 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
407
396
|
from vnai.scope.profile import inspector
|
408
397
|
environment = inspector.examine().get("environment", None)
|
409
398
|
manager.present_content(environment=environment, context="loop")
|
410
|
-
session_displayed = True
|
411
|
-
|
399
|
+
session_displayed = True # Mark that we've displayed
|
412
400
|
last_ad_time = current_time
|
413
401
|
except ImportError:
|
414
402
|
manager.present_content(context="loop")
|
415
403
|
session_displayed = True
|
416
404
|
last_ad_time = current_time
|
417
405
|
except Exception:
|
418
|
-
pass
|
419
|
-
|
406
|
+
pass # Don't let content errors affect the retry logic
|
420
407
|
|
421
|
-
|
422
|
-
|
408
|
+
# Continue with retry logic
|
423
409
|
if retries < max_retries:
|
424
410
|
wait_time = backoff_factor ** retries
|
425
411
|
retries += 1
|
426
412
|
|
427
|
-
|
428
|
-
|
413
|
+
# If the exception has a retry_after value, use that instead
|
429
414
|
if hasattr(e, "retry_after") and e.retry_after:
|
430
415
|
wait_time = min(wait_time, e.retry_after)
|
431
416
|
|
@@ -433,22 +418,18 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
433
418
|
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})")
|
434
419
|
|
435
420
|
time.sleep(wait_time)
|
436
|
-
continue
|
437
|
-
|
421
|
+
continue # Retry the call
|
438
422
|
else:
|
439
|
-
|
440
|
-
|
423
|
+
# No more retries, re-raise the exception
|
441
424
|
raise
|
442
425
|
|
443
|
-
|
444
|
-
|
426
|
+
# ===== FUNCTION EXECUTION & METRICS =====
|
445
427
|
start_time = time.time()
|
446
428
|
success = False
|
447
429
|
error = None
|
448
430
|
|
449
431
|
try:
|
450
|
-
|
451
|
-
|
432
|
+
# Execute the original function
|
452
433
|
result = func(*args, **kwargs)
|
453
434
|
success = True
|
454
435
|
return result
|
@@ -456,12 +437,10 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
456
437
|
error = str(e)
|
457
438
|
raise
|
458
439
|
finally:
|
459
|
-
|
460
|
-
|
440
|
+
# Calculate execution metrics
|
461
441
|
execution_time = time.time() - start_time
|
462
442
|
|
463
|
-
|
464
|
-
|
443
|
+
# Record metrics
|
465
444
|
try:
|
466
445
|
from vnai.beam.metrics import collector
|
467
446
|
collector.record(
|
@@ -480,8 +459,7 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
480
459
|
}
|
481
460
|
)
|
482
461
|
|
483
|
-
|
484
|
-
|
462
|
+
# Record content opportunity metrics if detected
|
485
463
|
if content_triggered:
|
486
464
|
collector.record(
|
487
465
|
"ad_opportunity",
|
@@ -494,19 +472,16 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
494
472
|
}
|
495
473
|
)
|
496
474
|
except ImportError:
|
497
|
-
|
498
|
-
|
475
|
+
# Metrics module not available, just continue
|
499
476
|
pass
|
500
477
|
|
501
|
-
|
502
|
-
|
478
|
+
# If we got here, the function executed successfully, so break the retry loop
|
503
479
|
break
|
504
480
|
|
505
481
|
return wrapper
|
506
482
|
|
507
483
|
|
508
|
-
|
509
|
-
|
484
|
+
# Helper function for getting the current rate limit status
|
510
485
|
def rate_limit_status(resource_type="default"):
|
511
|
-
|
486
|
+
"""Get the current rate limit status for a resource type"""
|
512
487
|
return guardian.get_limit_status(resource_type)
|
vnai/flow/__init__.py
CHANGED
vnai/flow/queue.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
##
|
4
|
-
|
1
|
+
# vnai/flow/queue.py
|
2
|
+
# Data buffering system
|
5
3
|
|
6
4
|
import time
|
7
5
|
import threading
|
@@ -10,7 +8,7 @@ from datetime import datetime
|
|
10
8
|
from pathlib import Path
|
11
9
|
|
12
10
|
class Buffer:
|
13
|
-
|
11
|
+
"""Manages data buffering with persistence"""
|
14
12
|
|
15
13
|
_instance = None
|
16
14
|
_lock = threading.Lock()
|
@@ -23,15 +21,13 @@ class Buffer:
|
|
23
21
|
return cls._instance
|
24
22
|
|
25
23
|
def _initialize(self):
|
26
|
-
|
24
|
+
"""Initialize buffer"""
|
27
25
|
self.data = []
|
28
26
|
self.lock = threading.Lock()
|
29
27
|
self.max_size = 1000
|
30
|
-
self.backup_interval = 300
|
31
|
-
|
28
|
+
self.backup_interval = 300 # 5 minutes
|
32
29
|
|
33
|
-
|
34
|
-
|
30
|
+
# Setup data directory
|
35
31
|
self.home_dir = Path.home()
|
36
32
|
self.project_dir = self.home_dir / ".vnstock"
|
37
33
|
self.project_dir.mkdir(exist_ok=True)
|
@@ -39,16 +35,14 @@ class Buffer:
|
|
39
35
|
self.data_dir.mkdir(exist_ok=True)
|
40
36
|
self.backup_path = self.data_dir / "buffer_backup.json"
|
41
37
|
|
42
|
-
|
43
|
-
|
38
|
+
# Load from backup if exists
|
44
39
|
self._load_from_backup()
|
45
40
|
|
46
|
-
|
47
|
-
|
41
|
+
# Start backup thread
|
48
42
|
self._start_backup_thread()
|
49
43
|
|
50
44
|
def _load_from_backup(self):
|
51
|
-
|
45
|
+
"""Load data from backup file"""
|
52
46
|
if self.backup_path.exists():
|
53
47
|
try:
|
54
48
|
with open(self.backup_path, 'r') as f:
|
@@ -60,7 +54,7 @@ class Buffer:
|
|
60
54
|
pass
|
61
55
|
|
62
56
|
def _save_to_backup(self):
|
63
|
-
|
57
|
+
"""Save data to backup file"""
|
64
58
|
with self.lock:
|
65
59
|
if not self.data:
|
66
60
|
return
|
@@ -72,7 +66,7 @@ class Buffer:
|
|
72
66
|
pass
|
73
67
|
|
74
68
|
def _start_backup_thread(self):
|
75
|
-
|
69
|
+
"""Start background backup thread"""
|
76
70
|
def backup_task():
|
77
71
|
while True:
|
78
72
|
time.sleep(self.backup_interval)
|
@@ -82,34 +76,30 @@ class Buffer:
|
|
82
76
|
backup_thread.start()
|
83
77
|
|
84
78
|
def add(self, item, category=None):
|
85
|
-
|
79
|
+
"""Add item to buffer"""
|
86
80
|
with self.lock:
|
87
|
-
|
88
|
-
|
81
|
+
# Add metadata
|
89
82
|
if isinstance(item, dict):
|
90
83
|
if "timestamp" not in item:
|
91
84
|
item["timestamp"] = datetime.now().isoformat()
|
92
85
|
if category:
|
93
86
|
item["category"] = category
|
94
87
|
|
95
|
-
|
96
|
-
|
88
|
+
# Add to buffer
|
97
89
|
self.data.append(item)
|
98
90
|
|
99
|
-
|
100
|
-
|
91
|
+
# Trim if exceeds max size
|
101
92
|
if len(self.data) > self.max_size:
|
102
93
|
self.data = self.data[-self.max_size:]
|
103
94
|
|
104
|
-
|
105
|
-
|
95
|
+
# Save to backup if buffer gets large
|
106
96
|
if len(self.data) % 100 == 0:
|
107
97
|
self._save_to_backup()
|
108
98
|
|
109
99
|
return len(self.data)
|
110
100
|
|
111
101
|
def get(self, count=None, category=None):
|
112
|
-
|
102
|
+
"""Get items from buffer with optional filtering"""
|
113
103
|
with self.lock:
|
114
104
|
if category:
|
115
105
|
filtered_data = [item for item in self.data if item.get("category") == category]
|
@@ -122,7 +112,7 @@ class Buffer:
|
|
122
112
|
return filtered_data
|
123
113
|
|
124
114
|
def clear(self, category=None):
|
125
|
-
|
115
|
+
"""Clear buffer, optionally by category"""
|
126
116
|
with self.lock:
|
127
117
|
if category:
|
128
118
|
self.data = [item for item in self.data if item.get("category") != category]
|
@@ -133,13 +123,12 @@ class Buffer:
|
|
133
123
|
return len(self.data)
|
134
124
|
|
135
125
|
def size(self, category=None):
|
136
|
-
|
126
|
+
"""Get buffer size, optionally by category"""
|
137
127
|
with self.lock:
|
138
128
|
if category:
|
139
129
|
return len([item for item in self.data if item.get("category") == category])
|
140
130
|
else:
|
141
131
|
return len(self.data)
|
142
132
|
|
143
|
-
|
144
|
-
|
133
|
+
# Create singleton instance
|
145
134
|
buffer = Buffer()
|