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.
Files changed (228) hide show
  1. isa_model/client.py +1166 -584
  2. isa_model/core/cache/redis_cache.py +410 -0
  3. isa_model/core/config/config_manager.py +282 -12
  4. isa_model/core/config.py +91 -1
  5. isa_model/core/database/__init__.py +1 -0
  6. isa_model/core/database/direct_db_client.py +114 -0
  7. isa_model/core/database/migration_manager.py +563 -0
  8. isa_model/core/database/migrations.py +297 -0
  9. isa_model/core/database/supabase_client.py +258 -0
  10. isa_model/core/dependencies.py +316 -0
  11. isa_model/core/discovery/__init__.py +19 -0
  12. isa_model/core/discovery/consul_discovery.py +190 -0
  13. isa_model/core/logging/__init__.py +54 -0
  14. isa_model/core/logging/influx_logger.py +523 -0
  15. isa_model/core/logging/loki_logger.py +160 -0
  16. isa_model/core/models/__init__.py +46 -0
  17. isa_model/core/models/config_models.py +625 -0
  18. isa_model/core/models/deployment_billing_tracker.py +430 -0
  19. isa_model/core/models/model_billing_tracker.py +60 -88
  20. isa_model/core/models/model_manager.py +66 -25
  21. isa_model/core/models/model_metadata.py +690 -0
  22. isa_model/core/models/model_repo.py +217 -55
  23. isa_model/core/models/model_statistics_tracker.py +234 -0
  24. isa_model/core/models/model_storage.py +0 -1
  25. isa_model/core/models/model_version_manager.py +959 -0
  26. isa_model/core/models/system_models.py +857 -0
  27. isa_model/core/pricing_manager.py +2 -249
  28. isa_model/core/repositories/__init__.py +9 -0
  29. isa_model/core/repositories/config_repository.py +912 -0
  30. isa_model/core/resilience/circuit_breaker.py +366 -0
  31. isa_model/core/security/secrets.py +358 -0
  32. isa_model/core/services/__init__.py +2 -4
  33. isa_model/core/services/intelligent_model_selector.py +479 -370
  34. isa_model/core/storage/hf_storage.py +2 -2
  35. isa_model/core/types.py +8 -0
  36. isa_model/deployment/__init__.py +5 -48
  37. isa_model/deployment/core/__init__.py +2 -31
  38. isa_model/deployment/core/deployment_manager.py +1278 -368
  39. isa_model/deployment/local/__init__.py +31 -0
  40. isa_model/deployment/local/config.py +248 -0
  41. isa_model/deployment/local/gpu_gateway.py +607 -0
  42. isa_model/deployment/local/health_checker.py +428 -0
  43. isa_model/deployment/local/provider.py +586 -0
  44. isa_model/deployment/local/tensorrt_service.py +621 -0
  45. isa_model/deployment/local/transformers_service.py +644 -0
  46. isa_model/deployment/local/vllm_service.py +527 -0
  47. isa_model/deployment/modal/__init__.py +8 -0
  48. isa_model/deployment/modal/config.py +136 -0
  49. isa_model/deployment/modal/deployer.py +894 -0
  50. isa_model/deployment/modal/services/__init__.py +3 -0
  51. isa_model/deployment/modal/services/audio/__init__.py +1 -0
  52. isa_model/deployment/modal/services/audio/isa_audio_chatTTS_service.py +520 -0
  53. isa_model/deployment/modal/services/audio/isa_audio_openvoice_service.py +758 -0
  54. isa_model/deployment/modal/services/audio/isa_audio_service_v2.py +1044 -0
  55. isa_model/deployment/modal/services/embedding/__init__.py +1 -0
  56. isa_model/deployment/modal/services/embedding/isa_embed_rerank_service.py +296 -0
  57. isa_model/deployment/modal/services/llm/__init__.py +1 -0
  58. isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
  59. isa_model/deployment/modal/services/video/__init__.py +1 -0
  60. isa_model/deployment/modal/services/video/isa_video_hunyuan_service.py +423 -0
  61. isa_model/deployment/modal/services/vision/__init__.py +1 -0
  62. isa_model/deployment/modal/services/vision/isa_vision_ocr_service.py +519 -0
  63. isa_model/deployment/modal/services/vision/isa_vision_qwen25_service.py +709 -0
  64. isa_model/deployment/modal/services/vision/isa_vision_table_service.py +676 -0
  65. isa_model/deployment/modal/services/vision/isa_vision_ui_service.py +833 -0
  66. isa_model/deployment/modal/services/vision/isa_vision_ui_service_optimized.py +660 -0
  67. isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
  68. isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
  69. isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
  70. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
  71. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
  72. isa_model/deployment/storage/__init__.py +5 -0
  73. isa_model/deployment/storage/deployment_repository.py +824 -0
  74. isa_model/deployment/triton/__init__.py +10 -0
  75. isa_model/deployment/triton/config.py +196 -0
  76. isa_model/deployment/triton/configs/__init__.py +1 -0
  77. isa_model/deployment/triton/provider.py +512 -0
  78. isa_model/deployment/triton/scripts/__init__.py +1 -0
  79. isa_model/deployment/triton/templates/__init__.py +1 -0
  80. isa_model/inference/__init__.py +47 -1
  81. isa_model/inference/ai_factory.py +179 -16
  82. isa_model/inference/legacy_services/__init__.py +21 -0
  83. isa_model/inference/legacy_services/model_evaluation.py +637 -0
  84. isa_model/inference/legacy_services/model_service.py +573 -0
  85. isa_model/inference/legacy_services/model_serving.py +717 -0
  86. isa_model/inference/legacy_services/model_training.py +561 -0
  87. isa_model/inference/models/__init__.py +21 -0
  88. isa_model/inference/models/inference_config.py +551 -0
  89. isa_model/inference/models/inference_record.py +675 -0
  90. isa_model/inference/models/performance_models.py +714 -0
  91. isa_model/inference/repositories/__init__.py +9 -0
  92. isa_model/inference/repositories/inference_repository.py +828 -0
  93. isa_model/inference/services/audio/__init__.py +21 -0
  94. isa_model/inference/services/audio/base_realtime_service.py +225 -0
  95. isa_model/inference/services/audio/base_stt_service.py +184 -11
  96. isa_model/inference/services/audio/isa_tts_service.py +0 -0
  97. isa_model/inference/services/audio/openai_realtime_service.py +320 -124
  98. isa_model/inference/services/audio/openai_stt_service.py +53 -11
  99. isa_model/inference/services/base_service.py +17 -1
  100. isa_model/inference/services/custom_model_manager.py +277 -0
  101. isa_model/inference/services/embedding/__init__.py +13 -0
  102. isa_model/inference/services/embedding/base_embed_service.py +111 -8
  103. isa_model/inference/services/embedding/isa_embed_service.py +305 -0
  104. isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
  105. isa_model/inference/services/embedding/openai_embed_service.py +2 -4
  106. isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
  107. isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
  108. isa_model/inference/services/img/__init__.py +2 -2
  109. isa_model/inference/services/img/base_image_gen_service.py +24 -7
  110. isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
  111. isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
  112. isa_model/inference/services/img/services/replicate_flux.py +226 -0
  113. isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
  114. isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
  115. isa_model/inference/services/img/tests/test_img_client.py +297 -0
  116. isa_model/inference/services/llm/__init__.py +10 -2
  117. isa_model/inference/services/llm/base_llm_service.py +361 -26
  118. isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
  119. isa_model/inference/services/llm/helpers/llm_adapter.py +71 -12
  120. isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
  121. isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
  122. isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
  123. isa_model/inference/services/llm/local_llm_service.py +747 -0
  124. isa_model/inference/services/llm/ollama_llm_service.py +11 -3
  125. isa_model/inference/services/llm/openai_llm_service.py +670 -56
  126. isa_model/inference/services/llm/yyds_llm_service.py +10 -3
  127. isa_model/inference/services/vision/__init__.py +27 -6
  128. isa_model/inference/services/vision/base_vision_service.py +118 -185
  129. isa_model/inference/services/vision/blip_vision_service.py +359 -0
  130. isa_model/inference/services/vision/helpers/image_utils.py +19 -10
  131. isa_model/inference/services/vision/isa_vision_service.py +634 -0
  132. isa_model/inference/services/vision/openai_vision_service.py +19 -10
  133. isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
  134. isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
  135. isa_model/serving/api/cache_manager.py +245 -0
  136. isa_model/serving/api/dependencies/__init__.py +1 -0
  137. isa_model/serving/api/dependencies/auth.py +194 -0
  138. isa_model/serving/api/dependencies/database.py +139 -0
  139. isa_model/serving/api/error_handlers.py +284 -0
  140. isa_model/serving/api/fastapi_server.py +240 -18
  141. isa_model/serving/api/middleware/auth.py +317 -0
  142. isa_model/serving/api/middleware/security.py +268 -0
  143. isa_model/serving/api/middleware/tenant_context.py +414 -0
  144. isa_model/serving/api/routes/analytics.py +489 -0
  145. isa_model/serving/api/routes/config.py +645 -0
  146. isa_model/serving/api/routes/deployment_billing.py +315 -0
  147. isa_model/serving/api/routes/deployments.py +475 -0
  148. isa_model/serving/api/routes/gpu_gateway.py +440 -0
  149. isa_model/serving/api/routes/health.py +32 -12
  150. isa_model/serving/api/routes/inference_monitoring.py +486 -0
  151. isa_model/serving/api/routes/local_deployments.py +448 -0
  152. isa_model/serving/api/routes/logs.py +430 -0
  153. isa_model/serving/api/routes/settings.py +582 -0
  154. isa_model/serving/api/routes/tenants.py +575 -0
  155. isa_model/serving/api/routes/unified.py +992 -171
  156. isa_model/serving/api/routes/webhooks.py +479 -0
  157. isa_model/serving/api/startup.py +318 -0
  158. isa_model/serving/modal_proxy_server.py +249 -0
  159. isa_model/utils/gpu_utils.py +311 -0
  160. {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/METADATA +76 -22
  161. isa_model-0.4.3.dist-info/RECORD +193 -0
  162. isa_model/deployment/cloud/__init__.py +0 -9
  163. isa_model/deployment/cloud/modal/__init__.py +0 -10
  164. isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
  165. isa_model/deployment/cloud/modal/isa_vision_table_service.py +0 -532
  166. isa_model/deployment/cloud/modal/isa_vision_ui_service.py +0 -406
  167. isa_model/deployment/cloud/modal/register_models.py +0 -321
  168. isa_model/deployment/core/deployment_config.py +0 -356
  169. isa_model/deployment/core/isa_deployment_service.py +0 -401
  170. isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
  171. isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
  172. isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
  173. isa_model/deployment/runtime/deployed_service.py +0 -338
  174. isa_model/deployment/services/__init__.py +0 -9
  175. isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
  176. isa_model/deployment/services/model_service.py +0 -332
  177. isa_model/deployment/services/service_monitor.py +0 -356
  178. isa_model/deployment/services/service_registry.py +0 -527
  179. isa_model/eval/__init__.py +0 -92
  180. isa_model/eval/benchmarks.py +0 -469
  181. isa_model/eval/config/__init__.py +0 -10
  182. isa_model/eval/config/evaluation_config.py +0 -108
  183. isa_model/eval/evaluators/__init__.py +0 -18
  184. isa_model/eval/evaluators/base_evaluator.py +0 -503
  185. isa_model/eval/evaluators/llm_evaluator.py +0 -472
  186. isa_model/eval/factory.py +0 -531
  187. isa_model/eval/infrastructure/__init__.py +0 -24
  188. isa_model/eval/infrastructure/experiment_tracker.py +0 -466
  189. isa_model/eval/metrics.py +0 -798
  190. isa_model/inference/adapter/unified_api.py +0 -248
  191. isa_model/inference/services/helpers/stacked_config.py +0 -148
  192. isa_model/inference/services/img/flux_professional_service.py +0 -603
  193. isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
  194. isa_model/inference/services/others/table_transformer_service.py +0 -61
  195. isa_model/inference/services/vision/doc_analysis_service.py +0 -640
  196. isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
  197. isa_model/inference/services/vision/ui_analysis_service.py +0 -823
  198. isa_model/scripts/inference_tracker.py +0 -283
  199. isa_model/scripts/mlflow_manager.py +0 -379
  200. isa_model/scripts/model_registry.py +0 -465
  201. isa_model/scripts/register_models.py +0 -370
  202. isa_model/scripts/register_models_with_embeddings.py +0 -510
  203. isa_model/scripts/start_mlflow.py +0 -95
  204. isa_model/scripts/training_tracker.py +0 -257
  205. isa_model/training/__init__.py +0 -74
  206. isa_model/training/annotation/annotation_schema.py +0 -47
  207. isa_model/training/annotation/processors/annotation_processor.py +0 -126
  208. isa_model/training/annotation/storage/dataset_manager.py +0 -131
  209. isa_model/training/annotation/storage/dataset_schema.py +0 -44
  210. isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
  211. isa_model/training/annotation/tests/test_minio copy.py +0 -113
  212. isa_model/training/annotation/tests/test_minio_upload.py +0 -43
  213. isa_model/training/annotation/views/annotation_controller.py +0 -158
  214. isa_model/training/cloud/__init__.py +0 -22
  215. isa_model/training/cloud/job_orchestrator.py +0 -402
  216. isa_model/training/cloud/runpod_trainer.py +0 -454
  217. isa_model/training/cloud/storage_manager.py +0 -482
  218. isa_model/training/core/__init__.py +0 -23
  219. isa_model/training/core/config.py +0 -181
  220. isa_model/training/core/dataset.py +0 -222
  221. isa_model/training/core/trainer.py +0 -720
  222. isa_model/training/core/utils.py +0 -213
  223. isa_model/training/factory.py +0 -424
  224. isa_model-0.3.91.dist-info/RECORD +0 -138
  225. /isa_model/{core/storage/minio_storage.py → deployment/modal/services/audio/isa_audio_fish_service.py} +0 -0
  226. /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
  227. {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/WHEEL +0 -0
  228. {isa_model-0.3.91.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
+ )