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/flow/relay.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
##
|
4
|
-
|
1
|
+
# vnai/flow/relay.py
|
2
|
+
# Data transmission system (formerly sync)
|
5
3
|
|
6
4
|
import time
|
7
5
|
import threading
|
@@ -13,7 +11,7 @@ from pathlib import Path
|
|
13
11
|
from typing import Dict, List, Any, Optional
|
14
12
|
|
15
13
|
class Conduit:
|
16
|
-
|
14
|
+
"""Handles system telemetry flow"""
|
17
15
|
|
18
16
|
_instance = None
|
19
17
|
_lock = threading.Lock()
|
@@ -26,13 +24,12 @@ class Conduit:
|
|
26
24
|
return cls._instance
|
27
25
|
|
28
26
|
def _initialize(self, webhook_url, buffer_size, sync_interval):
|
29
|
-
|
27
|
+
"""Initialize conduit"""
|
30
28
|
self.webhook_url = webhook_url
|
31
29
|
self.buffer_size = buffer_size
|
32
30
|
self.sync_interval = sync_interval
|
33
31
|
|
34
|
-
|
35
|
-
|
32
|
+
# Separate buffers for different data types
|
36
33
|
self.buffer = {
|
37
34
|
"function_calls": [],
|
38
35
|
"api_requests": [],
|
@@ -44,8 +41,7 @@ class Conduit:
|
|
44
41
|
self.sync_count = 0
|
45
42
|
self.failed_queue = []
|
46
43
|
|
47
|
-
|
48
|
-
|
44
|
+
# Home directory setup
|
49
45
|
self.home_dir = Path.home()
|
50
46
|
self.project_dir = self.home_dir / ".vnstock"
|
51
47
|
self.project_dir.mkdir(exist_ok=True)
|
@@ -53,31 +49,27 @@ class Conduit:
|
|
53
49
|
self.data_dir.mkdir(exist_ok=True)
|
54
50
|
self.config_path = self.data_dir / "relay_config.json"
|
55
51
|
|
56
|
-
|
57
|
-
|
52
|
+
# Get machine identifier from system profile
|
58
53
|
try:
|
59
54
|
from vnai.scope.profile import inspector
|
60
55
|
self.machine_id = inspector.fingerprint()
|
61
56
|
except:
|
62
57
|
self.machine_id = self._generate_fallback_id()
|
63
58
|
|
64
|
-
|
65
|
-
|
59
|
+
# Load config if exists
|
66
60
|
self._load_config()
|
67
61
|
|
68
|
-
|
69
|
-
|
62
|
+
# Start periodic sync
|
70
63
|
self._start_periodic_sync()
|
71
64
|
|
72
65
|
def _generate_fallback_id(self) -> str:
|
73
|
-
|
66
|
+
"""Generate a fallback machine identifier if profile is unavailable"""
|
74
67
|
try:
|
75
68
|
import platform
|
76
69
|
import hashlib
|
77
70
|
import uuid
|
78
71
|
|
79
|
-
|
80
|
-
|
72
|
+
# Try to get machine-specific information
|
81
73
|
system_info = platform.node() + platform.platform() + platform.processor()
|
82
74
|
return hashlib.md5(system_info.encode()).hexdigest()
|
83
75
|
except:
|
@@ -85,7 +77,7 @@ class Conduit:
|
|
85
77
|
return str(uuid.uuid4())
|
86
78
|
|
87
79
|
def _load_config(self):
|
88
|
-
|
80
|
+
"""Load configuration from file"""
|
89
81
|
if self.config_path.exists():
|
90
82
|
try:
|
91
83
|
with open(self.config_path, 'r') as f:
|
@@ -105,7 +97,7 @@ class Conduit:
|
|
105
97
|
pass
|
106
98
|
|
107
99
|
def _save_config(self):
|
108
|
-
|
100
|
+
"""Save configuration to file"""
|
109
101
|
config = {
|
110
102
|
'webhook_url': self.webhook_url,
|
111
103
|
'buffer_size': self.buffer_size,
|
@@ -121,7 +113,7 @@ class Conduit:
|
|
121
113
|
pass
|
122
114
|
|
123
115
|
def _start_periodic_sync(self):
|
124
|
-
|
116
|
+
"""Start periodic sync thread"""
|
125
117
|
def periodic_sync():
|
126
118
|
while True:
|
127
119
|
time.sleep(self.sync_interval)
|
@@ -131,9 +123,8 @@ class Conduit:
|
|
131
123
|
sync_thread.start()
|
132
124
|
|
133
125
|
def add_function_call(self, record):
|
134
|
-
|
135
|
-
|
136
|
-
|
126
|
+
"""Add function call record"""
|
127
|
+
# Ensure record is a dictionary
|
137
128
|
if not isinstance(record, dict):
|
138
129
|
record = {"value": str(record)}
|
139
130
|
|
@@ -142,9 +133,8 @@ class Conduit:
|
|
142
133
|
self._check_triggers("function_calls")
|
143
134
|
|
144
135
|
def add_api_request(self, record):
|
145
|
-
|
146
|
-
|
147
|
-
|
136
|
+
"""Add API request record"""
|
137
|
+
# Ensure record is a dictionary
|
148
138
|
if not isinstance(record, dict):
|
149
139
|
record = {"value": str(record)}
|
150
140
|
|
@@ -153,9 +143,8 @@ class Conduit:
|
|
153
143
|
self._check_triggers("api_requests")
|
154
144
|
|
155
145
|
def add_rate_limit(self, record):
|
156
|
-
|
157
|
-
|
158
|
-
|
146
|
+
"""Add rate limit record"""
|
147
|
+
# Ensure record is a dictionary
|
159
148
|
if not isinstance(record, dict):
|
160
149
|
record = {"value": str(record)}
|
161
150
|
|
@@ -164,36 +153,33 @@ class Conduit:
|
|
164
153
|
self._check_triggers("rate_limits")
|
165
154
|
|
166
155
|
def _check_triggers(self, record_type: str):
|
167
|
-
|
156
|
+
"""Check if any sync triggers are met"""
|
168
157
|
current_time = time.time()
|
169
158
|
should_trigger = False
|
170
159
|
trigger_reason = None
|
171
160
|
|
172
|
-
|
173
|
-
|
161
|
+
# Get total buffer size
|
174
162
|
total_records = sum(len(buffer) for buffer in self.buffer.values())
|
175
163
|
|
176
|
-
|
177
|
-
|
164
|
+
# SIZE TRIGGER: Buffer size threshold reached
|
178
165
|
if total_records >= self.buffer_size:
|
179
166
|
should_trigger = True
|
180
167
|
trigger_reason = "buffer_full"
|
181
168
|
|
182
|
-
|
183
|
-
|
184
|
-
|
169
|
+
# EVENT TRIGGER: Critical events (errors, rate limit warnings)
|
170
|
+
elif record_type == "rate_limits" and self.buffer["rate_limits"] and \
|
171
|
+
any(item.get("is_exceeded") for item in self.buffer["rate_limits"] if isinstance(item, dict)):
|
185
172
|
should_trigger = True
|
186
173
|
trigger_reason = "rate_limit_exceeded"
|
187
|
-
elif record_type == "function_calls" and self.buffer["function_calls"] and
|
174
|
+
elif record_type == "function_calls" and self.buffer["function_calls"] and \
|
175
|
+
any(not item.get("success") for item in self.buffer["function_calls"] if isinstance(item, dict)):
|
188
176
|
should_trigger = True
|
189
177
|
trigger_reason = "function_error"
|
190
178
|
|
191
|
-
|
192
|
-
|
179
|
+
# TIME-WEIGHTED RANDOM TRIGGER: More likely as time since last sync increases
|
193
180
|
else:
|
194
181
|
time_factor = min(1.0, (current_time - self.last_sync_time) / (self.sync_interval / 2))
|
195
|
-
if random.random() < 0.05 * time_factor:
|
196
|
-
|
182
|
+
if random.random() < 0.05 * time_factor: # 0-5% chance based on time
|
197
183
|
should_trigger = True
|
198
184
|
trigger_reason = "random_time_weighted"
|
199
185
|
|
@@ -205,32 +191,27 @@ class Conduit:
|
|
205
191
|
).start()
|
206
192
|
|
207
193
|
def queue(self, package, priority=None):
|
208
|
-
|
194
|
+
"""Queue data package"""
|
209
195
|
if not package:
|
210
196
|
return False
|
211
197
|
|
212
|
-
|
213
|
-
|
198
|
+
# Handle non-dictionary packages
|
214
199
|
if not isinstance(package, dict):
|
215
200
|
self.add_function_call({"message": str(package)})
|
216
201
|
return True
|
217
202
|
|
218
|
-
|
219
|
-
|
203
|
+
# Add timestamp if not present
|
220
204
|
if "timestamp" not in package:
|
221
205
|
package["timestamp"] = datetime.now().isoformat()
|
222
206
|
|
223
|
-
|
224
|
-
|
207
|
+
# Route based on package type
|
225
208
|
if "type" in package:
|
226
209
|
package_type = package["type"]
|
227
210
|
data = package.get("data", {})
|
228
211
|
|
229
|
-
|
230
|
-
|
212
|
+
# Remove system info if present to avoid duplication
|
231
213
|
if isinstance(data, dict) and "system" in data:
|
232
|
-
|
233
|
-
|
214
|
+
# Get machine_id for reference but don't duplicate the whole system info
|
234
215
|
machine_id = data["system"].get("machine_id")
|
235
216
|
data.pop("system")
|
236
217
|
if machine_id:
|
@@ -243,10 +224,8 @@ class Conduit:
|
|
243
224
|
elif package_type == "rate_limit":
|
244
225
|
self.add_rate_limit(data)
|
245
226
|
elif package_type == "system_info":
|
246
|
-
|
247
|
-
|
248
|
-
##
|
249
|
-
|
227
|
+
# For system info, we'll add it as a special function call
|
228
|
+
# but remove duplicated data
|
250
229
|
self.add_function_call({
|
251
230
|
"type": "system_info",
|
252
231
|
"commercial": data.get("commercial"),
|
@@ -254,8 +233,7 @@ class Conduit:
|
|
254
233
|
"timestamp": package.get("timestamp")
|
255
234
|
})
|
256
235
|
elif package_type == "metrics":
|
257
|
-
|
258
|
-
|
236
|
+
# Handle metrics package with multiple categories
|
259
237
|
metrics_data = data
|
260
238
|
for metric_type, metrics_list in metrics_data.items():
|
261
239
|
if isinstance(metrics_list, list):
|
@@ -269,68 +247,58 @@ class Conduit:
|
|
269
247
|
for item in metrics_list:
|
270
248
|
self.add_api_request(item)
|
271
249
|
else:
|
272
|
-
|
273
|
-
|
250
|
+
# Default to function calls
|
274
251
|
self.add_function_call(data)
|
275
252
|
else:
|
276
|
-
|
277
|
-
|
253
|
+
# No type specified, default to function call
|
278
254
|
self.add_function_call(package)
|
279
255
|
|
280
|
-
|
281
|
-
|
256
|
+
# Handle high priority items
|
282
257
|
if priority == "high":
|
283
258
|
self.dispatch("high_priority")
|
284
259
|
|
285
260
|
return True
|
286
261
|
|
287
262
|
def dispatch(self, reason="manual"):
|
288
|
-
|
263
|
+
"""Send queued data"""
|
289
264
|
if not self.webhook_url:
|
290
265
|
return False
|
291
266
|
|
292
267
|
with self.lock:
|
293
|
-
|
294
|
-
|
268
|
+
# Check if all buffers are empty
|
295
269
|
if all(len(records) == 0 for records in self.buffer.values()):
|
296
270
|
return False
|
297
271
|
|
298
|
-
|
299
|
-
|
272
|
+
# Create a copy of the buffer for sending
|
300
273
|
data_to_send = {
|
301
274
|
"function_calls": self.buffer["function_calls"].copy(),
|
302
275
|
"api_requests": self.buffer["api_requests"].copy(),
|
303
276
|
"rate_limits": self.buffer["rate_limits"].copy()
|
304
277
|
}
|
305
278
|
|
306
|
-
|
307
|
-
|
279
|
+
# Clear buffer
|
308
280
|
self.buffer = {
|
309
281
|
"function_calls": [],
|
310
282
|
"api_requests": [],
|
311
283
|
"rate_limits": []
|
312
284
|
}
|
313
285
|
|
314
|
-
|
315
|
-
|
286
|
+
# Update sync time and count
|
316
287
|
self.last_sync_time = time.time()
|
317
288
|
self.sync_count += 1
|
318
289
|
self._save_config()
|
319
290
|
|
320
|
-
|
321
|
-
|
291
|
+
# Get environment information ONCE
|
322
292
|
try:
|
323
293
|
from vnai.scope.profile import inspector
|
324
294
|
environment_info = inspector.examine()
|
325
295
|
machine_id = environment_info.get("machine_id", self.machine_id)
|
326
296
|
except:
|
327
|
-
|
328
|
-
|
297
|
+
# Fallback if environment info isn't available
|
329
298
|
environment_info = {"machine_id": self.machine_id}
|
330
299
|
machine_id = self.machine_id
|
331
300
|
|
332
|
-
|
333
|
-
|
301
|
+
# Create payload with environment info only in metadata
|
334
302
|
payload = {
|
335
303
|
"analytics_data": data_to_send,
|
336
304
|
"metadata": {
|
@@ -347,8 +315,7 @@ class Conduit:
|
|
347
315
|
}
|
348
316
|
}
|
349
317
|
|
350
|
-
|
351
|
-
|
318
|
+
# Send data
|
352
319
|
success = self._send_data(payload)
|
353
320
|
|
354
321
|
if not success:
|
@@ -360,7 +327,7 @@ class Conduit:
|
|
360
327
|
return success
|
361
328
|
|
362
329
|
def _send_data(self, payload):
|
363
|
-
|
330
|
+
"""Send data to webhook"""
|
364
331
|
if not self.webhook_url:
|
365
332
|
return False
|
366
333
|
|
@@ -368,8 +335,7 @@ class Conduit:
|
|
368
335
|
response = requests.post(
|
369
336
|
self.webhook_url,
|
370
337
|
json=payload,
|
371
|
-
timeout=5
|
372
|
-
|
338
|
+
timeout=5 # 5 second timeout
|
373
339
|
)
|
374
340
|
|
375
341
|
return response.status_code == 200
|
@@ -377,7 +343,7 @@ class Conduit:
|
|
377
343
|
return False
|
378
344
|
|
379
345
|
def retry_failed(self):
|
380
|
-
|
346
|
+
"""Retry sending failed data"""
|
381
347
|
if not self.failed_queue:
|
382
348
|
return 0
|
383
349
|
|
@@ -396,20 +362,18 @@ class Conduit:
|
|
396
362
|
return success_count
|
397
363
|
|
398
364
|
def configure(self, webhook_url):
|
399
|
-
|
365
|
+
"""Configure webhook URL"""
|
400
366
|
with self.lock:
|
401
367
|
self.webhook_url = webhook_url
|
402
368
|
self._save_config()
|
403
369
|
return True
|
404
370
|
|
405
|
-
|
406
|
-
|
371
|
+
# Create singleton instance
|
407
372
|
conduit = Conduit()
|
408
373
|
|
409
|
-
|
410
|
-
|
374
|
+
# Exposed functions that match sync.py naming pattern
|
411
375
|
def track_function_call(function_name, source, execution_time, success=True, error=None, args=None):
|
412
|
-
|
376
|
+
"""Track function call (bridge to add_function_call)"""
|
413
377
|
record = {
|
414
378
|
"function": function_name,
|
415
379
|
"source": source,
|
@@ -422,8 +386,7 @@ def track_function_call(function_name, source, execution_time, success=True, err
|
|
422
386
|
record["error"] = error
|
423
387
|
|
424
388
|
if args:
|
425
|
-
|
426
|
-
|
389
|
+
# Sanitize arguments
|
427
390
|
sanitized_args = {}
|
428
391
|
if isinstance(args, dict):
|
429
392
|
for key, value in args.items():
|
@@ -438,7 +401,7 @@ def track_function_call(function_name, source, execution_time, success=True, err
|
|
438
401
|
conduit.add_function_call(record)
|
439
402
|
|
440
403
|
def track_rate_limit(source, limit_type, limit_value, current_usage, is_exceeded):
|
441
|
-
|
404
|
+
"""Track rate limit checks (bridge to add_rate_limit)"""
|
442
405
|
record = {
|
443
406
|
"source": source,
|
444
407
|
"limit_type": limit_type,
|
@@ -452,7 +415,7 @@ def track_rate_limit(source, limit_type, limit_value, current_usage, is_exceeded
|
|
452
415
|
conduit.add_rate_limit(record)
|
453
416
|
|
454
417
|
def track_api_request(endpoint, source, method, status_code, execution_time, request_size=0, response_size=0):
|
455
|
-
|
418
|
+
"""Track API requests (bridge to add_api_request)"""
|
456
419
|
record = {
|
457
420
|
"endpoint": endpoint,
|
458
421
|
"source": source,
|
@@ -467,13 +430,13 @@ def track_api_request(endpoint, source, method, status_code, execution_time, req
|
|
467
430
|
conduit.add_api_request(record)
|
468
431
|
|
469
432
|
def configure(webhook_url):
|
470
|
-
|
433
|
+
"""Configure webhook URL"""
|
471
434
|
return conduit.configure(webhook_url)
|
472
435
|
|
473
436
|
def sync_now():
|
474
|
-
|
437
|
+
"""Manually trigger synchronization"""
|
475
438
|
return conduit.dispatch("manual")
|
476
439
|
|
477
440
|
def retry_failed():
|
478
|
-
|
441
|
+
"""Retry failed synchronizations"""
|
479
442
|
return conduit.retry_failed()
|