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.
- isa_model/client.py +466 -43
- isa_model/core/cache/redis_cache.py +12 -3
- isa_model/core/config/config_manager.py +230 -3
- isa_model/core/config.py +90 -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 +21 -1
- isa_model/core/database/supabase_client.py +154 -19
- 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 +27 -18
- isa_model/core/models/config_models.py +625 -0
- isa_model/core/models/deployment_billing_tracker.py +430 -0
- isa_model/core/models/model_manager.py +35 -80
- isa_model/core/models/model_metadata.py +690 -0
- isa_model/core/models/model_repo.py +174 -18
- isa_model/core/models/system_models.py +857 -0
- isa_model/core/repositories/__init__.py +9 -0
- isa_model/core/repositories/config_repository.py +912 -0
- isa_model/core/services/intelligent_model_selector.py +399 -21
- isa_model/core/types.py +1 -0
- isa_model/deployment/__init__.py +5 -48
- isa_model/deployment/core/__init__.py +2 -31
- isa_model/deployment/core/deployment_manager.py +1278 -370
- isa_model/deployment/modal/__init__.py +8 -0
- isa_model/deployment/modal/config.py +136 -0
- isa_model/deployment/{services/auto_hf_modal_deployer.py → modal/deployer.py} +1 -1
- isa_model/deployment/modal/services/__init__.py +3 -0
- isa_model/deployment/modal/services/audio/__init__.py +1 -0
- isa_model/deployment/modal/services/embedding/__init__.py +1 -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/vision/__init__.py +1 -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 +137 -10
- 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/base_stt_service.py +184 -11
- isa_model/inference/services/audio/openai_stt_service.py +22 -6
- isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
- isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
- isa_model/inference/services/llm/__init__.py +10 -2
- isa_model/inference/services/llm/base_llm_service.py +335 -24
- isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
- isa_model/inference/services/llm/helpers/llm_adapter.py +9 -4
- 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/ollama_llm_service.py +9 -2
- isa_model/inference/services/llm/openai_llm_service.py +33 -16
- isa_model/inference/services/llm/yyds_llm_service.py +8 -2
- isa_model/inference/services/vision/__init__.py +22 -1
- isa_model/inference/services/vision/helpers/image_utils.py +8 -5
- isa_model/inference/services/vision/isa_vision_service.py +65 -4
- isa_model/inference/services/vision/openai_vision_service.py +19 -10
- 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 +172 -22
- isa_model/serving/api/middleware/auth.py +8 -2
- isa_model/serving/api/middleware/security.py +23 -33
- isa_model/serving/api/middleware/tenant_context.py +414 -0
- isa_model/serving/api/routes/analytics.py +4 -1
- 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 +138 -2
- 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/tenants.py +575 -0
- isa_model/serving/api/routes/unified.py +680 -18
- isa_model/serving/api/routes/webhooks.py +479 -0
- isa_model/serving/api/startup.py +68 -54
- isa_model/utils/gpu_utils.py +311 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/METADATA +71 -24
- isa_model-0.4.4.dist-info/RECORD +180 -0
- isa_model/core/security/secrets.py +0 -358
- isa_model/core/storage/hf_storage.py +0 -419
- isa_model/core/storage/minio_storage.py +0 -0
- isa_model/deployment/cloud/__init__.py +0 -9
- isa_model/deployment/cloud/modal/__init__.py +0 -10
- 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/__init__.py +0 -27
- isa_model/eval/benchmarks/multimodal_datasets.py +0 -460
- isa_model/eval/benchmarks.py +0 -701
- isa_model/eval/config/__init__.py +0 -10
- isa_model/eval/config/evaluation_config.py +0 -108
- isa_model/eval/evaluators/__init__.py +0 -24
- isa_model/eval/evaluators/audio_evaluator.py +0 -727
- isa_model/eval/evaluators/base_evaluator.py +0 -503
- isa_model/eval/evaluators/embedding_evaluator.py +0 -742
- isa_model/eval/evaluators/llm_evaluator.py +0 -472
- isa_model/eval/evaluators/vision_evaluator.py +0 -564
- isa_model/eval/example_evaluation.py +0 -395
- isa_model/eval/factory.py +0 -798
- isa_model/eval/infrastructure/__init__.py +0 -24
- isa_model/eval/infrastructure/experiment_tracker.py +0 -466
- isa_model/eval/isa_benchmarks.py +0 -700
- isa_model/eval/isa_integration.py +0 -582
- isa_model/eval/metrics.py +0 -951
- isa_model/eval/tests/unit/test_basic.py +0 -396
- isa_model/serving/api/routes/evaluations.py +0 -579
- isa_model/training/__init__.py +0 -168
- 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 -26
- 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/examples/intelligent_training_example.py +0 -281
- isa_model/training/factory.py +0 -424
- isa_model/training/intelligent/__init__.py +0 -25
- isa_model/training/intelligent/decision_engine.py +0 -643
- isa_model/training/intelligent/intelligent_factory.py +0 -888
- isa_model/training/intelligent/knowledge_base.py +0 -751
- isa_model/training/intelligent/resource_optimizer.py +0 -839
- isa_model/training/intelligent/task_classifier.py +0 -576
- isa_model/training/storage/__init__.py +0 -24
- isa_model/training/storage/core_integration.py +0 -439
- isa_model/training/storage/training_repository.py +0 -552
- isa_model/training/storage/training_storage.py +0 -628
- isa_model-0.4.0.dist-info/RECORD +0 -182
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_chatTTS_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_fish_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_openvoice_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_service_v2.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/embedding}/isa_embed_rerank_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/video}/isa_video_hunyuan_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ocr_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_qwen25_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_table_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service_optimized.py +0 -0
- /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/WHEEL +0 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,575 @@
|
|
1
|
+
"""
|
2
|
+
Tenant Management API
|
3
|
+
|
4
|
+
Handles multi-tenancy operations including:
|
5
|
+
- Organization/tenant management
|
6
|
+
- Resource quotas and limits
|
7
|
+
- Tenant isolation
|
8
|
+
- Billing and usage tracking per tenant
|
9
|
+
"""
|
10
|
+
|
11
|
+
from fastapi import APIRouter, HTTPException, Depends, Request
|
12
|
+
from pydantic import BaseModel, Field
|
13
|
+
from typing import Dict, Any, List, Optional, Union
|
14
|
+
import uuid
|
15
|
+
import asyncio
|
16
|
+
import logging
|
17
|
+
from datetime import datetime, timedelta
|
18
|
+
import json
|
19
|
+
|
20
|
+
from ..dependencies.auth import get_current_user, get_current_organization, require_admin
|
21
|
+
from ..dependencies.database import get_database_connection
|
22
|
+
from ..middleware.tenant_context import get_tenant_context, TenantContext
|
23
|
+
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
router = APIRouter()
|
26
|
+
|
27
|
+
# ============= Pydantic Models =============
|
28
|
+
|
29
|
+
class TenantCreateRequest(BaseModel):
|
30
|
+
"""Request model for creating a new tenant/organization"""
|
31
|
+
name: str = Field(..., min_length=1, max_length=100, description="Organization name")
|
32
|
+
domain: Optional[str] = Field(None, description="Organization domain (optional)")
|
33
|
+
plan: str = Field("starter", description="Subscription plan: starter, pro, enterprise")
|
34
|
+
billing_email: str = Field(..., description="Billing contact email")
|
35
|
+
admin_user_email: str = Field(..., description="Initial admin user email")
|
36
|
+
admin_user_name: str = Field(..., description="Initial admin user name")
|
37
|
+
|
38
|
+
class Config:
|
39
|
+
json_schema_extra = {
|
40
|
+
"example": {
|
41
|
+
"name": "Acme Corporation",
|
42
|
+
"domain": "acme.com",
|
43
|
+
"plan": "pro",
|
44
|
+
"billing_email": "billing@acme.com",
|
45
|
+
"admin_user_email": "admin@acme.com",
|
46
|
+
"admin_user_name": "John Admin"
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
class TenantUpdateRequest(BaseModel):
|
51
|
+
"""Request model for updating tenant settings"""
|
52
|
+
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
53
|
+
plan: Optional[str] = Field(None, description="starter, pro, enterprise")
|
54
|
+
billing_email: Optional[str] = Field(None)
|
55
|
+
status: Optional[str] = Field(None, description="active, suspended, inactive")
|
56
|
+
settings: Optional[Dict[str, Any]] = Field(None, description="Tenant-specific settings")
|
57
|
+
|
58
|
+
class TenantQuotaRequest(BaseModel):
|
59
|
+
"""Request model for setting tenant resource quotas"""
|
60
|
+
api_calls_per_month: Optional[int] = Field(None, ge=0)
|
61
|
+
max_concurrent_requests: Optional[int] = Field(None, ge=1)
|
62
|
+
max_storage_gb: Optional[float] = Field(None, ge=0)
|
63
|
+
max_training_jobs: Optional[int] = Field(None, ge=0)
|
64
|
+
max_deployments: Optional[int] = Field(None, ge=0)
|
65
|
+
max_users: Optional[int] = Field(None, ge=1)
|
66
|
+
|
67
|
+
class TenantResponse(BaseModel):
|
68
|
+
"""Response model for tenant information"""
|
69
|
+
organization_id: str
|
70
|
+
name: str
|
71
|
+
domain: Optional[str]
|
72
|
+
plan: str
|
73
|
+
status: str
|
74
|
+
billing_email: str
|
75
|
+
created_at: datetime
|
76
|
+
updated_at: datetime
|
77
|
+
member_count: int
|
78
|
+
current_usage: Dict[str, Any]
|
79
|
+
quotas: Dict[str, Any]
|
80
|
+
settings: Dict[str, Any]
|
81
|
+
|
82
|
+
# ============= Tenant Management =============
|
83
|
+
|
84
|
+
@router.post("/", response_model=TenantResponse)
|
85
|
+
async def create_tenant(
|
86
|
+
request: TenantCreateRequest,
|
87
|
+
current_user = Depends(require_admin)
|
88
|
+
):
|
89
|
+
"""
|
90
|
+
Create a new tenant/organization.
|
91
|
+
Requires system admin privileges.
|
92
|
+
"""
|
93
|
+
try:
|
94
|
+
async with get_database_connection() as conn:
|
95
|
+
# Generate unique organization ID
|
96
|
+
org_id = f"org_{uuid.uuid4().hex[:12]}"
|
97
|
+
|
98
|
+
# Create organization
|
99
|
+
org_query = """
|
100
|
+
INSERT INTO organizations (
|
101
|
+
organization_id, name, domain, plan, billing_email,
|
102
|
+
status, settings, credits_pool, created_at, updated_at
|
103
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
104
|
+
RETURNING *
|
105
|
+
"""
|
106
|
+
|
107
|
+
default_settings = {
|
108
|
+
"api_rate_limit": 1000,
|
109
|
+
"max_file_upload_mb": 100,
|
110
|
+
"enable_webhooks": True,
|
111
|
+
"data_retention_days": 90
|
112
|
+
}
|
113
|
+
|
114
|
+
now = datetime.utcnow()
|
115
|
+
org_result = await conn.fetchrow(
|
116
|
+
org_query, org_id, request.name, request.domain,
|
117
|
+
request.plan, request.billing_email, "active",
|
118
|
+
json.dumps(default_settings), 1000.0, now, now
|
119
|
+
)
|
120
|
+
|
121
|
+
# Create default quotas based on plan
|
122
|
+
quotas = get_default_quotas(request.plan)
|
123
|
+
quota_query = """
|
124
|
+
INSERT INTO organization_quotas (
|
125
|
+
organization_id, quotas, created_at, updated_at
|
126
|
+
) VALUES ($1, $2, $3, $4)
|
127
|
+
"""
|
128
|
+
await conn.execute(quota_query, org_id, json.dumps(quotas), now, now)
|
129
|
+
|
130
|
+
# Create admin user if doesn't exist
|
131
|
+
user_query = """
|
132
|
+
INSERT INTO users (user_id, email, name, role, organization_id, created_at, updated_at)
|
133
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
134
|
+
ON CONFLICT (email) DO UPDATE SET
|
135
|
+
organization_id = $5, updated_at = $7
|
136
|
+
RETURNING user_id
|
137
|
+
"""
|
138
|
+
|
139
|
+
admin_user_id = f"user_{uuid.uuid4().hex[:12]}"
|
140
|
+
await conn.execute(
|
141
|
+
user_query, admin_user_id, request.admin_user_email,
|
142
|
+
request.admin_user_name, "admin", org_id, now, now
|
143
|
+
)
|
144
|
+
|
145
|
+
# Get full tenant info for response
|
146
|
+
tenant_info = await get_tenant_info(org_id, conn)
|
147
|
+
|
148
|
+
logger.info(f"Created tenant {org_id} ({request.name}) with admin {request.admin_user_email}")
|
149
|
+
return tenant_info
|
150
|
+
|
151
|
+
except Exception as e:
|
152
|
+
logger.error(f"Error creating tenant: {e}")
|
153
|
+
raise HTTPException(status_code=500, detail=f"Failed to create tenant: {str(e)}")
|
154
|
+
|
155
|
+
@router.get("/{organization_id}", response_model=TenantResponse)
|
156
|
+
async def get_tenant(
|
157
|
+
organization_id: str,
|
158
|
+
current_org = Depends(get_current_organization)
|
159
|
+
):
|
160
|
+
"""Get tenant information"""
|
161
|
+
try:
|
162
|
+
# Ensure user can only access their own tenant (unless system admin)
|
163
|
+
if current_org.organization_id != organization_id:
|
164
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
165
|
+
|
166
|
+
async with get_database_connection() as conn:
|
167
|
+
tenant_info = await get_tenant_info(organization_id, conn)
|
168
|
+
if not tenant_info:
|
169
|
+
raise HTTPException(status_code=404, detail="Tenant not found")
|
170
|
+
return tenant_info
|
171
|
+
|
172
|
+
except HTTPException:
|
173
|
+
raise
|
174
|
+
except Exception as e:
|
175
|
+
logger.error(f"Error getting tenant {organization_id}: {e}")
|
176
|
+
raise HTTPException(status_code=500, detail="Failed to get tenant information")
|
177
|
+
|
178
|
+
@router.put("/{organization_id}", response_model=TenantResponse)
|
179
|
+
async def update_tenant(
|
180
|
+
organization_id: str,
|
181
|
+
request: TenantUpdateRequest,
|
182
|
+
current_org = Depends(get_current_organization)
|
183
|
+
):
|
184
|
+
"""Update tenant settings"""
|
185
|
+
try:
|
186
|
+
# Check access permissions
|
187
|
+
if current_org.organization_id != organization_id:
|
188
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
189
|
+
|
190
|
+
async with get_database_connection() as conn:
|
191
|
+
# Build update query dynamically
|
192
|
+
update_fields = []
|
193
|
+
values = []
|
194
|
+
param_count = 1
|
195
|
+
|
196
|
+
for field, value in request.dict(exclude_unset=True).items():
|
197
|
+
if field == "settings":
|
198
|
+
update_fields.append(f"settings = ${param_count}")
|
199
|
+
values.append(json.dumps(value))
|
200
|
+
else:
|
201
|
+
update_fields.append(f"{field} = ${param_count}")
|
202
|
+
values.append(value)
|
203
|
+
param_count += 1
|
204
|
+
|
205
|
+
if not update_fields:
|
206
|
+
raise HTTPException(status_code=400, detail="No fields to update")
|
207
|
+
|
208
|
+
update_fields.append(f"updated_at = ${param_count}")
|
209
|
+
values.append(datetime.utcnow())
|
210
|
+
values.append(organization_id) # WHERE clause
|
211
|
+
|
212
|
+
query = f"""
|
213
|
+
UPDATE organizations
|
214
|
+
SET {', '.join(update_fields)}
|
215
|
+
WHERE organization_id = ${param_count + 1}
|
216
|
+
RETURNING *
|
217
|
+
"""
|
218
|
+
|
219
|
+
result = await conn.fetchrow(query, *values)
|
220
|
+
if not result:
|
221
|
+
raise HTTPException(status_code=404, detail="Tenant not found")
|
222
|
+
|
223
|
+
# Get updated tenant info
|
224
|
+
tenant_info = await get_tenant_info(organization_id, conn)
|
225
|
+
|
226
|
+
logger.info(f"Updated tenant {organization_id}")
|
227
|
+
return tenant_info
|
228
|
+
|
229
|
+
except HTTPException:
|
230
|
+
raise
|
231
|
+
except Exception as e:
|
232
|
+
logger.error(f"Error updating tenant {organization_id}: {e}")
|
233
|
+
raise HTTPException(status_code=500, detail="Failed to update tenant")
|
234
|
+
|
235
|
+
@router.delete("/{organization_id}")
|
236
|
+
async def delete_tenant(
|
237
|
+
organization_id: str,
|
238
|
+
current_user = Depends(require_admin)
|
239
|
+
):
|
240
|
+
"""
|
241
|
+
Delete a tenant and all associated data.
|
242
|
+
Requires system admin privileges.
|
243
|
+
"""
|
244
|
+
try:
|
245
|
+
async with get_database_connection() as conn:
|
246
|
+
# Start transaction
|
247
|
+
async with conn.transaction():
|
248
|
+
# First, delete all tenant-related data
|
249
|
+
tables_to_cleanup = [
|
250
|
+
'training_jobs',
|
251
|
+
'evaluations',
|
252
|
+
'organization_members',
|
253
|
+
'organization_usage',
|
254
|
+
'organization_credit_transactions',
|
255
|
+
'users'
|
256
|
+
]
|
257
|
+
|
258
|
+
for table in tables_to_cleanup:
|
259
|
+
await conn.execute(
|
260
|
+
f"DELETE FROM {table} WHERE organization_id = $1",
|
261
|
+
organization_id
|
262
|
+
)
|
263
|
+
|
264
|
+
# Finally delete the organization
|
265
|
+
result = await conn.execute(
|
266
|
+
"DELETE FROM organizations WHERE organization_id = $1",
|
267
|
+
organization_id
|
268
|
+
)
|
269
|
+
|
270
|
+
if result == "DELETE 0":
|
271
|
+
raise HTTPException(status_code=404, detail="Tenant not found")
|
272
|
+
|
273
|
+
logger.info(f"Deleted tenant {organization_id} and all associated data")
|
274
|
+
return {"message": "Tenant deleted successfully", "organization_id": organization_id}
|
275
|
+
|
276
|
+
except HTTPException:
|
277
|
+
raise
|
278
|
+
except Exception as e:
|
279
|
+
logger.error(f"Error deleting tenant {organization_id}: {e}")
|
280
|
+
raise HTTPException(status_code=500, detail="Failed to delete tenant")
|
281
|
+
|
282
|
+
# ============= Quota Management =============
|
283
|
+
|
284
|
+
@router.get("/{organization_id}/quotas")
|
285
|
+
async def get_tenant_quotas(
|
286
|
+
organization_id: str,
|
287
|
+
current_org = Depends(get_current_organization)
|
288
|
+
):
|
289
|
+
"""Get tenant resource quotas and current usage"""
|
290
|
+
try:
|
291
|
+
if current_org.organization_id != organization_id:
|
292
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
293
|
+
|
294
|
+
async with get_database_connection() as conn:
|
295
|
+
# Get quotas
|
296
|
+
quota_result = await conn.fetchrow(
|
297
|
+
"SELECT quotas FROM organization_quotas WHERE organization_id = $1",
|
298
|
+
organization_id
|
299
|
+
)
|
300
|
+
|
301
|
+
if not quota_result:
|
302
|
+
raise HTTPException(status_code=404, detail="Quotas not found")
|
303
|
+
|
304
|
+
quotas = quota_result['quotas']
|
305
|
+
|
306
|
+
# Get current usage
|
307
|
+
usage = await get_current_usage(organization_id, conn)
|
308
|
+
|
309
|
+
return {
|
310
|
+
"organization_id": organization_id,
|
311
|
+
"quotas": quotas,
|
312
|
+
"current_usage": usage,
|
313
|
+
"usage_percentage": calculate_usage_percentage(quotas, usage)
|
314
|
+
}
|
315
|
+
|
316
|
+
except HTTPException:
|
317
|
+
raise
|
318
|
+
except Exception as e:
|
319
|
+
logger.error(f"Error getting quotas for {organization_id}: {e}")
|
320
|
+
raise HTTPException(status_code=500, detail="Failed to get quotas")
|
321
|
+
|
322
|
+
@router.put("/{organization_id}/quotas")
|
323
|
+
async def update_tenant_quotas(
|
324
|
+
organization_id: str,
|
325
|
+
request: TenantQuotaRequest,
|
326
|
+
current_user = Depends(require_admin)
|
327
|
+
):
|
328
|
+
"""Update tenant resource quotas (admin only)"""
|
329
|
+
try:
|
330
|
+
async with get_database_connection() as conn:
|
331
|
+
# Get current quotas
|
332
|
+
current_result = await conn.fetchrow(
|
333
|
+
"SELECT quotas FROM organization_quotas WHERE organization_id = $1",
|
334
|
+
organization_id
|
335
|
+
)
|
336
|
+
|
337
|
+
if not current_result:
|
338
|
+
raise HTTPException(status_code=404, detail="Tenant not found")
|
339
|
+
|
340
|
+
current_quotas = current_result['quotas']
|
341
|
+
|
342
|
+
# Update with new values
|
343
|
+
for field, value in request.dict(exclude_unset=True).items():
|
344
|
+
if value is not None:
|
345
|
+
current_quotas[field] = value
|
346
|
+
|
347
|
+
# Save updated quotas
|
348
|
+
await conn.execute(
|
349
|
+
"UPDATE organization_quotas SET quotas = $1, updated_at = $2 WHERE organization_id = $3",
|
350
|
+
json.dumps(current_quotas), datetime.utcnow(), organization_id
|
351
|
+
)
|
352
|
+
|
353
|
+
logger.info(f"Updated quotas for tenant {organization_id}")
|
354
|
+
return {
|
355
|
+
"organization_id": organization_id,
|
356
|
+
"quotas": current_quotas,
|
357
|
+
"message": "Quotas updated successfully"
|
358
|
+
}
|
359
|
+
|
360
|
+
except HTTPException:
|
361
|
+
raise
|
362
|
+
except Exception as e:
|
363
|
+
logger.error(f"Error updating quotas for {organization_id}: {e}")
|
364
|
+
raise HTTPException(status_code=500, detail="Failed to update quotas")
|
365
|
+
|
366
|
+
# ============= Tenant Listing =============
|
367
|
+
|
368
|
+
@router.get("/", response_model=List[TenantResponse])
|
369
|
+
async def list_tenants(
|
370
|
+
limit: int = 50,
|
371
|
+
offset: int = 0,
|
372
|
+
status: Optional[str] = None,
|
373
|
+
plan: Optional[str] = None,
|
374
|
+
current_user = Depends(require_admin)
|
375
|
+
):
|
376
|
+
"""List all tenants (admin only)"""
|
377
|
+
try:
|
378
|
+
async with get_database_connection() as conn:
|
379
|
+
# Build query with filters
|
380
|
+
where_conditions = []
|
381
|
+
params = []
|
382
|
+
param_count = 1
|
383
|
+
|
384
|
+
if status:
|
385
|
+
where_conditions.append(f"status = ${param_count}")
|
386
|
+
params.append(status)
|
387
|
+
param_count += 1
|
388
|
+
|
389
|
+
if plan:
|
390
|
+
where_conditions.append(f"plan = ${param_count}")
|
391
|
+
params.append(plan)
|
392
|
+
param_count += 1
|
393
|
+
|
394
|
+
where_clause = ""
|
395
|
+
if where_conditions:
|
396
|
+
where_clause = f"WHERE {' AND '.join(where_conditions)}"
|
397
|
+
|
398
|
+
# Add limit and offset
|
399
|
+
params.extend([limit, offset])
|
400
|
+
|
401
|
+
query = f"""
|
402
|
+
SELECT organization_id FROM organizations
|
403
|
+
{where_clause}
|
404
|
+
ORDER BY created_at DESC
|
405
|
+
LIMIT ${param_count} OFFSET ${param_count + 1}
|
406
|
+
"""
|
407
|
+
|
408
|
+
org_results = await conn.fetch(query, *params)
|
409
|
+
|
410
|
+
# Get full info for each tenant
|
411
|
+
tenants = []
|
412
|
+
for org in org_results:
|
413
|
+
tenant_info = await get_tenant_info(org['organization_id'], conn)
|
414
|
+
if tenant_info:
|
415
|
+
tenants.append(tenant_info)
|
416
|
+
|
417
|
+
return tenants
|
418
|
+
|
419
|
+
except Exception as e:
|
420
|
+
logger.error(f"Error listing tenants: {e}")
|
421
|
+
raise HTTPException(status_code=500, detail="Failed to list tenants")
|
422
|
+
|
423
|
+
# ============= Helper Functions =============
|
424
|
+
|
425
|
+
async def get_tenant_info(organization_id: str, conn) -> TenantResponse:
|
426
|
+
"""Get complete tenant information"""
|
427
|
+
try:
|
428
|
+
# Get organization data
|
429
|
+
org_result = await conn.fetchrow(
|
430
|
+
"SELECT * FROM organizations WHERE organization_id = $1",
|
431
|
+
organization_id
|
432
|
+
)
|
433
|
+
|
434
|
+
if not org_result:
|
435
|
+
return None
|
436
|
+
|
437
|
+
# Get member count
|
438
|
+
member_count = await conn.fetchval(
|
439
|
+
"SELECT COUNT(*) FROM users WHERE organization_id = $1",
|
440
|
+
organization_id
|
441
|
+
)
|
442
|
+
|
443
|
+
# Get quotas
|
444
|
+
quota_result = await conn.fetchrow(
|
445
|
+
"SELECT quotas FROM organization_quotas WHERE organization_id = $1",
|
446
|
+
organization_id
|
447
|
+
)
|
448
|
+
|
449
|
+
quotas = quota_result['quotas'] if quota_result else {}
|
450
|
+
|
451
|
+
# Get current usage
|
452
|
+
usage = await get_current_usage(organization_id, conn)
|
453
|
+
|
454
|
+
return TenantResponse(
|
455
|
+
organization_id=org_result['organization_id'],
|
456
|
+
name=org_result['name'],
|
457
|
+
domain=org_result['domain'],
|
458
|
+
plan=org_result['plan'],
|
459
|
+
status=org_result['status'],
|
460
|
+
billing_email=org_result['billing_email'],
|
461
|
+
created_at=org_result['created_at'],
|
462
|
+
updated_at=org_result['updated_at'],
|
463
|
+
member_count=member_count or 0,
|
464
|
+
current_usage=usage,
|
465
|
+
quotas=quotas,
|
466
|
+
settings=org_result['settings'] or {}
|
467
|
+
)
|
468
|
+
|
469
|
+
except Exception as e:
|
470
|
+
logger.error(f"Error getting tenant info for {organization_id}: {e}")
|
471
|
+
return None
|
472
|
+
|
473
|
+
async def get_current_usage(organization_id: str, conn) -> Dict[str, Any]:
|
474
|
+
"""Calculate current resource usage for tenant"""
|
475
|
+
try:
|
476
|
+
usage = {}
|
477
|
+
|
478
|
+
# Training jobs count
|
479
|
+
usage['training_jobs'] = await conn.fetchval(
|
480
|
+
"SELECT COUNT(*) FROM training_jobs WHERE organization_id = $1",
|
481
|
+
organization_id
|
482
|
+
) or 0
|
483
|
+
|
484
|
+
# Active deployments count
|
485
|
+
usage['deployments'] = 0 # TODO: Add deployment tracking
|
486
|
+
|
487
|
+
# Users count
|
488
|
+
usage['users'] = await conn.fetchval(
|
489
|
+
"SELECT COUNT(*) FROM users WHERE organization_id = $1",
|
490
|
+
organization_id
|
491
|
+
) or 0
|
492
|
+
|
493
|
+
# API calls this month (TODO: implement API call tracking)
|
494
|
+
usage['api_calls_this_month'] = 0
|
495
|
+
|
496
|
+
# Storage usage (TODO: implement storage tracking)
|
497
|
+
usage['storage_gb'] = 0.0
|
498
|
+
|
499
|
+
return usage
|
500
|
+
|
501
|
+
except Exception as e:
|
502
|
+
logger.error(f"Error calculating usage for {organization_id}: {e}")
|
503
|
+
return {}
|
504
|
+
|
505
|
+
def get_default_quotas(plan: str) -> Dict[str, Any]:
|
506
|
+
"""Get default quotas based on subscription plan"""
|
507
|
+
quotas = {
|
508
|
+
"starter": {
|
509
|
+
"api_calls_per_month": 10000,
|
510
|
+
"max_concurrent_requests": 5,
|
511
|
+
"max_storage_gb": 1.0,
|
512
|
+
"max_training_jobs": 2,
|
513
|
+
"max_deployments": 1,
|
514
|
+
"max_users": 3
|
515
|
+
},
|
516
|
+
"pro": {
|
517
|
+
"api_calls_per_month": 100000,
|
518
|
+
"max_concurrent_requests": 20,
|
519
|
+
"max_storage_gb": 10.0,
|
520
|
+
"max_training_jobs": 10,
|
521
|
+
"max_deployments": 5,
|
522
|
+
"max_users": 10
|
523
|
+
},
|
524
|
+
"enterprise": {
|
525
|
+
"api_calls_per_month": 1000000,
|
526
|
+
"max_concurrent_requests": 100,
|
527
|
+
"max_storage_gb": 100.0,
|
528
|
+
"max_training_jobs": 50,
|
529
|
+
"max_deployments": 25,
|
530
|
+
"max_users": 100
|
531
|
+
}
|
532
|
+
}
|
533
|
+
|
534
|
+
return quotas.get(plan, quotas["starter"])
|
535
|
+
|
536
|
+
def calculate_usage_percentage(quotas: Dict[str, Any], usage: Dict[str, Any]) -> Dict[str, float]:
|
537
|
+
"""Calculate usage percentages for each quota"""
|
538
|
+
percentages = {}
|
539
|
+
|
540
|
+
for quota_key, quota_value in quotas.items():
|
541
|
+
if quota_key in usage and quota_value > 0:
|
542
|
+
usage_value = usage[quota_key]
|
543
|
+
percentages[quota_key] = (usage_value / quota_value) * 100
|
544
|
+
else:
|
545
|
+
percentages[quota_key] = 0.0
|
546
|
+
|
547
|
+
return percentages
|
548
|
+
|
549
|
+
# ============= Health Check =============
|
550
|
+
|
551
|
+
@router.get("/health")
|
552
|
+
async def tenant_service_health():
|
553
|
+
"""Health check for tenant management service"""
|
554
|
+
try:
|
555
|
+
async with get_database_connection() as conn:
|
556
|
+
# Test database connectivity
|
557
|
+
await conn.fetchval("SELECT 1")
|
558
|
+
|
559
|
+
# Count total tenants
|
560
|
+
tenant_count = await conn.fetchval("SELECT COUNT(*) FROM organizations")
|
561
|
+
|
562
|
+
return {
|
563
|
+
"status": "healthy",
|
564
|
+
"service": "tenant-management",
|
565
|
+
"timestamp": datetime.utcnow().isoformat(),
|
566
|
+
"database": "connected",
|
567
|
+
"total_tenants": tenant_count
|
568
|
+
}
|
569
|
+
|
570
|
+
except Exception as e:
|
571
|
+
logger.error(f"Tenant service health check failed: {e}")
|
572
|
+
raise HTTPException(
|
573
|
+
status_code=503,
|
574
|
+
detail=f"Tenant service unhealthy: {str(e)}"
|
575
|
+
)
|