abdocode 1.2.0__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.
hotkey_engine/core.py
ADDED
|
@@ -0,0 +1,1455 @@
|
|
|
1
|
+
from tkinter import messagebox, simpledialog
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
import numpy as np
|
|
4
|
+
import sounddevice as sd
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import re
|
|
8
|
+
import string
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import statistics
|
|
14
|
+
import random
|
|
15
|
+
import datetime
|
|
16
|
+
import urllib.request
|
|
17
|
+
import urllib.parse
|
|
18
|
+
import urllib.error
|
|
19
|
+
import subprocess
|
|
20
|
+
import shutil
|
|
21
|
+
import logging
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from collections import defaultdict, deque
|
|
24
|
+
|
|
25
|
+
_used_shortcuts = {}
|
|
26
|
+
_enabled = True
|
|
27
|
+
_clipboard_history = []
|
|
28
|
+
_timer_registry = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# =========================
|
|
32
|
+
# CORE SAFETY FUNCTION
|
|
33
|
+
# =========================
|
|
34
|
+
|
|
35
|
+
def _safe_event(root, event):
|
|
36
|
+
widget = root.focus_get()
|
|
37
|
+
if widget:
|
|
38
|
+
try:
|
|
39
|
+
widget.event_generate(event)
|
|
40
|
+
except:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =========================
|
|
45
|
+
# BASIC SHORTCUT SYSTEM
|
|
46
|
+
# =========================
|
|
47
|
+
|
|
48
|
+
def add_shortcut(root, key, event):
|
|
49
|
+
if key in _used_shortcuts:
|
|
50
|
+
messagebox.showwarning(
|
|
51
|
+
"Hotkey Warning",
|
|
52
|
+
f"{key} already used for { _used_shortcuts[key] }"
|
|
53
|
+
)
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def handler(e):
|
|
57
|
+
if not _enabled:
|
|
58
|
+
return
|
|
59
|
+
_safe_event(root, event)
|
|
60
|
+
|
|
61
|
+
root.bind_all(key, handler)
|
|
62
|
+
_used_shortcuts[key] = event
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def remove_shortcut(root, key):
|
|
67
|
+
if key in _used_shortcuts:
|
|
68
|
+
root.unbind_all(key)
|
|
69
|
+
del _used_shortcuts[key]
|
|
70
|
+
return True
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def disable_all():
|
|
75
|
+
global _enabled
|
|
76
|
+
_enabled = False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def enable_all():
|
|
80
|
+
global _enabled
|
|
81
|
+
_enabled = True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def reset_all(root):
|
|
85
|
+
for key in list(_used_shortcuts.keys()):
|
|
86
|
+
root.unbind_all(key)
|
|
87
|
+
_used_shortcuts.clear()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def list_shortcuts():
|
|
91
|
+
return _used_shortcuts.copy()
|
|
92
|
+
def copy(root): add_shortcut(root, "<Control-c>", "<<Copy>>")
|
|
93
|
+
def paste(root): add_shortcut(root, "<Control-v>", "<<Paste>>")
|
|
94
|
+
def cut(root): add_shortcut(root, "<Control-x>", "<<Cut>>")
|
|
95
|
+
def select_all(root): add_shortcut(root, "<Control-a>", "<<SelectAll>>")
|
|
96
|
+
def undo(root): add_shortcut(root, "<Control-z>", "<<Undo>>")
|
|
97
|
+
def redo(root): add_shortcut(root, "<Control-y>", "<<Redo>>")
|
|
98
|
+
def find(root): add_shortcut(root, "<Control-f>", "<<Find>>")
|
|
99
|
+
def replace(root): add_shortcut(root, "<Control-h>", "<<Replace>>")
|
|
100
|
+
def goto(root): add_shortcut(root, "<Control-g>", "<<Goto>>")
|
|
101
|
+
def duplicate(root): add_shortcut(root, "<Control-d>", "<<Duplicate>>")
|
|
102
|
+
def select_line(root): add_shortcut(root, "<Control-l>", "<<SelectLine>>")
|
|
103
|
+
def delete_word(root): add_shortcut(root, "<Control-BackSpace>", "<<DeleteWord>>")
|
|
104
|
+
def delete_line(root): add_shortcut(root, "<Control-Delete>", "<<DeleteLine>>")
|
|
105
|
+
def cut_alt(root): add_shortcut(root, "<Shift-Delete>", "<<Cut>>")
|
|
106
|
+
def home(root): add_shortcut(root, "<Home>", "<<Home>>")
|
|
107
|
+
def end(root): add_shortcut(root, "<End>", "<<End>>")
|
|
108
|
+
def word_left(root): add_shortcut(root, "<Control-Left>", "<<WordLeft>>")
|
|
109
|
+
def word_right(root): add_shortcut(root, "<Control-Right>", "<<WordRight>>")
|
|
110
|
+
def page_up(root): add_shortcut(root, "<Control-Up>", "<<PageUp>>")
|
|
111
|
+
def page_down(root): add_shortcut(root, "<Control-Down>", "<<PageDown>>")
|
|
112
|
+
def select_left(root): add_shortcut(root, "<Shift-Left>", "<<SelectLeft>>")
|
|
113
|
+
def select_right(root): add_shortcut(root, "<Shift-Right>", "<<SelectRight>>")
|
|
114
|
+
def select_up(root): add_shortcut(root, "<Shift-Up>", "<<SelectUp>>")
|
|
115
|
+
def select_down(root): add_shortcut(root, "<Shift-Down>", "<<SelectDown>>")
|
|
116
|
+
def select_word_left(root): add_shortcut(root, "<Control-Shift-Left>", "<<SelectWordLeft>>")
|
|
117
|
+
def select_word_right(root): add_shortcut(root, "<Control-Shift-Right>", "<<SelectWordRight>>")
|
|
118
|
+
def save(root): add_shortcut(root, "<Control-s>", "<<Save>>")
|
|
119
|
+
def open_file(root): add_shortcut(root, "<Control-o>", "<<Open>>")
|
|
120
|
+
def new_file(root): add_shortcut(root, "<Control-n>", "<<New>>")
|
|
121
|
+
def print_file(root): add_shortcut(root, "<Control-p>", "<<Print>>")
|
|
122
|
+
def refresh(root): add_shortcut(root, "<F5>", "<<Refresh>>")
|
|
123
|
+
def use_all(root):
|
|
124
|
+
copy(root)
|
|
125
|
+
paste(root)
|
|
126
|
+
cut(root)
|
|
127
|
+
select_all(root)
|
|
128
|
+
undo(root)
|
|
129
|
+
redo(root)
|
|
130
|
+
find(root)
|
|
131
|
+
replace(root)
|
|
132
|
+
goto(root)
|
|
133
|
+
duplicate(root)
|
|
134
|
+
select_line(root)
|
|
135
|
+
delete_word(root)
|
|
136
|
+
delete_line(root)
|
|
137
|
+
cut_alt(root)
|
|
138
|
+
home(root)
|
|
139
|
+
end(root)
|
|
140
|
+
word_left(root)
|
|
141
|
+
word_right(root)
|
|
142
|
+
page_up(root)
|
|
143
|
+
page_down(root)
|
|
144
|
+
select_left(root)
|
|
145
|
+
select_right(root)
|
|
146
|
+
select_up(root)
|
|
147
|
+
select_down(root)
|
|
148
|
+
select_word_left(root)
|
|
149
|
+
select_word_right(root)
|
|
150
|
+
save(root)
|
|
151
|
+
open_file(root)
|
|
152
|
+
new_file(root)
|
|
153
|
+
print_file(root)
|
|
154
|
+
refresh(root)
|
|
155
|
+
def show_all_shortcuts():
|
|
156
|
+
shortcuts = list_shortcuts()
|
|
157
|
+
if not shortcuts:
|
|
158
|
+
messagebox.showinfo("Hotkeys", "No shortcuts defined.")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
msg = "Current Shortcuts:\n\n"
|
|
162
|
+
for key, event in shortcuts.items():
|
|
163
|
+
msg += f"{key} -> {event}\n"
|
|
164
|
+
|
|
165
|
+
messagebox.showinfo("Hotkeys", msg)
|
|
166
|
+
def shortcuts_popup_list(root):
|
|
167
|
+
shortcuts = list_shortcuts()
|
|
168
|
+
if not shortcuts:
|
|
169
|
+
messagebox.showinfo("Hotkeys", "No shortcuts defined.")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
msg = "Current Shortcuts:\n\n"
|
|
173
|
+
for key, event in shortcuts.items():
|
|
174
|
+
msg += f"{key} -> {event}\n"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
shortcuts_list = tk.Listbox(root)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
for key, event in shortcuts.items():
|
|
181
|
+
shortcuts_list.insert(tk.END, f"{key} -> {event}")
|
|
182
|
+
root.bind_all("<Button-3>", lambda e: shortcuts_list.tk_popup(e.x_root, e.y_root))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# =========================
|
|
186
|
+
# CLIPBOARD UTILITIES
|
|
187
|
+
# =========================
|
|
188
|
+
|
|
189
|
+
def _add_clipboard_history(text):
|
|
190
|
+
text = str(text)
|
|
191
|
+
if text and (_clipboard_history == [] or _clipboard_history[-1] != text):
|
|
192
|
+
_clipboard_history.append(text)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def write_clipboard(root, text):
|
|
196
|
+
try:
|
|
197
|
+
root.clipboard_clear()
|
|
198
|
+
root.clipboard_append(str(text))
|
|
199
|
+
root.update()
|
|
200
|
+
_add_clipboard_history(text)
|
|
201
|
+
return True
|
|
202
|
+
except Exception:
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def read_clipboard(root):
|
|
207
|
+
try:
|
|
208
|
+
return root.clipboard_get()
|
|
209
|
+
except Exception:
|
|
210
|
+
return ""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def clear_clipboard(root):
|
|
214
|
+
try:
|
|
215
|
+
root.clipboard_clear()
|
|
216
|
+
root.update()
|
|
217
|
+
return True
|
|
218
|
+
except Exception:
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_clipboard_history():
|
|
223
|
+
return list(_clipboard_history)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def paste_clipboard_history_item(root, index):
|
|
227
|
+
try:
|
|
228
|
+
text = _clipboard_history[index]
|
|
229
|
+
return write_clipboard(root, text)
|
|
230
|
+
except Exception:
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# =========================
|
|
235
|
+
# FILE UTILITIES
|
|
236
|
+
# =========================
|
|
237
|
+
|
|
238
|
+
def read_text_file(path, encoding='utf-8'):
|
|
239
|
+
path_obj = Path(path)
|
|
240
|
+
with path_obj.open('r', encoding=encoding) as file:
|
|
241
|
+
return file.read()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def write_text_file(path, text, encoding='utf-8'):
|
|
245
|
+
path_obj = Path(path)
|
|
246
|
+
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
with path_obj.open('w', encoding=encoding) as file:
|
|
248
|
+
file.write(str(text))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def append_text_file(path, text, encoding='utf-8'):
|
|
252
|
+
path_obj = Path(path)
|
|
253
|
+
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
254
|
+
with path_obj.open('a', encoding=encoding) as file:
|
|
255
|
+
file.write(str(text))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def list_files(folder, extension=None, recursive=False):
|
|
259
|
+
base = Path(folder)
|
|
260
|
+
if recursive:
|
|
261
|
+
items = base.rglob('*')
|
|
262
|
+
else:
|
|
263
|
+
items = base.glob('*')
|
|
264
|
+
files = [str(p) for p in items if p.is_file()]
|
|
265
|
+
if extension:
|
|
266
|
+
return [f for f in files if f.lower().endswith(extension.lower())]
|
|
267
|
+
return files
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def find_files(folder, pattern, recursive=True, extension=None):
|
|
271
|
+
base = Path(folder)
|
|
272
|
+
matcher = re.compile(pattern)
|
|
273
|
+
paths = base.rglob('*') if recursive else base.glob('*')
|
|
274
|
+
matches = []
|
|
275
|
+
for p in paths:
|
|
276
|
+
if p.is_file():
|
|
277
|
+
if matcher.search(str(p)):
|
|
278
|
+
if extension is None or str(p).lower().endswith(extension.lower()):
|
|
279
|
+
matches.append(str(p))
|
|
280
|
+
return matches
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# =========================
|
|
284
|
+
# TEXT UTILITIES
|
|
285
|
+
# =========================
|
|
286
|
+
|
|
287
|
+
def normalize_whitespace(text):
|
|
288
|
+
return re.sub(r'\s+', ' ', str(text)).strip()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def count_words(text):
|
|
292
|
+
return len(normalize_whitespace(text).split(' ')) if normalize_whitespace(text) else 0
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def to_title_case(text):
|
|
296
|
+
return str(text).title()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def snake_to_camel(text):
|
|
300
|
+
parts = str(text).split('_')
|
|
301
|
+
return parts[0].lower() + ''.join(word.capitalize() for word in parts[1:])
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def camel_to_snake(text):
|
|
305
|
+
s = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', str(text))
|
|
306
|
+
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s).lower()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def remove_punctuation(text):
|
|
310
|
+
return str(text).translate(str.maketrans('', '', string.punctuation))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# =========================
|
|
314
|
+
# TIMER & SCHEDULER UTILITIES
|
|
315
|
+
# =========================
|
|
316
|
+
|
|
317
|
+
def set_timeout(callback, delay, *args, **kwargs):
|
|
318
|
+
timer = threading.Timer(delay, callback, args=args, kwargs=kwargs)
|
|
319
|
+
timer.daemon = True
|
|
320
|
+
timer.start()
|
|
321
|
+
timer_id = id(timer)
|
|
322
|
+
_timer_registry[timer_id] = timer
|
|
323
|
+
return timer_id
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _interval_runner(timer_id, callback, interval, args, kwargs):
|
|
327
|
+
if timer_id not in _timer_registry:
|
|
328
|
+
return
|
|
329
|
+
callback(*args, **kwargs)
|
|
330
|
+
timer = threading.Timer(interval, _interval_runner, args=(timer_id, callback, interval, args, kwargs))
|
|
331
|
+
timer.daemon = True
|
|
332
|
+
_timer_registry[timer_id] = timer
|
|
333
|
+
timer.start()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def set_interval(callback, interval, *args, **kwargs):
|
|
337
|
+
timer_id = int(time.time() * 1000000)
|
|
338
|
+
timer = threading.Timer(interval, _interval_runner, args=(timer_id, callback, interval, args, kwargs))
|
|
339
|
+
timer.daemon = True
|
|
340
|
+
_timer_registry[timer_id] = timer
|
|
341
|
+
timer.start()
|
|
342
|
+
return timer_id
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def cancel_timer(timer_id):
|
|
346
|
+
timer = _timer_registry.pop(timer_id, None)
|
|
347
|
+
if timer:
|
|
348
|
+
timer.cancel()
|
|
349
|
+
return True
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def active_timers():
|
|
354
|
+
return list(_timer_registry.keys())
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# =========================
|
|
358
|
+
# SYSTEM UTILITIES
|
|
359
|
+
# =========================
|
|
360
|
+
|
|
361
|
+
def get_system_info():
|
|
362
|
+
try:
|
|
363
|
+
user = os.getlogin()
|
|
364
|
+
except Exception:
|
|
365
|
+
user = None
|
|
366
|
+
return {
|
|
367
|
+
'platform': platform.system(),
|
|
368
|
+
'release': platform.release(),
|
|
369
|
+
'version': platform.version(),
|
|
370
|
+
'machine': platform.machine(),
|
|
371
|
+
'processor': platform.processor(),
|
|
372
|
+
'python_version': platform.python_version(),
|
|
373
|
+
'cwd': os.getcwd(),
|
|
374
|
+
'user': user,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def get_environment_variables():
|
|
379
|
+
return dict(os.environ)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def open_folder(path):
|
|
383
|
+
path_obj = Path(path)
|
|
384
|
+
if not path_obj.exists():
|
|
385
|
+
return False
|
|
386
|
+
try:
|
|
387
|
+
if platform.system() == 'Windows':
|
|
388
|
+
os.startfile(str(path_obj))
|
|
389
|
+
elif platform.system() == 'Darwin':
|
|
390
|
+
os.system(f'open "{path_obj}"')
|
|
391
|
+
else:
|
|
392
|
+
os.system(f'xdg-open "{path_obj}"')
|
|
393
|
+
return True
|
|
394
|
+
except Exception:
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def get_file_size(path):
|
|
399
|
+
path_obj = Path(path)
|
|
400
|
+
return path_obj.stat().st_size if path_obj.exists() else 0
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# =========================
|
|
404
|
+
# UI CONVENIENCE SHORTCUTS
|
|
405
|
+
# =========================
|
|
406
|
+
|
|
407
|
+
def show_info(title, message):
|
|
408
|
+
messagebox.showinfo(title, str(message))
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def show_warning(title, message):
|
|
412
|
+
messagebox.showwarning(title, str(message))
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def show_error(title, message):
|
|
416
|
+
messagebox.showerror(title, str(message))
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def ask_yes_no(title, message):
|
|
420
|
+
return messagebox.askyesno(title, str(message))
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def ask_input(title, prompt, default=''):
|
|
424
|
+
return simpledialog.askstring(title, prompt, initialvalue=default)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def simple_popup(root, title, message):
|
|
428
|
+
popup = tk.Toplevel(root)
|
|
429
|
+
popup.title(title)
|
|
430
|
+
label = tk.Label(popup, text=str(message), padx=10, pady=10)
|
|
431
|
+
label.pack()
|
|
432
|
+
btn = tk.Button(popup, text='OK', command=popup.destroy, padx=8, pady=4)
|
|
433
|
+
btn.pack(pady=10)
|
|
434
|
+
popup.transient(root)
|
|
435
|
+
popup.grab_set()
|
|
436
|
+
root.wait_window(popup)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def note_to_frequency(note):
|
|
440
|
+
note = note.strip()
|
|
441
|
+
if note.upper() == "REST":
|
|
442
|
+
return 0.0
|
|
443
|
+
|
|
444
|
+
octave = int(note[-1])
|
|
445
|
+
name = note[:-1]
|
|
446
|
+
if len(name) > 1 and name[-1] in "#b":
|
|
447
|
+
base_name = name
|
|
448
|
+
else:
|
|
449
|
+
base_name = name
|
|
450
|
+
|
|
451
|
+
semitone_map = {
|
|
452
|
+
'C': 0, 'C#': 1, 'Db': 1,
|
|
453
|
+
'D': 2, 'D#': 3, 'Eb': 3,
|
|
454
|
+
'E': 4, 'F': 5, 'F#': 6,
|
|
455
|
+
'Gb': 6, 'G': 7, 'G#': 8,
|
|
456
|
+
'Ab': 8, 'A': 9, 'A#': 10,
|
|
457
|
+
'Bb': 10, 'B': 11,
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
semitone = semitone_map.get(base_name, 0) + (octave + 1) * 12
|
|
461
|
+
return 440.0 * 2 ** ((semitone - 69) / 12.0)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def play_tone(note, duration=0.2, volume=0.5, waveform='sine'):
|
|
465
|
+
freq = note_to_frequency(note) if isinstance(note, str) else note
|
|
466
|
+
if freq == 0:
|
|
467
|
+
sd.sleep(int(duration * 1000))
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
fs = 44100
|
|
471
|
+
t = np.linspace(0, duration, int(fs * duration), endpoint=False)
|
|
472
|
+
|
|
473
|
+
if waveform == 'square':
|
|
474
|
+
wave = np.sign(np.sin(2 * np.pi * freq * t))
|
|
475
|
+
elif waveform == 'triangle':
|
|
476
|
+
wave = 2 * np.abs(2 * ((freq * t) % 1) - 1) - 1
|
|
477
|
+
else:
|
|
478
|
+
wave = np.sin(2 * np.pi * freq * t)
|
|
479
|
+
|
|
480
|
+
envelope = np.ones_like(wave)
|
|
481
|
+
attack = min(0.02, duration * 0.2)
|
|
482
|
+
release = min(0.03, duration * 0.2)
|
|
483
|
+
attack_len = int(fs * attack)
|
|
484
|
+
release_len = int(fs * release)
|
|
485
|
+
if attack_len > 0:
|
|
486
|
+
envelope[:attack_len] = np.linspace(0.0, 1.0, attack_len)
|
|
487
|
+
if release_len > 0:
|
|
488
|
+
envelope[-release_len:] = np.linspace(1.0, 0.0, release_len)
|
|
489
|
+
|
|
490
|
+
wave = volume * envelope * wave
|
|
491
|
+
sd.play(wave.astype(np.float32), fs)
|
|
492
|
+
sd.wait()
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def play_notes(sequence, tempo=120, volume=0.4, waveform='sine'):
|
|
496
|
+
beat = 60.0 / tempo
|
|
497
|
+
for note, length in sequence:
|
|
498
|
+
play_tone(note, duration=length * beat, volume=volume, waveform=waveform)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
sound_map = {}
|
|
502
|
+
|
|
503
|
+
sound_map["Victory"] = lambda: play_notes([('C4', 0.25), ('E4', 0.25), ('G4', 0.5), ('C5', 0.3)], tempo=140)
|
|
504
|
+
sound_map["Defeat"] = lambda: play_notes([('E4', 0.3), ('C4', 0.3), ('A3', 0.4)], tempo=90)
|
|
505
|
+
sound_map["Win"] = lambda: play_notes([('G4', 0.2), ('B4', 0.2), ('D5', 0.3)], tempo=150)
|
|
506
|
+
sound_map["Lose"] = lambda: play_notes([('D4', 0.2), ('C4', 0.3), ('A3', 0.4)], tempo=100)
|
|
507
|
+
sound_map["Click"] = lambda: play_notes([('C5', 0.1)], tempo=180)
|
|
508
|
+
sound_map["Hover"] = lambda: play_notes([('E5', 0.08), ('D5', 0.08)], tempo=200)
|
|
509
|
+
sound_map["Select"] = lambda: play_notes([('G4', 0.1), ('B4', 0.1)], tempo=180)
|
|
510
|
+
sound_map["Cancel"] = lambda: play_notes([('D4', 0.2)], tempo=120)
|
|
511
|
+
sound_map["Start"] = lambda: play_notes([('E4', 0.15), ('G4', 0.15), ('C5', 0.25)], tempo=150)
|
|
512
|
+
sound_map["Finish"] = lambda: play_notes([('C5', 0.2), ('E5', 0.2), ('G5', 0.3)], tempo=130)
|
|
513
|
+
sound_map["LevelUp"] = lambda: play_notes([('C4', 0.15), ('E4', 0.15), ('G4', 0.2), ('C5', 0.25)], tempo=170)
|
|
514
|
+
sound_map["LevelDown"] = lambda: play_notes([('G4', 0.2), ('E4', 0.2), ('C4', 0.3)], tempo=100)
|
|
515
|
+
sound_map["Coin"] = lambda: play_notes([('C6', 0.1), ('E6', 0.1)], tempo=180)
|
|
516
|
+
sound_map["Score"] = lambda: play_notes([('G5', 0.15), ('A5', 0.15), ('C6', 0.2)], tempo=160)
|
|
517
|
+
sound_map["Bonus"] = lambda: play_notes([('A4', 0.12), ('C5', 0.12), ('E5', 0.2)], tempo=170)
|
|
518
|
+
sound_map["Combo"] = lambda: play_notes([('B4', 0.1), ('D5', 0.1), ('F#5', 0.2)], tempo=180)
|
|
519
|
+
sound_map["UltraWin"] = lambda: play_notes([('C5', 0.15), ('E5', 0.15), ('G5', 0.15), ('B5', 0.3)], tempo=160)
|
|
520
|
+
sound_map["GameOver"] = lambda: play_notes([('A3', 0.4), ('F3', 0.4)], tempo=70)
|
|
521
|
+
sound_map["Restart"] = lambda: play_notes([('D4', 0.2), ('F#4', 0.2)], tempo=130)
|
|
522
|
+
sound_map["Pause"] = lambda: play_notes([('E4', 0.3), ('D4', 0.3)], tempo=90)
|
|
523
|
+
sound_map["Resume"] = lambda: play_notes([('G4', 0.2), ('A4', 0.2)], tempo=140)
|
|
524
|
+
sound_map["Unlock"] = lambda: play_notes([('C5', 0.2), ('D5', 0.2), ('E5', 0.2)], tempo=140)
|
|
525
|
+
sound_map["Lock"] = lambda: play_notes([('C4', 0.3), ('A3', 0.3)], tempo=80)
|
|
526
|
+
sound_map["Achievement"] = lambda: play_notes([('G4', 0.18), ('B4', 0.18), ('D5', 0.24)], tempo=150)
|
|
527
|
+
sound_map["Boss"] = lambda: play_notes([('C2', 0.4), ('G2', 0.4)], tempo=60, waveform='triangle')
|
|
528
|
+
sound_map["Alert"] = lambda: play_notes([('G5', 0.15), ('E5', 0.15)], tempo=160)
|
|
529
|
+
sound_map["Notification"] = lambda: play_notes([('E5', 0.2), ('G5', 0.2)], tempo=150)
|
|
530
|
+
sound_map["Message"] = lambda: play_notes([('F5', 0.15), ('E5', 0.15)], tempo=170)
|
|
531
|
+
sound_map["IncomingCall"] = lambda: play_notes([('A5', 0.25), ('C6', 0.25)], tempo=120)
|
|
532
|
+
sound_map["OutgoingCall"] = lambda: play_notes([('G5', 0.25), ('B5', 0.25)], tempo=120)
|
|
533
|
+
sound_map["Email"] = lambda: play_notes([('F5', 0.2), ('A5', 0.2)], tempo=140)
|
|
534
|
+
sound_map["Success"] = lambda: play_notes([('C5', 0.2), ('E5', 0.2), ('G5', 0.2)], tempo=150)
|
|
535
|
+
sound_map["Fail"] = lambda: play_notes([('D4', 0.25), ('C4', 0.25)], tempo=90)
|
|
536
|
+
sound_map["Error"] = lambda: play_notes([('F4', 0.25), ('D4', 0.25)], tempo=90)
|
|
537
|
+
sound_map["Warning"] = lambda: play_notes([('B4', 0.2), ('G4', 0.2)], tempo=110)
|
|
538
|
+
sound_map["Connect"] = lambda: play_notes([('E5', 0.2), ('G5', 0.2)], tempo=150)
|
|
539
|
+
sound_map["Disconnect"] = lambda: play_notes([('B4', 0.2), ('A4', 0.2)], tempo=110)
|
|
540
|
+
sound_map["Loading"] = lambda: play_notes([('C5', 0.12), ('D5', 0.12), ('E5', 0.12)], tempo=220)
|
|
541
|
+
sound_map["Done"] = lambda: play_notes([('G5', 0.2), ('C6', 0.2)], tempo=140)
|
|
542
|
+
sound_map["Save"] = lambda: play_notes([('E5', 0.18), ('C5', 0.18)], tempo=140)
|
|
543
|
+
sound_map["Delete"] = lambda: play_notes([('C4', 0.2), ('E4', 0.2)], tempo=110)
|
|
544
|
+
sound_map["Copy"] = lambda: play_notes([('G4', 0.15), ('B4', 0.15)], tempo=180)
|
|
545
|
+
sound_map["Paste"] = lambda: play_notes([('A4', 0.15), ('C5', 0.15)], tempo=180)
|
|
546
|
+
sound_map["Cut"] = lambda: play_notes([('F4', 0.15), ('D4', 0.15)], tempo=170)
|
|
547
|
+
sound_map["Refresh"] = lambda: play_notes([('C5', 0.12), ('D5', 0.12), ('C5', 0.12)], tempo=220)
|
|
548
|
+
sound_map["Sync"] = lambda: play_notes([('E4', 0.12), ('G4', 0.12), ('B4', 0.12)], tempo=200)
|
|
549
|
+
sound_map["Update"] = lambda: play_notes([('A4', 0.2), ('B4', 0.2)], tempo=140)
|
|
550
|
+
sound_map["Install"] = lambda: play_notes([('C4', 0.12), ('E4', 0.12), ('G4', 0.2)], tempo=170)
|
|
551
|
+
sound_map["Boot"] = lambda: play_notes([('C5', 0.25), ('E5', 0.25)], tempo=120)
|
|
552
|
+
sound_map["Shutdown"] = lambda: play_notes([('A3', 0.4), ('F3', 0.4)], tempo=65, waveform='triangle')
|
|
553
|
+
sound_map["Tap"] = lambda: play_notes([('D5', 0.08)], tempo=220)
|
|
554
|
+
sound_map["Pop"] = lambda: play_notes([('F5', 0.1)], tempo=200)
|
|
555
|
+
sound_map["Beep"] = lambda: play_notes([('G4', 0.1)], tempo=220)
|
|
556
|
+
sound_map["Ding"] = lambda: play_notes([('E5', 0.18)], tempo=190)
|
|
557
|
+
sound_map["ClickSoft"] = lambda: play_notes([('F4', 0.1)], tempo=210)
|
|
558
|
+
sound_map["ClickHard"] = lambda: play_notes([('C4', 0.1)], tempo=200)
|
|
559
|
+
sound_map["Swipe"] = lambda: play_notes([('G5', 0.1), ('A5', 0.1)], tempo=180)
|
|
560
|
+
sound_map["Slide"] = lambda: play_notes([('A4', 0.1), ('G4', 0.1)], tempo=180)
|
|
561
|
+
sound_map["Open"] = lambda: play_notes([('E5', 0.2), ('G5', 0.2)], tempo=150)
|
|
562
|
+
sound_map["Close"] = lambda: play_notes([('D4', 0.2), ('C4', 0.2)], tempo=110)
|
|
563
|
+
sound_map["MenuOpen"] = lambda: play_notes([('G4', 0.12), ('A4', 0.12)], tempo=200)
|
|
564
|
+
sound_map["MenuClose"] = lambda: play_notes([('E4', 0.12), ('D4', 0.12)], tempo=190)
|
|
565
|
+
sound_map["TabSwitch"] = lambda: play_notes([('F4', 0.12), ('A4', 0.12)], tempo=190)
|
|
566
|
+
sound_map["Drag"] = lambda: play_notes([('C4', 0.12), ('D4', 0.12)], tempo=180)
|
|
567
|
+
sound_map["Drop"] = lambda: play_notes([('E4', 0.18), ('C4', 0.18)], tempo=140)
|
|
568
|
+
sound_map["Focus"] = lambda: play_notes([('A5', 0.1), ('G5', 0.1)], tempo=200)
|
|
569
|
+
sound_map["Unfocus"] = lambda: play_notes([('D4', 0.1), ('C4', 0.1)], tempo=180)
|
|
570
|
+
sound_map["SelectItem"] = lambda: play_notes([('B4', 0.15), ('D5', 0.15)], tempo=170)
|
|
571
|
+
sound_map["Deselect"] = lambda: play_notes([('C4', 0.15), ('Bb3', 0.15)], tempo=160)
|
|
572
|
+
sound_map["Confirm"] = lambda: play_notes([('C5', 0.2), ('G5', 0.2)], tempo=150)
|
|
573
|
+
sound_map["Back"] = lambda: play_notes([('D4', 0.2), ('C4', 0.2)], tempo=120)
|
|
574
|
+
sound_map["Forward"] = lambda: play_notes([('G4', 0.2), ('A4', 0.2)], tempo=140)
|
|
575
|
+
sound_map["Scroll"] = lambda: play_notes([('E5', 0.08), ('F5', 0.08), ('G5', 0.08)], tempo=240)
|
|
576
|
+
sound_map["ZoomIn"] = lambda: play_notes([('C5', 0.2), ('E5', 0.2)], tempo=160)
|
|
577
|
+
sound_map["ZoomOut"] = lambda: play_notes([('G4', 0.2), ('E4', 0.2)], tempo=120)
|
|
578
|
+
sound_map["Laser"] = lambda: play_notes([('A5', 0.1), ('C6', 0.1), ('E6', 0.15)], tempo=220, waveform='square')
|
|
579
|
+
sound_map["Plasma"] = lambda: play_notes([('F5', 0.15), ('G5', 0.15), ('A5', 0.2)], tempo=200, waveform='triangle')
|
|
580
|
+
sound_map["Warp"] = lambda: play_notes([('C4', 0.1), ('C5', 0.3)], tempo=130, waveform='triangle')
|
|
581
|
+
sound_map["Robot"] = lambda: play_notes([('G3', 0.2), ('D4', 0.2), ('G4', 0.2)], tempo=110, waveform='square')
|
|
582
|
+
sound_map["AI"] = lambda: play_notes([('F4', 0.15), ('A4', 0.15), ('C5', 0.2)], tempo=140, waveform='sine')
|
|
583
|
+
sound_map["Drone"] = lambda: play_notes([('D3', 0.4), ('F3', 0.4)], tempo=70, waveform='triangle')
|
|
584
|
+
sound_map["Scan"] = lambda: play_notes([('E4', 0.12), ('F4', 0.12), ('G4', 0.12)], tempo=210)
|
|
585
|
+
sound_map["Hack"] = lambda: play_notes([('D4', 0.1), ('F4', 0.1), ('A4', 0.2)], tempo=180, waveform='square')
|
|
586
|
+
sound_map["Glitch"] = lambda: play_notes([('G5', 0.05), ('C5', 0.05), ('E5', 0.05)], tempo=260, waveform='square')
|
|
587
|
+
sound_map["Signal"] = lambda: play_notes([('A4', 0.2), ('C5', 0.2)], tempo=160)
|
|
588
|
+
|
|
589
|
+
# Add 100 musical-style sound names with melodic variations.
|
|
590
|
+
base_sequences = [
|
|
591
|
+
[('C4', 0.2), ('E4', 0.2), ('G4', 0.2), ('C5', 0.3)],
|
|
592
|
+
[('D4', 0.18), ('F#4', 0.18), ('A4', 0.18), ('D5', 0.3)],
|
|
593
|
+
[('E4', 0.15), ('G#4', 0.15), ('B4', 0.15), ('E5', 0.3)],
|
|
594
|
+
[('F4', 0.2), ('A4', 0.2), ('C5', 0.2), ('F5', 0.3)],
|
|
595
|
+
[('G4', 0.15), ('B4', 0.15), ('D5', 0.15), ('G5', 0.3)],
|
|
596
|
+
[('A4', 0.18), ('C#5', 0.18), ('E5', 0.18), ('A5', 0.3)],
|
|
597
|
+
[('B4', 0.15), ('D5', 0.15), ('F#5', 0.15), ('B5', 0.3)],
|
|
598
|
+
]
|
|
599
|
+
for i in range(1, 101):
|
|
600
|
+
pattern = base_sequences[(i - 1) % len(base_sequences)]
|
|
601
|
+
tempo = 100 + ((i - 1) % 5) * 10
|
|
602
|
+
waveform = 'triangle' if i % 7 == 0 else 'square' if i % 11 == 0 else 'sine'
|
|
603
|
+
sound_map[f"Melody{i:03d}"] = lambda p=pattern, t=tempo, w=waveform: play_notes(p, tempo=t, waveform=w)
|
|
604
|
+
|
|
605
|
+
# Add 300 extra musical-style sound names with richer, more varied melodic textures.
|
|
606
|
+
extra_sequences = [
|
|
607
|
+
[('C4', 0.15), ('D4', 0.15), ('E4', 0.15), ('G4', 0.25)],
|
|
608
|
+
[('E4', 0.18), ('G4', 0.18), ('B4', 0.18), ('E5', 0.3)],
|
|
609
|
+
[('G3', 0.2), ('B3', 0.2), ('D4', 0.2), ('G4', 0.3)],
|
|
610
|
+
[('A3', 0.16), ('C4', 0.16), ('E4', 0.16), ('A4', 0.28)],
|
|
611
|
+
[('F4', 0.14), ('A4', 0.14), ('C5', 0.14), ('F5', 0.28)],
|
|
612
|
+
[('D4', 0.2), ('F#4', 0.15), ('A4', 0.15), ('D5', 0.3)],
|
|
613
|
+
[('B3', 0.2), ('D4', 0.2), ('F#4', 0.2), ('B4', 0.3)],
|
|
614
|
+
[('C5', 0.12), ('E5', 0.12), ('G5', 0.12), ('C6', 0.24)],
|
|
615
|
+
[('A4', 0.15), ('B4', 0.15), ('C#5', 0.15), ('E5', 0.3)],
|
|
616
|
+
[('G4', 0.18), ('A4', 0.18), ('B4', 0.18), ('D5', 0.3)],
|
|
617
|
+
[('F#4', 0.14), ('A4', 0.14), ('C#5', 0.14), ('F#5', 0.3)],
|
|
618
|
+
[('E4', 0.16), ('F#4', 0.16), ('G#4', 0.16), ('B4', 0.3)],
|
|
619
|
+
[('D5', 0.15), ('C5', 0.15), ('A4', 0.15), ('F4', 0.25)],
|
|
620
|
+
[('C4', 0.2), ('B3', 0.2), ('A3', 0.2), ('G3', 0.3)],
|
|
621
|
+
[('E5', 0.1), ('D5', 0.1), ('C5', 0.1), ('B4', 0.1), ('A4', 0.2)],
|
|
622
|
+
]
|
|
623
|
+
for i in range(101, 401):
|
|
624
|
+
pattern = extra_sequences[(i - 101) % len(extra_sequences)]
|
|
625
|
+
tempo = 90 + ((i - 101) % 10) * 7
|
|
626
|
+
waveform = 'square' if i % 13 == 0 else 'triangle' if i % 7 == 0 else 'sine'
|
|
627
|
+
volume = 0.35 + ((i - 101) % 4) * 0.05
|
|
628
|
+
sound_map[f"Melody{i:03d}"] = lambda p=pattern, t=tempo, w=waveform, v=volume: play_notes(p, tempo=t, volume=v, waveform=w)
|
|
629
|
+
|
|
630
|
+
# General utilities added to make the library competitive.
|
|
631
|
+
|
|
632
|
+
# =========================
|
|
633
|
+
# VALIDATION UTILITIES
|
|
634
|
+
# =========================
|
|
635
|
+
|
|
636
|
+
def is_integer(value):
|
|
637
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def is_float(value):
|
|
641
|
+
return isinstance(value, float)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def is_string(value):
|
|
645
|
+
return isinstance(value, str)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def is_list(value):
|
|
649
|
+
return isinstance(value, list)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def is_dict(value):
|
|
653
|
+
return isinstance(value, dict)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def is_boolean(value):
|
|
657
|
+
return isinstance(value, bool)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def validate_schema(data, schema, path=''):
|
|
661
|
+
errors = []
|
|
662
|
+
if not isinstance(schema, dict):
|
|
663
|
+
return False, ['schema must be a dict']
|
|
664
|
+
|
|
665
|
+
if 'type' in schema:
|
|
666
|
+
expected = schema['type']
|
|
667
|
+
if expected == 'string' and not isinstance(data, str):
|
|
668
|
+
errors.append(f'{path or "root"}: expected string')
|
|
669
|
+
elif expected == 'integer' and not is_integer(data):
|
|
670
|
+
errors.append(f'{path or "root"}: expected integer')
|
|
671
|
+
elif expected == 'number' and not isinstance(data, (int, float)):
|
|
672
|
+
errors.append(f'{path or "root"}: expected number')
|
|
673
|
+
elif expected == 'boolean' and not isinstance(data, bool):
|
|
674
|
+
errors.append(f'{path or "root"}: expected boolean')
|
|
675
|
+
elif expected == 'object' and not isinstance(data, dict):
|
|
676
|
+
errors.append(f'{path or "root"}: expected object')
|
|
677
|
+
elif expected == 'array' and not isinstance(data, list):
|
|
678
|
+
errors.append(f'{path or "root"}: expected array')
|
|
679
|
+
|
|
680
|
+
if isinstance(data, dict) and 'properties' in schema:
|
|
681
|
+
for key, subschema in schema['properties'].items():
|
|
682
|
+
if key in data:
|
|
683
|
+
valid, sub_errors = validate_schema(data[key], subschema, f'{path}.{key}' if path else key)
|
|
684
|
+
errors.extend(sub_errors)
|
|
685
|
+
elif subschema.get('required', False):
|
|
686
|
+
errors.append(f'{path}.{key}' if path else key + ': required field missing')
|
|
687
|
+
|
|
688
|
+
if isinstance(data, list) and 'items' in schema:
|
|
689
|
+
for index, item in enumerate(data):
|
|
690
|
+
valid, sub_errors = validate_schema(item, schema['items'], f'{path}[{index}]')
|
|
691
|
+
errors.extend(sub_errors)
|
|
692
|
+
|
|
693
|
+
return len(errors) == 0, errors
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def ensure_schema(data, schema):
|
|
697
|
+
valid, errors = validate_schema(data, schema)
|
|
698
|
+
if not valid:
|
|
699
|
+
raise ValueError('Validation failed: ' + '; '.join(errors))
|
|
700
|
+
return True
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def has_keys(obj, keys):
|
|
704
|
+
return all(k in obj for k in keys)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def assert_type(value, expected_type):
|
|
708
|
+
return isinstance(value, expected_type)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def is_truthy(value):
|
|
712
|
+
return bool(value)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def is_blank(text):
|
|
716
|
+
return not str(text).strip()
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
# =========================
|
|
720
|
+
# JSON & CONFIG UTILITIES
|
|
721
|
+
# =========================
|
|
722
|
+
|
|
723
|
+
def load_json(path, encoding='utf-8'):
|
|
724
|
+
with open(path, 'r', encoding=encoding) as file:
|
|
725
|
+
return json.load(file)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def save_json(path, data, encoding='utf-8', indent=4):
|
|
729
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
730
|
+
with open(path, 'w', encoding=encoding) as file:
|
|
731
|
+
json.dump(data, file, ensure_ascii=False, indent=indent)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def merge_dicts(*dicts, deep=True):
|
|
735
|
+
result = {}
|
|
736
|
+
for d in dicts:
|
|
737
|
+
if not isinstance(d, dict):
|
|
738
|
+
continue
|
|
739
|
+
for key, value in d.items():
|
|
740
|
+
if deep and key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
741
|
+
result[key] = merge_dicts(result[key], value, deep=True)
|
|
742
|
+
else:
|
|
743
|
+
result[key] = value
|
|
744
|
+
return result
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def deep_get(data, path, default=None, separator='.'):
|
|
748
|
+
if data is None:
|
|
749
|
+
return default
|
|
750
|
+
if separator in path:
|
|
751
|
+
parts = path.split(separator)
|
|
752
|
+
else:
|
|
753
|
+
parts = [path]
|
|
754
|
+
current = data
|
|
755
|
+
for part in parts:
|
|
756
|
+
if isinstance(current, dict) and part in current:
|
|
757
|
+
current = current[part]
|
|
758
|
+
else:
|
|
759
|
+
return default
|
|
760
|
+
return current
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def deep_set(data, path, value, separator='.'):
|
|
764
|
+
if not isinstance(data, dict):
|
|
765
|
+
raise ValueError('deep_set target must be a dict')
|
|
766
|
+
keys = path.split(separator)
|
|
767
|
+
current = data
|
|
768
|
+
for key in keys[:-1]:
|
|
769
|
+
if key not in current or not isinstance(current[key], dict):
|
|
770
|
+
current[key] = {}
|
|
771
|
+
current = current[key]
|
|
772
|
+
current[keys[-1]] = value
|
|
773
|
+
return data
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def load_json_url(url, headers=None, timeout=10):
|
|
777
|
+
request = urllib.request.Request(url, headers=headers or {})
|
|
778
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
779
|
+
return json.loads(response.read().decode('utf-8'))
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
# =========================
|
|
783
|
+
# HTTP UTILITIES
|
|
784
|
+
# =========================
|
|
785
|
+
|
|
786
|
+
def http_get(url, headers=None, timeout=10):
|
|
787
|
+
request = urllib.request.Request(url, headers=headers or {}, method='GET')
|
|
788
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
789
|
+
return response.read(), response.getcode(), dict(response.headers)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def http_post(url, data=None, json_data=None, headers=None, timeout=10):
|
|
793
|
+
request_headers = headers.copy() if headers else {}
|
|
794
|
+
body = None
|
|
795
|
+
if json_data is not None:
|
|
796
|
+
body = json.dumps(json_data).encode('utf-8')
|
|
797
|
+
request_headers['Content-Type'] = 'application/json'
|
|
798
|
+
elif data is not None:
|
|
799
|
+
if isinstance(data, dict):
|
|
800
|
+
body = urllib.parse.urlencode(data).encode('utf-8')
|
|
801
|
+
else:
|
|
802
|
+
body = str(data).encode('utf-8')
|
|
803
|
+
request = urllib.request.Request(url, data=body, headers=request_headers, method='POST')
|
|
804
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
805
|
+
return response.read(), response.getcode(), dict(response.headers)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def fetch_json(url, headers=None, timeout=10):
|
|
809
|
+
body, code, headers = http_get(url, headers=headers, timeout=timeout)
|
|
810
|
+
return json.loads(body.decode('utf-8'))
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def check_url(url, timeout=5):
|
|
814
|
+
try:
|
|
815
|
+
_, code, _ = http_get(url, timeout=timeout)
|
|
816
|
+
return 200 <= code < 400
|
|
817
|
+
except Exception:
|
|
818
|
+
return False
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def download_file(url, dest, chunk_size=8192, timeout=10):
|
|
822
|
+
Path(dest).parent.mkdir(parents=True, exist_ok=True)
|
|
823
|
+
request = urllib.request.Request(url)
|
|
824
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
825
|
+
with open(dest, 'wb') as out_file:
|
|
826
|
+
while True:
|
|
827
|
+
chunk = response.read(chunk_size)
|
|
828
|
+
if not chunk:
|
|
829
|
+
break
|
|
830
|
+
out_file.write(chunk)
|
|
831
|
+
return dest
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
# =========================
|
|
835
|
+
# DATE / TIME UTILITIES
|
|
836
|
+
# =========================
|
|
837
|
+
|
|
838
|
+
def current_timestamp():
|
|
839
|
+
return datetime.datetime.now().timestamp()
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def format_datetime(value=None, fmt='%Y-%m-%d %H:%M:%S'):
|
|
843
|
+
if value is None:
|
|
844
|
+
value = datetime.datetime.now()
|
|
845
|
+
if isinstance(value, (int, float)):
|
|
846
|
+
value = datetime.datetime.fromtimestamp(value)
|
|
847
|
+
return value.strftime(fmt)
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def parse_datetime(text, fmt='%Y-%m-%d %H:%M:%S'):
|
|
851
|
+
return datetime.datetime.strptime(str(text), fmt)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def relative_time(value, now=None):
|
|
855
|
+
if now is None:
|
|
856
|
+
now = datetime.datetime.now()
|
|
857
|
+
if isinstance(value, (int, float)):
|
|
858
|
+
value = datetime.datetime.fromtimestamp(value)
|
|
859
|
+
delta = now - value
|
|
860
|
+
seconds = int(delta.total_seconds())
|
|
861
|
+
if seconds < 0:
|
|
862
|
+
return 'in the future'
|
|
863
|
+
if seconds < 60:
|
|
864
|
+
return f'{seconds}s ago'
|
|
865
|
+
if seconds < 3600:
|
|
866
|
+
return f'{seconds // 60}m ago'
|
|
867
|
+
if seconds < 86400:
|
|
868
|
+
return f'{seconds // 3600}h ago'
|
|
869
|
+
return f'{seconds // 86400}d ago'
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def countdown(seconds, callback=None, interval=1):
|
|
873
|
+
start = time.time()
|
|
874
|
+
remaining = seconds
|
|
875
|
+
while remaining > 0:
|
|
876
|
+
time.sleep(min(interval, remaining))
|
|
877
|
+
remaining = seconds - (time.time() - start)
|
|
878
|
+
if callback:
|
|
879
|
+
callback(max(0, int(remaining)))
|
|
880
|
+
return 0
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def is_weekend(value=None):
|
|
884
|
+
if value is None:
|
|
885
|
+
value = datetime.datetime.now()
|
|
886
|
+
if isinstance(value, (int, float)):
|
|
887
|
+
value = datetime.datetime.fromtimestamp(value)
|
|
888
|
+
return value.weekday() >= 5
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def add_business_days(value, days):
|
|
892
|
+
if isinstance(value, (int, float)):
|
|
893
|
+
value = datetime.datetime.fromtimestamp(value)
|
|
894
|
+
result = value
|
|
895
|
+
while days > 0:
|
|
896
|
+
result += datetime.timedelta(days=1)
|
|
897
|
+
if result.weekday() < 5:
|
|
898
|
+
days -= 1
|
|
899
|
+
return result
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
# =========================
|
|
903
|
+
# MATH / STATISTICS UTILITIES
|
|
904
|
+
# =========================
|
|
905
|
+
|
|
906
|
+
def clamp(value, minimum, maximum):
|
|
907
|
+
return max(minimum, min(maximum, value))
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def lerp(start, end, fraction):
|
|
911
|
+
return start + (end - start) * fraction
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def remap(value, old_min, old_max, new_min, new_max):
|
|
915
|
+
if old_max == old_min:
|
|
916
|
+
raise ValueError('old_min and old_max cannot be equal')
|
|
917
|
+
ratio = (value - old_min) / (old_max - old_min)
|
|
918
|
+
return new_min + ratio * (new_max - new_min)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def mean_values(values):
|
|
922
|
+
return statistics.mean(values)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def median_values(values):
|
|
926
|
+
return statistics.median(values)
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def mode_values(values):
|
|
930
|
+
try:
|
|
931
|
+
return statistics.mode(values)
|
|
932
|
+
except statistics.StatisticsError:
|
|
933
|
+
return None
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def variance(values, sample=False):
|
|
937
|
+
return statistics.pvariance(values) if not sample else statistics.variance(values)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def stddev(values, sample=False):
|
|
941
|
+
return math.sqrt(variance(values, sample=sample))
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def percent_change(original, new):
|
|
945
|
+
if original == 0:
|
|
946
|
+
return float('inf') if new else 0.0
|
|
947
|
+
return ((new - original) / abs(original)) * 100.0
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def safe_divide(a, b, default=0.0):
|
|
951
|
+
try:
|
|
952
|
+
return a / b
|
|
953
|
+
except Exception:
|
|
954
|
+
return default
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def random_choice_weighted(choices):
|
|
958
|
+
total = sum(weight for item, weight in choices)
|
|
959
|
+
if total <= 0:
|
|
960
|
+
return None
|
|
961
|
+
r = random.uniform(0, total)
|
|
962
|
+
upto = 0
|
|
963
|
+
for item, weight in choices:
|
|
964
|
+
if upto + weight >= r:
|
|
965
|
+
return item
|
|
966
|
+
upto += weight
|
|
967
|
+
return None
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
# =========================
|
|
971
|
+
# PROCESS & COMMAND UTILITIES
|
|
972
|
+
# =========================
|
|
973
|
+
|
|
974
|
+
def run_command(command, shell=False, cwd=None, env=None, timeout=None):
|
|
975
|
+
if isinstance(command, str) and not shell:
|
|
976
|
+
command = command.split()
|
|
977
|
+
result = subprocess.run(command, shell=shell, cwd=cwd, env=env, capture_output=True, text=True, timeout=timeout)
|
|
978
|
+
return {
|
|
979
|
+
'returncode': result.returncode,
|
|
980
|
+
'stdout': result.stdout,
|
|
981
|
+
'stderr': result.stderr,
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def run_command_async(command, callback=None, shell=False, cwd=None, env=None, timeout=None):
|
|
986
|
+
def worker():
|
|
987
|
+
result = run_command(command, shell=shell, cwd=cwd, env=env, timeout=timeout)
|
|
988
|
+
if callback:
|
|
989
|
+
callback(result)
|
|
990
|
+
thread = threading.Thread(target=worker, daemon=True)
|
|
991
|
+
thread.start()
|
|
992
|
+
return thread
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def which(program):
|
|
996
|
+
return shutil.which(program)
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def is_process_running(name):
|
|
1000
|
+
if platform.system() == 'Windows':
|
|
1001
|
+
tasklist = subprocess.run(['tasklist'], capture_output=True, text=True)
|
|
1002
|
+
return name.lower() in tasklist.stdout.lower()
|
|
1003
|
+
else:
|
|
1004
|
+
ps = subprocess.run(['ps', 'ax'], capture_output=True, text=True)
|
|
1005
|
+
return name.lower() in ps.stdout.lower()
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def kill_process(pid):
|
|
1009
|
+
try:
|
|
1010
|
+
os.kill(pid, 9)
|
|
1011
|
+
return True
|
|
1012
|
+
except Exception:
|
|
1013
|
+
return False
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def copy_file(src, dst):
|
|
1017
|
+
Path(dst).parent.mkdir(parents=True, exist_ok=True)
|
|
1018
|
+
shutil.copy2(src, dst)
|
|
1019
|
+
return dst
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def move_file(src, dst):
|
|
1023
|
+
Path(dst).parent.mkdir(parents=True, exist_ok=True)
|
|
1024
|
+
shutil.move(src, dst)
|
|
1025
|
+
return dst
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def delete_file(path):
|
|
1029
|
+
try:
|
|
1030
|
+
Path(path).unlink()
|
|
1031
|
+
return True
|
|
1032
|
+
except Exception:
|
|
1033
|
+
return False
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
# =========================
|
|
1037
|
+
# DATA STRUCTURE UTILITIES
|
|
1038
|
+
# =========================
|
|
1039
|
+
|
|
1040
|
+
def chunk_list(sequence, chunk_size):
|
|
1041
|
+
if chunk_size <= 0:
|
|
1042
|
+
raise ValueError('chunk_size must be positive')
|
|
1043
|
+
return [sequence[i:i + chunk_size] for i in range(0, len(sequence), chunk_size)]
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def flatten_list(nested_list):
|
|
1047
|
+
result = []
|
|
1048
|
+
for item in nested_list:
|
|
1049
|
+
if isinstance(item, list):
|
|
1050
|
+
result.extend(flatten_list(item))
|
|
1051
|
+
else:
|
|
1052
|
+
result.append(item)
|
|
1053
|
+
return result
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def unique_preserve(sequence):
|
|
1057
|
+
seen = set()
|
|
1058
|
+
result = []
|
|
1059
|
+
for item in sequence:
|
|
1060
|
+
if item not in seen:
|
|
1061
|
+
seen.add(item)
|
|
1062
|
+
result.append(item)
|
|
1063
|
+
return result
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def group_by(sequence, key_function):
|
|
1067
|
+
groups = defaultdict(list)
|
|
1068
|
+
for item in sequence:
|
|
1069
|
+
groups[key_function(item)].append(item)
|
|
1070
|
+
return dict(groups)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def coalesce(*values):
|
|
1074
|
+
for value in values:
|
|
1075
|
+
if value is not None:
|
|
1076
|
+
return value
|
|
1077
|
+
return None
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def dict_merge(*dicts):
|
|
1081
|
+
merged = {}
|
|
1082
|
+
for d in dicts:
|
|
1083
|
+
if isinstance(d, dict):
|
|
1084
|
+
merged.update(d)
|
|
1085
|
+
return merged
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
# =========================
|
|
1089
|
+
# SETTINGS & STORAGE UTILITIES
|
|
1090
|
+
# =========================
|
|
1091
|
+
|
|
1092
|
+
def load_settings(path, default=None, encoding='utf-8'):
|
|
1093
|
+
if not Path(path).exists():
|
|
1094
|
+
return default if default is not None else {}
|
|
1095
|
+
return load_json(path, encoding=encoding)
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def save_settings(path, settings, encoding='utf-8', indent=4):
|
|
1099
|
+
return save_json(path, settings, encoding=encoding, indent=indent)
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def get_setting(settings, key, default=None):
|
|
1103
|
+
if isinstance(settings, dict):
|
|
1104
|
+
return deep_get(settings, key, default=default)
|
|
1105
|
+
return default
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def set_setting(settings, key, value):
|
|
1109
|
+
if not isinstance(settings, dict):
|
|
1110
|
+
raise ValueError('settings must be a dict')
|
|
1111
|
+
return deep_set(settings, key, value)
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def update_settings(settings, updates):
|
|
1115
|
+
if not isinstance(settings, dict) or not isinstance(updates, dict):
|
|
1116
|
+
raise ValueError('update_settings requires dicts')
|
|
1117
|
+
return merge_dicts(settings, updates)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def flatten_settings(settings, separator='.'):
|
|
1121
|
+
result = {}
|
|
1122
|
+
def _flatten(source, parent_key=''):
|
|
1123
|
+
for key, value in source.items():
|
|
1124
|
+
full_key = f'{parent_key}{separator}{key}' if parent_key else key
|
|
1125
|
+
if isinstance(value, dict):
|
|
1126
|
+
_flatten(value, full_key)
|
|
1127
|
+
else:
|
|
1128
|
+
result[full_key] = value
|
|
1129
|
+
_flatten(settings)
|
|
1130
|
+
return result
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
# =========================
|
|
1134
|
+
# EVENT BUS / PUBLISH-SUBSCRIBE
|
|
1135
|
+
# =========================
|
|
1136
|
+
|
|
1137
|
+
class EventBus:
|
|
1138
|
+
def __init__(self):
|
|
1139
|
+
self._listeners = defaultdict(list)
|
|
1140
|
+
|
|
1141
|
+
def subscribe(self, event_name, listener):
|
|
1142
|
+
if callable(listener):
|
|
1143
|
+
self._listeners[event_name].append(listener)
|
|
1144
|
+
return True
|
|
1145
|
+
return False
|
|
1146
|
+
|
|
1147
|
+
def unsubscribe(self, event_name, listener=None):
|
|
1148
|
+
if event_name not in self._listeners:
|
|
1149
|
+
return False
|
|
1150
|
+
if listener is None:
|
|
1151
|
+
self._listeners.pop(event_name, None)
|
|
1152
|
+
return True
|
|
1153
|
+
try:
|
|
1154
|
+
self._listeners[event_name].remove(listener)
|
|
1155
|
+
return True
|
|
1156
|
+
except ValueError:
|
|
1157
|
+
return False
|
|
1158
|
+
|
|
1159
|
+
def emit(self, event_name, *args, **kwargs):
|
|
1160
|
+
for listener in list(self._listeners.get(event_name, [])):
|
|
1161
|
+
try:
|
|
1162
|
+
listener(*args, **kwargs)
|
|
1163
|
+
except Exception:
|
|
1164
|
+
pass
|
|
1165
|
+
|
|
1166
|
+
def clear(self):
|
|
1167
|
+
self._listeners.clear()
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
# =========================
|
|
1171
|
+
# LOGGING UTILITIES
|
|
1172
|
+
# =========================
|
|
1173
|
+
|
|
1174
|
+
_log_file = None
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def set_log_file(path, level=logging.INFO, mode='a'):
|
|
1178
|
+
global _log_file
|
|
1179
|
+
logger = logging.getLogger(__name__)
|
|
1180
|
+
logger.setLevel(level)
|
|
1181
|
+
formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s')
|
|
1182
|
+
file_handler = logging.FileHandler(path, mode=mode, encoding='utf-8')
|
|
1183
|
+
file_handler.setFormatter(formatter)
|
|
1184
|
+
logger.handlers = [file_handler]
|
|
1185
|
+
_log_file = path
|
|
1186
|
+
return path
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def log_info(message):
|
|
1190
|
+
logging.getLogger(__name__).info(str(message))
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def log_warning(message):
|
|
1194
|
+
logging.getLogger(__name__).warning(str(message))
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def log_error(message):
|
|
1198
|
+
logging.getLogger(__name__).error(str(message))
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def log_debug(message):
|
|
1202
|
+
logging.getLogger(__name__).debug(str(message))
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def get_log_file():
|
|
1206
|
+
return _log_file
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def configure_logging(level=logging.INFO, fmt='[%(levelname)s] %(message)s'):
|
|
1210
|
+
logging.basicConfig(level=level, format=fmt)
|
|
1211
|
+
return logging.getLogger(__name__)
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
# =========================
|
|
1215
|
+
# CLI / OUTPUT UTILITIES
|
|
1216
|
+
# =========================
|
|
1217
|
+
|
|
1218
|
+
def print_table(rows, headers=None, padding=2):
|
|
1219
|
+
if headers:
|
|
1220
|
+
rows = [headers] + rows
|
|
1221
|
+
widths = [max(len(str(value)) for value in column) for column in zip(*rows)]
|
|
1222
|
+
lines = []
|
|
1223
|
+
for row in rows:
|
|
1224
|
+
line = ' '.join(str(value).ljust(widths[index] + padding) for index, value in enumerate(row))
|
|
1225
|
+
lines.append(line)
|
|
1226
|
+
print('\n'.join(lines))
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def progress_bar(iterable, prefix='', size=40, fill='█'):
|
|
1230
|
+
total = len(iterable)
|
|
1231
|
+
for index, item in enumerate(iterable, start=1):
|
|
1232
|
+
filled = int(size * index / total)
|
|
1233
|
+
bar = fill * filled + '-' * (size - filled)
|
|
1234
|
+
print(f'\r{prefix} |{bar}| {index}/{total}', end='')
|
|
1235
|
+
yield item
|
|
1236
|
+
print()
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
def prompt_choices(root, title, options, default_index=0):
|
|
1240
|
+
if not options:
|
|
1241
|
+
return None
|
|
1242
|
+
choice = simpledialog.askstring(title, 'Choose: ' + ', '.join(str(i+1) + ':' + str(opt) for i, opt in enumerate(options)), initialvalue=str(default_index+1), parent=root)
|
|
1243
|
+
try:
|
|
1244
|
+
idx = int(choice) - 1
|
|
1245
|
+
return options[idx] if 0 <= idx < len(options) else None
|
|
1246
|
+
except Exception:
|
|
1247
|
+
return None
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
def clear_console():
|
|
1251
|
+
if platform.system() == 'Windows':
|
|
1252
|
+
os.system('cls')
|
|
1253
|
+
else:
|
|
1254
|
+
os.system('clear')
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
def confirm_action(message='Proceed?', default=True):
|
|
1258
|
+
return messagebox.askyesno('Confirm', message, icon='question')
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
def show_progress(title, current, total):
|
|
1262
|
+
percent = int((current / total) * 100) if total else 0
|
|
1263
|
+
return f'{title}: {percent}% ({current}/{total})'
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def play(name):
|
|
1267
|
+
if name in sound_map:
|
|
1268
|
+
sound_map[name]()
|
|
1269
|
+
else:
|
|
1270
|
+
print(f"Sound '{name}' not found in sound_map.")
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
def add_sound(name, func):
|
|
1274
|
+
sound_map[name] = lambda: func()
|
|
1275
|
+
|
|
1276
|
+
# =========================
|
|
1277
|
+
# MAJOR ARCHITECTURE EXPANSION
|
|
1278
|
+
# =========================
|
|
1279
|
+
|
|
1280
|
+
class PluginManager:
|
|
1281
|
+
"""Manages plugins and extension lifecycle."""
|
|
1282
|
+
|
|
1283
|
+
def __init__(self):
|
|
1284
|
+
self.plugins = {}
|
|
1285
|
+
|
|
1286
|
+
def register(self, name, plugin):
|
|
1287
|
+
self.plugins[name] = plugin
|
|
1288
|
+
return True
|
|
1289
|
+
|
|
1290
|
+
def unregister(self, name):
|
|
1291
|
+
if name in self.plugins:
|
|
1292
|
+
del self.plugins[name]
|
|
1293
|
+
return True
|
|
1294
|
+
return False
|
|
1295
|
+
|
|
1296
|
+
def get_plugin(self, name, default=None):
|
|
1297
|
+
return self.plugins.get(name, default)
|
|
1298
|
+
|
|
1299
|
+
def list_plugins(self):
|
|
1300
|
+
return list(self.plugins.keys())
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
class WorkflowEngine:
|
|
1304
|
+
"""Orchestrates workflow stages with dependencies."""
|
|
1305
|
+
|
|
1306
|
+
def __init__(self):
|
|
1307
|
+
self.stages = {}
|
|
1308
|
+
self.dependencies = {}
|
|
1309
|
+
|
|
1310
|
+
def add_stage(self, name, callback, depends_on=None):
|
|
1311
|
+
self.stages[name] = callback
|
|
1312
|
+
self.dependencies[name] = depends_on or []
|
|
1313
|
+
return self
|
|
1314
|
+
|
|
1315
|
+
def run_stage(self, name, context=None):
|
|
1316
|
+
if name not in self.stages:
|
|
1317
|
+
raise KeyError(f'Stage {name} is not registered')
|
|
1318
|
+
context = context or {}
|
|
1319
|
+
for dependency in self.dependencies.get(name, []):
|
|
1320
|
+
self.run_stage(dependency, context)
|
|
1321
|
+
return self.stages[name](context)
|
|
1322
|
+
|
|
1323
|
+
def run_all(self, context=None):
|
|
1324
|
+
context = context or {}
|
|
1325
|
+
for name in list(self.stages.keys()):
|
|
1326
|
+
self.run_stage(name, context)
|
|
1327
|
+
return context
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
class ResourceRegistry:
|
|
1331
|
+
"""Tracks named resources across application layers."""
|
|
1332
|
+
|
|
1333
|
+
def __init__(self):
|
|
1334
|
+
self.resources = {}
|
|
1335
|
+
|
|
1336
|
+
def register_resource(self, name, resource):
|
|
1337
|
+
self.resources[name] = resource
|
|
1338
|
+
return self
|
|
1339
|
+
|
|
1340
|
+
def get_resource(self, name, default=None):
|
|
1341
|
+
return self.resources.get(name, default)
|
|
1342
|
+
|
|
1343
|
+
def list_resources(self):
|
|
1344
|
+
return list(self.resources.keys())
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
class DistributedJobScheduler:
|
|
1348
|
+
"""Schedules jobs using a simple distributed work queue."""
|
|
1349
|
+
|
|
1350
|
+
def __init__(self):
|
|
1351
|
+
self.jobs = []
|
|
1352
|
+
self.results = {}
|
|
1353
|
+
|
|
1354
|
+
def schedule(self, job_id, callback, *args, **kwargs):
|
|
1355
|
+
self.jobs.append((job_id, callback, args, kwargs))
|
|
1356
|
+
return self
|
|
1357
|
+
|
|
1358
|
+
def run_next(self):
|
|
1359
|
+
if not self.jobs:
|
|
1360
|
+
return None
|
|
1361
|
+
job_id, callback, args, kwargs = self.jobs.pop(0)
|
|
1362
|
+
result = callback(*args, **kwargs)
|
|
1363
|
+
self.results[job_id] = result
|
|
1364
|
+
return result
|
|
1365
|
+
|
|
1366
|
+
def run_all(self):
|
|
1367
|
+
while self.jobs:
|
|
1368
|
+
self.run_next()
|
|
1369
|
+
return self.results
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
class SecurityPolicyManager:
|
|
1373
|
+
"""Manages security policies and validation rules."""
|
|
1374
|
+
|
|
1375
|
+
def __init__(self):
|
|
1376
|
+
self.policies = {}
|
|
1377
|
+
|
|
1378
|
+
def add_policy(self, name, callback):
|
|
1379
|
+
self.policies[name] = callback
|
|
1380
|
+
return self
|
|
1381
|
+
|
|
1382
|
+
def validate(self, name, context=None):
|
|
1383
|
+
if name not in self.policies:
|
|
1384
|
+
raise KeyError(f'Policy {name} not found')
|
|
1385
|
+
return bool(self.policies[name](context or {}))
|
|
1386
|
+
|
|
1387
|
+
def list_policies(self):
|
|
1388
|
+
return list(self.policies.keys())
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
class AuditTrail:
|
|
1392
|
+
"""Records immutable audit events."""
|
|
1393
|
+
|
|
1394
|
+
def __init__(self):
|
|
1395
|
+
self.events = []
|
|
1396
|
+
|
|
1397
|
+
def record_event(self, event_type, details=None):
|
|
1398
|
+
self.events.append({
|
|
1399
|
+
'type': event_type,
|
|
1400
|
+
'details': details,
|
|
1401
|
+
'timestamp': current_timestamp(),
|
|
1402
|
+
})
|
|
1403
|
+
return self
|
|
1404
|
+
|
|
1405
|
+
def query(self, event_type=None):
|
|
1406
|
+
if event_type is None:
|
|
1407
|
+
return list(self.events)
|
|
1408
|
+
return [item for item in self.events if item['type'] == event_type]
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
class ModelRegistry:
|
|
1412
|
+
"""Stores models and their metadata."""
|
|
1413
|
+
|
|
1414
|
+
def __init__(self):
|
|
1415
|
+
self.models = {}
|
|
1416
|
+
|
|
1417
|
+
def register_model(self, name, model, metadata=None):
|
|
1418
|
+
self.models[name] = {
|
|
1419
|
+
'model': model,
|
|
1420
|
+
'metadata': metadata or {},
|
|
1421
|
+
}
|
|
1422
|
+
return self
|
|
1423
|
+
|
|
1424
|
+
def get_model(self, name):
|
|
1425
|
+
return self.models.get(name)
|
|
1426
|
+
|
|
1427
|
+
def list_models(self):
|
|
1428
|
+
return list(self.models.keys())
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
class AnalyticsEngine:
|
|
1432
|
+
"""Processes metrics and analytics streams."""
|
|
1433
|
+
|
|
1434
|
+
def __init__(self):
|
|
1435
|
+
self.events = []
|
|
1436
|
+
|
|
1437
|
+
def track_event(self, name, properties=None):
|
|
1438
|
+
self.events.append({'name': name, 'properties': properties or {}, 'time': current_timestamp()})
|
|
1439
|
+
return self
|
|
1440
|
+
|
|
1441
|
+
def summarize(self):
|
|
1442
|
+
return {
|
|
1443
|
+
'total_events': len(self.events),
|
|
1444
|
+
'event_names': list({event['name'] for event in self.events}),
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
plugin_manager = PluginManager()
|
|
1449
|
+
workflow_engine = WorkflowEngine()
|
|
1450
|
+
resource_registry = ResourceRegistry()
|
|
1451
|
+
job_scheduler = DistributedJobScheduler()
|
|
1452
|
+
security_policy_manager = SecurityPolicyManager()
|
|
1453
|
+
audit_trail = AuditTrail()
|
|
1454
|
+
model_registry = ModelRegistry()
|
|
1455
|
+
analytics_engine = AnalyticsEngine()
|