vnai 0.1.3__py3-none-any.whl → 2.3.7__py3-none-any.whl

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