isa-model 0.3.91__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- isa_model/client.py +1166 -584
- isa_model/core/cache/redis_cache.py +410 -0
- isa_model/core/config/config_manager.py +282 -12
- isa_model/core/config.py +91 -1
- isa_model/core/database/__init__.py +1 -0
- isa_model/core/database/direct_db_client.py +114 -0
- isa_model/core/database/migration_manager.py +563 -0
- isa_model/core/database/migrations.py +297 -0
- isa_model/core/database/supabase_client.py +258 -0
- isa_model/core/dependencies.py +316 -0
- isa_model/core/discovery/__init__.py +19 -0
- isa_model/core/discovery/consul_discovery.py +190 -0
- isa_model/core/logging/__init__.py +54 -0
- isa_model/core/logging/influx_logger.py +523 -0
- isa_model/core/logging/loki_logger.py +160 -0
- isa_model/core/models/__init__.py +46 -0
- isa_model/core/models/config_models.py +625 -0
- isa_model/core/models/deployment_billing_tracker.py +430 -0
- isa_model/core/models/model_billing_tracker.py +60 -88
- isa_model/core/models/model_manager.py +66 -25
- isa_model/core/models/model_metadata.py +690 -0
- isa_model/core/models/model_repo.py +217 -55
- isa_model/core/models/model_statistics_tracker.py +234 -0
- isa_model/core/models/model_storage.py +0 -1
- isa_model/core/models/model_version_manager.py +959 -0
- isa_model/core/models/system_models.py +857 -0
- isa_model/core/pricing_manager.py +2 -249
- isa_model/core/repositories/__init__.py +9 -0
- isa_model/core/repositories/config_repository.py +912 -0
- isa_model/core/resilience/circuit_breaker.py +366 -0
- isa_model/core/security/secrets.py +358 -0
- isa_model/core/services/__init__.py +2 -4
- isa_model/core/services/intelligent_model_selector.py +479 -370
- isa_model/core/storage/hf_storage.py +2 -2
- isa_model/core/types.py +8 -0
- isa_model/deployment/__init__.py +5 -48
- isa_model/deployment/core/__init__.py +2 -31
- isa_model/deployment/core/deployment_manager.py +1278 -368
- isa_model/deployment/local/__init__.py +31 -0
- isa_model/deployment/local/config.py +248 -0
- isa_model/deployment/local/gpu_gateway.py +607 -0
- isa_model/deployment/local/health_checker.py +428 -0
- isa_model/deployment/local/provider.py +586 -0
- isa_model/deployment/local/tensorrt_service.py +621 -0
- isa_model/deployment/local/transformers_service.py +644 -0
- isa_model/deployment/local/vllm_service.py +527 -0
- isa_model/deployment/modal/__init__.py +8 -0
- isa_model/deployment/modal/config.py +136 -0
- isa_model/deployment/modal/deployer.py +894 -0
- isa_model/deployment/modal/services/__init__.py +3 -0
- isa_model/deployment/modal/services/audio/__init__.py +1 -0
- isa_model/deployment/modal/services/audio/isa_audio_chatTTS_service.py +520 -0
- isa_model/deployment/modal/services/audio/isa_audio_openvoice_service.py +758 -0
- isa_model/deployment/modal/services/audio/isa_audio_service_v2.py +1044 -0
- isa_model/deployment/modal/services/embedding/__init__.py +1 -0
- isa_model/deployment/modal/services/embedding/isa_embed_rerank_service.py +296 -0
- isa_model/deployment/modal/services/llm/__init__.py +1 -0
- isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
- isa_model/deployment/modal/services/video/__init__.py +1 -0
- isa_model/deployment/modal/services/video/isa_video_hunyuan_service.py +423 -0
- isa_model/deployment/modal/services/vision/__init__.py +1 -0
- isa_model/deployment/modal/services/vision/isa_vision_ocr_service.py +519 -0
- isa_model/deployment/modal/services/vision/isa_vision_qwen25_service.py +709 -0
- isa_model/deployment/modal/services/vision/isa_vision_table_service.py +676 -0
- isa_model/deployment/modal/services/vision/isa_vision_ui_service.py +833 -0
- isa_model/deployment/modal/services/vision/isa_vision_ui_service_optimized.py +660 -0
- isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
- isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
- isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/storage/__init__.py +5 -0
- isa_model/deployment/storage/deployment_repository.py +824 -0
- isa_model/deployment/triton/__init__.py +10 -0
- isa_model/deployment/triton/config.py +196 -0
- isa_model/deployment/triton/configs/__init__.py +1 -0
- isa_model/deployment/triton/provider.py +512 -0
- isa_model/deployment/triton/scripts/__init__.py +1 -0
- isa_model/deployment/triton/templates/__init__.py +1 -0
- isa_model/inference/__init__.py +47 -1
- isa_model/inference/ai_factory.py +179 -16
- isa_model/inference/legacy_services/__init__.py +21 -0
- isa_model/inference/legacy_services/model_evaluation.py +637 -0
- isa_model/inference/legacy_services/model_service.py +573 -0
- isa_model/inference/legacy_services/model_serving.py +717 -0
- isa_model/inference/legacy_services/model_training.py +561 -0
- isa_model/inference/models/__init__.py +21 -0
- isa_model/inference/models/inference_config.py +551 -0
- isa_model/inference/models/inference_record.py +675 -0
- isa_model/inference/models/performance_models.py +714 -0
- isa_model/inference/repositories/__init__.py +9 -0
- isa_model/inference/repositories/inference_repository.py +828 -0
- isa_model/inference/services/audio/__init__.py +21 -0
- isa_model/inference/services/audio/base_realtime_service.py +225 -0
- isa_model/inference/services/audio/base_stt_service.py +184 -11
- isa_model/inference/services/audio/isa_tts_service.py +0 -0
- isa_model/inference/services/audio/openai_realtime_service.py +320 -124
- isa_model/inference/services/audio/openai_stt_service.py +53 -11
- isa_model/inference/services/base_service.py +17 -1
- isa_model/inference/services/custom_model_manager.py +277 -0
- isa_model/inference/services/embedding/__init__.py +13 -0
- isa_model/inference/services/embedding/base_embed_service.py +111 -8
- isa_model/inference/services/embedding/isa_embed_service.py +305 -0
- isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
- isa_model/inference/services/embedding/openai_embed_service.py +2 -4
- isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
- isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
- isa_model/inference/services/img/__init__.py +2 -2
- isa_model/inference/services/img/base_image_gen_service.py +24 -7
- isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
- isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
- isa_model/inference/services/img/services/replicate_flux.py +226 -0
- isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
- isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
- isa_model/inference/services/img/tests/test_img_client.py +297 -0
- isa_model/inference/services/llm/__init__.py +10 -2
- isa_model/inference/services/llm/base_llm_service.py +361 -26
- isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
- isa_model/inference/services/llm/helpers/llm_adapter.py +71 -12
- isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
- isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
- isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
- isa_model/inference/services/llm/local_llm_service.py +747 -0
- isa_model/inference/services/llm/ollama_llm_service.py +11 -3
- isa_model/inference/services/llm/openai_llm_service.py +670 -56
- isa_model/inference/services/llm/yyds_llm_service.py +10 -3
- isa_model/inference/services/vision/__init__.py +27 -6
- isa_model/inference/services/vision/base_vision_service.py +118 -185
- isa_model/inference/services/vision/blip_vision_service.py +359 -0
- isa_model/inference/services/vision/helpers/image_utils.py +19 -10
- isa_model/inference/services/vision/isa_vision_service.py +634 -0
- isa_model/inference/services/vision/openai_vision_service.py +19 -10
- isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
- isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
- isa_model/serving/api/cache_manager.py +245 -0
- isa_model/serving/api/dependencies/__init__.py +1 -0
- isa_model/serving/api/dependencies/auth.py +194 -0
- isa_model/serving/api/dependencies/database.py +139 -0
- isa_model/serving/api/error_handlers.py +284 -0
- isa_model/serving/api/fastapi_server.py +240 -18
- isa_model/serving/api/middleware/auth.py +317 -0
- isa_model/serving/api/middleware/security.py +268 -0
- isa_model/serving/api/middleware/tenant_context.py +414 -0
- isa_model/serving/api/routes/analytics.py +489 -0
- isa_model/serving/api/routes/config.py +645 -0
- isa_model/serving/api/routes/deployment_billing.py +315 -0
- isa_model/serving/api/routes/deployments.py +475 -0
- isa_model/serving/api/routes/gpu_gateway.py +440 -0
- isa_model/serving/api/routes/health.py +32 -12
- isa_model/serving/api/routes/inference_monitoring.py +486 -0
- isa_model/serving/api/routes/local_deployments.py +448 -0
- isa_model/serving/api/routes/logs.py +430 -0
- isa_model/serving/api/routes/settings.py +582 -0
- isa_model/serving/api/routes/tenants.py +575 -0
- isa_model/serving/api/routes/unified.py +992 -171
- isa_model/serving/api/routes/webhooks.py +479 -0
- isa_model/serving/api/startup.py +318 -0
- isa_model/serving/modal_proxy_server.py +249 -0
- isa_model/utils/gpu_utils.py +311 -0
- {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/METADATA +76 -22
- isa_model-0.4.3.dist-info/RECORD +193 -0
- isa_model/deployment/cloud/__init__.py +0 -9
- isa_model/deployment/cloud/modal/__init__.py +0 -10
- isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
- isa_model/deployment/cloud/modal/isa_vision_table_service.py +0 -532
- isa_model/deployment/cloud/modal/isa_vision_ui_service.py +0 -406
- isa_model/deployment/cloud/modal/register_models.py +0 -321
- isa_model/deployment/core/deployment_config.py +0 -356
- isa_model/deployment/core/isa_deployment_service.py +0 -401
- isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
- isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
- isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
- isa_model/deployment/runtime/deployed_service.py +0 -338
- isa_model/deployment/services/__init__.py +0 -9
- isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
- isa_model/deployment/services/model_service.py +0 -332
- isa_model/deployment/services/service_monitor.py +0 -356
- isa_model/deployment/services/service_registry.py +0 -527
- isa_model/eval/__init__.py +0 -92
- isa_model/eval/benchmarks.py +0 -469
- isa_model/eval/config/__init__.py +0 -10
- isa_model/eval/config/evaluation_config.py +0 -108
- isa_model/eval/evaluators/__init__.py +0 -18
- isa_model/eval/evaluators/base_evaluator.py +0 -503
- isa_model/eval/evaluators/llm_evaluator.py +0 -472
- isa_model/eval/factory.py +0 -531
- isa_model/eval/infrastructure/__init__.py +0 -24
- isa_model/eval/infrastructure/experiment_tracker.py +0 -466
- isa_model/eval/metrics.py +0 -798
- isa_model/inference/adapter/unified_api.py +0 -248
- isa_model/inference/services/helpers/stacked_config.py +0 -148
- isa_model/inference/services/img/flux_professional_service.py +0 -603
- isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/others/table_transformer_service.py +0 -61
- isa_model/inference/services/vision/doc_analysis_service.py +0 -640
- isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/vision/ui_analysis_service.py +0 -823
- isa_model/scripts/inference_tracker.py +0 -283
- isa_model/scripts/mlflow_manager.py +0 -379
- isa_model/scripts/model_registry.py +0 -465
- isa_model/scripts/register_models.py +0 -370
- isa_model/scripts/register_models_with_embeddings.py +0 -510
- isa_model/scripts/start_mlflow.py +0 -95
- isa_model/scripts/training_tracker.py +0 -257
- isa_model/training/__init__.py +0 -74
- isa_model/training/annotation/annotation_schema.py +0 -47
- isa_model/training/annotation/processors/annotation_processor.py +0 -126
- isa_model/training/annotation/storage/dataset_manager.py +0 -131
- isa_model/training/annotation/storage/dataset_schema.py +0 -44
- isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
- isa_model/training/annotation/tests/test_minio copy.py +0 -113
- isa_model/training/annotation/tests/test_minio_upload.py +0 -43
- isa_model/training/annotation/views/annotation_controller.py +0 -158
- isa_model/training/cloud/__init__.py +0 -22
- isa_model/training/cloud/job_orchestrator.py +0 -402
- isa_model/training/cloud/runpod_trainer.py +0 -454
- isa_model/training/cloud/storage_manager.py +0 -482
- isa_model/training/core/__init__.py +0 -23
- isa_model/training/core/config.py +0 -181
- isa_model/training/core/dataset.py +0 -222
- isa_model/training/core/trainer.py +0 -720
- isa_model/training/core/utils.py +0 -213
- isa_model/training/factory.py +0 -424
- isa_model-0.3.91.dist-info/RECORD +0 -138
- /isa_model/{core/storage/minio_storage.py → deployment/modal/services/audio/isa_audio_fish_service.py} +0 -0
- /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
- {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/WHEEL +0 -0
- {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,245 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
"""
|
5
|
+
Simple Cache Manager for ISA Model API
|
6
|
+
Provides in-memory caching to improve API performance
|
7
|
+
"""
|
8
|
+
|
9
|
+
import time
|
10
|
+
import logging
|
11
|
+
from typing import Dict, Any, Optional, Callable
|
12
|
+
from dataclasses import dataclass
|
13
|
+
from threading import RLock
|
14
|
+
import asyncio
|
15
|
+
import hashlib
|
16
|
+
import json
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class CacheEntry:
|
22
|
+
"""Cache entry with data and metadata"""
|
23
|
+
data: Any
|
24
|
+
created_at: float
|
25
|
+
ttl: float
|
26
|
+
access_count: int = 0
|
27
|
+
last_accessed: float = None
|
28
|
+
|
29
|
+
def is_expired(self) -> bool:
|
30
|
+
"""Check if cache entry is expired"""
|
31
|
+
return time.time() - self.created_at > self.ttl
|
32
|
+
|
33
|
+
def access(self) -> Any:
|
34
|
+
"""Mark as accessed and return data"""
|
35
|
+
self.access_count += 1
|
36
|
+
self.last_accessed = time.time()
|
37
|
+
return self.data
|
38
|
+
|
39
|
+
class APICache:
|
40
|
+
"""
|
41
|
+
Simple in-memory cache for API responses
|
42
|
+
Thread-safe with automatic expiration
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(self, default_ttl: float = 300.0, max_size: int = 1000):
|
46
|
+
self.default_ttl = default_ttl # 5 minutes default
|
47
|
+
self.max_size = max_size
|
48
|
+
self._cache: Dict[str, CacheEntry] = {}
|
49
|
+
self._lock = RLock()
|
50
|
+
self._stats = {
|
51
|
+
"hits": 0,
|
52
|
+
"misses": 0,
|
53
|
+
"evictions": 0,
|
54
|
+
"total_requests": 0
|
55
|
+
}
|
56
|
+
|
57
|
+
def _generate_key(self, *args, **kwargs) -> str:
|
58
|
+
"""Generate cache key from arguments"""
|
59
|
+
# Create a stable key from arguments
|
60
|
+
key_data = {
|
61
|
+
"args": args,
|
62
|
+
"kwargs": sorted(kwargs.items()) if kwargs else {}
|
63
|
+
}
|
64
|
+
key_string = json.dumps(key_data, sort_keys=True, default=str)
|
65
|
+
return hashlib.md5(key_string.encode()).hexdigest()
|
66
|
+
|
67
|
+
def _cleanup_expired(self):
|
68
|
+
"""Remove expired entries"""
|
69
|
+
with self._lock:
|
70
|
+
current_time = time.time()
|
71
|
+
expired_keys = [
|
72
|
+
key for key, entry in self._cache.items()
|
73
|
+
if entry.is_expired()
|
74
|
+
]
|
75
|
+
|
76
|
+
for key in expired_keys:
|
77
|
+
del self._cache[key]
|
78
|
+
|
79
|
+
if expired_keys:
|
80
|
+
logger.debug(f"Cleaned up {len(expired_keys)} expired cache entries")
|
81
|
+
|
82
|
+
def _evict_lru(self):
|
83
|
+
"""Evict least recently used entries when cache is full"""
|
84
|
+
with self._lock:
|
85
|
+
if len(self._cache) >= self.max_size:
|
86
|
+
# Sort by last_accessed time (LRU)
|
87
|
+
sorted_entries = sorted(
|
88
|
+
self._cache.items(),
|
89
|
+
key=lambda x: x[1].last_accessed or x[1].created_at
|
90
|
+
)
|
91
|
+
|
92
|
+
# Remove oldest 20% of entries
|
93
|
+
num_to_remove = max(1, len(sorted_entries) // 5)
|
94
|
+
for key, _ in sorted_entries[:num_to_remove]:
|
95
|
+
del self._cache[key]
|
96
|
+
self._stats["evictions"] += 1
|
97
|
+
|
98
|
+
logger.debug(f"Evicted {num_to_remove} LRU cache entries")
|
99
|
+
|
100
|
+
def get(self, key: str) -> Optional[Any]:
|
101
|
+
"""Get cached value by key"""
|
102
|
+
with self._lock:
|
103
|
+
self._stats["total_requests"] += 1
|
104
|
+
|
105
|
+
if key in self._cache:
|
106
|
+
entry = self._cache[key]
|
107
|
+
if not entry.is_expired():
|
108
|
+
self._stats["hits"] += 1
|
109
|
+
return entry.access()
|
110
|
+
else:
|
111
|
+
# Remove expired entry
|
112
|
+
del self._cache[key]
|
113
|
+
|
114
|
+
self._stats["misses"] += 1
|
115
|
+
return None
|
116
|
+
|
117
|
+
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
|
118
|
+
"""Set cached value with optional TTL"""
|
119
|
+
with self._lock:
|
120
|
+
# Cleanup and eviction
|
121
|
+
self._cleanup_expired()
|
122
|
+
self._evict_lru()
|
123
|
+
|
124
|
+
entry = CacheEntry(
|
125
|
+
data=value,
|
126
|
+
created_at=time.time(),
|
127
|
+
ttl=ttl or self.default_ttl,
|
128
|
+
last_accessed=time.time()
|
129
|
+
)
|
130
|
+
|
131
|
+
self._cache[key] = entry
|
132
|
+
|
133
|
+
def delete(self, key: str) -> bool:
|
134
|
+
"""Delete cached value"""
|
135
|
+
with self._lock:
|
136
|
+
if key in self._cache:
|
137
|
+
del self._cache[key]
|
138
|
+
return True
|
139
|
+
return False
|
140
|
+
|
141
|
+
def clear(self) -> None:
|
142
|
+
"""Clear all cached values"""
|
143
|
+
with self._lock:
|
144
|
+
self._cache.clear()
|
145
|
+
logger.info("Cache cleared")
|
146
|
+
|
147
|
+
def get_stats(self) -> Dict[str, Any]:
|
148
|
+
"""Get cache statistics"""
|
149
|
+
with self._lock:
|
150
|
+
hit_rate = (
|
151
|
+
self._stats["hits"] / self._stats["total_requests"]
|
152
|
+
if self._stats["total_requests"] > 0 else 0
|
153
|
+
)
|
154
|
+
|
155
|
+
return {
|
156
|
+
"cache_size": len(self._cache),
|
157
|
+
"max_size": self.max_size,
|
158
|
+
"default_ttl": self.default_ttl,
|
159
|
+
"hit_rate": round(hit_rate * 100, 2),
|
160
|
+
**self._stats
|
161
|
+
}
|
162
|
+
|
163
|
+
# Decorator for caching function results
|
164
|
+
def cached(ttl: float = 300.0, cache_key_func: Optional[Callable] = None):
|
165
|
+
"""
|
166
|
+
Decorator to cache function results
|
167
|
+
|
168
|
+
Args:
|
169
|
+
ttl: Time to live in seconds
|
170
|
+
cache_key_func: Custom function to generate cache key
|
171
|
+
"""
|
172
|
+
def decorator(func):
|
173
|
+
async def async_wrapper(*args, **kwargs):
|
174
|
+
# Generate cache key
|
175
|
+
if cache_key_func:
|
176
|
+
cache_key = cache_key_func(*args, **kwargs)
|
177
|
+
else:
|
178
|
+
cache_key = api_cache._generate_key(func.__name__, *args, **kwargs)
|
179
|
+
|
180
|
+
# Try to get from cache
|
181
|
+
cached_result = api_cache.get(cache_key)
|
182
|
+
if cached_result is not None:
|
183
|
+
logger.debug(f"Cache hit for {func.__name__}")
|
184
|
+
return cached_result
|
185
|
+
|
186
|
+
# Execute function and cache result
|
187
|
+
try:
|
188
|
+
result = await func(*args, **kwargs)
|
189
|
+
api_cache.set(cache_key, result, ttl)
|
190
|
+
logger.debug(f"Cached result for {func.__name__}")
|
191
|
+
return result
|
192
|
+
except Exception as e:
|
193
|
+
logger.error(f"Function {func.__name__} failed: {e}")
|
194
|
+
raise
|
195
|
+
|
196
|
+
def sync_wrapper(*args, **kwargs):
|
197
|
+
# Generate cache key
|
198
|
+
if cache_key_func:
|
199
|
+
cache_key = cache_key_func(*args, **kwargs)
|
200
|
+
else:
|
201
|
+
cache_key = api_cache._generate_key(func.__name__, *args, **kwargs)
|
202
|
+
|
203
|
+
# Try to get from cache
|
204
|
+
cached_result = api_cache.get(cache_key)
|
205
|
+
if cached_result is not None:
|
206
|
+
logger.debug(f"Cache hit for {func.__name__}")
|
207
|
+
return cached_result
|
208
|
+
|
209
|
+
# Execute function and cache result
|
210
|
+
try:
|
211
|
+
result = func(*args, **kwargs)
|
212
|
+
api_cache.set(cache_key, result, ttl)
|
213
|
+
logger.debug(f"Cached result for {func.__name__}")
|
214
|
+
return result
|
215
|
+
except Exception as e:
|
216
|
+
logger.error(f"Function {func.__name__} failed: {e}")
|
217
|
+
raise
|
218
|
+
|
219
|
+
# Return appropriate wrapper based on function type
|
220
|
+
if asyncio.iscoroutinefunction(func):
|
221
|
+
return async_wrapper
|
222
|
+
else:
|
223
|
+
return sync_wrapper
|
224
|
+
|
225
|
+
return decorator
|
226
|
+
|
227
|
+
# Global cache instance
|
228
|
+
api_cache = APICache(default_ttl=300.0, max_size=1000)
|
229
|
+
|
230
|
+
def get_api_cache() -> APICache:
|
231
|
+
"""Get the global API cache instance"""
|
232
|
+
return api_cache
|
233
|
+
|
234
|
+
# Cache key generators for common patterns
|
235
|
+
def model_list_cache_key(service_type=None):
|
236
|
+
"""Generate cache key for model list API"""
|
237
|
+
return f"models_list_{service_type or 'all'}"
|
238
|
+
|
239
|
+
def provider_list_cache_key():
|
240
|
+
"""Generate cache key for provider list API"""
|
241
|
+
return "providers_list"
|
242
|
+
|
243
|
+
def custom_models_cache_key(model_type=None, provider=None):
|
244
|
+
"""Generate cache key for custom models API"""
|
245
|
+
return f"custom_models_{model_type or 'all'}_{provider or 'all'}"
|
@@ -0,0 +1 @@
|
|
1
|
+
# Dependencies module
|
@@ -0,0 +1,194 @@
|
|
1
|
+
"""
|
2
|
+
Authentication and Authorization Dependencies
|
3
|
+
|
4
|
+
FastAPI dependencies for handling authentication, authorization,
|
5
|
+
and tenant access control. Integrates with existing auth middleware.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from fastapi import HTTPException, Depends, Request
|
9
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
10
|
+
from typing import Optional, Dict, Any
|
11
|
+
import logging
|
12
|
+
|
13
|
+
from ..middleware.tenant_context import get_tenant_context, require_tenant_context, TenantContext
|
14
|
+
from ..middleware.auth import authenticate_api_key, require_admin_access as require_api_admin
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
class User:
|
19
|
+
"""User model for authentication"""
|
20
|
+
def __init__(self, user_id: str, email: str, role: str = "member", organization_id: str = None):
|
21
|
+
self.user_id = user_id
|
22
|
+
self.email = email
|
23
|
+
self.role = role
|
24
|
+
self.organization_id = organization_id
|
25
|
+
|
26
|
+
def is_admin(self) -> bool:
|
27
|
+
return self.role in ["admin", "owner"]
|
28
|
+
|
29
|
+
def is_system_admin(self) -> bool:
|
30
|
+
return self.role == "system_admin"
|
31
|
+
|
32
|
+
class Organization:
|
33
|
+
"""Organization model"""
|
34
|
+
def __init__(self, organization_id: str, name: str, plan: str = "starter", status: str = "active"):
|
35
|
+
self.organization_id = organization_id
|
36
|
+
self.name = name
|
37
|
+
self.plan = plan
|
38
|
+
self.status = status
|
39
|
+
|
40
|
+
async def get_current_user(
|
41
|
+
auth_data: Dict = Depends(authenticate_api_key)
|
42
|
+
) -> Optional[User]:
|
43
|
+
"""
|
44
|
+
Get current authenticated user from existing auth system.
|
45
|
+
Integrates with the existing API key authentication.
|
46
|
+
"""
|
47
|
+
try:
|
48
|
+
if not auth_data.get("authenticated", True): # Anonymous when auth disabled
|
49
|
+
return None
|
50
|
+
|
51
|
+
# Get tenant context which should contain user info
|
52
|
+
tenant_context = get_tenant_context()
|
53
|
+
|
54
|
+
# Create user from auth data and tenant context
|
55
|
+
user_id = tenant_context.user_id if tenant_context else auth_data.get("name", "anonymous")
|
56
|
+
organization_id = tenant_context.organization_id if tenant_context else None
|
57
|
+
|
58
|
+
# Map API key scopes to user roles
|
59
|
+
scopes = auth_data.get("scopes", [])
|
60
|
+
if "admin" in scopes:
|
61
|
+
role = "admin"
|
62
|
+
elif "write" in scopes:
|
63
|
+
role = "member"
|
64
|
+
else:
|
65
|
+
role = "viewer"
|
66
|
+
|
67
|
+
return User(
|
68
|
+
user_id=user_id,
|
69
|
+
email=f"{user_id}@example.com", # TODO: Get from database
|
70
|
+
role=role,
|
71
|
+
organization_id=organization_id
|
72
|
+
)
|
73
|
+
|
74
|
+
except Exception as e:
|
75
|
+
logger.error(f"Error getting current user: {e}")
|
76
|
+
return None
|
77
|
+
|
78
|
+
async def require_authenticated_user(
|
79
|
+
current_user: Optional[User] = Depends(get_current_user)
|
80
|
+
) -> User:
|
81
|
+
"""
|
82
|
+
Require authenticated user or raise 401 error.
|
83
|
+
"""
|
84
|
+
if not current_user:
|
85
|
+
raise HTTPException(
|
86
|
+
status_code=401,
|
87
|
+
detail="Authentication required"
|
88
|
+
)
|
89
|
+
return current_user
|
90
|
+
|
91
|
+
async def get_current_organization() -> Optional[Organization]:
|
92
|
+
"""
|
93
|
+
Get current organization from tenant context.
|
94
|
+
"""
|
95
|
+
try:
|
96
|
+
tenant_context = get_tenant_context()
|
97
|
+
if tenant_context:
|
98
|
+
return Organization(
|
99
|
+
organization_id=tenant_context.organization_id,
|
100
|
+
name="Organization", # TODO: Get from database
|
101
|
+
plan=tenant_context.plan,
|
102
|
+
status="active"
|
103
|
+
)
|
104
|
+
return None
|
105
|
+
|
106
|
+
except Exception as e:
|
107
|
+
logger.error(f"Error getting current organization: {e}")
|
108
|
+
return None
|
109
|
+
|
110
|
+
async def require_organization_access() -> Organization:
|
111
|
+
"""
|
112
|
+
Require organization context or raise 401 error.
|
113
|
+
"""
|
114
|
+
org = await get_current_organization()
|
115
|
+
if not org:
|
116
|
+
raise HTTPException(
|
117
|
+
status_code=401,
|
118
|
+
detail="Organization access required"
|
119
|
+
)
|
120
|
+
return org
|
121
|
+
|
122
|
+
async def require_admin(
|
123
|
+
auth_data: Dict = Depends(require_api_admin),
|
124
|
+
current_user: User = Depends(get_current_user)
|
125
|
+
) -> User:
|
126
|
+
"""
|
127
|
+
Require admin role within organization.
|
128
|
+
Uses existing API key admin check plus tenant context.
|
129
|
+
"""
|
130
|
+
if not current_user or not current_user.is_admin():
|
131
|
+
raise HTTPException(
|
132
|
+
status_code=403,
|
133
|
+
detail="Admin privileges required"
|
134
|
+
)
|
135
|
+
return current_user
|
136
|
+
|
137
|
+
async def require_system_admin(
|
138
|
+
auth_data: Dict = Depends(require_api_admin)
|
139
|
+
) -> Dict:
|
140
|
+
"""
|
141
|
+
Require system admin role (for tenant management).
|
142
|
+
For now, maps to API key admin access.
|
143
|
+
"""
|
144
|
+
# For system admin, require API key admin access
|
145
|
+
return auth_data
|
146
|
+
|
147
|
+
def require_plan(min_plan: str):
|
148
|
+
"""
|
149
|
+
Factory function to create dependency that requires specific plan level.
|
150
|
+
Usage: Depends(require_plan("pro"))
|
151
|
+
"""
|
152
|
+
async def _check_plan():
|
153
|
+
tenant_context = get_tenant_context()
|
154
|
+
if not tenant_context:
|
155
|
+
raise HTTPException(
|
156
|
+
status_code=401,
|
157
|
+
detail="Authentication required"
|
158
|
+
)
|
159
|
+
|
160
|
+
plan_hierarchy = {
|
161
|
+
"starter": 1,
|
162
|
+
"pro": 2,
|
163
|
+
"enterprise": 3
|
164
|
+
}
|
165
|
+
|
166
|
+
current_plan_level = plan_hierarchy.get(tenant_context.plan, 0)
|
167
|
+
required_plan_level = plan_hierarchy.get(min_plan, 99)
|
168
|
+
|
169
|
+
if current_plan_level < required_plan_level:
|
170
|
+
raise HTTPException(
|
171
|
+
status_code=403,
|
172
|
+
detail=f"Plan upgrade required. Current: {tenant_context.plan}, Required: {min_plan}"
|
173
|
+
)
|
174
|
+
|
175
|
+
return tenant_context
|
176
|
+
|
177
|
+
return _check_plan
|
178
|
+
|
179
|
+
def check_resource_access(resource_type: str, action: str = "read"):
|
180
|
+
"""
|
181
|
+
Factory function to create dependency that checks resource access permissions.
|
182
|
+
"""
|
183
|
+
async def _check_access():
|
184
|
+
tenant_context = require_tenant_context()
|
185
|
+
|
186
|
+
if not tenant_context.can_access_resource(resource_type, action):
|
187
|
+
raise HTTPException(
|
188
|
+
status_code=403,
|
189
|
+
detail=f"Access denied to {resource_type}"
|
190
|
+
)
|
191
|
+
|
192
|
+
return tenant_context
|
193
|
+
|
194
|
+
return _check_access
|
@@ -0,0 +1,139 @@
|
|
1
|
+
"""
|
2
|
+
Database Connection Dependencies
|
3
|
+
|
4
|
+
Provides database connections and transaction management
|
5
|
+
with automatic tenant context handling.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
import asyncpg
|
10
|
+
import logging
|
11
|
+
import os
|
12
|
+
from contextlib import asynccontextmanager
|
13
|
+
from typing import Optional
|
14
|
+
|
15
|
+
from ..middleware.tenant_context import get_tenant_context
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
# Global connection pool
|
20
|
+
_connection_pool: Optional[asyncpg.Pool] = None
|
21
|
+
|
22
|
+
async def initialize_database_pool():
|
23
|
+
"""Initialize the database connection pool"""
|
24
|
+
global _connection_pool
|
25
|
+
|
26
|
+
if _connection_pool:
|
27
|
+
return _connection_pool
|
28
|
+
|
29
|
+
database_url = os.getenv("DATABASE_URL")
|
30
|
+
if not database_url:
|
31
|
+
raise RuntimeError("DATABASE_URL environment variable not set")
|
32
|
+
|
33
|
+
try:
|
34
|
+
_connection_pool = await asyncpg.create_pool(
|
35
|
+
database_url,
|
36
|
+
min_size=5,
|
37
|
+
max_size=20,
|
38
|
+
command_timeout=60,
|
39
|
+
server_settings={
|
40
|
+
'search_path': 'dev',
|
41
|
+
'timezone': 'UTC'
|
42
|
+
}
|
43
|
+
)
|
44
|
+
|
45
|
+
logger.info("Database connection pool initialized")
|
46
|
+
return _connection_pool
|
47
|
+
|
48
|
+
except Exception as e:
|
49
|
+
logger.error(f"Failed to initialize database pool: {e}")
|
50
|
+
raise
|
51
|
+
|
52
|
+
async def close_database_pool():
|
53
|
+
"""Close the database connection pool"""
|
54
|
+
global _connection_pool
|
55
|
+
|
56
|
+
if _connection_pool:
|
57
|
+
await _connection_pool.close()
|
58
|
+
_connection_pool = None
|
59
|
+
logger.info("Database connection pool closed")
|
60
|
+
|
61
|
+
@asynccontextmanager
|
62
|
+
async def get_database_connection():
|
63
|
+
"""
|
64
|
+
Get a database connection from the pool with automatic tenant context.
|
65
|
+
|
66
|
+
This context manager automatically:
|
67
|
+
1. Gets a connection from the pool
|
68
|
+
2. Sets the tenant context if available
|
69
|
+
3. Handles transactions
|
70
|
+
4. Returns the connection to the pool
|
71
|
+
"""
|
72
|
+
if not _connection_pool:
|
73
|
+
await initialize_database_pool()
|
74
|
+
|
75
|
+
async with _connection_pool.acquire() as conn:
|
76
|
+
try:
|
77
|
+
# Set tenant context if available
|
78
|
+
tenant_context = get_tenant_context()
|
79
|
+
if tenant_context:
|
80
|
+
await conn.execute(
|
81
|
+
"SELECT set_config('app.current_organization_id', $1, true)",
|
82
|
+
tenant_context.organization_id
|
83
|
+
)
|
84
|
+
|
85
|
+
yield conn
|
86
|
+
|
87
|
+
except Exception as e:
|
88
|
+
logger.error(f"Database operation error: {e}")
|
89
|
+
raise
|
90
|
+
finally:
|
91
|
+
# Clear tenant context
|
92
|
+
try:
|
93
|
+
await conn.execute(
|
94
|
+
"SELECT set_config('app.current_organization_id', '', true)"
|
95
|
+
)
|
96
|
+
except:
|
97
|
+
pass # Ignore cleanup errors
|
98
|
+
|
99
|
+
@asynccontextmanager
|
100
|
+
async def get_database_transaction():
|
101
|
+
"""
|
102
|
+
Get a database connection with an explicit transaction.
|
103
|
+
"""
|
104
|
+
async with get_database_connection() as conn:
|
105
|
+
async with conn.transaction():
|
106
|
+
yield conn
|
107
|
+
|
108
|
+
async def execute_query(query: str, *args, fetch_type: str = "fetch"):
|
109
|
+
"""
|
110
|
+
Execute a query with automatic connection management.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
query: SQL query
|
114
|
+
*args: Query parameters
|
115
|
+
fetch_type: 'fetch', 'fetchrow', 'fetchval', or 'execute'
|
116
|
+
"""
|
117
|
+
async with get_database_connection() as conn:
|
118
|
+
if fetch_type == "fetch":
|
119
|
+
return await conn.fetch(query, *args)
|
120
|
+
elif fetch_type == "fetchrow":
|
121
|
+
return await conn.fetchrow(query, *args)
|
122
|
+
elif fetch_type == "fetchval":
|
123
|
+
return await conn.fetchval(query, *args)
|
124
|
+
elif fetch_type == "execute":
|
125
|
+
return await conn.execute(query, *args)
|
126
|
+
else:
|
127
|
+
raise ValueError(f"Invalid fetch_type: {fetch_type}")
|
128
|
+
|
129
|
+
# FastAPI dependency functions
|
130
|
+
|
131
|
+
async def get_db_connection():
|
132
|
+
"""FastAPI dependency to get database connection"""
|
133
|
+
async with get_database_connection() as conn:
|
134
|
+
yield conn
|
135
|
+
|
136
|
+
async def get_db_transaction():
|
137
|
+
"""FastAPI dependency to get database transaction"""
|
138
|
+
async with get_database_transaction() as conn:
|
139
|
+
yield conn
|