mcli-framework 7.0.0__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.

Potentially problematic release.


This version of mcli-framework might be problematic. Click here for more details.

Files changed (186) hide show
  1. mcli/app/chat_cmd.py +42 -0
  2. mcli/app/commands_cmd.py +226 -0
  3. mcli/app/completion_cmd.py +216 -0
  4. mcli/app/completion_helpers.py +288 -0
  5. mcli/app/cron_test_cmd.py +697 -0
  6. mcli/app/logs_cmd.py +419 -0
  7. mcli/app/main.py +492 -0
  8. mcli/app/model/model.py +1060 -0
  9. mcli/app/model_cmd.py +227 -0
  10. mcli/app/redis_cmd.py +269 -0
  11. mcli/app/video/video.py +1114 -0
  12. mcli/app/visual_cmd.py +303 -0
  13. mcli/chat/chat.py +2409 -0
  14. mcli/chat/command_rag.py +514 -0
  15. mcli/chat/enhanced_chat.py +652 -0
  16. mcli/chat/system_controller.py +1010 -0
  17. mcli/chat/system_integration.py +1016 -0
  18. mcli/cli.py +25 -0
  19. mcli/config.toml +20 -0
  20. mcli/lib/api/api.py +586 -0
  21. mcli/lib/api/daemon_client.py +203 -0
  22. mcli/lib/api/daemon_client_local.py +44 -0
  23. mcli/lib/api/daemon_decorator.py +217 -0
  24. mcli/lib/api/mcli_decorators.py +1032 -0
  25. mcli/lib/auth/auth.py +85 -0
  26. mcli/lib/auth/aws_manager.py +85 -0
  27. mcli/lib/auth/azure_manager.py +91 -0
  28. mcli/lib/auth/credential_manager.py +192 -0
  29. mcli/lib/auth/gcp_manager.py +93 -0
  30. mcli/lib/auth/key_manager.py +117 -0
  31. mcli/lib/auth/mcli_manager.py +93 -0
  32. mcli/lib/auth/token_manager.py +75 -0
  33. mcli/lib/auth/token_util.py +1011 -0
  34. mcli/lib/config/config.py +47 -0
  35. mcli/lib/discovery/__init__.py +1 -0
  36. mcli/lib/discovery/command_discovery.py +274 -0
  37. mcli/lib/erd/erd.py +1345 -0
  38. mcli/lib/erd/generate_graph.py +453 -0
  39. mcli/lib/files/files.py +76 -0
  40. mcli/lib/fs/fs.py +109 -0
  41. mcli/lib/lib.py +29 -0
  42. mcli/lib/logger/logger.py +611 -0
  43. mcli/lib/performance/optimizer.py +409 -0
  44. mcli/lib/performance/rust_bridge.py +502 -0
  45. mcli/lib/performance/uvloop_config.py +154 -0
  46. mcli/lib/pickles/pickles.py +50 -0
  47. mcli/lib/search/cached_vectorizer.py +479 -0
  48. mcli/lib/services/data_pipeline.py +460 -0
  49. mcli/lib/services/lsh_client.py +441 -0
  50. mcli/lib/services/redis_service.py +387 -0
  51. mcli/lib/shell/shell.py +137 -0
  52. mcli/lib/toml/toml.py +33 -0
  53. mcli/lib/ui/styling.py +47 -0
  54. mcli/lib/ui/visual_effects.py +634 -0
  55. mcli/lib/watcher/watcher.py +185 -0
  56. mcli/ml/api/app.py +215 -0
  57. mcli/ml/api/middleware.py +224 -0
  58. mcli/ml/api/routers/admin_router.py +12 -0
  59. mcli/ml/api/routers/auth_router.py +244 -0
  60. mcli/ml/api/routers/backtest_router.py +12 -0
  61. mcli/ml/api/routers/data_router.py +12 -0
  62. mcli/ml/api/routers/model_router.py +302 -0
  63. mcli/ml/api/routers/monitoring_router.py +12 -0
  64. mcli/ml/api/routers/portfolio_router.py +12 -0
  65. mcli/ml/api/routers/prediction_router.py +267 -0
  66. mcli/ml/api/routers/trade_router.py +12 -0
  67. mcli/ml/api/routers/websocket_router.py +76 -0
  68. mcli/ml/api/schemas.py +64 -0
  69. mcli/ml/auth/auth_manager.py +425 -0
  70. mcli/ml/auth/models.py +154 -0
  71. mcli/ml/auth/permissions.py +302 -0
  72. mcli/ml/backtesting/backtest_engine.py +502 -0
  73. mcli/ml/backtesting/performance_metrics.py +393 -0
  74. mcli/ml/cache.py +400 -0
  75. mcli/ml/cli/main.py +398 -0
  76. mcli/ml/config/settings.py +394 -0
  77. mcli/ml/configs/dvc_config.py +230 -0
  78. mcli/ml/configs/mlflow_config.py +131 -0
  79. mcli/ml/configs/mlops_manager.py +293 -0
  80. mcli/ml/dashboard/app.py +532 -0
  81. mcli/ml/dashboard/app_integrated.py +738 -0
  82. mcli/ml/dashboard/app_supabase.py +560 -0
  83. mcli/ml/dashboard/app_training.py +615 -0
  84. mcli/ml/dashboard/cli.py +51 -0
  85. mcli/ml/data_ingestion/api_connectors.py +501 -0
  86. mcli/ml/data_ingestion/data_pipeline.py +567 -0
  87. mcli/ml/data_ingestion/stream_processor.py +512 -0
  88. mcli/ml/database/migrations/env.py +94 -0
  89. mcli/ml/database/models.py +667 -0
  90. mcli/ml/database/session.py +200 -0
  91. mcli/ml/experimentation/ab_testing.py +845 -0
  92. mcli/ml/features/ensemble_features.py +607 -0
  93. mcli/ml/features/political_features.py +676 -0
  94. mcli/ml/features/recommendation_engine.py +809 -0
  95. mcli/ml/features/stock_features.py +573 -0
  96. mcli/ml/features/test_feature_engineering.py +346 -0
  97. mcli/ml/logging.py +85 -0
  98. mcli/ml/mlops/data_versioning.py +518 -0
  99. mcli/ml/mlops/experiment_tracker.py +377 -0
  100. mcli/ml/mlops/model_serving.py +481 -0
  101. mcli/ml/mlops/pipeline_orchestrator.py +614 -0
  102. mcli/ml/models/base_models.py +324 -0
  103. mcli/ml/models/ensemble_models.py +675 -0
  104. mcli/ml/models/recommendation_models.py +474 -0
  105. mcli/ml/models/test_models.py +487 -0
  106. mcli/ml/monitoring/drift_detection.py +676 -0
  107. mcli/ml/monitoring/metrics.py +45 -0
  108. mcli/ml/optimization/portfolio_optimizer.py +834 -0
  109. mcli/ml/preprocessing/data_cleaners.py +451 -0
  110. mcli/ml/preprocessing/feature_extractors.py +491 -0
  111. mcli/ml/preprocessing/ml_pipeline.py +382 -0
  112. mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
  113. mcli/ml/preprocessing/test_preprocessing.py +294 -0
  114. mcli/ml/scripts/populate_sample_data.py +200 -0
  115. mcli/ml/tasks.py +400 -0
  116. mcli/ml/tests/test_integration.py +429 -0
  117. mcli/ml/tests/test_training_dashboard.py +387 -0
  118. mcli/public/oi/oi.py +15 -0
  119. mcli/public/public.py +4 -0
  120. mcli/self/self_cmd.py +1246 -0
  121. mcli/workflow/daemon/api_daemon.py +800 -0
  122. mcli/workflow/daemon/async_command_database.py +681 -0
  123. mcli/workflow/daemon/async_process_manager.py +591 -0
  124. mcli/workflow/daemon/client.py +530 -0
  125. mcli/workflow/daemon/commands.py +1196 -0
  126. mcli/workflow/daemon/daemon.py +905 -0
  127. mcli/workflow/daemon/daemon_api.py +59 -0
  128. mcli/workflow/daemon/enhanced_daemon.py +571 -0
  129. mcli/workflow/daemon/process_cli.py +244 -0
  130. mcli/workflow/daemon/process_manager.py +439 -0
  131. mcli/workflow/daemon/test_daemon.py +275 -0
  132. mcli/workflow/dashboard/dashboard_cmd.py +113 -0
  133. mcli/workflow/docker/docker.py +0 -0
  134. mcli/workflow/file/file.py +100 -0
  135. mcli/workflow/gcloud/config.toml +21 -0
  136. mcli/workflow/gcloud/gcloud.py +58 -0
  137. mcli/workflow/git_commit/ai_service.py +328 -0
  138. mcli/workflow/git_commit/commands.py +430 -0
  139. mcli/workflow/lsh_integration.py +355 -0
  140. mcli/workflow/model_service/client.py +594 -0
  141. mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
  142. mcli/workflow/model_service/lightweight_embedder.py +397 -0
  143. mcli/workflow/model_service/lightweight_model_server.py +714 -0
  144. mcli/workflow/model_service/lightweight_test.py +241 -0
  145. mcli/workflow/model_service/model_service.py +1955 -0
  146. mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
  147. mcli/workflow/model_service/pdf_processor.py +386 -0
  148. mcli/workflow/model_service/test_efficient_runner.py +234 -0
  149. mcli/workflow/model_service/test_example.py +315 -0
  150. mcli/workflow/model_service/test_integration.py +131 -0
  151. mcli/workflow/model_service/test_new_features.py +149 -0
  152. mcli/workflow/openai/openai.py +99 -0
  153. mcli/workflow/politician_trading/commands.py +1790 -0
  154. mcli/workflow/politician_trading/config.py +134 -0
  155. mcli/workflow/politician_trading/connectivity.py +490 -0
  156. mcli/workflow/politician_trading/data_sources.py +395 -0
  157. mcli/workflow/politician_trading/database.py +410 -0
  158. mcli/workflow/politician_trading/demo.py +248 -0
  159. mcli/workflow/politician_trading/models.py +165 -0
  160. mcli/workflow/politician_trading/monitoring.py +413 -0
  161. mcli/workflow/politician_trading/scrapers.py +966 -0
  162. mcli/workflow/politician_trading/scrapers_california.py +412 -0
  163. mcli/workflow/politician_trading/scrapers_eu.py +377 -0
  164. mcli/workflow/politician_trading/scrapers_uk.py +350 -0
  165. mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
  166. mcli/workflow/politician_trading/supabase_functions.py +354 -0
  167. mcli/workflow/politician_trading/workflow.py +852 -0
  168. mcli/workflow/registry/registry.py +180 -0
  169. mcli/workflow/repo/repo.py +223 -0
  170. mcli/workflow/scheduler/commands.py +493 -0
  171. mcli/workflow/scheduler/cron_parser.py +238 -0
  172. mcli/workflow/scheduler/job.py +182 -0
  173. mcli/workflow/scheduler/monitor.py +139 -0
  174. mcli/workflow/scheduler/persistence.py +324 -0
  175. mcli/workflow/scheduler/scheduler.py +679 -0
  176. mcli/workflow/sync/sync_cmd.py +437 -0
  177. mcli/workflow/sync/test_cmd.py +314 -0
  178. mcli/workflow/videos/videos.py +242 -0
  179. mcli/workflow/wakatime/wakatime.py +11 -0
  180. mcli/workflow/workflow.py +37 -0
  181. mcli_framework-7.0.0.dist-info/METADATA +479 -0
  182. mcli_framework-7.0.0.dist-info/RECORD +186 -0
  183. mcli_framework-7.0.0.dist-info/WHEEL +5 -0
  184. mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
  185. mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
  186. mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,481 @@
1
+ """REST API for model serving"""
2
+
3
+ from fastapi import FastAPI, HTTPException, BackgroundTasks, File, UploadFile
4
+ from fastapi.responses import JSONResponse
5
+ from pydantic import BaseModel, Field
6
+ from typing import Dict, Any, Optional, List, Union
7
+ import torch
8
+ import numpy as np
9
+ import pandas as pd
10
+ from datetime import datetime
11
+ import logging
12
+ import asyncio
13
+ from pathlib import Path
14
+ import json
15
+ import uvicorn
16
+ from contextlib import asynccontextmanager
17
+
18
+ import sys
19
+ import os
20
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))
21
+
22
+ from ml.models.recommendation_models import StockRecommendationModel, PortfolioRecommendation
23
+ from ml.features.stock_features import StockRecommendationFeatures
24
+ from ml.features.political_features import PoliticalInfluenceFeatures
25
+ from ml.features.ensemble_features import EnsembleFeatureBuilder
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ # Pydantic models for API
31
+ class PredictionRequest(BaseModel):
32
+ """Request model for predictions"""
33
+ trading_data: Dict[str, Any] = Field(..., description="Politician trading data")
34
+ stock_data: Optional[Dict[str, Any]] = Field(None, description="Stock price data")
35
+ tickers: List[str] = Field(..., description="Stock tickers to analyze")
36
+ features: Optional[Dict[str, float]] = Field(None, description="Pre-computed features")
37
+
38
+
39
+ class PredictionResponse(BaseModel):
40
+ """Response model for predictions"""
41
+ recommendations: List[Dict[str, Any]]
42
+ timestamp: str
43
+ model_version: str
44
+ processing_time_ms: float
45
+
46
+
47
+ class HealthResponse(BaseModel):
48
+ """Health check response"""
49
+ status: str
50
+ model_loaded: bool
51
+ model_version: Optional[str]
52
+ uptime_seconds: float
53
+
54
+
55
+ class ModelMetricsResponse(BaseModel):
56
+ """Model metrics response"""
57
+ accuracy: float
58
+ precision: float
59
+ recall: float
60
+ f1_score: float
61
+ last_updated: str
62
+
63
+
64
+ class BatchPredictionRequest(BaseModel):
65
+ """Batch prediction request"""
66
+ batch_id: str
67
+ data: List[PredictionRequest]
68
+
69
+
70
+ class BatchPredictionResponse(BaseModel):
71
+ """Batch prediction response"""
72
+ batch_id: str
73
+ status: str
74
+ progress: float
75
+ results: Optional[List[PredictionResponse]]
76
+
77
+
78
+ class ModelEndpoint:
79
+ """Model endpoint manager"""
80
+
81
+ def __init__(self, model_path: Optional[str] = None):
82
+ self.model = None
83
+ self.model_path = model_path
84
+ self.model_version = "1.0.0"
85
+ self.feature_extractors = {}
86
+ self.start_time = datetime.now()
87
+ self.prediction_cache = {}
88
+ self.metrics = {
89
+ "total_predictions": 0,
90
+ "avg_latency_ms": 0,
91
+ "cache_hits": 0,
92
+ "errors": 0
93
+ }
94
+
95
+ if model_path:
96
+ self.load_model(model_path)
97
+
98
+ self._setup_feature_extractors()
99
+
100
+ def _setup_feature_extractors(self):
101
+ """Initialize feature extractors"""
102
+ self.feature_extractors = {
103
+ "stock": StockRecommendationFeatures(),
104
+ "political": PoliticalInfluenceFeatures(),
105
+ "ensemble": EnsembleFeatureBuilder()
106
+ }
107
+ logger.info("Feature extractors initialized")
108
+
109
+ def load_model(self, model_path: str):
110
+ """Load model from file"""
111
+ try:
112
+ checkpoint = torch.load(model_path, map_location='cpu')
113
+
114
+ # Reconstruct model (simplified - would need proper config)
115
+ from ml.models.ensemble_models import EnsembleConfig, ModelConfig
116
+ from ml.models.recommendation_models import RecommendationConfig
117
+
118
+ # Create dummy config for loading
119
+ model_configs = [
120
+ ModelConfig(
121
+ model_type="mlp",
122
+ hidden_dims=[256, 128],
123
+ dropout_rate=0.3,
124
+ learning_rate=0.001,
125
+ weight_decay=1e-4,
126
+ batch_size=32,
127
+ epochs=10
128
+ )
129
+ ]
130
+
131
+ ensemble_config = EnsembleConfig(base_models=model_configs)
132
+ recommendation_config = RecommendationConfig(ensemble_config=ensemble_config)
133
+
134
+ # Initialize model (need to know input dimension)
135
+ input_dim = 100 # Default, would be stored in checkpoint
136
+ self.model = StockRecommendationModel(input_dim, recommendation_config)
137
+
138
+ # Load state
139
+ self.model.load_state_dict(checkpoint['model_state_dict'])
140
+ self.model.eval()
141
+
142
+ self.model_version = checkpoint.get('version', '1.0.0')
143
+ logger.info(f"Model loaded from {model_path} (version: {self.model_version})")
144
+
145
+ except Exception as e:
146
+ logger.error(f"Failed to load model: {e}")
147
+ raise
148
+
149
+ def extract_features(self, trading_data: pd.DataFrame,
150
+ stock_data: Optional[pd.DataFrame] = None) -> np.ndarray:
151
+ """Extract features from raw data"""
152
+ features = pd.DataFrame()
153
+
154
+ # Extract political features
155
+ if not trading_data.empty:
156
+ political_features = self.feature_extractors["political"].extract_influence_features(trading_data)
157
+ features = pd.concat([features, political_features], axis=1)
158
+
159
+ # Extract stock features if available
160
+ if stock_data is not None and not stock_data.empty:
161
+ try:
162
+ stock_features = self.feature_extractors["stock"].extract_features(stock_data)
163
+ features = pd.concat([features, stock_features], axis=1)
164
+ except Exception as e:
165
+ logger.warning(f"Could not extract stock features: {e}")
166
+
167
+ # Build ensemble features
168
+ if not features.empty:
169
+ features = self.feature_extractors["ensemble"].build_ensemble_features(features)
170
+
171
+ return features.values if not features.empty else np.array([[]])
172
+
173
+ async def predict(self, request: PredictionRequest) -> PredictionResponse:
174
+ """Generate predictions"""
175
+ start_time = datetime.now()
176
+
177
+ try:
178
+ # Check cache
179
+ cache_key = json.dumps(request.dict(), sort_keys=True)
180
+ if cache_key in self.prediction_cache:
181
+ self.metrics["cache_hits"] += 1
182
+ cached_response = self.prediction_cache[cache_key]
183
+ cached_response.processing_time_ms = 0.1 # Cache hit is fast
184
+ return cached_response
185
+
186
+ # Convert request data to DataFrames
187
+ trading_df = pd.DataFrame([request.trading_data])
188
+ stock_df = pd.DataFrame([request.stock_data]) if request.stock_data else None
189
+
190
+ # Extract features or use provided features
191
+ if request.features:
192
+ features = np.array([list(request.features.values())])
193
+ else:
194
+ features = self.extract_features(trading_df, stock_df)
195
+
196
+ # Generate recommendations
197
+ if self.model:
198
+ recommendations = self.model.generate_recommendations(
199
+ features,
200
+ request.tickers,
201
+ market_data=stock_df
202
+ )
203
+ else:
204
+ # Mock recommendations if no model loaded
205
+ recommendations = self._generate_mock_recommendations(request.tickers)
206
+
207
+ # Convert recommendations to dict
208
+ recommendations_dict = [
209
+ {
210
+ "ticker": rec.ticker,
211
+ "score": rec.recommendation_score,
212
+ "confidence": rec.confidence,
213
+ "risk_level": rec.risk_level,
214
+ "expected_return": rec.expected_return,
215
+ "position_size": rec.position_size,
216
+ "entry_price": rec.entry_price,
217
+ "target_price": rec.target_price,
218
+ "stop_loss": rec.stop_loss,
219
+ "reason": rec.recommendation_reason
220
+ }
221
+ for rec in recommendations
222
+ ]
223
+
224
+ # Calculate processing time
225
+ processing_time = (datetime.now() - start_time).total_seconds() * 1000
226
+
227
+ # Update metrics
228
+ self.metrics["total_predictions"] += 1
229
+ self.metrics["avg_latency_ms"] = (
230
+ (self.metrics["avg_latency_ms"] * (self.metrics["total_predictions"] - 1) + processing_time)
231
+ / self.metrics["total_predictions"]
232
+ )
233
+
234
+ response = PredictionResponse(
235
+ recommendations=recommendations_dict,
236
+ timestamp=datetime.now().isoformat(),
237
+ model_version=self.model_version,
238
+ processing_time_ms=processing_time
239
+ )
240
+
241
+ # Cache response
242
+ self.prediction_cache[cache_key] = response
243
+
244
+ # Limit cache size
245
+ if len(self.prediction_cache) > 1000:
246
+ # Remove oldest entries
247
+ keys_to_remove = list(self.prediction_cache.keys())[:100]
248
+ for key in keys_to_remove:
249
+ del self.prediction_cache[key]
250
+
251
+ return response
252
+
253
+ except Exception as e:
254
+ self.metrics["errors"] += 1
255
+ logger.error(f"Prediction error: {e}")
256
+ raise HTTPException(status_code=500, detail=str(e))
257
+
258
+ def _generate_mock_recommendations(self, tickers: List[str]) -> List[PortfolioRecommendation]:
259
+ """Generate mock recommendations for testing"""
260
+ recommendations = []
261
+ for ticker in tickers:
262
+ rec = PortfolioRecommendation(
263
+ ticker=ticker,
264
+ recommendation_score=np.random.random(),
265
+ confidence=np.random.random(),
266
+ risk_level=np.random.choice(["low", "medium", "high"]),
267
+ expected_return=np.random.normal(0.05, 0.15),
268
+ risk_adjusted_score=np.random.random(),
269
+ position_size=np.random.uniform(0.01, 0.1),
270
+ recommendation_reason="Mock recommendation for testing"
271
+ )
272
+ recommendations.append(rec)
273
+ return recommendations
274
+
275
+ def get_health(self) -> HealthResponse:
276
+ """Get endpoint health status"""
277
+ uptime = (datetime.now() - self.start_time).total_seconds()
278
+
279
+ return HealthResponse(
280
+ status="healthy",
281
+ model_loaded=self.model is not None,
282
+ model_version=self.model_version,
283
+ uptime_seconds=uptime
284
+ )
285
+
286
+ def get_metrics(self) -> Dict[str, Any]:
287
+ """Get endpoint metrics"""
288
+ return {
289
+ **self.metrics,
290
+ "model_version": self.model_version,
291
+ "uptime_seconds": (datetime.now() - self.start_time).total_seconds(),
292
+ "cache_size": len(self.prediction_cache)
293
+ }
294
+
295
+
296
+ class PredictionService:
297
+ """Async prediction service for batch processing"""
298
+
299
+ def __init__(self, model_endpoint: ModelEndpoint):
300
+ self.model_endpoint = model_endpoint
301
+ self.batch_jobs = {}
302
+ self.executor = None
303
+
304
+ async def process_batch(self, batch_request: BatchPredictionRequest) -> BatchPredictionResponse:
305
+ """Process batch predictions asynchronously"""
306
+ batch_id = batch_request.batch_id
307
+
308
+ # Initialize batch job
309
+ self.batch_jobs[batch_id] = {
310
+ "status": "processing",
311
+ "progress": 0.0,
312
+ "results": [],
313
+ "start_time": datetime.now()
314
+ }
315
+
316
+ # Process each request
317
+ results = []
318
+ total = len(batch_request.data)
319
+
320
+ for i, request in enumerate(batch_request.data):
321
+ try:
322
+ result = await self.model_endpoint.predict(request)
323
+ results.append(result)
324
+ except Exception as e:
325
+ logger.error(f"Batch prediction error for item {i}: {e}")
326
+ results.append(None)
327
+
328
+ # Update progress
329
+ self.batch_jobs[batch_id]["progress"] = (i + 1) / total
330
+
331
+ # Update job status
332
+ self.batch_jobs[batch_id]["status"] = "completed"
333
+ self.batch_jobs[batch_id]["results"] = results
334
+ self.batch_jobs[batch_id]["end_time"] = datetime.now()
335
+
336
+ return BatchPredictionResponse(
337
+ batch_id=batch_id,
338
+ status="completed",
339
+ progress=1.0,
340
+ results=results
341
+ )
342
+
343
+ def get_batch_status(self, batch_id: str) -> BatchPredictionResponse:
344
+ """Get batch job status"""
345
+ if batch_id not in self.batch_jobs:
346
+ raise HTTPException(status_code=404, detail="Batch job not found")
347
+
348
+ job = self.batch_jobs[batch_id]
349
+
350
+ return BatchPredictionResponse(
351
+ batch_id=batch_id,
352
+ status=job["status"],
353
+ progress=job["progress"],
354
+ results=job.get("results")
355
+ )
356
+
357
+
358
+ class ModelServer:
359
+ """FastAPI model server"""
360
+
361
+ def __init__(self, model_path: Optional[str] = None):
362
+ self.model_endpoint = ModelEndpoint(model_path)
363
+ self.prediction_service = PredictionService(self.model_endpoint)
364
+ self.app = self._create_app()
365
+
366
+ @asynccontextmanager
367
+ async def lifespan(self, app: FastAPI):
368
+ """Manage application lifecycle"""
369
+ # Startup
370
+ logger.info("Starting model server...")
371
+ yield
372
+ # Shutdown
373
+ logger.info("Shutting down model server...")
374
+
375
+ def _create_app(self) -> FastAPI:
376
+ """Create FastAPI application"""
377
+ app = FastAPI(
378
+ title="Stock Recommendation API",
379
+ description="ML-powered stock recommendation system based on politician trading data",
380
+ version="1.0.0",
381
+ lifespan=self.lifespan
382
+ )
383
+
384
+ # Health check
385
+ @app.get("/health", response_model=HealthResponse)
386
+ async def health():
387
+ """Health check endpoint"""
388
+ return self.model_endpoint.get_health()
389
+
390
+ # Metrics
391
+ @app.get("/metrics")
392
+ async def metrics():
393
+ """Get service metrics"""
394
+ return self.model_endpoint.get_metrics()
395
+
396
+ # Single prediction
397
+ @app.post("/predict", response_model=PredictionResponse)
398
+ async def predict(request: PredictionRequest):
399
+ """Generate stock recommendations"""
400
+ return await self.model_endpoint.predict(request)
401
+
402
+ # Batch prediction
403
+ @app.post("/batch/predict", response_model=BatchPredictionResponse)
404
+ async def batch_predict(batch_request: BatchPredictionRequest,
405
+ background_tasks: BackgroundTasks):
406
+ """Submit batch prediction job"""
407
+ background_tasks.add_task(
408
+ self.prediction_service.process_batch,
409
+ batch_request
410
+ )
411
+
412
+ return BatchPredictionResponse(
413
+ batch_id=batch_request.batch_id,
414
+ status="submitted",
415
+ progress=0.0,
416
+ results=None
417
+ )
418
+
419
+ # Batch status
420
+ @app.get("/batch/{batch_id}", response_model=BatchPredictionResponse)
421
+ async def batch_status(batch_id: str):
422
+ """Get batch job status"""
423
+ return self.prediction_service.get_batch_status(batch_id)
424
+
425
+ # Model reload
426
+ @app.post("/model/reload")
427
+ async def reload_model(model_path: str):
428
+ """Reload model from new path"""
429
+ try:
430
+ self.model_endpoint.load_model(model_path)
431
+ return {"status": "success", "model_version": self.model_endpoint.model_version}
432
+ except Exception as e:
433
+ raise HTTPException(status_code=500, detail=str(e))
434
+
435
+ # Upload and predict
436
+ @app.post("/upload/predict")
437
+ async def upload_predict(file: UploadFile = File(...)):
438
+ """Upload CSV and get predictions"""
439
+ try:
440
+ # Read uploaded file
441
+ content = await file.read()
442
+ df = pd.read_csv(pd.io.common.BytesIO(content))
443
+
444
+ # Extract tickers
445
+ tickers = df['ticker_cleaned'].unique().tolist() if 'ticker_cleaned' in df.columns else []
446
+
447
+ # Create request
448
+ request = PredictionRequest(
449
+ trading_data=df.to_dict(orient='records')[0] if len(df) > 0 else {},
450
+ tickers=tickers[:5] # Limit to 5 tickers
451
+ )
452
+
453
+ return await self.model_endpoint.predict(request)
454
+
455
+ except Exception as e:
456
+ raise HTTPException(status_code=400, detail=str(e))
457
+
458
+ return app
459
+
460
+ def run(self, host: str = "0.0.0.0", port: int = 8000):
461
+ """Run the server"""
462
+ uvicorn.run(self.app, host=host, port=port)
463
+
464
+
465
+ def main():
466
+ """Run the model server"""
467
+ import argparse
468
+
469
+ parser = argparse.ArgumentParser(description="Stock Recommendation Model Server")
470
+ parser.add_argument("--model-path", type=str, help="Path to model file")
471
+ parser.add_argument("--host", type=str, default="0.0.0.0", help="Server host")
472
+ parser.add_argument("--port", type=int, default=8000, help="Server port")
473
+
474
+ args = parser.parse_args()
475
+
476
+ server = ModelServer(args.model_path)
477
+ server.run(args.host, args.port)
478
+
479
+
480
+ if __name__ == "__main__":
481
+ main()