AbstractIntegratedModule 0.9.7__tar.gz → 0.9.8__tar.gz
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.
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.egg-info/PKG-INFO +2 -2
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.py +304 -71
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/AbstractOptimizedModules.c +200 -200
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/PKG-INFO +2 -2
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/README.md +1 -1
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/setup.py +1 -1
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.egg-info/SOURCES.txt +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.egg-info/dependency_links.txt +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.egg-info/requires.txt +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.egg-info/top_level.txt +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/AbstractOptimizedModules.pyx +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/MANIFEST.in +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/abstract_model_storage/Cargo.toml +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/abstract_model_storage/pyproject.toml +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/abstract_model_storage/src/lib.rs +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/debug/build/libsqlite3-sys-ed07b882cd2aa5e2/out/bindgen.rs +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/debug/build/serde_core-ebc15f2e9cad7f5f/out/private.rs +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/debug/build/target-lexicon-08527f45de28143d/out/host.rs +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/release/build/libsqlite3-sys-bf0400df4523274c/out/bindgen.rs +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/release/build/serde_core-5cdb76131825e4af/out/private.rs +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/abstract_model_storage/target/release/build/target-lexicon-43eb95a0588bf457/out/host.rs +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/pyproject.toml +0 -0
- {abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AbstractIntegratedModule
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.8
|
|
4
4
|
Summary: Library for Advanced Integrated Non-LLM AI Models - Optimized Backend Framework For Non-LLM AI Agent
|
|
5
5
|
Author: Micro-Novelty
|
|
6
6
|
Author-email: hernikpuspita5@gmail.com
|
|
@@ -42,7 +42,7 @@ https://github.com/Micro-Novelty/IntegratedPipeline-Specialized-Non-LLM-AI-Agent
|
|
|
42
42
|
#### Note: The README here you are reading is a direct copy from my README Repository, to download the necessary files, you can visit my Repository with the provided link above.
|
|
43
43
|
|
|
44
44
|
### Library Short Description:
|
|
45
|
-
- Development Stage: 0.9.
|
|
45
|
+
- Development Stage: 0.9.8 Official Release.
|
|
46
46
|
- Maintainer: Micro-Novelty.
|
|
47
47
|
- library Source-Code is Open-sourced on github.
|
|
48
48
|
- Purpose: Specifically Designed for providing Non-LLM AI Agent Framework for edge Devices, Optimized for ARM64 architecture.
|
{abstractintegratedmodule-0.9.7 → abstractintegratedmodule-0.9.8}/AbstractIntegratedModule.py
RENAMED
|
@@ -3484,11 +3484,15 @@ class WeightedEnsemblePredictor:
|
|
|
3484
3484
|
def _meta_ensemble(self, trans_probs, mlp_probs, attn_weights, X_mlp,
|
|
3485
3485
|
lstm_probs=None, lstm_weight_hint=0.0):
|
|
3486
3486
|
lstm_row = None
|
|
3487
|
+
|
|
3488
|
+
if trans_probs.ndim == 1: trans_probs = trans_probs[np.newaxis, :]
|
|
3489
|
+
if mlp_probs.ndim == 1: mlp_probs = mlp_probs[np.newaxis, :]
|
|
3490
|
+
|
|
3491
|
+
B = trans_probs.shape[0]
|
|
3487
3492
|
|
|
3488
3493
|
batch_size = trans_probs.shape[0]
|
|
3489
|
-
n_classes = trans_probs.shape[1]
|
|
3490
3494
|
threshold_feature = 0.1 + self.pipeline.confidence_threshold
|
|
3491
|
-
|
|
3495
|
+
|
|
3492
3496
|
n_trans_classes = trans_probs.shape[1]
|
|
3493
3497
|
n_mlp_classes = mlp_probs.shape[1]
|
|
3494
3498
|
|
|
@@ -3504,6 +3508,13 @@ class WeightedEnsemblePredictor:
|
|
|
3504
3508
|
n_classes = max(n_trans_classes, n_lstm, n_mlp_classes)
|
|
3505
3509
|
print(f"🔄 Aligning classes: trans={n_trans_classes} mlp={n_mlp_classes} "
|
|
3506
3510
|
f"lstm={n_lstm} → {n_classes}")
|
|
3511
|
+
|
|
3512
|
+
if isinstance(lstm_weight_hint, (int, float)):
|
|
3513
|
+
lstm_weight_hints = np.full(B, float(lstm_weight_hint), dtype=np.float64)
|
|
3514
|
+
else:
|
|
3515
|
+
lstm_weight_hints = np.asarray(lstm_weight_hint, dtype=np.float64)
|
|
3516
|
+
if lstm_weight_hints.ndim == 0:
|
|
3517
|
+
lstm_weight_hints = np.full(B, float(lstm_weight_hints))
|
|
3507
3518
|
|
|
3508
3519
|
# Create meta features
|
|
3509
3520
|
meta_features = []
|
|
@@ -3584,7 +3595,7 @@ class WeightedEnsemblePredictor:
|
|
|
3584
3595
|
lstm_row = np.zeros(n_classes)
|
|
3585
3596
|
lstm_row[:n_lstm] = lstm_probs[i]
|
|
3586
3597
|
lstm_row /= lstm_row.sum() + 1e-8
|
|
3587
|
-
|
|
3598
|
+
|
|
3588
3599
|
lstm_pred = int(np.argmax(lstm_probs[i]))
|
|
3589
3600
|
la = 1.0 if (lstm_pred == trans_pred or lstm_pred == mlp_pred) \
|
|
3590
3601
|
else self.pipeline.confidence_threshold
|
|
@@ -12914,117 +12925,272 @@ class AccurateAnswerCache:
|
|
|
12914
12925
|
|
|
12915
12926
|
|
|
12916
12927
|
|
|
12917
|
-
|
|
12918
12928
|
class RateLimiter:
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12929
|
+
"""
|
|
12930
|
+
Token bucket rate limiter.
|
|
12931
|
+
|
|
12932
|
+
Supports both:
|
|
12933
|
+
- a single shared bucket (original behavior, backward compatible)
|
|
12934
|
+
- independent per-key buckets (e.g. per peer IP/agent_id),
|
|
12935
|
+
so one noisy peer cannot starve rate-limit capacity from others
|
|
12936
|
+
"""
|
|
12937
|
+
|
|
12938
|
+
def __init__(self, requests_per_minute: int = 60, per_key: bool = False,
|
|
12939
|
+
max_keys: int = 1000):
|
|
12940
|
+
#validate config, refuse a limiter that can never refill
|
|
12941
|
+
if requests_per_minute <= 0:
|
|
12942
|
+
raise ValueError(
|
|
12943
|
+
f"[-] requests_per_minute must be > 0, got {requests_per_minute}"
|
|
12944
|
+
)
|
|
12945
|
+
|
|
12922
12946
|
self.requests_per_minute = requests_per_minute
|
|
12923
|
-
self.
|
|
12924
|
-
self.
|
|
12947
|
+
self.per_key = per_key
|
|
12948
|
+
self.max_keys = max_keys
|
|
12925
12949
|
self._lock = threading.Lock()
|
|
12926
|
-
|
|
12927
|
-
|
|
12950
|
+
|
|
12951
|
+
if per_key:
|
|
12952
|
+
# one bucket per key, so peers don't share capacity here
|
|
12953
|
+
self._buckets: Dict[str, dict] = {}
|
|
12954
|
+
else:
|
|
12955
|
+
self.tokens = float(requests_per_minute)
|
|
12956
|
+
self.last_refill = time.time()
|
|
12957
|
+
|
|
12958
|
+
def acquire(self, key: str = None) -> bool:
|
|
12928
12959
|
with self._lock:
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12960
|
+
if self.per_key:
|
|
12961
|
+
return self._acquire_keyed(key or "_default")
|
|
12962
|
+
return self._acquire_global()
|
|
12963
|
+
|
|
12964
|
+
def _acquire_global(self) -> bool:
|
|
12965
|
+
now = time.time()
|
|
12966
|
+
elapsed = now - self.last_refill
|
|
12967
|
+
|
|
12968
|
+
# guarded against clock skew producing negative elapsed
|
|
12969
|
+
if elapsed < 0:
|
|
12970
|
+
logger.warning(
|
|
12971
|
+
f'[!] RateLimiter: system clock moved backward by '
|
|
12972
|
+
f'{-elapsed:.3f}s — ignoring this interval for refill'
|
|
12973
|
+
)
|
|
12974
|
+
elapsed = 0.0
|
|
12975
|
+
|
|
12976
|
+
new_tokens = elapsed * (self.requests_per_minute / 60.0)
|
|
12977
|
+
self.tokens = min(self.requests_per_minute, self.tokens + new_tokens)
|
|
12978
|
+
self.last_refill = now
|
|
12979
|
+
|
|
12980
|
+
if self.tokens >= 1:
|
|
12981
|
+
self.tokens -= 1
|
|
12982
|
+
return True
|
|
12983
|
+
return False
|
|
12984
|
+
|
|
12985
|
+
def _acquire_keyed(self, key: str) -> bool:
|
|
12986
|
+
now = time.time()
|
|
12987
|
+
|
|
12988
|
+
if key not in self._buckets:
|
|
12989
|
+
# cap number of tracked keys to prevent unbounded
|
|
12990
|
+
# growth from an attacker cycling through many fake peer IDs
|
|
12991
|
+
if len(self._buckets) >= self.max_keys:
|
|
12992
|
+
self._evict_oldest_bucket()
|
|
12993
|
+
|
|
12994
|
+
self._buckets[key] = {
|
|
12995
|
+
'tokens': float(self.requests_per_minute),
|
|
12996
|
+
'last_refill': now,
|
|
12997
|
+
}
|
|
12998
|
+
|
|
12999
|
+
bucket = self._buckets[key]
|
|
13000
|
+
elapsed = now - bucket['last_refill']
|
|
13001
|
+
|
|
13002
|
+
if elapsed < 0:
|
|
13003
|
+
logger.warning(
|
|
13004
|
+
f'[!] RateLimiter[{key}]: clock moved backward by '
|
|
13005
|
+
f'{-elapsed:.3f}s — ignoring interval'
|
|
13006
|
+
)
|
|
13007
|
+
elapsed = 0.0
|
|
13008
|
+
|
|
13009
|
+
new_tokens = elapsed * (self.requests_per_minute / 60.0)
|
|
13010
|
+
bucket['tokens'] = min(self.requests_per_minute, bucket['tokens'] + new_tokens)
|
|
13011
|
+
bucket['last_refill'] = now
|
|
13012
|
+
|
|
13013
|
+
if bucket['tokens'] >= 1:
|
|
13014
|
+
bucket['tokens'] -= 1
|
|
13015
|
+
return True
|
|
13016
|
+
return False
|
|
13017
|
+
|
|
13018
|
+
def _evict_oldest_bucket(self):
|
|
13019
|
+
"""Evict the least-recently-refilled bucket to bound memory use."""
|
|
13020
|
+
if not self._buckets:
|
|
13021
|
+
return
|
|
13022
|
+
oldest_key = min(
|
|
13023
|
+
self._buckets.keys(),
|
|
13024
|
+
key=lambda k: self._buckets[k]['last_refill']
|
|
13025
|
+
)
|
|
13026
|
+
del self._buckets[oldest_key]
|
|
13027
|
+
|
|
13028
|
+
def get_wait_time(self, key: str = None) -> float:
|
|
13029
|
+
"""
|
|
13030
|
+
Seconds until at least 1 token will be available.
|
|
13031
|
+
Useful for callers that want to back off.
|
|
13032
|
+
"""
|
|
13033
|
+
with self._lock:
|
|
13034
|
+
if self.per_key:
|
|
13035
|
+
bucket = self._buckets.get(key or "_default")
|
|
13036
|
+
tokens = bucket['tokens'] if bucket else self.requests_per_minute
|
|
13037
|
+
else:
|
|
13038
|
+
tokens = self.tokens
|
|
13039
|
+
|
|
13040
|
+
if tokens >= 1:
|
|
13041
|
+
return 0.0
|
|
13042
|
+
tokens_needed = 1 - tokens
|
|
13043
|
+
return tokens_needed / (self.requests_per_minute / 60.0)
|
|
13044
|
+
|
|
13045
|
+
def get_stats(self) -> Dict:
|
|
13046
|
+
"""Visibility into limiter state"""
|
|
13047
|
+
with self._lock:
|
|
13048
|
+
if self.per_key:
|
|
13049
|
+
return {
|
|
13050
|
+
'mode' : 'per_key',
|
|
13051
|
+
'tracked_keys' : len(self._buckets),
|
|
13052
|
+
'max_keys' : self.max_keys,
|
|
13053
|
+
'requests_per_minute': self.requests_per_minute,
|
|
13054
|
+
}
|
|
13055
|
+
return {
|
|
13056
|
+
'mode' : 'global',
|
|
13057
|
+
'current_tokens': round(self.tokens, 2),
|
|
13058
|
+
'requests_per_minute': self.requests_per_minute,
|
|
13059
|
+
}
|
|
13060
|
+
|
|
12940
13061
|
|
|
12941
13062
|
class InputSanitizer:
|
|
12942
|
-
|
|
12943
|
-
|
|
13063
|
+
"""Sanitize and validate inputs."""
|
|
13064
|
+
|
|
13065
|
+
# comprehensive control-char stripping including
|
|
13066
|
+
# newline/CR, since this class explicitly exists to protect
|
|
13067
|
+
# logging/serialization from injection
|
|
13068
|
+
_CONTROL_CHARS_PATTERN = re.compile(
|
|
13069
|
+
r'[\x00-\x1f\x7f]'
|
|
13070
|
+
)
|
|
13071
|
+
|
|
13072
|
+
# OPT-IN for genuinely
|
|
13073
|
+
# multi-line legitimate text (e.g. free-form descriptions).
|
|
13074
|
+
_CONTROL_CHARS_ALLOW_WHITESPACE = re.compile(
|
|
13075
|
+
r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'
|
|
13076
|
+
)
|
|
13077
|
+
|
|
12944
13078
|
@staticmethod
|
|
12945
|
-
def sanitize_text(text: str, max_length: int = 10000
|
|
13079
|
+
def sanitize_text(text: str, max_length: int = 10000,
|
|
13080
|
+
allow_newlines: bool = False) -> str:
|
|
12946
13081
|
if not isinstance(text, str):
|
|
12947
13082
|
raise SecurityError("[-] Input must be a string")
|
|
12948
|
-
|
|
12949
|
-
#
|
|
12950
|
-
|
|
12951
|
-
|
|
12952
|
-
# Limit length
|
|
13083
|
+
|
|
13084
|
+
# check length BEFORE any transformation, so length
|
|
13085
|
+
# limits reflect what was actually submitted.
|
|
12953
13086
|
if len(text) > max_length:
|
|
12954
13087
|
raise SecurityError(f"[-] Text exceeds maximum length of {max_length}")
|
|
12955
|
-
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
|
|
13088
|
+
|
|
13089
|
+
if allow_newlines:
|
|
13090
|
+
text = InputSanitizer._CONTROL_CHARS_ALLOW_WHITESPACE.sub('', text)
|
|
13091
|
+
# even when newlines are allowed, escape them for log safety
|
|
13092
|
+
else:
|
|
13093
|
+
text = InputSanitizer._CONTROL_CHARS_PATTERN.sub('', text)
|
|
13094
|
+
|
|
13095
|
+
escape_patterns = [
|
|
13096
|
+
r'\\x[0-9a-fA-F]{2}', # literal "\x41" style escapes
|
|
13097
|
+
r'\\u[0-9a-fA-F]{4}', # literal "\u0041" style escapes
|
|
12961
13098
|
]
|
|
12962
|
-
|
|
12963
|
-
for pattern in dangerous_patterns:
|
|
13099
|
+
for pattern in escape_patterns:
|
|
12964
13100
|
if re.search(pattern, text):
|
|
12965
|
-
|
|
13101
|
+
logger.warning(
|
|
13102
|
+
f'[!] sanitize_text: literal escape sequence pattern '
|
|
13103
|
+
f'detected and neutralized (pattern={pattern})'
|
|
13104
|
+
)
|
|
12966
13105
|
text = re.sub(pattern, '?', text)
|
|
12967
|
-
|
|
13106
|
+
|
|
12968
13107
|
return text.strip()
|
|
12969
|
-
|
|
13108
|
+
|
|
12970
13109
|
@staticmethod
|
|
12971
13110
|
def validate_batch_size(size: int, max_batch: int = 100) -> bool:
|
|
13111
|
+
# validated type before comparison to avoid a confusing
|
|
13112
|
+
# TypeError if a non-int slips through
|
|
13113
|
+
if not isinstance(size, int):
|
|
13114
|
+
raise SecurityError(f"[-] Batch size must be an integer, got {type(size).__name__}")
|
|
12972
13115
|
if size <= 0 or size > max_batch:
|
|
12973
13116
|
raise SecurityError(f"[-] Batch size must be between 1 and {max_batch}")
|
|
12974
13117
|
return True
|
|
12975
13118
|
|
|
13119
|
+
|
|
12976
13120
|
class ApiKeyManager:
|
|
12977
|
-
|
|
12978
|
-
|
|
12979
|
-
|
|
12980
|
-
|
|
13121
|
+
"""Manage API keys with rotation and bounded storage."""
|
|
13122
|
+
|
|
13123
|
+
MIN_KEY_LENGTH = 16 # minimum acceptable length for caller-supplied keys
|
|
13124
|
+
|
|
13125
|
+
def __init__(self, rotation_days: int = 30, max_keys: int = 10000):
|
|
13126
|
+
self.keys: Dict[str, dict] = {}
|
|
12981
13127
|
self.rotation_days = rotation_days
|
|
13128
|
+
self.max_keys = max_keys
|
|
12982
13129
|
self._lock = threading.Lock()
|
|
12983
|
-
|
|
13130
|
+
|
|
12984
13131
|
def generate_key(self, metadata: dict = None, key_value: str = None) -> str:
|
|
12985
|
-
# Generate a new API key
|
|
12986
13132
|
if key_value:
|
|
12987
|
-
#
|
|
13133
|
+
# reject obviously weak caller-supplied keys
|
|
13134
|
+
if not isinstance(key_value, str) or len(key_value) < self.MIN_KEY_LENGTH:
|
|
13135
|
+
raise SecurityError(
|
|
13136
|
+
f"[-] Provided key must be a string of at least "
|
|
13137
|
+
f"{self.MIN_KEY_LENGTH} characters"
|
|
13138
|
+
)
|
|
12988
13139
|
raw_key = key_value
|
|
12989
13140
|
else:
|
|
12990
|
-
|
|
12991
|
-
raw_key = secrets.token_urlsafe(32)
|
|
13141
|
+
raw_key = secrets.token_urlsafe(32)
|
|
12992
13142
|
|
|
12993
13143
|
key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
|
|
12994
|
-
|
|
13144
|
+
|
|
12995
13145
|
with self._lock:
|
|
13146
|
+
# warn on collision
|
|
13147
|
+
if key_hash in self.keys and self.keys[key_hash].get('is_active'):
|
|
13148
|
+
logger.warning(
|
|
13149
|
+
f'[!] generate_key: key hash collision with an '
|
|
13150
|
+
f'already-active key — overwriting metadata'
|
|
13151
|
+
)
|
|
13152
|
+
|
|
13153
|
+
# evict oldest inactive entries if at capacity,
|
|
13154
|
+
if len(self.keys) >= self.max_keys:
|
|
13155
|
+
self._evict_oldest_inactive()
|
|
13156
|
+
|
|
12996
13157
|
self.keys[key_hash] = {
|
|
12997
13158
|
'created_at': datetime.now(),
|
|
12998
|
-
'last_used': None,
|
|
12999
|
-
'metadata': metadata or {},
|
|
13000
|
-
'is_active': True
|
|
13159
|
+
'last_used' : None,
|
|
13160
|
+
'metadata' : metadata or {},
|
|
13161
|
+
'is_active' : True
|
|
13001
13162
|
}
|
|
13002
|
-
|
|
13163
|
+
|
|
13003
13164
|
return raw_key
|
|
13004
|
-
|
|
13165
|
+
|
|
13005
13166
|
def validate_key(self, api_key: str) -> bool:
|
|
13006
|
-
#
|
|
13007
|
-
|
|
13167
|
+
# explicit type check before .encode(), avoids an
|
|
13168
|
+
# uncontrolled AttributeError on non-string input
|
|
13169
|
+
if not api_key or not isinstance(api_key, str):
|
|
13008
13170
|
return False
|
|
13009
|
-
|
|
13010
|
-
|
|
13011
|
-
|
|
13171
|
+
|
|
13172
|
+
try:
|
|
13173
|
+
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
|
|
13174
|
+
except (UnicodeEncodeError, AttributeError):
|
|
13175
|
+
return False
|
|
13176
|
+
|
|
13012
13177
|
with self._lock:
|
|
13013
13178
|
key_info = self.keys.get(key_hash)
|
|
13014
13179
|
if not key_info or not key_info.get('is_active', False):
|
|
13015
13180
|
return False
|
|
13016
|
-
|
|
13017
|
-
# Check rotation
|
|
13181
|
+
|
|
13018
13182
|
age = (datetime.now() - key_info['created_at']).days
|
|
13019
13183
|
if age >= self.rotation_days:
|
|
13020
13184
|
key_info['is_active'] = False
|
|
13021
13185
|
return False
|
|
13022
|
-
|
|
13186
|
+
|
|
13023
13187
|
key_info['last_used'] = datetime.now()
|
|
13024
13188
|
return True
|
|
13025
|
-
|
|
13189
|
+
|
|
13026
13190
|
def revoke_key(self, api_key: str) -> bool:
|
|
13027
|
-
|
|
13191
|
+
if not api_key or not isinstance(api_key, str):
|
|
13192
|
+
return False
|
|
13193
|
+
|
|
13028
13194
|
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
|
|
13029
13195
|
with self._lock:
|
|
13030
13196
|
if key_hash in self.keys:
|
|
@@ -13032,6 +13198,54 @@ class ApiKeyManager:
|
|
|
13032
13198
|
return True
|
|
13033
13199
|
return False
|
|
13034
13200
|
|
|
13201
|
+
def cleanup_expired(self, grace_period_days: int = 7):
|
|
13202
|
+
"""
|
|
13203
|
+
periodic sweep to actually remove long-inactive keys.
|
|
13204
|
+
"""
|
|
13205
|
+
with self._lock:
|
|
13206
|
+
now = datetime.now()
|
|
13207
|
+
to_remove = [
|
|
13208
|
+
key_hash for key_hash, info in self.keys.items()
|
|
13209
|
+
if not info.get('is_active', False)
|
|
13210
|
+
and (now - info['created_at']).days >= (self.rotation_days + grace_period_days)
|
|
13211
|
+
]
|
|
13212
|
+
for key_hash in to_remove:
|
|
13213
|
+
del self.keys[key_hash]
|
|
13214
|
+
|
|
13215
|
+
if to_remove:
|
|
13216
|
+
logger.info(f'[=] Cleaned up {len(to_remove)} expired API key entries')
|
|
13217
|
+
|
|
13218
|
+
return len(to_remove)
|
|
13219
|
+
|
|
13220
|
+
def _evict_oldest_inactive(self):
|
|
13221
|
+
"""Evict the oldest inactive key to make room, called under lock."""
|
|
13222
|
+
inactive = [
|
|
13223
|
+
(h, info) for h, info in self.keys.items()
|
|
13224
|
+
if not info.get('is_active', False)
|
|
13225
|
+
]
|
|
13226
|
+
if inactive:
|
|
13227
|
+
oldest_hash = min(inactive, key=lambda x: x[1]['created_at'])[0]
|
|
13228
|
+
del self.keys[oldest_hash]
|
|
13229
|
+
else:
|
|
13230
|
+
# no inactive keys to evict and still at capacity
|
|
13231
|
+
logger.warning(
|
|
13232
|
+
f'[!] ApiKeyManager at max_keys={self.max_keys} capacity '
|
|
13233
|
+
f'with no inactive keys to evict — consider raising max_keys '
|
|
13234
|
+
f'or auditing why so many keys remain active'
|
|
13235
|
+
)
|
|
13236
|
+
|
|
13237
|
+
def get_stats(self) -> Dict:
|
|
13238
|
+
"""Visibility into key store health — same pattern as WorkerPool.get_health()."""
|
|
13239
|
+
with self._lock:
|
|
13240
|
+
active = sum(1 for k in self.keys.values() if k.get('is_active'))
|
|
13241
|
+
inactive = len(self.keys) - active
|
|
13242
|
+
return {
|
|
13243
|
+
'total_keys' : len(self.keys),
|
|
13244
|
+
'active_keys' : active,
|
|
13245
|
+
'inactive_keys': inactive,
|
|
13246
|
+
'at_capacity' : len(self.keys) >= self.max_keys,
|
|
13247
|
+
}
|
|
13248
|
+
|
|
13035
13249
|
class AsyncResultQueue:
|
|
13036
13250
|
"""
|
|
13037
13251
|
Complete result queue with integrated processor.
|
|
@@ -16037,6 +16251,7 @@ class PipelinePredictionManager:
|
|
|
16037
16251
|
|
|
16038
16252
|
if X is None or len(X) == 0 or isinstance(X, int) or (isinstance(X, np.ndarray) and X.size == 0):
|
|
16039
16253
|
# Get MLP predictions
|
|
16254
|
+
titles = None
|
|
16040
16255
|
if titles is not None and len(titles) > 0:
|
|
16041
16256
|
if isinstance(titles[0], tuple):
|
|
16042
16257
|
mlp_titles = [t[0] for t in titles]
|
|
@@ -16045,8 +16260,16 @@ class PipelinePredictionManager:
|
|
|
16045
16260
|
|
|
16046
16261
|
if not hasattr(self.pipeline, 'tfidf') or self.pipeline.tfidf is None:
|
|
16047
16262
|
self.pipeline.initialize_fitting(mlp_titles)
|
|
16048
|
-
|
|
16049
|
-
|
|
16263
|
+
|
|
16264
|
+
if isinstance(mlp_titles, (list, tuple, np.ndarray)):
|
|
16265
|
+
titles = mlp_titles[0]
|
|
16266
|
+
if isinstance(mlp_titles, (list, tuple, np.ndarray)):
|
|
16267
|
+
titles = titles[0]
|
|
16268
|
+
|
|
16269
|
+
if not isinstance(mlp_titles, list) or isinstance(titles, str):
|
|
16270
|
+
if titles is not None:
|
|
16271
|
+
mlp_titles = titles
|
|
16272
|
+
X = self.pipeline.tfidf.transform(mlp_titles).toarray()
|
|
16050
16273
|
|
|
16051
16274
|
if X_gen is not None:
|
|
16052
16275
|
X = X_gen
|
|
@@ -16380,9 +16603,19 @@ class PipelinePredictionManager:
|
|
|
16380
16603
|
}
|
|
16381
16604
|
results.append(result)
|
|
16382
16605
|
|
|
16383
|
-
|
|
16384
|
-
|
|
16385
|
-
|
|
16606
|
+
if results is not None and isinstance(results[0], dict):
|
|
16607
|
+
try:
|
|
16608
|
+
final_probs = results[0]['final_probs'] if results else None
|
|
16609
|
+
final_class_idx = results[0]['predicted_idx'] if results else None
|
|
16610
|
+
agreement = results[0]['models_agree'] if results else None
|
|
16611
|
+
except:
|
|
16612
|
+
final_probs = results[0].get('final_probs', None)
|
|
16613
|
+
final_class_idx = results[0].get('predicted_idx', None)
|
|
16614
|
+
agreement = results[0].get('models_agree', None)
|
|
16615
|
+
else:
|
|
16616
|
+
final_probs = mlp_probs
|
|
16617
|
+
final_class_idx = target_pred_indices
|
|
16618
|
+
agreement = False
|
|
16386
16619
|
|
|
16387
16620
|
# Display results
|
|
16388
16621
|
verbose = False
|