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.
Files changed (228) hide show
  1. isa_model/client.py +1166 -584
  2. isa_model/core/cache/redis_cache.py +410 -0
  3. isa_model/core/config/config_manager.py +282 -12
  4. isa_model/core/config.py +91 -1
  5. isa_model/core/database/__init__.py +1 -0
  6. isa_model/core/database/direct_db_client.py +114 -0
  7. isa_model/core/database/migration_manager.py +563 -0
  8. isa_model/core/database/migrations.py +297 -0
  9. isa_model/core/database/supabase_client.py +258 -0
  10. isa_model/core/dependencies.py +316 -0
  11. isa_model/core/discovery/__init__.py +19 -0
  12. isa_model/core/discovery/consul_discovery.py +190 -0
  13. isa_model/core/logging/__init__.py +54 -0
  14. isa_model/core/logging/influx_logger.py +523 -0
  15. isa_model/core/logging/loki_logger.py +160 -0
  16. isa_model/core/models/__init__.py +46 -0
  17. isa_model/core/models/config_models.py +625 -0
  18. isa_model/core/models/deployment_billing_tracker.py +430 -0
  19. isa_model/core/models/model_billing_tracker.py +60 -88
  20. isa_model/core/models/model_manager.py +66 -25
  21. isa_model/core/models/model_metadata.py +690 -0
  22. isa_model/core/models/model_repo.py +217 -55
  23. isa_model/core/models/model_statistics_tracker.py +234 -0
  24. isa_model/core/models/model_storage.py +0 -1
  25. isa_model/core/models/model_version_manager.py +959 -0
  26. isa_model/core/models/system_models.py +857 -0
  27. isa_model/core/pricing_manager.py +2 -249
  28. isa_model/core/repositories/__init__.py +9 -0
  29. isa_model/core/repositories/config_repository.py +912 -0
  30. isa_model/core/resilience/circuit_breaker.py +366 -0
  31. isa_model/core/security/secrets.py +358 -0
  32. isa_model/core/services/__init__.py +2 -4
  33. isa_model/core/services/intelligent_model_selector.py +479 -370
  34. isa_model/core/storage/hf_storage.py +2 -2
  35. isa_model/core/types.py +8 -0
  36. isa_model/deployment/__init__.py +5 -48
  37. isa_model/deployment/core/__init__.py +2 -31
  38. isa_model/deployment/core/deployment_manager.py +1278 -368
  39. isa_model/deployment/local/__init__.py +31 -0
  40. isa_model/deployment/local/config.py +248 -0
  41. isa_model/deployment/local/gpu_gateway.py +607 -0
  42. isa_model/deployment/local/health_checker.py +428 -0
  43. isa_model/deployment/local/provider.py +586 -0
  44. isa_model/deployment/local/tensorrt_service.py +621 -0
  45. isa_model/deployment/local/transformers_service.py +644 -0
  46. isa_model/deployment/local/vllm_service.py +527 -0
  47. isa_model/deployment/modal/__init__.py +8 -0
  48. isa_model/deployment/modal/config.py +136 -0
  49. isa_model/deployment/modal/deployer.py +894 -0
  50. isa_model/deployment/modal/services/__init__.py +3 -0
  51. isa_model/deployment/modal/services/audio/__init__.py +1 -0
  52. isa_model/deployment/modal/services/audio/isa_audio_chatTTS_service.py +520 -0
  53. isa_model/deployment/modal/services/audio/isa_audio_openvoice_service.py +758 -0
  54. isa_model/deployment/modal/services/audio/isa_audio_service_v2.py +1044 -0
  55. isa_model/deployment/modal/services/embedding/__init__.py +1 -0
  56. isa_model/deployment/modal/services/embedding/isa_embed_rerank_service.py +296 -0
  57. isa_model/deployment/modal/services/llm/__init__.py +1 -0
  58. isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
  59. isa_model/deployment/modal/services/video/__init__.py +1 -0
  60. isa_model/deployment/modal/services/video/isa_video_hunyuan_service.py +423 -0
  61. isa_model/deployment/modal/services/vision/__init__.py +1 -0
  62. isa_model/deployment/modal/services/vision/isa_vision_ocr_service.py +519 -0
  63. isa_model/deployment/modal/services/vision/isa_vision_qwen25_service.py +709 -0
  64. isa_model/deployment/modal/services/vision/isa_vision_table_service.py +676 -0
  65. isa_model/deployment/modal/services/vision/isa_vision_ui_service.py +833 -0
  66. isa_model/deployment/modal/services/vision/isa_vision_ui_service_optimized.py +660 -0
  67. isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
  68. isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
  69. isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
  70. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
  71. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
  72. isa_model/deployment/storage/__init__.py +5 -0
  73. isa_model/deployment/storage/deployment_repository.py +824 -0
  74. isa_model/deployment/triton/__init__.py +10 -0
  75. isa_model/deployment/triton/config.py +196 -0
  76. isa_model/deployment/triton/configs/__init__.py +1 -0
  77. isa_model/deployment/triton/provider.py +512 -0
  78. isa_model/deployment/triton/scripts/__init__.py +1 -0
  79. isa_model/deployment/triton/templates/__init__.py +1 -0
  80. isa_model/inference/__init__.py +47 -1
  81. isa_model/inference/ai_factory.py +179 -16
  82. isa_model/inference/legacy_services/__init__.py +21 -0
  83. isa_model/inference/legacy_services/model_evaluation.py +637 -0
  84. isa_model/inference/legacy_services/model_service.py +573 -0
  85. isa_model/inference/legacy_services/model_serving.py +717 -0
  86. isa_model/inference/legacy_services/model_training.py +561 -0
  87. isa_model/inference/models/__init__.py +21 -0
  88. isa_model/inference/models/inference_config.py +551 -0
  89. isa_model/inference/models/inference_record.py +675 -0
  90. isa_model/inference/models/performance_models.py +714 -0
  91. isa_model/inference/repositories/__init__.py +9 -0
  92. isa_model/inference/repositories/inference_repository.py +828 -0
  93. isa_model/inference/services/audio/__init__.py +21 -0
  94. isa_model/inference/services/audio/base_realtime_service.py +225 -0
  95. isa_model/inference/services/audio/base_stt_service.py +184 -11
  96. isa_model/inference/services/audio/isa_tts_service.py +0 -0
  97. isa_model/inference/services/audio/openai_realtime_service.py +320 -124
  98. isa_model/inference/services/audio/openai_stt_service.py +53 -11
  99. isa_model/inference/services/base_service.py +17 -1
  100. isa_model/inference/services/custom_model_manager.py +277 -0
  101. isa_model/inference/services/embedding/__init__.py +13 -0
  102. isa_model/inference/services/embedding/base_embed_service.py +111 -8
  103. isa_model/inference/services/embedding/isa_embed_service.py +305 -0
  104. isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
  105. isa_model/inference/services/embedding/openai_embed_service.py +2 -4
  106. isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
  107. isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
  108. isa_model/inference/services/img/__init__.py +2 -2
  109. isa_model/inference/services/img/base_image_gen_service.py +24 -7
  110. isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
  111. isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
  112. isa_model/inference/services/img/services/replicate_flux.py +226 -0
  113. isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
  114. isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
  115. isa_model/inference/services/img/tests/test_img_client.py +297 -0
  116. isa_model/inference/services/llm/__init__.py +10 -2
  117. isa_model/inference/services/llm/base_llm_service.py +361 -26
  118. isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
  119. isa_model/inference/services/llm/helpers/llm_adapter.py +71 -12
  120. isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
  121. isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
  122. isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
  123. isa_model/inference/services/llm/local_llm_service.py +747 -0
  124. isa_model/inference/services/llm/ollama_llm_service.py +11 -3
  125. isa_model/inference/services/llm/openai_llm_service.py +670 -56
  126. isa_model/inference/services/llm/yyds_llm_service.py +10 -3
  127. isa_model/inference/services/vision/__init__.py +27 -6
  128. isa_model/inference/services/vision/base_vision_service.py +118 -185
  129. isa_model/inference/services/vision/blip_vision_service.py +359 -0
  130. isa_model/inference/services/vision/helpers/image_utils.py +19 -10
  131. isa_model/inference/services/vision/isa_vision_service.py +634 -0
  132. isa_model/inference/services/vision/openai_vision_service.py +19 -10
  133. isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
  134. isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
  135. isa_model/serving/api/cache_manager.py +245 -0
  136. isa_model/serving/api/dependencies/__init__.py +1 -0
  137. isa_model/serving/api/dependencies/auth.py +194 -0
  138. isa_model/serving/api/dependencies/database.py +139 -0
  139. isa_model/serving/api/error_handlers.py +284 -0
  140. isa_model/serving/api/fastapi_server.py +240 -18
  141. isa_model/serving/api/middleware/auth.py +317 -0
  142. isa_model/serving/api/middleware/security.py +268 -0
  143. isa_model/serving/api/middleware/tenant_context.py +414 -0
  144. isa_model/serving/api/routes/analytics.py +489 -0
  145. isa_model/serving/api/routes/config.py +645 -0
  146. isa_model/serving/api/routes/deployment_billing.py +315 -0
  147. isa_model/serving/api/routes/deployments.py +475 -0
  148. isa_model/serving/api/routes/gpu_gateway.py +440 -0
  149. isa_model/serving/api/routes/health.py +32 -12
  150. isa_model/serving/api/routes/inference_monitoring.py +486 -0
  151. isa_model/serving/api/routes/local_deployments.py +448 -0
  152. isa_model/serving/api/routes/logs.py +430 -0
  153. isa_model/serving/api/routes/settings.py +582 -0
  154. isa_model/serving/api/routes/tenants.py +575 -0
  155. isa_model/serving/api/routes/unified.py +992 -171
  156. isa_model/serving/api/routes/webhooks.py +479 -0
  157. isa_model/serving/api/startup.py +318 -0
  158. isa_model/serving/modal_proxy_server.py +249 -0
  159. isa_model/utils/gpu_utils.py +311 -0
  160. {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/METADATA +76 -22
  161. isa_model-0.4.3.dist-info/RECORD +193 -0
  162. isa_model/deployment/cloud/__init__.py +0 -9
  163. isa_model/deployment/cloud/modal/__init__.py +0 -10
  164. isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
  165. isa_model/deployment/cloud/modal/isa_vision_table_service.py +0 -532
  166. isa_model/deployment/cloud/modal/isa_vision_ui_service.py +0 -406
  167. isa_model/deployment/cloud/modal/register_models.py +0 -321
  168. isa_model/deployment/core/deployment_config.py +0 -356
  169. isa_model/deployment/core/isa_deployment_service.py +0 -401
  170. isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
  171. isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
  172. isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
  173. isa_model/deployment/runtime/deployed_service.py +0 -338
  174. isa_model/deployment/services/__init__.py +0 -9
  175. isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
  176. isa_model/deployment/services/model_service.py +0 -332
  177. isa_model/deployment/services/service_monitor.py +0 -356
  178. isa_model/deployment/services/service_registry.py +0 -527
  179. isa_model/eval/__init__.py +0 -92
  180. isa_model/eval/benchmarks.py +0 -469
  181. isa_model/eval/config/__init__.py +0 -10
  182. isa_model/eval/config/evaluation_config.py +0 -108
  183. isa_model/eval/evaluators/__init__.py +0 -18
  184. isa_model/eval/evaluators/base_evaluator.py +0 -503
  185. isa_model/eval/evaluators/llm_evaluator.py +0 -472
  186. isa_model/eval/factory.py +0 -531
  187. isa_model/eval/infrastructure/__init__.py +0 -24
  188. isa_model/eval/infrastructure/experiment_tracker.py +0 -466
  189. isa_model/eval/metrics.py +0 -798
  190. isa_model/inference/adapter/unified_api.py +0 -248
  191. isa_model/inference/services/helpers/stacked_config.py +0 -148
  192. isa_model/inference/services/img/flux_professional_service.py +0 -603
  193. isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
  194. isa_model/inference/services/others/table_transformer_service.py +0 -61
  195. isa_model/inference/services/vision/doc_analysis_service.py +0 -640
  196. isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
  197. isa_model/inference/services/vision/ui_analysis_service.py +0 -823
  198. isa_model/scripts/inference_tracker.py +0 -283
  199. isa_model/scripts/mlflow_manager.py +0 -379
  200. isa_model/scripts/model_registry.py +0 -465
  201. isa_model/scripts/register_models.py +0 -370
  202. isa_model/scripts/register_models_with_embeddings.py +0 -510
  203. isa_model/scripts/start_mlflow.py +0 -95
  204. isa_model/scripts/training_tracker.py +0 -257
  205. isa_model/training/__init__.py +0 -74
  206. isa_model/training/annotation/annotation_schema.py +0 -47
  207. isa_model/training/annotation/processors/annotation_processor.py +0 -126
  208. isa_model/training/annotation/storage/dataset_manager.py +0 -131
  209. isa_model/training/annotation/storage/dataset_schema.py +0 -44
  210. isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
  211. isa_model/training/annotation/tests/test_minio copy.py +0 -113
  212. isa_model/training/annotation/tests/test_minio_upload.py +0 -43
  213. isa_model/training/annotation/views/annotation_controller.py +0 -158
  214. isa_model/training/cloud/__init__.py +0 -22
  215. isa_model/training/cloud/job_orchestrator.py +0 -402
  216. isa_model/training/cloud/runpod_trainer.py +0 -454
  217. isa_model/training/cloud/storage_manager.py +0 -482
  218. isa_model/training/core/__init__.py +0 -23
  219. isa_model/training/core/config.py +0 -181
  220. isa_model/training/core/dataset.py +0 -222
  221. isa_model/training/core/trainer.py +0 -720
  222. isa_model/training/core/utils.py +0 -213
  223. isa_model/training/factory.py +0 -424
  224. isa_model-0.3.91.dist-info/RECORD +0 -138
  225. /isa_model/{core/storage/minio_storage.py → deployment/modal/services/audio/isa_audio_fish_service.py} +0 -0
  226. /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
  227. {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/WHEEL +0 -0
  228. {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