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
@@ -13,7 +13,6 @@ import time
13
13
  import logging
14
14
  import os
15
15
  import redis
16
- import structlog
17
16
  from typing import Dict, Any, Optional, Callable
18
17
  from fastapi import FastAPI, Request, Response, HTTPException, status
19
18
  from fastapi.middleware.cors import CORSMiddleware
@@ -25,12 +24,16 @@ from starlette.middleware.base import BaseHTTPMiddleware
25
24
  from starlette.responses import JSONResponse
26
25
  import html
27
26
 
28
- # Configure structured logging
29
- logger = structlog.get_logger(__name__)
27
+ from ....core.config.config_manager import ConfigManager
28
+
29
+ # Configure logging
30
+ logger = logging.getLogger(__name__)
30
31
 
31
32
  # Configuration from environment variables
33
+ config_manager = ConfigManager()
32
34
  MAX_REQUEST_SIZE = int(os.getenv("MAX_REQUEST_SIZE_MB", "50")) * 1024 * 1024 # 50MB default
33
- REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
35
+ # Use Consul discovery for Redis URL with fallback
36
+ REDIS_URL = os.getenv("REDIS_URL", config_manager.get_redis_url())
34
37
  ENABLE_RATE_LIMITING = os.getenv("ENABLE_RATE_LIMITING", "true").lower() == "true"
35
38
  RATE_LIMIT_PER_MINUTE = os.getenv("RATE_LIMIT_PER_MINUTE", "100")
36
39
  RATE_LIMIT_PER_HOUR = os.getenv("RATE_LIMIT_PER_HOUR", "1000")
@@ -41,7 +44,7 @@ SECURITY_HEADERS = {
41
44
  "X-Frame-Options": "DENY",
42
45
  "X-XSS-Protection": "1; mode=block",
43
46
  "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
44
- "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self'",
47
+ "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; font-src 'self' https://fonts.gstatic.com; img-src 'self' https://fastapi.tiangolo.com data:; connect-src 'self'",
45
48
  "Referrer-Policy": "strict-origin-when-cross-origin",
46
49
  "Permissions-Policy": "geolocation=(), microphone=(), camera=()"
47
50
  }
@@ -100,7 +103,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
100
103
  return response
101
104
 
102
105
  except Exception as e:
103
- logger.error("Error in security headers middleware", error=str(e))
106
+ logger.error(f"Error in security headers middleware: {e}")
104
107
  return JSONResponse(
105
108
  status_code=500,
106
109
  content={"error": "Internal server error"},
@@ -119,10 +122,8 @@ class RequestValidationMiddleware(BaseHTTPMiddleware):
119
122
  content_length = request.headers.get("content-length")
120
123
  if content_length and int(content_length) > MAX_REQUEST_SIZE:
121
124
  logger.warning(
122
- "Request too large",
123
- content_length=content_length,
124
- max_size=MAX_REQUEST_SIZE,
125
- client_ip=get_remote_address_with_proxy(request)
125
+ f"Request too large: {content_length} bytes > {MAX_REQUEST_SIZE} bytes "
126
+ f"from client {get_remote_address_with_proxy(request)}"
126
127
  )
127
128
  raise HTTPException(
128
129
  status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
@@ -134,19 +135,15 @@ class RequestValidationMiddleware(BaseHTTPMiddleware):
134
135
  sanitized_query = html.escape(request.url.query)
135
136
  if sanitized_query != request.url.query:
136
137
  logger.warning(
137
- "Potentially malicious query parameters detected",
138
- original=request.url.query,
139
- sanitized=sanitized_query,
140
- client_ip=get_remote_address_with_proxy(request)
138
+ f"Potentially malicious query parameters detected from client {get_remote_address_with_proxy(request)}: "
139
+ f"'{request.url.query}' -> '{sanitized_query}'"
141
140
  )
142
141
 
143
142
  # Log request details for monitoring
144
143
  logger.info(
145
- "Request received",
146
- method=request.method,
147
- path=request.url.path,
148
- client_ip=get_remote_address_with_proxy(request),
149
- user_agent=request.headers.get("user-agent", "unknown")
144
+ f"Request received: {request.method} {request.url.path} "
145
+ f"from {get_remote_address_with_proxy(request)} "
146
+ f"(UA: {request.headers.get('user-agent', 'unknown')})"
150
147
  )
151
148
 
152
149
  response = await call_next(request)
@@ -154,12 +151,8 @@ class RequestValidationMiddleware(BaseHTTPMiddleware):
154
151
  # Log response details
155
152
  process_time = time.time() - request.state.start_time
156
153
  logger.info(
157
- "Request completed",
158
- method=request.method,
159
- path=request.url.path,
160
- status_code=response.status_code,
161
- process_time=process_time,
162
- client_ip=get_remote_address_with_proxy(request)
154
+ f"Request completed: {request.method} {request.url.path} -> {response.status_code} "
155
+ f"in {process_time:.3f}s from {get_remote_address_with_proxy(request)}"
163
156
  )
164
157
 
165
158
  return response
@@ -168,11 +161,8 @@ class RequestValidationMiddleware(BaseHTTPMiddleware):
168
161
  raise
169
162
  except Exception as e:
170
163
  logger.error(
171
- "Error in request validation middleware",
172
- error=str(e),
173
- path=request.url.path,
174
- method=request.method,
175
- client_ip=get_remote_address_with_proxy(request)
164
+ f"Error in request validation middleware: {e} "
165
+ f"({request.method} {request.url.path} from {get_remote_address_with_proxy(request)})"
176
166
  )
177
167
  raise HTTPException(
178
168
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -186,13 +176,13 @@ def setup_security_middleware(app: FastAPI):
186
176
  if ENABLE_RATE_LIMITING:
187
177
  app.state.limiter = limiter
188
178
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
189
- logger.info("Rate limiting enabled", redis_backend=redis_client is not None)
179
+ logger.info(f"Rate limiting enabled (Redis backend: {redis_client is not None})")
190
180
 
191
181
  # Trusted hosts (production should specify allowed hosts)
192
182
  allowed_hosts = os.getenv("ALLOWED_HOSTS", "*").split(",")
193
183
  if allowed_hosts != ["*"]:
194
184
  app.add_middleware(TrustedHostMiddleware, allowed_hosts=allowed_hosts)
195
- logger.info("Trusted hosts middleware enabled", allowed_hosts=allowed_hosts)
185
+ logger.info(f"Trusted hosts middleware enabled: {allowed_hosts}")
196
186
 
197
187
  # CORS configuration
198
188
  cors_origins = os.getenv("CORS_ORIGINS", "*").split(",")
@@ -204,7 +194,7 @@ def setup_security_middleware(app: FastAPI):
204
194
  allow_headers=["*"],
205
195
  expose_headers=["X-Process-Time"]
206
196
  )
207
- logger.info("CORS middleware enabled", origins=cors_origins)
197
+ logger.info(f"CORS middleware enabled for origins: {cors_origins}")
208
198
 
209
199
  # Custom security middleware
210
200
  app.add_middleware(SecurityHeadersMiddleware)
@@ -0,0 +1,414 @@
1
+ """
2
+ Tenant Context Middleware
3
+
4
+ Handles tenant isolation by:
5
+ 1. Extracting tenant info from requests (API keys, JWT tokens, headers)
6
+ 2. Setting tenant context for all database operations
7
+ 3. Enforcing resource quotas and access control
8
+ 4. Logging tenant-specific activities
9
+ """
10
+
11
+ from fastapi import Request, HTTPException
12
+ from starlette.middleware.base import BaseHTTPMiddleware
13
+ from starlette.responses import Response
14
+ from contextlib import contextmanager
15
+ from contextvars import ContextVar
16
+ from typing import Optional, Dict, Any
17
+ import logging
18
+ import json
19
+ import asyncio
20
+ import time
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Context variables for tenant isolation
25
+ _tenant_context: ContextVar[Optional['TenantContext']] = ContextVar('tenant_context', default=None)
26
+
27
+ class TenantContext:
28
+ """Container for tenant-specific context information"""
29
+
30
+ def __init__(
31
+ self,
32
+ organization_id: str,
33
+ user_id: Optional[str] = None,
34
+ role: Optional[str] = None,
35
+ plan: str = "starter",
36
+ quotas: Optional[Dict[str, Any]] = None,
37
+ settings: Optional[Dict[str, Any]] = None
38
+ ):
39
+ self.organization_id = organization_id
40
+ self.user_id = user_id
41
+ self.role = role
42
+ self.plan = plan
43
+ self.quotas = quotas or {}
44
+ self.settings = settings or {}
45
+ self.request_start_time = time.time()
46
+
47
+ def __str__(self):
48
+ return f"TenantContext(org={self.organization_id}, user={self.user_id}, role={self.role})"
49
+
50
+ def is_admin(self) -> bool:
51
+ """Check if current user is admin"""
52
+ return self.role in ["admin", "owner"]
53
+
54
+ def can_access_resource(self, resource_type: str, action: str = "read") -> bool:
55
+ """Check if tenant can access a specific resource type"""
56
+ # TODO: Implement fine-grained permissions
57
+ return True
58
+
59
+ def check_quota(self, resource: str, current_usage: int = 0) -> bool:
60
+ """Check if tenant is within quota limits"""
61
+ if resource not in self.quotas:
62
+ return True
63
+
64
+ quota_limit = self.quotas[resource]
65
+ return current_usage < quota_limit
66
+
67
+ def get_database_filter(self) -> Dict[str, Any]:
68
+ """Get database filter parameters for tenant isolation"""
69
+ return {"organization_id": self.organization_id}
70
+
71
+ def get_tenant_context() -> Optional[TenantContext]:
72
+ """Get current tenant context"""
73
+ return _tenant_context.get()
74
+
75
+ def require_tenant_context() -> TenantContext:
76
+ """Get tenant context or raise error if not available"""
77
+ context = get_tenant_context()
78
+ if not context:
79
+ raise HTTPException(
80
+ status_code=401,
81
+ detail="Tenant context required - invalid or missing authentication"
82
+ )
83
+ return context
84
+
85
+ @contextmanager
86
+ def set_tenant_context(context: TenantContext):
87
+ """Context manager to set tenant context"""
88
+ token = _tenant_context.set(context)
89
+ try:
90
+ yield context
91
+ finally:
92
+ _tenant_context.reset(token)
93
+
94
+ class TenantContextMiddleware(BaseHTTPMiddleware):
95
+ """Middleware to extract and set tenant context for requests"""
96
+
97
+ def __init__(self, app, database_pool=None):
98
+ super().__init__(app)
99
+ self.database_pool = database_pool
100
+ # Initialize database pool if not provided
101
+ if not self.database_pool:
102
+ try:
103
+ import asyncio
104
+ from ..dependencies.database import initialize_database_pool
105
+ # Will be initialized in first request
106
+ self.database_pool = None
107
+ except ImportError:
108
+ pass
109
+
110
+ async def dispatch(self, request: Request, call_next) -> Response:
111
+ """Process request and set tenant context"""
112
+ start_time = time.time()
113
+
114
+ try:
115
+ # Extract tenant information from request
116
+ tenant_context = await self.extract_tenant_context(request)
117
+
118
+ # Set context for this request
119
+ if tenant_context:
120
+ token = _tenant_context.set(tenant_context)
121
+ try:
122
+ # Check quotas before processing request
123
+ await self.enforce_quotas(tenant_context, request)
124
+
125
+ # Process the request
126
+ response = await call_next(request)
127
+
128
+ # Log successful request
129
+ await self.log_tenant_activity(tenant_context, request, response, start_time)
130
+
131
+ return response
132
+ finally:
133
+ _tenant_context.reset(token)
134
+ else:
135
+ # No tenant context - allow for public endpoints
136
+ return await call_next(request)
137
+
138
+ except HTTPException:
139
+ raise
140
+ except Exception as e:
141
+ logger.error(f"Error in tenant context middleware: {e}", exc_info=True)
142
+ raise HTTPException(status_code=500, detail="Internal server error")
143
+
144
+ async def extract_tenant_context(self, request: Request) -> Optional[TenantContext]:
145
+ """Extract tenant information from request"""
146
+ try:
147
+ # Skip tenant context for certain paths
148
+ if self.should_skip_tenant_context(request.url.path):
149
+ return None
150
+
151
+ # Method 1: Extract from Authorization header (API key or JWT)
152
+ auth_header = request.headers.get("Authorization")
153
+ if auth_header:
154
+ tenant_context = await self.extract_from_auth_header(auth_header)
155
+ if tenant_context:
156
+ return tenant_context
157
+
158
+ # Method 2: Extract from X-Organization-ID header (for service-to-service calls)
159
+ org_header = request.headers.get("X-Organization-ID")
160
+ if org_header:
161
+ return await self.extract_from_org_header(org_header)
162
+
163
+ # Method 3: Extract from query parameters (for some public APIs)
164
+ org_param = request.query_params.get("organization_id")
165
+ if org_param:
166
+ return await self.extract_from_org_param(org_param)
167
+
168
+ return None
169
+
170
+ except Exception as e:
171
+ logger.error(f"Error extracting tenant context: {e}")
172
+ return None
173
+
174
+ def should_skip_tenant_context(self, path: str) -> bool:
175
+ """Check if path should skip tenant context extraction"""
176
+ skip_paths = [
177
+ "/health",
178
+ "/docs",
179
+ "/redoc",
180
+ "/openapi.json",
181
+ "/api/v1/tenants", # Tenant management endpoints handle their own context
182
+ "/static"
183
+ ]
184
+
185
+ return any(path.startswith(skip_path) for skip_path in skip_paths)
186
+
187
+ async def extract_from_auth_header(self, auth_header: str) -> Optional[TenantContext]:
188
+ """Extract tenant context from Authorization header"""
189
+ try:
190
+ if not auth_header.startswith("Bearer "):
191
+ return None
192
+
193
+ token = auth_header[7:] # Remove "Bearer "
194
+
195
+ # If it looks like an API key
196
+ if token.startswith("isa_"):
197
+ return await self.lookup_api_key(token)
198
+
199
+ # If it looks like a JWT token
200
+ if "." in token:
201
+ return await self.decode_jwt_token(token)
202
+
203
+ return None
204
+
205
+ except Exception as e:
206
+ logger.error(f"Error extracting from auth header: {e}")
207
+ return None
208
+
209
+ async def extract_from_org_header(self, org_id: str) -> Optional[TenantContext]:
210
+ """Extract tenant context from organization header"""
211
+ try:
212
+ # For service-to-service calls, just create basic context
213
+ return await self.lookup_organization(org_id)
214
+
215
+ except Exception as e:
216
+ logger.error(f"Error extracting from org header: {e}")
217
+ return None
218
+
219
+ async def extract_from_org_param(self, org_id: str) -> Optional[TenantContext]:
220
+ """Extract tenant context from query parameter"""
221
+ # Similar to org header but maybe more restricted
222
+ return await self.lookup_organization(org_id)
223
+
224
+ async def lookup_api_key(self, api_key: str) -> Optional[TenantContext]:
225
+ """Look up tenant context from API key"""
226
+ try:
227
+ # For now, create a simple tenant context based on API key
228
+ # In a real implementation, this would lookup the organization
229
+ # associated with the API key from the database
230
+
231
+ # Create a default organization for testing
232
+ if api_key.startswith("isa_"):
233
+ return TenantContext(
234
+ organization_id="org_default_test_123",
235
+ user_id="user_admin",
236
+ role="admin",
237
+ plan="pro",
238
+ quotas={
239
+ "api_calls_per_month": 100000,
240
+ "max_training_jobs": 10,
241
+ "max_deployments": 5
242
+ },
243
+ settings={}
244
+ )
245
+
246
+ return None
247
+
248
+ except Exception as e:
249
+ logger.error(f"Error looking up API key: {e}")
250
+ return None
251
+
252
+ async def decode_jwt_token(self, token: str) -> Optional[TenantContext]:
253
+ """Decode JWT token and extract tenant context"""
254
+ try:
255
+ # TODO: Implement JWT token decoding
256
+ # This would involve verifying the token signature and extracting claims
257
+ logger.info("JWT token decoding not yet implemented")
258
+ return None
259
+
260
+ except Exception as e:
261
+ logger.error(f"Error decoding JWT token: {e}")
262
+ return None
263
+
264
+ async def lookup_organization(self, org_id: str) -> Optional[TenantContext]:
265
+ """Look up organization details"""
266
+ try:
267
+ if not self.database_pool:
268
+ return None
269
+
270
+ async with self.database_pool.acquire() as conn:
271
+ result = await conn.fetchrow("""
272
+ SELECT o.organization_id, o.plan, o.settings, oq.quotas
273
+ FROM organizations o
274
+ LEFT JOIN organization_quotas oq ON o.organization_id = oq.organization_id
275
+ WHERE o.organization_id = $1 AND o.status = 'active'
276
+ """, org_id)
277
+
278
+ if result:
279
+ return TenantContext(
280
+ organization_id=result['organization_id'],
281
+ plan=result['plan'],
282
+ quotas=result['quotas'] or {},
283
+ settings=result['settings'] or {}
284
+ )
285
+
286
+ return None
287
+
288
+ except Exception as e:
289
+ logger.error(f"Error looking up organization {org_id}: {e}")
290
+ return None
291
+
292
+ async def enforce_quotas(self, context: TenantContext, request: Request):
293
+ """Enforce tenant quotas before processing request"""
294
+ try:
295
+ # Check concurrent request quota
296
+ # TODO: Implement concurrent request tracking
297
+
298
+ # Check API rate limits
299
+ if not context.check_quota("requests_per_minute", 0): # TODO: Get actual usage
300
+ raise HTTPException(
301
+ status_code=429,
302
+ detail="Request rate limit exceeded for your organization"
303
+ )
304
+
305
+ # Check plan-specific restrictions
306
+ if context.plan == "starter" and request.method in ["POST", "PUT", "DELETE"]:
307
+ # Maybe starter plans have restricted write access to some endpoints
308
+ pass
309
+
310
+ except HTTPException:
311
+ raise
312
+ except Exception as e:
313
+ logger.error(f"Error enforcing quotas: {e}")
314
+ # Don't block request on quota enforcement errors
315
+
316
+ async def log_tenant_activity(
317
+ self,
318
+ context: TenantContext,
319
+ request: Request,
320
+ response: Response,
321
+ start_time: float
322
+ ):
323
+ """Log tenant-specific activity for billing and monitoring"""
324
+ try:
325
+ duration = time.time() - start_time
326
+
327
+ activity_log = {
328
+ "timestamp": time.time(),
329
+ "organization_id": context.organization_id,
330
+ "user_id": context.user_id,
331
+ "method": request.method,
332
+ "path": str(request.url.path),
333
+ "status_code": response.status_code,
334
+ "duration_ms": duration * 1000,
335
+ "plan": context.plan
336
+ }
337
+
338
+ # Log to structured logger for processing
339
+ logger.info(f"TENANT_ACTIVITY: {json.dumps(activity_log)}")
340
+
341
+ # TODO: Store in database for billing/analytics
342
+ # await self.store_activity_log(activity_log)
343
+
344
+ except Exception as e:
345
+ logger.error(f"Error logging tenant activity: {e}")
346
+
347
+ # Dependency functions for FastAPI
348
+
349
+ def get_current_tenant() -> TenantContext:
350
+ """FastAPI dependency to get current tenant context"""
351
+ return require_tenant_context()
352
+
353
+ def get_current_organization_id() -> str:
354
+ """FastAPI dependency to get current organization ID"""
355
+ context = require_tenant_context()
356
+ return context.organization_id
357
+
358
+ def require_admin_role() -> TenantContext:
359
+ """FastAPI dependency to require admin role"""
360
+ context = require_tenant_context()
361
+ if not context.is_admin():
362
+ raise HTTPException(
363
+ status_code=403,
364
+ detail="Admin role required for this operation"
365
+ )
366
+ return context
367
+
368
+ def check_resource_quota(resource_type: str):
369
+ """FastAPI dependency factory to check specific resource quotas"""
370
+ def _check_quota():
371
+ context = require_tenant_context()
372
+ # TODO: Get current usage and check against quota
373
+ if not context.check_quota(resource_type):
374
+ raise HTTPException(
375
+ status_code=429,
376
+ detail=f"Quota exceeded for {resource_type}"
377
+ )
378
+ return context
379
+ return _check_quota
380
+
381
+ # Database query helpers that respect tenant context
382
+
383
+ def add_tenant_filter(base_query: str, params: list, table_alias: str = "") -> tuple[str, list]:
384
+ """Add tenant filter to database queries"""
385
+ context = get_tenant_context()
386
+ if not context:
387
+ return base_query, params
388
+
389
+ # Add organization_id filter
390
+ table_prefix = f"{table_alias}." if table_alias else ""
391
+
392
+ if "WHERE" in base_query.upper():
393
+ filtered_query = f"{base_query} AND {table_prefix}organization_id = ${len(params) + 1}"
394
+ else:
395
+ filtered_query = f"{base_query} WHERE {table_prefix}organization_id = ${len(params) + 1}"
396
+
397
+ params.append(context.organization_id)
398
+
399
+ return filtered_query, params
400
+
401
+ async def tenant_safe_query(conn, query: str, *params, table_alias: str = ""):
402
+ """Execute query with automatic tenant filtering"""
403
+ filtered_query, filtered_params = add_tenant_filter(query, list(params), table_alias)
404
+ return await conn.fetch(filtered_query, *filtered_params)
405
+
406
+ async def tenant_safe_fetchrow(conn, query: str, *params, table_alias: str = ""):
407
+ """Execute fetchrow with automatic tenant filtering"""
408
+ filtered_query, filtered_params = add_tenant_filter(query, list(params), table_alias)
409
+ return await conn.fetchrow(filtered_query, *filtered_params)
410
+
411
+ async def tenant_safe_execute(conn, query: str, *params, table_alias: str = ""):
412
+ """Execute query with automatic tenant filtering"""
413
+ filtered_query, filtered_params = add_tenant_filter(query, list(params), table_alias)
414
+ return await conn.execute(filtered_query, *filtered_params)
@@ -14,12 +14,15 @@ import asyncpg
14
14
  import os
15
15
  from collections import defaultdict
16
16
 
17
+ from ....core.config.config_manager import ConfigManager
18
+
17
19
  logger = logging.getLogger(__name__)
18
20
 
19
21
  router = APIRouter()
20
22
 
21
23
  # Database connection configuration
22
- DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@127.0.0.1:54322/postgres?options=-c%20search_path%3Ddev")
24
+ config_manager = ConfigManager()
25
+ DATABASE_URL = os.getenv("DATABASE_URL", config_manager.get_global_config().database.default_database_url)
23
26
 
24
27
  class AnalyticsDateRange(BaseModel):
25
28
  start_date: Optional[str] = None