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,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
+ )