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