isa-model 0.4.0__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 (199) 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 +40 -17
  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/storage/hf_storage.py +1 -1
  26. isa_model/core/types.py +1 -0
  27. isa_model/deployment/__init__.py +5 -48
  28. isa_model/deployment/core/__init__.py +2 -31
  29. isa_model/deployment/core/deployment_manager.py +1278 -370
  30. isa_model/deployment/local/__init__.py +31 -0
  31. isa_model/deployment/local/config.py +248 -0
  32. isa_model/deployment/local/gpu_gateway.py +607 -0
  33. isa_model/deployment/local/health_checker.py +428 -0
  34. isa_model/deployment/local/provider.py +586 -0
  35. isa_model/deployment/local/tensorrt_service.py +621 -0
  36. isa_model/deployment/local/transformers_service.py +644 -0
  37. isa_model/deployment/local/vllm_service.py +527 -0
  38. isa_model/deployment/modal/__init__.py +8 -0
  39. isa_model/deployment/modal/config.py +136 -0
  40. isa_model/deployment/{services/auto_hf_modal_deployer.py → modal/deployer.py} +1 -1
  41. isa_model/deployment/modal/services/__init__.py +3 -0
  42. isa_model/deployment/modal/services/audio/__init__.py +1 -0
  43. isa_model/deployment/modal/services/embedding/__init__.py +1 -0
  44. isa_model/deployment/modal/services/llm/__init__.py +1 -0
  45. isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
  46. isa_model/deployment/modal/services/video/__init__.py +1 -0
  47. isa_model/deployment/modal/services/vision/__init__.py +1 -0
  48. isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
  49. isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
  50. isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
  51. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
  52. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
  53. isa_model/deployment/storage/__init__.py +5 -0
  54. isa_model/deployment/storage/deployment_repository.py +824 -0
  55. isa_model/deployment/triton/__init__.py +10 -0
  56. isa_model/deployment/triton/config.py +196 -0
  57. isa_model/deployment/triton/configs/__init__.py +1 -0
  58. isa_model/deployment/triton/provider.py +512 -0
  59. isa_model/deployment/triton/scripts/__init__.py +1 -0
  60. isa_model/deployment/triton/templates/__init__.py +1 -0
  61. isa_model/inference/__init__.py +47 -1
  62. isa_model/inference/ai_factory.py +137 -10
  63. isa_model/inference/legacy_services/__init__.py +21 -0
  64. isa_model/inference/legacy_services/model_evaluation.py +637 -0
  65. isa_model/inference/legacy_services/model_service.py +573 -0
  66. isa_model/inference/legacy_services/model_serving.py +717 -0
  67. isa_model/inference/legacy_services/model_training.py +561 -0
  68. isa_model/inference/models/__init__.py +21 -0
  69. isa_model/inference/models/inference_config.py +551 -0
  70. isa_model/inference/models/inference_record.py +675 -0
  71. isa_model/inference/models/performance_models.py +714 -0
  72. isa_model/inference/repositories/__init__.py +9 -0
  73. isa_model/inference/repositories/inference_repository.py +828 -0
  74. isa_model/inference/services/audio/base_stt_service.py +184 -11
  75. isa_model/inference/services/audio/openai_stt_service.py +22 -6
  76. isa_model/inference/services/custom_model_manager.py +277 -0
  77. isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
  78. isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
  79. isa_model/inference/services/llm/__init__.py +10 -2
  80. isa_model/inference/services/llm/base_llm_service.py +335 -24
  81. isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
  82. isa_model/inference/services/llm/helpers/llm_adapter.py +9 -4
  83. isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
  84. isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
  85. isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
  86. isa_model/inference/services/llm/local_llm_service.py +747 -0
  87. isa_model/inference/services/llm/ollama_llm_service.py +9 -2
  88. isa_model/inference/services/llm/openai_llm_service.py +33 -16
  89. isa_model/inference/services/llm/yyds_llm_service.py +8 -2
  90. isa_model/inference/services/vision/__init__.py +22 -1
  91. isa_model/inference/services/vision/blip_vision_service.py +359 -0
  92. isa_model/inference/services/vision/helpers/image_utils.py +8 -5
  93. isa_model/inference/services/vision/isa_vision_service.py +65 -4
  94. isa_model/inference/services/vision/openai_vision_service.py +19 -10
  95. isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
  96. isa_model/serving/api/cache_manager.py +245 -0
  97. isa_model/serving/api/dependencies/__init__.py +1 -0
  98. isa_model/serving/api/dependencies/auth.py +194 -0
  99. isa_model/serving/api/dependencies/database.py +139 -0
  100. isa_model/serving/api/error_handlers.py +284 -0
  101. isa_model/serving/api/fastapi_server.py +172 -22
  102. isa_model/serving/api/middleware/auth.py +8 -2
  103. isa_model/serving/api/middleware/security.py +23 -33
  104. isa_model/serving/api/middleware/tenant_context.py +414 -0
  105. isa_model/serving/api/routes/analytics.py +4 -1
  106. isa_model/serving/api/routes/config.py +645 -0
  107. isa_model/serving/api/routes/deployment_billing.py +315 -0
  108. isa_model/serving/api/routes/deployments.py +138 -2
  109. isa_model/serving/api/routes/gpu_gateway.py +440 -0
  110. isa_model/serving/api/routes/health.py +32 -12
  111. isa_model/serving/api/routes/inference_monitoring.py +486 -0
  112. isa_model/serving/api/routes/local_deployments.py +448 -0
  113. isa_model/serving/api/routes/tenants.py +575 -0
  114. isa_model/serving/api/routes/unified.py +680 -18
  115. isa_model/serving/api/routes/webhooks.py +479 -0
  116. isa_model/serving/api/startup.py +68 -54
  117. isa_model/utils/gpu_utils.py +311 -0
  118. {isa_model-0.4.0.dist-info → isa_model-0.4.3.dist-info}/METADATA +66 -24
  119. isa_model-0.4.3.dist-info/RECORD +193 -0
  120. isa_model/core/storage/minio_storage.py +0 -0
  121. isa_model/deployment/cloud/__init__.py +0 -9
  122. isa_model/deployment/cloud/modal/__init__.py +0 -10
  123. isa_model/deployment/core/deployment_config.py +0 -356
  124. isa_model/deployment/core/isa_deployment_service.py +0 -401
  125. isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
  126. isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
  127. isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
  128. isa_model/deployment/runtime/deployed_service.py +0 -338
  129. isa_model/deployment/services/__init__.py +0 -9
  130. isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
  131. isa_model/deployment/services/model_service.py +0 -332
  132. isa_model/deployment/services/service_monitor.py +0 -356
  133. isa_model/deployment/services/service_registry.py +0 -527
  134. isa_model/eval/__init__.py +0 -92
  135. isa_model/eval/benchmarks/__init__.py +0 -27
  136. isa_model/eval/benchmarks/multimodal_datasets.py +0 -460
  137. isa_model/eval/benchmarks.py +0 -701
  138. isa_model/eval/config/__init__.py +0 -10
  139. isa_model/eval/config/evaluation_config.py +0 -108
  140. isa_model/eval/evaluators/__init__.py +0 -24
  141. isa_model/eval/evaluators/audio_evaluator.py +0 -727
  142. isa_model/eval/evaluators/base_evaluator.py +0 -503
  143. isa_model/eval/evaluators/embedding_evaluator.py +0 -742
  144. isa_model/eval/evaluators/llm_evaluator.py +0 -472
  145. isa_model/eval/evaluators/vision_evaluator.py +0 -564
  146. isa_model/eval/example_evaluation.py +0 -395
  147. isa_model/eval/factory.py +0 -798
  148. isa_model/eval/infrastructure/__init__.py +0 -24
  149. isa_model/eval/infrastructure/experiment_tracker.py +0 -466
  150. isa_model/eval/isa_benchmarks.py +0 -700
  151. isa_model/eval/isa_integration.py +0 -582
  152. isa_model/eval/metrics.py +0 -951
  153. isa_model/eval/tests/unit/test_basic.py +0 -396
  154. isa_model/serving/api/routes/evaluations.py +0 -579
  155. isa_model/training/__init__.py +0 -168
  156. isa_model/training/annotation/annotation_schema.py +0 -47
  157. isa_model/training/annotation/processors/annotation_processor.py +0 -126
  158. isa_model/training/annotation/storage/dataset_manager.py +0 -131
  159. isa_model/training/annotation/storage/dataset_schema.py +0 -44
  160. isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
  161. isa_model/training/annotation/tests/test_minio copy.py +0 -113
  162. isa_model/training/annotation/tests/test_minio_upload.py +0 -43
  163. isa_model/training/annotation/views/annotation_controller.py +0 -158
  164. isa_model/training/cloud/__init__.py +0 -22
  165. isa_model/training/cloud/job_orchestrator.py +0 -402
  166. isa_model/training/cloud/runpod_trainer.py +0 -454
  167. isa_model/training/cloud/storage_manager.py +0 -482
  168. isa_model/training/core/__init__.py +0 -26
  169. isa_model/training/core/config.py +0 -181
  170. isa_model/training/core/dataset.py +0 -222
  171. isa_model/training/core/trainer.py +0 -720
  172. isa_model/training/core/utils.py +0 -213
  173. isa_model/training/examples/intelligent_training_example.py +0 -281
  174. isa_model/training/factory.py +0 -424
  175. isa_model/training/intelligent/__init__.py +0 -25
  176. isa_model/training/intelligent/decision_engine.py +0 -643
  177. isa_model/training/intelligent/intelligent_factory.py +0 -888
  178. isa_model/training/intelligent/knowledge_base.py +0 -751
  179. isa_model/training/intelligent/resource_optimizer.py +0 -839
  180. isa_model/training/intelligent/task_classifier.py +0 -576
  181. isa_model/training/storage/__init__.py +0 -24
  182. isa_model/training/storage/core_integration.py +0 -439
  183. isa_model/training/storage/training_repository.py +0 -552
  184. isa_model/training/storage/training_storage.py +0 -628
  185. isa_model-0.4.0.dist-info/RECORD +0 -182
  186. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_chatTTS_service.py +0 -0
  187. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_fish_service.py +0 -0
  188. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_openvoice_service.py +0 -0
  189. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_service_v2.py +0 -0
  190. /isa_model/deployment/{cloud/modal → modal/services/embedding}/isa_embed_rerank_service.py +0 -0
  191. /isa_model/deployment/{cloud/modal → modal/services/video}/isa_video_hunyuan_service.py +0 -0
  192. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ocr_service.py +0 -0
  193. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_qwen25_service.py +0 -0
  194. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_table_service.py +0 -0
  195. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service.py +0 -0
  196. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service_optimized.py +0 -0
  197. /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
  198. {isa_model-0.4.0.dist-info → isa_model-0.4.3.dist-info}/WHEEL +0 -0
  199. {isa_model-0.4.0.dist-info → isa_model-0.4.3.dist-info}/top_level.txt +0 -0
@@ -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