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