isa-model 0.4.0__py3-none-any.whl → 0.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. isa_model/client.py +466 -43
  2. isa_model/core/cache/redis_cache.py +12 -3
  3. isa_model/core/config/config_manager.py +230 -3
  4. isa_model/core/config.py +90 -0
  5. isa_model/core/database/direct_db_client.py +114 -0
  6. isa_model/core/database/migration_manager.py +563 -0
  7. isa_model/core/database/migrations.py +21 -1
  8. isa_model/core/database/supabase_client.py +154 -19
  9. isa_model/core/dependencies.py +316 -0
  10. isa_model/core/discovery/__init__.py +19 -0
  11. isa_model/core/discovery/consul_discovery.py +190 -0
  12. isa_model/core/logging/__init__.py +54 -0
  13. isa_model/core/logging/influx_logger.py +523 -0
  14. isa_model/core/logging/loki_logger.py +160 -0
  15. isa_model/core/models/__init__.py +27 -18
  16. isa_model/core/models/config_models.py +625 -0
  17. isa_model/core/models/deployment_billing_tracker.py +430 -0
  18. isa_model/core/models/model_manager.py +35 -80
  19. isa_model/core/models/model_metadata.py +690 -0
  20. isa_model/core/models/model_repo.py +174 -18
  21. isa_model/core/models/system_models.py +857 -0
  22. isa_model/core/repositories/__init__.py +9 -0
  23. isa_model/core/repositories/config_repository.py +912 -0
  24. isa_model/core/services/intelligent_model_selector.py +399 -21
  25. isa_model/core/types.py +1 -0
  26. isa_model/deployment/__init__.py +5 -48
  27. isa_model/deployment/core/__init__.py +2 -31
  28. isa_model/deployment/core/deployment_manager.py +1278 -370
  29. isa_model/deployment/modal/__init__.py +8 -0
  30. isa_model/deployment/modal/config.py +136 -0
  31. isa_model/deployment/{services/auto_hf_modal_deployer.py → modal/deployer.py} +1 -1
  32. isa_model/deployment/modal/services/__init__.py +3 -0
  33. isa_model/deployment/modal/services/audio/__init__.py +1 -0
  34. isa_model/deployment/modal/services/embedding/__init__.py +1 -0
  35. isa_model/deployment/modal/services/llm/__init__.py +1 -0
  36. isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
  37. isa_model/deployment/modal/services/video/__init__.py +1 -0
  38. isa_model/deployment/modal/services/vision/__init__.py +1 -0
  39. isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
  40. isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
  41. isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
  42. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
  43. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
  44. isa_model/deployment/storage/__init__.py +5 -0
  45. isa_model/deployment/storage/deployment_repository.py +824 -0
  46. isa_model/deployment/triton/__init__.py +10 -0
  47. isa_model/deployment/triton/config.py +196 -0
  48. isa_model/deployment/triton/configs/__init__.py +1 -0
  49. isa_model/deployment/triton/provider.py +512 -0
  50. isa_model/deployment/triton/scripts/__init__.py +1 -0
  51. isa_model/deployment/triton/templates/__init__.py +1 -0
  52. isa_model/inference/__init__.py +47 -1
  53. isa_model/inference/ai_factory.py +137 -10
  54. isa_model/inference/legacy_services/__init__.py +21 -0
  55. isa_model/inference/legacy_services/model_evaluation.py +637 -0
  56. isa_model/inference/legacy_services/model_service.py +573 -0
  57. isa_model/inference/legacy_services/model_serving.py +717 -0
  58. isa_model/inference/legacy_services/model_training.py +561 -0
  59. isa_model/inference/models/__init__.py +21 -0
  60. isa_model/inference/models/inference_config.py +551 -0
  61. isa_model/inference/models/inference_record.py +675 -0
  62. isa_model/inference/models/performance_models.py +714 -0
  63. isa_model/inference/repositories/__init__.py +9 -0
  64. isa_model/inference/repositories/inference_repository.py +828 -0
  65. isa_model/inference/services/audio/base_stt_service.py +184 -11
  66. isa_model/inference/services/audio/openai_stt_service.py +22 -6
  67. isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
  68. isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
  69. isa_model/inference/services/llm/__init__.py +10 -2
  70. isa_model/inference/services/llm/base_llm_service.py +335 -24
  71. isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
  72. isa_model/inference/services/llm/helpers/llm_adapter.py +9 -4
  73. isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
  74. isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
  75. isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
  76. isa_model/inference/services/llm/ollama_llm_service.py +9 -2
  77. isa_model/inference/services/llm/openai_llm_service.py +33 -16
  78. isa_model/inference/services/llm/yyds_llm_service.py +8 -2
  79. isa_model/inference/services/vision/__init__.py +22 -1
  80. isa_model/inference/services/vision/helpers/image_utils.py +8 -5
  81. isa_model/inference/services/vision/isa_vision_service.py +65 -4
  82. isa_model/inference/services/vision/openai_vision_service.py +19 -10
  83. isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
  84. isa_model/serving/api/cache_manager.py +245 -0
  85. isa_model/serving/api/dependencies/__init__.py +1 -0
  86. isa_model/serving/api/dependencies/auth.py +194 -0
  87. isa_model/serving/api/dependencies/database.py +139 -0
  88. isa_model/serving/api/error_handlers.py +284 -0
  89. isa_model/serving/api/fastapi_server.py +172 -22
  90. isa_model/serving/api/middleware/auth.py +8 -2
  91. isa_model/serving/api/middleware/security.py +23 -33
  92. isa_model/serving/api/middleware/tenant_context.py +414 -0
  93. isa_model/serving/api/routes/analytics.py +4 -1
  94. isa_model/serving/api/routes/config.py +645 -0
  95. isa_model/serving/api/routes/deployment_billing.py +315 -0
  96. isa_model/serving/api/routes/deployments.py +138 -2
  97. isa_model/serving/api/routes/gpu_gateway.py +440 -0
  98. isa_model/serving/api/routes/health.py +32 -12
  99. isa_model/serving/api/routes/inference_monitoring.py +486 -0
  100. isa_model/serving/api/routes/local_deployments.py +448 -0
  101. isa_model/serving/api/routes/tenants.py +575 -0
  102. isa_model/serving/api/routes/unified.py +680 -18
  103. isa_model/serving/api/routes/webhooks.py +479 -0
  104. isa_model/serving/api/startup.py +68 -54
  105. isa_model/utils/gpu_utils.py +311 -0
  106. {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/METADATA +71 -24
  107. isa_model-0.4.4.dist-info/RECORD +180 -0
  108. isa_model/core/security/secrets.py +0 -358
  109. isa_model/core/storage/hf_storage.py +0 -419
  110. isa_model/core/storage/minio_storage.py +0 -0
  111. isa_model/deployment/cloud/__init__.py +0 -9
  112. isa_model/deployment/cloud/modal/__init__.py +0 -10
  113. isa_model/deployment/core/deployment_config.py +0 -356
  114. isa_model/deployment/core/isa_deployment_service.py +0 -401
  115. isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
  116. isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
  117. isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
  118. isa_model/deployment/runtime/deployed_service.py +0 -338
  119. isa_model/deployment/services/__init__.py +0 -9
  120. isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
  121. isa_model/deployment/services/model_service.py +0 -332
  122. isa_model/deployment/services/service_monitor.py +0 -356
  123. isa_model/deployment/services/service_registry.py +0 -527
  124. isa_model/eval/__init__.py +0 -92
  125. isa_model/eval/benchmarks/__init__.py +0 -27
  126. isa_model/eval/benchmarks/multimodal_datasets.py +0 -460
  127. isa_model/eval/benchmarks.py +0 -701
  128. isa_model/eval/config/__init__.py +0 -10
  129. isa_model/eval/config/evaluation_config.py +0 -108
  130. isa_model/eval/evaluators/__init__.py +0 -24
  131. isa_model/eval/evaluators/audio_evaluator.py +0 -727
  132. isa_model/eval/evaluators/base_evaluator.py +0 -503
  133. isa_model/eval/evaluators/embedding_evaluator.py +0 -742
  134. isa_model/eval/evaluators/llm_evaluator.py +0 -472
  135. isa_model/eval/evaluators/vision_evaluator.py +0 -564
  136. isa_model/eval/example_evaluation.py +0 -395
  137. isa_model/eval/factory.py +0 -798
  138. isa_model/eval/infrastructure/__init__.py +0 -24
  139. isa_model/eval/infrastructure/experiment_tracker.py +0 -466
  140. isa_model/eval/isa_benchmarks.py +0 -700
  141. isa_model/eval/isa_integration.py +0 -582
  142. isa_model/eval/metrics.py +0 -951
  143. isa_model/eval/tests/unit/test_basic.py +0 -396
  144. isa_model/serving/api/routes/evaluations.py +0 -579
  145. isa_model/training/__init__.py +0 -168
  146. isa_model/training/annotation/annotation_schema.py +0 -47
  147. isa_model/training/annotation/processors/annotation_processor.py +0 -126
  148. isa_model/training/annotation/storage/dataset_manager.py +0 -131
  149. isa_model/training/annotation/storage/dataset_schema.py +0 -44
  150. isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
  151. isa_model/training/annotation/tests/test_minio copy.py +0 -113
  152. isa_model/training/annotation/tests/test_minio_upload.py +0 -43
  153. isa_model/training/annotation/views/annotation_controller.py +0 -158
  154. isa_model/training/cloud/__init__.py +0 -22
  155. isa_model/training/cloud/job_orchestrator.py +0 -402
  156. isa_model/training/cloud/runpod_trainer.py +0 -454
  157. isa_model/training/cloud/storage_manager.py +0 -482
  158. isa_model/training/core/__init__.py +0 -26
  159. isa_model/training/core/config.py +0 -181
  160. isa_model/training/core/dataset.py +0 -222
  161. isa_model/training/core/trainer.py +0 -720
  162. isa_model/training/core/utils.py +0 -213
  163. isa_model/training/examples/intelligent_training_example.py +0 -281
  164. isa_model/training/factory.py +0 -424
  165. isa_model/training/intelligent/__init__.py +0 -25
  166. isa_model/training/intelligent/decision_engine.py +0 -643
  167. isa_model/training/intelligent/intelligent_factory.py +0 -888
  168. isa_model/training/intelligent/knowledge_base.py +0 -751
  169. isa_model/training/intelligent/resource_optimizer.py +0 -839
  170. isa_model/training/intelligent/task_classifier.py +0 -576
  171. isa_model/training/storage/__init__.py +0 -24
  172. isa_model/training/storage/core_integration.py +0 -439
  173. isa_model/training/storage/training_repository.py +0 -552
  174. isa_model/training/storage/training_storage.py +0 -628
  175. isa_model-0.4.0.dist-info/RECORD +0 -182
  176. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_chatTTS_service.py +0 -0
  177. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_fish_service.py +0 -0
  178. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_openvoice_service.py +0 -0
  179. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_service_v2.py +0 -0
  180. /isa_model/deployment/{cloud/modal → modal/services/embedding}/isa_embed_rerank_service.py +0 -0
  181. /isa_model/deployment/{cloud/modal → modal/services/video}/isa_video_hunyuan_service.py +0 -0
  182. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ocr_service.py +0 -0
  183. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_qwen25_service.py +0 -0
  184. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_table_service.py +0 -0
  185. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service.py +0 -0
  186. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service_optimized.py +0 -0
  187. /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
  188. {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/WHEEL +0 -0
  189. {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ VGG16 Vision Service
4
+ Computer vision service using VGG16 for image classification
5
+ Based on the aircraft damage detection notebook implementation
6
+ """
7
+
8
+ import os
9
+ import numpy as np
10
+ from typing import Dict, List, Any, Optional, Union, BinaryIO
11
+ import logging
12
+ from PIL import Image
13
+ import io
14
+
15
+ from .base_vision_service import BaseVisionService
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ def _lazy_import_vgg16_deps():
20
+ """Lazy import VGG16 dependencies"""
21
+ try:
22
+ import tensorflow as tf
23
+ from tensorflow.keras.applications import VGG16
24
+ from tensorflow.keras.layers import Dense, Dropout, Flatten
25
+ from tensorflow.keras.models import Sequential, Model
26
+ from tensorflow.keras.optimizers import Adam
27
+ from tensorflow.keras.preprocessing.image import ImageDataGenerator
28
+
29
+ return {
30
+ 'tf': tf,
31
+ 'VGG16': VGG16,
32
+ 'Dense': Dense,
33
+ 'Dropout': Dropout,
34
+ 'Flatten': Flatten,
35
+ 'Sequential': Sequential,
36
+ 'Model': Model,
37
+ 'Adam': Adam,
38
+ 'ImageDataGenerator': ImageDataGenerator,
39
+ 'available': True
40
+ }
41
+ except ImportError as e:
42
+ logger.warning(f"VGG16 dependencies not available: {e}")
43
+ return {'available': False}
44
+
45
+ class VGG16VisionService(BaseVisionService):
46
+ """
47
+ VGG16-based vision service for image classification
48
+ Provides an alternative implementation to VLM-based classification
49
+ """
50
+
51
+ def __init__(self, model_path: Optional[str] = None, class_names: Optional[List[str]] = None):
52
+ """
53
+ Initialize VGG16 vision service
54
+
55
+ Args:
56
+ model_path: Path to trained VGG16 model
57
+ class_names: List of class names for classification
58
+ """
59
+ super().__init__()
60
+
61
+ self.model_path = model_path
62
+ self.class_names = class_names or ["class_0", "class_1"]
63
+ self.model = None
64
+ self.input_shape = (224, 224, 3)
65
+
66
+ # Lazy load dependencies
67
+ self.vgg16_components = _lazy_import_vgg16_deps()
68
+
69
+ if not self.vgg16_components['available']:
70
+ raise ImportError("TensorFlow and VGG16 dependencies are required")
71
+
72
+ # Load model if path provided
73
+ if model_path and os.path.exists(model_path):
74
+ self._load_model(model_path)
75
+
76
+ def _load_model(self, model_path: str):
77
+ """Load trained VGG16 model"""
78
+ try:
79
+ tf = self.vgg16_components['tf']
80
+ self.model = tf.keras.models.load_model(model_path)
81
+ logger.info(f"VGG16 model loaded from {model_path}")
82
+ except Exception as e:
83
+ logger.error(f"Error loading VGG16 model: {e}")
84
+ raise
85
+
86
+ def _preprocess_image(self, image: Union[str, BinaryIO]) -> np.ndarray:
87
+ """
88
+ Preprocess image for VGG16 input
89
+
90
+ Args:
91
+ image: Image path or binary data
92
+
93
+ Returns:
94
+ Preprocessed image array
95
+ """
96
+ try:
97
+ # Handle different image input types
98
+ if isinstance(image, str):
99
+ # File path
100
+ pil_image = Image.open(image).convert('RGB')
101
+ elif hasattr(image, 'read'):
102
+ # Binary IO
103
+ image_data = image.read()
104
+ pil_image = Image.open(io.BytesIO(image_data)).convert('RGB')
105
+ else:
106
+ raise ValueError("Unsupported image format")
107
+
108
+ # Resize to VGG16 input size
109
+ pil_image = pil_image.resize((self.input_shape[0], self.input_shape[1]))
110
+
111
+ # Convert to array and normalize
112
+ image_array = np.array(pil_image) / 255.0
113
+
114
+ # Add batch dimension
115
+ image_batch = np.expand_dims(image_array, axis=0)
116
+
117
+ return image_batch, image_array
118
+
119
+ except Exception as e:
120
+ logger.error(f"Error preprocessing image: {e}")
121
+ raise
122
+
123
+ async def classify_image(self,
124
+ image: Union[str, BinaryIO],
125
+ categories: Optional[List[str]] = None) -> Dict[str, Any]:
126
+ """
127
+ Classify image using trained VGG16 model
128
+
129
+ Args:
130
+ image: Image path or binary data
131
+ categories: Optional list of categories (uses model's classes if None)
132
+
133
+ Returns:
134
+ Classification results
135
+ """
136
+ try:
137
+ if self.model is None:
138
+ return {
139
+ "error": "No trained model available. Please load a model first.",
140
+ "service": "VGG16VisionService"
141
+ }
142
+
143
+ # Preprocess image
144
+ image_batch, image_array = self._preprocess_image(image)
145
+
146
+ # Make prediction
147
+ predictions = self.model.predict(image_batch, verbose=0)
148
+
149
+ # Use provided categories or default class names
150
+ class_names = categories or self.class_names
151
+
152
+ # Process predictions based on model output
153
+ if len(predictions[0]) == 1: # Binary classification
154
+ predicted_class_idx = int(predictions[0] > 0.5)
155
+ confidence = float(predictions[0][0]) if predicted_class_idx == 1 else float(1 - predictions[0][0])
156
+
157
+ # Create probability distribution
158
+ probabilities = {
159
+ class_names[0]: float(1 - predictions[0][0]),
160
+ class_names[1]: float(predictions[0][0])
161
+ }
162
+ else: # Multiclass classification
163
+ predicted_class_idx = np.argmax(predictions[0])
164
+ confidence = float(predictions[0][predicted_class_idx])
165
+
166
+ # Create probability distribution
167
+ probabilities = {
168
+ class_names[i]: float(predictions[0][i])
169
+ for i in range(min(len(class_names), len(predictions[0])))
170
+ }
171
+
172
+ predicted_class = class_names[predicted_class_idx] if predicted_class_idx < len(class_names) else f"class_{predicted_class_idx}"
173
+
174
+ return {
175
+ "task": "classify",
176
+ "service": "VGG16VisionService",
177
+ "predicted_class": predicted_class,
178
+ "confidence": confidence,
179
+ "probabilities": probabilities,
180
+ "model_type": "VGG16",
181
+ "success": True
182
+ }
183
+
184
+ except Exception as e:
185
+ logger.error(f"Error classifying image: {e}")
186
+ return {
187
+ "error": str(e),
188
+ "service": "VGG16VisionService",
189
+ "success": False
190
+ }
191
+
192
+ async def analyze_image(self,
193
+ image: Union[str, BinaryIO],
194
+ prompt: Optional[str] = None,
195
+ max_tokens: int = 1000) -> Dict[str, Any]:
196
+ """
197
+ Analyze image using VGG16 classification
198
+
199
+ Args:
200
+ image: Image path or binary data
201
+ prompt: Optional prompt (used to guide interpretation)
202
+ max_tokens: Not used for classification
203
+
204
+ Returns:
205
+ Analysis results
206
+ """
207
+ # For VGG16, analysis is essentially classification
208
+ classification_result = await self.classify_image(image)
209
+
210
+ if classification_result.get("success"):
211
+ # Create analysis text based on classification
212
+ predicted_class = classification_result["predicted_class"]
213
+ confidence = classification_result["confidence"]
214
+
215
+ analysis_text = f"The image has been classified as '{predicted_class}' with {confidence:.2%} confidence."
216
+
217
+ if prompt:
218
+ analysis_text += f" Analysis context: {prompt}"
219
+
220
+ return {
221
+ "task": "analyze",
222
+ "service": "VGG16VisionService",
223
+ "text": analysis_text,
224
+ "confidence": confidence,
225
+ "classification": classification_result,
226
+ "success": True
227
+ }
228
+ else:
229
+ return classification_result
230
+
231
+ def set_class_names(self, class_names: List[str]):
232
+ """Set class names for classification"""
233
+ self.class_names = class_names
234
+
235
+ def load_trained_model(self, model_path: str, class_names: Optional[List[str]] = None):
236
+ """
237
+ Load a trained VGG16 model
238
+
239
+ Args:
240
+ model_path: Path to the trained model
241
+ class_names: Optional class names
242
+ """
243
+ self._load_model(model_path)
244
+ if class_names:
245
+ self.set_class_names(class_names)
246
+
247
+ def get_service_info(self) -> Dict[str, Any]:
248
+ """Get service information"""
249
+ return {
250
+ "service_name": "VGG16VisionService",
251
+ "model_type": "VGG16",
252
+ "capabilities": ["classify", "analyze"],
253
+ "model_loaded": self.model is not None,
254
+ "input_shape": self.input_shape,
255
+ "class_names": self.class_names,
256
+ "dependencies_available": self.vgg16_components['available']
257
+ }
@@ -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