isa-model 0.3.91__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- isa_model/client.py +1166 -584
- isa_model/core/cache/redis_cache.py +410 -0
- isa_model/core/config/config_manager.py +282 -12
- isa_model/core/config.py +91 -1
- isa_model/core/database/__init__.py +1 -0
- isa_model/core/database/direct_db_client.py +114 -0
- isa_model/core/database/migration_manager.py +563 -0
- isa_model/core/database/migrations.py +297 -0
- isa_model/core/database/supabase_client.py +258 -0
- isa_model/core/dependencies.py +316 -0
- isa_model/core/discovery/__init__.py +19 -0
- isa_model/core/discovery/consul_discovery.py +190 -0
- isa_model/core/logging/__init__.py +54 -0
- isa_model/core/logging/influx_logger.py +523 -0
- isa_model/core/logging/loki_logger.py +160 -0
- isa_model/core/models/__init__.py +46 -0
- isa_model/core/models/config_models.py +625 -0
- isa_model/core/models/deployment_billing_tracker.py +430 -0
- isa_model/core/models/model_billing_tracker.py +60 -88
- isa_model/core/models/model_manager.py +66 -25
- isa_model/core/models/model_metadata.py +690 -0
- isa_model/core/models/model_repo.py +217 -55
- isa_model/core/models/model_statistics_tracker.py +234 -0
- isa_model/core/models/model_storage.py +0 -1
- isa_model/core/models/model_version_manager.py +959 -0
- isa_model/core/models/system_models.py +857 -0
- isa_model/core/pricing_manager.py +2 -249
- isa_model/core/repositories/__init__.py +9 -0
- isa_model/core/repositories/config_repository.py +912 -0
- isa_model/core/resilience/circuit_breaker.py +366 -0
- isa_model/core/security/secrets.py +358 -0
- isa_model/core/services/__init__.py +2 -4
- isa_model/core/services/intelligent_model_selector.py +479 -370
- isa_model/core/storage/hf_storage.py +2 -2
- isa_model/core/types.py +8 -0
- isa_model/deployment/__init__.py +5 -48
- isa_model/deployment/core/__init__.py +2 -31
- isa_model/deployment/core/deployment_manager.py +1278 -368
- isa_model/deployment/local/__init__.py +31 -0
- isa_model/deployment/local/config.py +248 -0
- isa_model/deployment/local/gpu_gateway.py +607 -0
- isa_model/deployment/local/health_checker.py +428 -0
- isa_model/deployment/local/provider.py +586 -0
- isa_model/deployment/local/tensorrt_service.py +621 -0
- isa_model/deployment/local/transformers_service.py +644 -0
- isa_model/deployment/local/vllm_service.py +527 -0
- isa_model/deployment/modal/__init__.py +8 -0
- isa_model/deployment/modal/config.py +136 -0
- isa_model/deployment/modal/deployer.py +894 -0
- isa_model/deployment/modal/services/__init__.py +3 -0
- isa_model/deployment/modal/services/audio/__init__.py +1 -0
- isa_model/deployment/modal/services/audio/isa_audio_chatTTS_service.py +520 -0
- isa_model/deployment/modal/services/audio/isa_audio_openvoice_service.py +758 -0
- isa_model/deployment/modal/services/audio/isa_audio_service_v2.py +1044 -0
- isa_model/deployment/modal/services/embedding/__init__.py +1 -0
- isa_model/deployment/modal/services/embedding/isa_embed_rerank_service.py +296 -0
- isa_model/deployment/modal/services/llm/__init__.py +1 -0
- isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
- isa_model/deployment/modal/services/video/__init__.py +1 -0
- isa_model/deployment/modal/services/video/isa_video_hunyuan_service.py +423 -0
- isa_model/deployment/modal/services/vision/__init__.py +1 -0
- isa_model/deployment/modal/services/vision/isa_vision_ocr_service.py +519 -0
- isa_model/deployment/modal/services/vision/isa_vision_qwen25_service.py +709 -0
- isa_model/deployment/modal/services/vision/isa_vision_table_service.py +676 -0
- isa_model/deployment/modal/services/vision/isa_vision_ui_service.py +833 -0
- isa_model/deployment/modal/services/vision/isa_vision_ui_service_optimized.py +660 -0
- isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
- isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
- isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/storage/__init__.py +5 -0
- isa_model/deployment/storage/deployment_repository.py +824 -0
- isa_model/deployment/triton/__init__.py +10 -0
- isa_model/deployment/triton/config.py +196 -0
- isa_model/deployment/triton/configs/__init__.py +1 -0
- isa_model/deployment/triton/provider.py +512 -0
- isa_model/deployment/triton/scripts/__init__.py +1 -0
- isa_model/deployment/triton/templates/__init__.py +1 -0
- isa_model/inference/__init__.py +47 -1
- isa_model/inference/ai_factory.py +179 -16
- isa_model/inference/legacy_services/__init__.py +21 -0
- isa_model/inference/legacy_services/model_evaluation.py +637 -0
- isa_model/inference/legacy_services/model_service.py +573 -0
- isa_model/inference/legacy_services/model_serving.py +717 -0
- isa_model/inference/legacy_services/model_training.py +561 -0
- isa_model/inference/models/__init__.py +21 -0
- isa_model/inference/models/inference_config.py +551 -0
- isa_model/inference/models/inference_record.py +675 -0
- isa_model/inference/models/performance_models.py +714 -0
- isa_model/inference/repositories/__init__.py +9 -0
- isa_model/inference/repositories/inference_repository.py +828 -0
- isa_model/inference/services/audio/__init__.py +21 -0
- isa_model/inference/services/audio/base_realtime_service.py +225 -0
- isa_model/inference/services/audio/base_stt_service.py +184 -11
- isa_model/inference/services/audio/isa_tts_service.py +0 -0
- isa_model/inference/services/audio/openai_realtime_service.py +320 -124
- isa_model/inference/services/audio/openai_stt_service.py +53 -11
- isa_model/inference/services/base_service.py +17 -1
- isa_model/inference/services/custom_model_manager.py +277 -0
- isa_model/inference/services/embedding/__init__.py +13 -0
- isa_model/inference/services/embedding/base_embed_service.py +111 -8
- isa_model/inference/services/embedding/isa_embed_service.py +305 -0
- isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
- isa_model/inference/services/embedding/openai_embed_service.py +2 -4
- isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
- isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
- isa_model/inference/services/img/__init__.py +2 -2
- isa_model/inference/services/img/base_image_gen_service.py +24 -7
- isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
- isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
- isa_model/inference/services/img/services/replicate_flux.py +226 -0
- isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
- isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
- isa_model/inference/services/img/tests/test_img_client.py +297 -0
- isa_model/inference/services/llm/__init__.py +10 -2
- isa_model/inference/services/llm/base_llm_service.py +361 -26
- isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
- isa_model/inference/services/llm/helpers/llm_adapter.py +71 -12
- isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
- isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
- isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
- isa_model/inference/services/llm/local_llm_service.py +747 -0
- isa_model/inference/services/llm/ollama_llm_service.py +11 -3
- isa_model/inference/services/llm/openai_llm_service.py +670 -56
- isa_model/inference/services/llm/yyds_llm_service.py +10 -3
- isa_model/inference/services/vision/__init__.py +27 -6
- isa_model/inference/services/vision/base_vision_service.py +118 -185
- isa_model/inference/services/vision/blip_vision_service.py +359 -0
- isa_model/inference/services/vision/helpers/image_utils.py +19 -10
- isa_model/inference/services/vision/isa_vision_service.py +634 -0
- isa_model/inference/services/vision/openai_vision_service.py +19 -10
- isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
- isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
- isa_model/serving/api/cache_manager.py +245 -0
- isa_model/serving/api/dependencies/__init__.py +1 -0
- isa_model/serving/api/dependencies/auth.py +194 -0
- isa_model/serving/api/dependencies/database.py +139 -0
- isa_model/serving/api/error_handlers.py +284 -0
- isa_model/serving/api/fastapi_server.py +240 -18
- isa_model/serving/api/middleware/auth.py +317 -0
- isa_model/serving/api/middleware/security.py +268 -0
- isa_model/serving/api/middleware/tenant_context.py +414 -0
- isa_model/serving/api/routes/analytics.py +489 -0
- isa_model/serving/api/routes/config.py +645 -0
- isa_model/serving/api/routes/deployment_billing.py +315 -0
- isa_model/serving/api/routes/deployments.py +475 -0
- isa_model/serving/api/routes/gpu_gateway.py +440 -0
- isa_model/serving/api/routes/health.py +32 -12
- isa_model/serving/api/routes/inference_monitoring.py +486 -0
- isa_model/serving/api/routes/local_deployments.py +448 -0
- isa_model/serving/api/routes/logs.py +430 -0
- isa_model/serving/api/routes/settings.py +582 -0
- isa_model/serving/api/routes/tenants.py +575 -0
- isa_model/serving/api/routes/unified.py +992 -171
- isa_model/serving/api/routes/webhooks.py +479 -0
- isa_model/serving/api/startup.py +318 -0
- isa_model/serving/modal_proxy_server.py +249 -0
- isa_model/utils/gpu_utils.py +311 -0
- {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/METADATA +76 -22
- isa_model-0.4.3.dist-info/RECORD +193 -0
- isa_model/deployment/cloud/__init__.py +0 -9
- isa_model/deployment/cloud/modal/__init__.py +0 -10
- isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
- isa_model/deployment/cloud/modal/isa_vision_table_service.py +0 -532
- isa_model/deployment/cloud/modal/isa_vision_ui_service.py +0 -406
- isa_model/deployment/cloud/modal/register_models.py +0 -321
- isa_model/deployment/core/deployment_config.py +0 -356
- isa_model/deployment/core/isa_deployment_service.py +0 -401
- isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
- isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
- isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
- isa_model/deployment/runtime/deployed_service.py +0 -338
- isa_model/deployment/services/__init__.py +0 -9
- isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
- isa_model/deployment/services/model_service.py +0 -332
- isa_model/deployment/services/service_monitor.py +0 -356
- isa_model/deployment/services/service_registry.py +0 -527
- isa_model/eval/__init__.py +0 -92
- isa_model/eval/benchmarks.py +0 -469
- isa_model/eval/config/__init__.py +0 -10
- isa_model/eval/config/evaluation_config.py +0 -108
- isa_model/eval/evaluators/__init__.py +0 -18
- isa_model/eval/evaluators/base_evaluator.py +0 -503
- isa_model/eval/evaluators/llm_evaluator.py +0 -472
- isa_model/eval/factory.py +0 -531
- isa_model/eval/infrastructure/__init__.py +0 -24
- isa_model/eval/infrastructure/experiment_tracker.py +0 -466
- isa_model/eval/metrics.py +0 -798
- isa_model/inference/adapter/unified_api.py +0 -248
- isa_model/inference/services/helpers/stacked_config.py +0 -148
- isa_model/inference/services/img/flux_professional_service.py +0 -603
- isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/others/table_transformer_service.py +0 -61
- isa_model/inference/services/vision/doc_analysis_service.py +0 -640
- isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/vision/ui_analysis_service.py +0 -823
- isa_model/scripts/inference_tracker.py +0 -283
- isa_model/scripts/mlflow_manager.py +0 -379
- isa_model/scripts/model_registry.py +0 -465
- isa_model/scripts/register_models.py +0 -370
- isa_model/scripts/register_models_with_embeddings.py +0 -510
- isa_model/scripts/start_mlflow.py +0 -95
- isa_model/scripts/training_tracker.py +0 -257
- isa_model/training/__init__.py +0 -74
- isa_model/training/annotation/annotation_schema.py +0 -47
- isa_model/training/annotation/processors/annotation_processor.py +0 -126
- isa_model/training/annotation/storage/dataset_manager.py +0 -131
- isa_model/training/annotation/storage/dataset_schema.py +0 -44
- isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
- isa_model/training/annotation/tests/test_minio copy.py +0 -113
- isa_model/training/annotation/tests/test_minio_upload.py +0 -43
- isa_model/training/annotation/views/annotation_controller.py +0 -158
- isa_model/training/cloud/__init__.py +0 -22
- isa_model/training/cloud/job_orchestrator.py +0 -402
- isa_model/training/cloud/runpod_trainer.py +0 -454
- isa_model/training/cloud/storage_manager.py +0 -482
- isa_model/training/core/__init__.py +0 -23
- isa_model/training/core/config.py +0 -181
- isa_model/training/core/dataset.py +0 -222
- isa_model/training/core/trainer.py +0 -720
- isa_model/training/core/utils.py +0 -213
- isa_model/training/factory.py +0 -424
- isa_model-0.3.91.dist-info/RECORD +0 -138
- /isa_model/{core/storage/minio_storage.py → deployment/modal/services/audio/isa_audio_fish_service.py} +0 -0
- /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
- {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/WHEEL +0 -0
- {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,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)
|