vnai 2.0.1__py3-none-any.whl → 2.0.3__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 +84 -54
- vnai/beam/__init__.py +0 -3
- vnai/beam/metrics.py +57 -32
- vnai/beam/pulse.py +36 -21
- vnai/beam/quota.py +143 -109
- vnai/flow/__init__.py +4 -2
- vnai/flow/queue.py +31 -20
- vnai/flow/relay.py +101 -64
- vnai/scope/__init__.py +4 -2
- vnai/scope/profile.py +205 -110
- vnai/scope/promo.py +96 -114
- vnai/scope/state.py +64 -38
- {vnai-2.0.1.dist-info → vnai-2.0.3.dist-info}/METADATA +3 -2
- vnai-2.0.3.dist-info/RECORD +16 -0
- {vnai-2.0.1.dist-info → vnai-2.0.3.dist-info}/WHEEL +1 -1
- vnai-2.0.1.dist-info/RECORD +0 -16
- {vnai-2.0.1.dist-info → vnai-2.0.3.dist-info}/top_level.txt +0 -0
vnai/beam/quota.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
##
|
2
|
+
|
3
|
+
##
|
4
|
+
|
3
5
|
|
4
6
|
import time
|
5
7
|
import functools
|
@@ -8,7 +10,7 @@ from collections import defaultdict
|
|
8
10
|
from datetime import datetime
|
9
11
|
|
10
12
|
class RateLimitExceeded(Exception):
|
11
|
-
|
13
|
+
#--
|
12
14
|
def __init__(self, resource_type, limit_type="min", current_usage=None, limit_value=None, retry_after=None):
|
13
15
|
self.resource_type = resource_type
|
14
16
|
self.limit_type = limit_type
|
@@ -16,7 +18,8 @@ class RateLimitExceeded(Exception):
|
|
16
18
|
self.limit_value = limit_value
|
17
19
|
self.retry_after = retry_after
|
18
20
|
|
19
|
-
|
21
|
+
##
|
22
|
+
|
20
23
|
message = f"Bạn đã gửi quá nhiều request tới {resource_type}. "
|
21
24
|
if retry_after:
|
22
25
|
message += f"Vui lòng thử lại sau {round(retry_after)} giây."
|
@@ -26,7 +29,7 @@ class RateLimitExceeded(Exception):
|
|
26
29
|
super().__init__(message)
|
27
30
|
|
28
31
|
class Guardian:
|
29
|
-
|
32
|
+
#--
|
30
33
|
|
31
34
|
_instance = None
|
32
35
|
_lock = threading.Lock()
|
@@ -39,23 +42,32 @@ class Guardian:
|
|
39
42
|
return cls._instance
|
40
43
|
|
41
44
|
def _initialize(self):
|
42
|
-
|
45
|
+
#--
|
43
46
|
self.resource_limits = defaultdict(lambda: defaultdict(int))
|
44
47
|
self.usage_counters = defaultdict(lambda: defaultdict(list))
|
45
48
|
|
46
|
-
|
49
|
+
##
|
50
|
+
|
47
51
|
self.resource_limits["default"] = {"min": 60, "hour": 3000}
|
48
52
|
self.resource_limits["TCBS"] = {"min": 60, "hour": 3000}
|
49
53
|
self.resource_limits["VCI"] = {"min": 60, "hour": 3000}
|
54
|
+
self.resource_limits["VCI.ext"] = {"min": 600, "hour": 36000}
|
55
|
+
self.resource_limits["VND.ext"] = {"min": 600, "hour": 36000}
|
56
|
+
self.resource_limits["CAF.ext"] = {"min": 600, "hour": 36000}
|
57
|
+
self.resource_limits["SPL.ext"] = {"min": 600, "hour": 36000}
|
58
|
+
self.resource_limits["VDS.ext"] = {"min": 600, "hour": 36000}
|
59
|
+
self.resource_limits["FAD.ext"] = {"min": 600, "hour": 36000}
|
50
60
|
|
51
61
|
def verify(self, operation_id, resource_type="default"):
|
52
|
-
|
62
|
+
#--
|
53
63
|
current_time = time.time()
|
54
64
|
|
55
|
-
|
65
|
+
##
|
66
|
+
|
56
67
|
limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
|
57
68
|
|
58
|
-
|
69
|
+
##
|
70
|
+
|
59
71
|
minute_cutoff = current_time - 60
|
60
72
|
self.usage_counters[resource_type]["min"] = [
|
61
73
|
t for t in self.usage_counters[resource_type]["min"]
|
@@ -66,7 +78,8 @@ class Guardian:
|
|
66
78
|
minute_exceeded = minute_usage >= limits["min"]
|
67
79
|
|
68
80
|
if minute_exceeded:
|
69
|
-
|
81
|
+
##
|
82
|
+
|
70
83
|
from vnai.beam.metrics import collector
|
71
84
|
collector.record(
|
72
85
|
"rate_limit",
|
@@ -79,16 +92,19 @@ class Guardian:
|
|
79
92
|
},
|
80
93
|
priority="high"
|
81
94
|
)
|
82
|
-
|
95
|
+
##
|
96
|
+
|
83
97
|
raise RateLimitExceeded(
|
84
98
|
resource_type=resource_type,
|
85
99
|
limit_type="min",
|
86
100
|
current_usage=minute_usage,
|
87
101
|
limit_value=limits["min"],
|
88
|
-
retry_after=60 - (current_time % 60)
|
102
|
+
retry_after=60 - (current_time % 60) ##
|
103
|
+
|
89
104
|
)
|
90
105
|
|
91
|
-
|
106
|
+
##
|
107
|
+
|
92
108
|
hour_cutoff = current_time - 3600
|
93
109
|
self.usage_counters[resource_type]["hour"] = [
|
94
110
|
t for t in self.usage_counters[resource_type]["hour"]
|
@@ -98,7 +114,8 @@ class Guardian:
|
|
98
114
|
hour_usage = len(self.usage_counters[resource_type]["hour"])
|
99
115
|
hour_exceeded = hour_usage >= limits["hour"]
|
100
116
|
|
101
|
-
|
117
|
+
##
|
118
|
+
|
102
119
|
from vnai.beam.metrics import collector
|
103
120
|
collector.record(
|
104
121
|
"rate_limit",
|
@@ -112,26 +129,30 @@ class Guardian:
|
|
112
129
|
)
|
113
130
|
|
114
131
|
if hour_exceeded:
|
115
|
-
|
132
|
+
##
|
133
|
+
|
116
134
|
raise RateLimitExceeded(
|
117
135
|
resource_type=resource_type,
|
118
136
|
limit_type="hour",
|
119
137
|
current_usage=hour_usage,
|
120
138
|
limit_value=limits["hour"],
|
121
|
-
retry_after=3600 - (current_time % 3600)
|
139
|
+
retry_after=3600 - (current_time % 3600) ##
|
140
|
+
|
122
141
|
)
|
123
142
|
|
124
|
-
|
143
|
+
##
|
144
|
+
|
125
145
|
self.usage_counters[resource_type]["min"].append(current_time)
|
126
146
|
self.usage_counters[resource_type]["hour"].append(current_time)
|
127
147
|
return True
|
128
148
|
|
129
149
|
def usage(self, resource_type="default"):
|
130
|
-
|
150
|
+
#--
|
131
151
|
current_time = time.time()
|
132
152
|
limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
|
133
153
|
|
134
|
-
|
154
|
+
##
|
155
|
+
|
135
156
|
minute_cutoff = current_time - 60
|
136
157
|
hour_cutoff = current_time - 3600
|
137
158
|
|
@@ -145,22 +166,25 @@ class Guardian:
|
|
145
166
|
if t > hour_cutoff
|
146
167
|
]
|
147
168
|
|
148
|
-
|
169
|
+
##
|
170
|
+
|
149
171
|
minute_usage = len(self.usage_counters[resource_type]["min"])
|
150
172
|
hour_usage = len(self.usage_counters[resource_type]["hour"])
|
151
173
|
|
152
174
|
minute_percentage = (minute_usage / limits["min"]) * 100 if limits["min"] > 0 else 0
|
153
175
|
hour_percentage = (hour_usage / limits["hour"]) * 100 if limits["hour"] > 0 else 0
|
154
176
|
|
155
|
-
|
177
|
+
##
|
178
|
+
|
156
179
|
return max(minute_percentage, hour_percentage)
|
157
180
|
|
158
181
|
def get_limit_status(self, resource_type="default"):
|
159
|
-
|
182
|
+
#--
|
160
183
|
current_time = time.time()
|
161
184
|
limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
|
162
185
|
|
163
|
-
|
186
|
+
##
|
187
|
+
|
164
188
|
minute_cutoff = current_time - 60
|
165
189
|
hour_cutoff = current_time - 3600
|
166
190
|
|
@@ -185,14 +209,17 @@ class Guardian:
|
|
185
209
|
}
|
186
210
|
}
|
187
211
|
|
188
|
-
|
212
|
+
##
|
213
|
+
|
189
214
|
guardian = Guardian()
|
190
215
|
|
191
216
|
class CleanErrorContext:
|
192
|
-
|
193
|
-
|
217
|
+
#--
|
218
|
+
##
|
219
|
+
|
194
220
|
_last_message_time = 0
|
195
|
-
_message_cooldown = 5
|
221
|
+
_message_cooldown = 5 ##
|
222
|
+
|
196
223
|
|
197
224
|
def __enter__(self):
|
198
225
|
return self
|
@@ -201,64 +228,37 @@ class CleanErrorContext:
|
|
201
228
|
if exc_type is RateLimitExceeded:
|
202
229
|
current_time = time.time()
|
203
230
|
|
204
|
-
|
231
|
+
##
|
232
|
+
|
205
233
|
if current_time - CleanErrorContext._last_message_time >= CleanErrorContext._message_cooldown:
|
206
234
|
print(f"\n⚠️ {str(exc_val)}\n")
|
207
235
|
CleanErrorContext._last_message_time = current_time
|
208
236
|
|
209
|
-
|
210
|
-
|
237
|
+
##
|
238
|
+
|
239
|
+
##
|
240
|
+
|
211
241
|
import sys
|
212
242
|
sys.exit(f"Rate limit exceeded. {str(exc_val)} Process terminated.")
|
213
243
|
|
214
|
-
|
244
|
+
##
|
245
|
+
|
215
246
|
return False
|
216
247
|
return False
|
217
248
|
|
218
249
|
|
219
250
|
def optimize(resource_type='default', loop_threshold=10, time_window=5, ad_cooldown=150, content_trigger_threshold=3,
|
220
251
|
max_retries=2, backoff_factor=2, debug=False):
|
221
|
-
|
222
|
-
|
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
|
252
|
+
#--
|
253
|
+
##
|
254
|
+
|
256
255
|
if callable(resource_type):
|
257
256
|
func = resource_type
|
258
257
|
return _create_wrapper(func, 'default', loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
259
258
|
max_retries, backoff_factor, debug)
|
260
259
|
|
261
|
-
|
260
|
+
##
|
261
|
+
|
262
262
|
if loop_threshold < 2:
|
263
263
|
raise ValueError(f"loop_threshold must be at least 2, got {loop_threshold}")
|
264
264
|
if time_window <= 0:
|
@@ -270,7 +270,8 @@ def optimize(resource_type='default', loop_threshold=10, time_window=5, ad_coold
|
|
270
270
|
if backoff_factor <= 0:
|
271
271
|
raise ValueError(f"backoff_factor must be positive, got {backoff_factor}")
|
272
272
|
|
273
|
-
|
273
|
+
##
|
274
|
+
|
274
275
|
def decorator(func):
|
275
276
|
return _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
276
277
|
max_retries, backoff_factor, debug)
|
@@ -278,14 +279,17 @@ def optimize(resource_type='default', loop_threshold=10, time_window=5, ad_coold
|
|
278
279
|
|
279
280
|
def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
280
281
|
max_retries, backoff_factor, debug):
|
281
|
-
|
282
|
-
|
282
|
+
#--
|
283
|
+
##
|
284
|
+
|
283
285
|
call_history = []
|
284
286
|
last_ad_time = 0
|
285
287
|
consecutive_loop_detections = 0
|
286
|
-
session_displayed = False
|
288
|
+
session_displayed = False ##
|
289
|
+
|
287
290
|
session_start_time = time.time()
|
288
|
-
session_timeout = 1800
|
291
|
+
session_timeout = 1800 ##
|
292
|
+
|
289
293
|
|
290
294
|
@functools.wraps(func)
|
291
295
|
def wrapper(*args, **kwargs):
|
@@ -293,29 +297,36 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
293
297
|
current_time = time.time()
|
294
298
|
content_triggered = False
|
295
299
|
|
296
|
-
|
300
|
+
##
|
301
|
+
|
297
302
|
if current_time - session_start_time > session_timeout:
|
298
303
|
session_displayed = False
|
299
304
|
session_start_time = current_time
|
300
305
|
|
301
|
-
|
306
|
+
##
|
307
|
+
|
302
308
|
retries = 0
|
303
309
|
while True:
|
304
|
-
|
305
|
-
|
310
|
+
##
|
311
|
+
|
312
|
+
##
|
313
|
+
|
306
314
|
call_history.append(current_time)
|
307
315
|
|
308
|
-
|
316
|
+
##
|
317
|
+
|
309
318
|
while call_history and current_time - call_history[0] > time_window:
|
310
319
|
call_history.pop(0)
|
311
320
|
|
312
|
-
|
321
|
+
##
|
322
|
+
|
313
323
|
loop_detected = len(call_history) >= loop_threshold
|
314
324
|
|
315
325
|
if debug and loop_detected:
|
316
326
|
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
327
|
|
318
|
-
|
328
|
+
##
|
329
|
+
|
319
330
|
if loop_detected:
|
320
331
|
consecutive_loop_detections += 1
|
321
332
|
if debug:
|
@@ -323,26 +334,29 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
323
334
|
else:
|
324
335
|
consecutive_loop_detections = 0
|
325
336
|
|
326
|
-
|
327
|
-
|
328
|
-
(current_time - last_ad_time >= ad_cooldown) and
|
329
|
-
not session_displayed
|
337
|
+
##
|
338
|
+
|
339
|
+
should_show_content = (consecutive_loop_detections >= content_trigger_threshold) and (current_time - last_ad_time >= ad_cooldown) and not session_displayed
|
330
340
|
|
331
|
-
|
341
|
+
##
|
342
|
+
|
332
343
|
if should_show_content:
|
333
344
|
last_ad_time = current_time
|
334
345
|
consecutive_loop_detections = 0
|
335
346
|
content_triggered = True
|
336
|
-
session_displayed = True
|
347
|
+
session_displayed = True ##
|
348
|
+
|
337
349
|
|
338
350
|
if debug:
|
339
351
|
print(f"[OPTIMIZE] Đã kích hoạt nội dung cho {func.__name__}")
|
340
352
|
|
341
|
-
|
353
|
+
##
|
354
|
+
|
342
355
|
try:
|
343
356
|
from vnai.scope.promo import manager
|
344
357
|
|
345
|
-
|
358
|
+
##
|
359
|
+
|
346
360
|
try:
|
347
361
|
from vnai.scope.profile import inspector
|
348
362
|
environment = inspector.examine().get("environment", None)
|
@@ -351,21 +365,26 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
351
365
|
manager.present_content(context="loop")
|
352
366
|
|
353
367
|
except ImportError:
|
354
|
-
|
368
|
+
##
|
369
|
+
|
355
370
|
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
371
|
except Exception as e:
|
357
|
-
|
372
|
+
##
|
373
|
+
|
358
374
|
if debug:
|
359
375
|
print(f"[OPTIMIZE] Lỗi khi hiển thị nội dung: {str(e)}")
|
360
376
|
|
361
|
-
|
377
|
+
##
|
378
|
+
|
362
379
|
try:
|
363
|
-
|
380
|
+
##
|
381
|
+
|
364
382
|
with CleanErrorContext():
|
365
383
|
guardian.verify(func.__name__, resource_type)
|
366
384
|
|
367
385
|
except RateLimitExceeded as e:
|
368
|
-
|
386
|
+
##
|
387
|
+
|
369
388
|
from vnai.beam.metrics import collector
|
370
389
|
collector.record(
|
371
390
|
"error",
|
@@ -379,7 +398,8 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
379
398
|
priority="high"
|
380
399
|
)
|
381
400
|
|
382
|
-
|
401
|
+
##
|
402
|
+
|
383
403
|
if not session_displayed:
|
384
404
|
try:
|
385
405
|
from vnai.scope.promo import manager
|
@@ -387,21 +407,25 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
387
407
|
from vnai.scope.profile import inspector
|
388
408
|
environment = inspector.examine().get("environment", None)
|
389
409
|
manager.present_content(environment=environment, context="loop")
|
390
|
-
session_displayed = True
|
410
|
+
session_displayed = True ##
|
411
|
+
|
391
412
|
last_ad_time = current_time
|
392
413
|
except ImportError:
|
393
414
|
manager.present_content(context="loop")
|
394
415
|
session_displayed = True
|
395
416
|
last_ad_time = current_time
|
396
417
|
except Exception:
|
397
|
-
pass
|
418
|
+
pass ##
|
419
|
+
|
398
420
|
|
399
|
-
|
421
|
+
##
|
422
|
+
|
400
423
|
if retries < max_retries:
|
401
424
|
wait_time = backoff_factor ** retries
|
402
425
|
retries += 1
|
403
426
|
|
404
|
-
|
427
|
+
##
|
428
|
+
|
405
429
|
if hasattr(e, "retry_after") and e.retry_after:
|
406
430
|
wait_time = min(wait_time, e.retry_after)
|
407
431
|
|
@@ -409,18 +433,22 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
409
433
|
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
434
|
|
411
435
|
time.sleep(wait_time)
|
412
|
-
continue
|
436
|
+
continue ##
|
437
|
+
|
413
438
|
else:
|
414
|
-
|
439
|
+
##
|
440
|
+
|
415
441
|
raise
|
416
442
|
|
417
|
-
|
443
|
+
##
|
444
|
+
|
418
445
|
start_time = time.time()
|
419
446
|
success = False
|
420
447
|
error = None
|
421
448
|
|
422
449
|
try:
|
423
|
-
|
450
|
+
##
|
451
|
+
|
424
452
|
result = func(*args, **kwargs)
|
425
453
|
success = True
|
426
454
|
return result
|
@@ -428,10 +456,12 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
428
456
|
error = str(e)
|
429
457
|
raise
|
430
458
|
finally:
|
431
|
-
|
459
|
+
##
|
460
|
+
|
432
461
|
execution_time = time.time() - start_time
|
433
462
|
|
434
|
-
|
463
|
+
##
|
464
|
+
|
435
465
|
try:
|
436
466
|
from vnai.beam.metrics import collector
|
437
467
|
collector.record(
|
@@ -450,7 +480,8 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
450
480
|
}
|
451
481
|
)
|
452
482
|
|
453
|
-
|
483
|
+
##
|
484
|
+
|
454
485
|
if content_triggered:
|
455
486
|
collector.record(
|
456
487
|
"ad_opportunity",
|
@@ -463,16 +494,19 @@ def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldow
|
|
463
494
|
}
|
464
495
|
)
|
465
496
|
except ImportError:
|
466
|
-
|
497
|
+
##
|
498
|
+
|
467
499
|
pass
|
468
500
|
|
469
|
-
|
501
|
+
##
|
502
|
+
|
470
503
|
break
|
471
504
|
|
472
505
|
return wrapper
|
473
506
|
|
474
507
|
|
475
|
-
|
508
|
+
##
|
509
|
+
|
476
510
|
def rate_limit_status(resource_type="default"):
|
477
|
-
|
511
|
+
#--
|
478
512
|
return guardian.get_limit_status(resource_type)
|
vnai/flow/__init__.py
CHANGED
vnai/flow/queue.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
##
|
2
|
+
|
3
|
+
##
|
4
|
+
|
3
5
|
|
4
6
|
import time
|
5
7
|
import threading
|
@@ -8,7 +10,7 @@ from datetime import datetime
|
|
8
10
|
from pathlib import Path
|
9
11
|
|
10
12
|
class Buffer:
|
11
|
-
|
13
|
+
#--
|
12
14
|
|
13
15
|
_instance = None
|
14
16
|
_lock = threading.Lock()
|
@@ -21,13 +23,15 @@ class Buffer:
|
|
21
23
|
return cls._instance
|
22
24
|
|
23
25
|
def _initialize(self):
|
24
|
-
|
26
|
+
#--
|
25
27
|
self.data = []
|
26
28
|
self.lock = threading.Lock()
|
27
29
|
self.max_size = 1000
|
28
|
-
self.backup_interval = 300
|
30
|
+
self.backup_interval = 300 ##
|
31
|
+
|
29
32
|
|
30
|
-
|
33
|
+
##
|
34
|
+
|
31
35
|
self.home_dir = Path.home()
|
32
36
|
self.project_dir = self.home_dir / ".vnstock"
|
33
37
|
self.project_dir.mkdir(exist_ok=True)
|
@@ -35,14 +39,16 @@ class Buffer:
|
|
35
39
|
self.data_dir.mkdir(exist_ok=True)
|
36
40
|
self.backup_path = self.data_dir / "buffer_backup.json"
|
37
41
|
|
38
|
-
|
42
|
+
##
|
43
|
+
|
39
44
|
self._load_from_backup()
|
40
45
|
|
41
|
-
|
46
|
+
##
|
47
|
+
|
42
48
|
self._start_backup_thread()
|
43
49
|
|
44
50
|
def _load_from_backup(self):
|
45
|
-
|
51
|
+
#--
|
46
52
|
if self.backup_path.exists():
|
47
53
|
try:
|
48
54
|
with open(self.backup_path, 'r') as f:
|
@@ -54,7 +60,7 @@ class Buffer:
|
|
54
60
|
pass
|
55
61
|
|
56
62
|
def _save_to_backup(self):
|
57
|
-
|
63
|
+
#--
|
58
64
|
with self.lock:
|
59
65
|
if not self.data:
|
60
66
|
return
|
@@ -66,7 +72,7 @@ class Buffer:
|
|
66
72
|
pass
|
67
73
|
|
68
74
|
def _start_backup_thread(self):
|
69
|
-
|
75
|
+
#--
|
70
76
|
def backup_task():
|
71
77
|
while True:
|
72
78
|
time.sleep(self.backup_interval)
|
@@ -76,30 +82,34 @@ class Buffer:
|
|
76
82
|
backup_thread.start()
|
77
83
|
|
78
84
|
def add(self, item, category=None):
|
79
|
-
|
85
|
+
#--
|
80
86
|
with self.lock:
|
81
|
-
|
87
|
+
##
|
88
|
+
|
82
89
|
if isinstance(item, dict):
|
83
90
|
if "timestamp" not in item:
|
84
91
|
item["timestamp"] = datetime.now().isoformat()
|
85
92
|
if category:
|
86
93
|
item["category"] = category
|
87
94
|
|
88
|
-
|
95
|
+
##
|
96
|
+
|
89
97
|
self.data.append(item)
|
90
98
|
|
91
|
-
|
99
|
+
##
|
100
|
+
|
92
101
|
if len(self.data) > self.max_size:
|
93
102
|
self.data = self.data[-self.max_size:]
|
94
103
|
|
95
|
-
|
104
|
+
##
|
105
|
+
|
96
106
|
if len(self.data) % 100 == 0:
|
97
107
|
self._save_to_backup()
|
98
108
|
|
99
109
|
return len(self.data)
|
100
110
|
|
101
111
|
def get(self, count=None, category=None):
|
102
|
-
|
112
|
+
#--
|
103
113
|
with self.lock:
|
104
114
|
if category:
|
105
115
|
filtered_data = [item for item in self.data if item.get("category") == category]
|
@@ -112,7 +122,7 @@ class Buffer:
|
|
112
122
|
return filtered_data
|
113
123
|
|
114
124
|
def clear(self, category=None):
|
115
|
-
|
125
|
+
#--
|
116
126
|
with self.lock:
|
117
127
|
if category:
|
118
128
|
self.data = [item for item in self.data if item.get("category") != category]
|
@@ -123,12 +133,13 @@ class Buffer:
|
|
123
133
|
return len(self.data)
|
124
134
|
|
125
135
|
def size(self, category=None):
|
126
|
-
|
136
|
+
#--
|
127
137
|
with self.lock:
|
128
138
|
if category:
|
129
139
|
return len([item for item in self.data if item.get("category") == category])
|
130
140
|
else:
|
131
141
|
return len(self.data)
|
132
142
|
|
133
|
-
|
143
|
+
##
|
144
|
+
|
134
145
|
buffer = Buffer()
|