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.
- mcli/app/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# import click
|
|
2
|
+
# import os
|
|
3
|
+
# from watchdog.observers import Observer
|
|
4
|
+
# from watchdog.events import FileSystemEventHandler
|
|
5
|
+
# # from .config import PACKAGES_TO_SYNC, PATH_TO_PACKAGE_REPO, ENDPOINT
|
|
6
|
+
# # # from mcli.types.file.file import NO_CHANGE_TO_FILE
|
|
7
|
+
# # import re
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# # # TODO: To test
|
|
11
|
+
# # class Watcher:
|
|
12
|
+
# # def __init__(self, directories):
|
|
13
|
+
# # self.observer = Observer()
|
|
14
|
+
# # self.directories = directories
|
|
15
|
+
|
|
16
|
+
# # def run(self):
|
|
17
|
+
# # event_handler = Handler()
|
|
18
|
+
# # for directory in self.directories:
|
|
19
|
+
# # self.observer.schedule(event_handler, directory, recursive=True)
|
|
20
|
+
# # self.observer.start()
|
|
21
|
+
# # try:
|
|
22
|
+
# # while True:
|
|
23
|
+
# # pass
|
|
24
|
+
# # except KeyboardInterrupt:
|
|
25
|
+
# # self.observer.stop()
|
|
26
|
+
# # self.observer.join()
|
|
27
|
+
|
|
28
|
+
# # class Handler(FileSystemEventHandler):
|
|
29
|
+
# # @staticmethod
|
|
30
|
+
# # def process(event):
|
|
31
|
+
# # if (event.is_directory):
|
|
32
|
+
# # logger.info("No rwx")
|
|
33
|
+
# # # # or re.search(r'\.\w+$', event.src_path)
|
|
34
|
+
# # # or event.src_path.endswith('~')):
|
|
35
|
+
# # # logger.info("Badly formatted file string")
|
|
36
|
+
# # # logger.info(event)
|
|
37
|
+
# # # logger.info(event.src_path)
|
|
38
|
+
# # # logger.info(event.is_synthetic)
|
|
39
|
+
# # return
|
|
40
|
+
# # elif event.event_type == 'created' or event.event_type == 'modified':
|
|
41
|
+
# # logger.info("writing content")
|
|
42
|
+
# # # logger.info(event)
|
|
43
|
+
# # # logger.info(event.src_path)
|
|
44
|
+
# # write_content(event.src_path)
|
|
45
|
+
# # elif event.event_type == 'deleted':
|
|
46
|
+
# # logger.info("delete content")
|
|
47
|
+
# # logger.info(event.src_path)
|
|
48
|
+
# # delete_content(event.src_path)
|
|
49
|
+
|
|
50
|
+
# # def on_created(self, event):
|
|
51
|
+
# # self.process(event)
|
|
52
|
+
|
|
53
|
+
# # def on_modified(self, event):
|
|
54
|
+
# # self.process(event)
|
|
55
|
+
|
|
56
|
+
# # def on_deleted(self, event):
|
|
57
|
+
# # self.process(event)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# # def get_metadata_path(path):
|
|
61
|
+
# # return path[len(PATH_TO_PACKAGE_REPO):]
|
|
62
|
+
|
|
63
|
+
# # def get_pkg_id():
|
|
64
|
+
# # global pkg_id
|
|
65
|
+
# # if pkg_id:
|
|
66
|
+
# # return pkg_id
|
|
67
|
+
|
|
68
|
+
# # def handle_response(body):
|
|
69
|
+
# # global pkg_id
|
|
70
|
+
# # pkg_id = body
|
|
71
|
+
|
|
72
|
+
# # make_post_request('Pkg', 'inst', ['Pkg'], handle_response)
|
|
73
|
+
# # return pkg_id
|
|
74
|
+
|
|
75
|
+
# # def write_content(path):
|
|
76
|
+
# # logger.info("write_content")
|
|
77
|
+
# # # if re.search(r'\.\w+$', path) or path.endswith('~'):
|
|
78
|
+
# # # logger.info("Badly formatted file")
|
|
79
|
+
# # # pass
|
|
80
|
+
# # # pkg_id = get_pkg_id()
|
|
81
|
+
# # metadata_path = get_metadata_path(path)
|
|
82
|
+
# # content = None
|
|
83
|
+
# # with open(path, 'rb') as file:
|
|
84
|
+
# # logger.info(file)
|
|
85
|
+
# # content = file
|
|
86
|
+
# # logger.info(metadata_path)
|
|
87
|
+
# # logger.info(content)
|
|
88
|
+
# # if content == NO_CHANGE_TO_FILE:
|
|
89
|
+
# # return
|
|
90
|
+
# # # return make_post_request('Pkg', 'writeContent', [pkg_id, metadata_path, {
|
|
91
|
+
# # # 'type': 'ContentValue',
|
|
92
|
+
# # # 'content': content
|
|
93
|
+
# # # }])
|
|
94
|
+
|
|
95
|
+
# # def delete_content(path):
|
|
96
|
+
# # # pkg_id = get_pkg_id()
|
|
97
|
+
# # metadata_path = get_metadata_path(path)
|
|
98
|
+
# # logger.info(metadata_path)
|
|
99
|
+
# # # return make_post_request('Pkg', 'deleteContent', [pkg_id, metadata_path, True])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# # @click.command()
|
|
103
|
+
# # def watch():
|
|
104
|
+
# # """watcher utility - use this to watch changes to your packages"""
|
|
105
|
+
# # watch_dirs = [os.path.join(PATH_TO_PACKAGE_REPO, pkg) for pkg in PACKAGES_TO_SYNC]
|
|
106
|
+
# # watcher = Watcher(watch_dirs)
|
|
107
|
+
# # click.echo(f"Listening to file updates at: {', '.join(watch_dirs)}")
|
|
108
|
+
# # watcher.run()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# # # @pkg.command()
|
|
112
|
+
# # # @click.argument('path')
|
|
113
|
+
# # # def deploy(path):
|
|
114
|
+
# # # """Deploy a package at a given directory"""
|
|
115
|
+
# # # return _deploy(path)
|
|
116
|
+
|
|
117
|
+
# # # def _deploy(path):
|
|
118
|
+
# # # def _find_repos(path):
|
|
119
|
+
# # # repos = []
|
|
120
|
+
# # # logger.info(f"PATH: {path}")
|
|
121
|
+
# # # for root, dirs, files in os.walk(path):
|
|
122
|
+
# # # logger.info(f"\troot: {root}")
|
|
123
|
+
# # # # logger.info(f"\tdirs: {dirs}")
|
|
124
|
+
# # # logger.info(f"\tfiles: {files}")
|
|
125
|
+
# # # # logger.info(f"_find_repos\npath: {path}\n")
|
|
126
|
+
# # # # for file in files:
|
|
127
|
+
# # # # logger.info(file)
|
|
128
|
+
# # # return repos
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# # # if len(repos) == 0:
|
|
132
|
+
# # # raise FileNotFoundError('Could not find a `repository.json` file. Are you provisioning from the right location?')
|
|
133
|
+
# # # if len(repos) > 1:
|
|
134
|
+
# # # raise NotImplementedError(f'Too many repositories ({len(repos)}) found! This is not yet implemented')
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# # # def _generate_zip_content(repos):
|
|
138
|
+
# # # tmpZipFile = _generate_zip(repos)
|
|
139
|
+
# # # return _zip_file_to_content(tmpZipFile)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# # # def _generate_zip(repos):
|
|
143
|
+
# # # repo = repos[0]
|
|
144
|
+
# # # repoDir = OS_SEP.join(repo.split(OS_SEP)[:-1])
|
|
145
|
+
# # # zipDir = f'{LOCAL_ZIP_DIR}{OS_SEP}provtmp-{round(time.time())}'
|
|
146
|
+
# # # shutil.make_archive(zipDir, 'zip', repoDir)
|
|
147
|
+
# # # return zipDir
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# # # def _zip_file_to_content(zipFile):
|
|
151
|
+
# # # with open(zipFile + '.zip', 'rb') as f:
|
|
152
|
+
# # # bytes = f.read()
|
|
153
|
+
# # # zipContent = base64.b64encode(bytes).decode()
|
|
154
|
+
# # # shutil.rmtree(LOCAL_ZIP_DIR)
|
|
155
|
+
# # # return zip
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# # # return _find_repos(path)
|
|
159
|
+
|
|
160
|
+
# # # # @pkg.command()
|
|
161
|
+
# # # # @click.argument('path')
|
|
162
|
+
# # # # def write(path):
|
|
163
|
+
# # # # """Write/Edit content from mcli server."""
|
|
164
|
+
# # # # click.echo(f"TODO: Not tested")
|
|
165
|
+
# # # return
|
|
166
|
+
# # # write_content(path)
|
|
167
|
+
# # # click.echo(f"Content written for path: {path}")
|
|
168
|
+
|
|
169
|
+
# # # @pkg.command()
|
|
170
|
+
# # # @click.argument('path')
|
|
171
|
+
# # # def delete(path):p
|
|
172
|
+
# # # """Delete content from mcli server."""
|
|
173
|
+
# # # click.echo(f"TODO: Not tested")
|
|
174
|
+
# # # return
|
|
175
|
+
# # # delete_content(path)
|
|
176
|
+
# # # click.echo(f"Content deleted for path: {path}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# # if __name__ == "__main__":
|
|
180
|
+
# # watch()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def watch(*args, **kwargs):
|
|
184
|
+
"""Dummy watch function for CLI test pass."""
|
|
185
|
+
pass
|
mcli/ml/api/app.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""FastAPI application factory and configuration"""
|
|
2
|
+
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI, Request, Response
|
|
7
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
+
from fastapi.middleware.gzip import GZipMiddleware
|
|
9
|
+
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
from starlette.middleware.sessions import SessionMiddleware
|
|
12
|
+
import uvicorn
|
|
13
|
+
|
|
14
|
+
from mcli.ml.config import settings
|
|
15
|
+
from mcli.ml.database.session import init_db
|
|
16
|
+
from mcli.ml.cache import init_cache
|
|
17
|
+
from mcli.ml.logging import setup_logging, get_logger
|
|
18
|
+
from .routers import (
|
|
19
|
+
auth_router,
|
|
20
|
+
model_router,
|
|
21
|
+
prediction_router,
|
|
22
|
+
portfolio_router,
|
|
23
|
+
data_router,
|
|
24
|
+
trade_router,
|
|
25
|
+
backtest_router,
|
|
26
|
+
monitoring_router,
|
|
27
|
+
admin_router,
|
|
28
|
+
websocket_router,
|
|
29
|
+
)
|
|
30
|
+
from .middleware import (
|
|
31
|
+
RequestLoggingMiddleware,
|
|
32
|
+
RateLimitMiddleware,
|
|
33
|
+
ErrorHandlingMiddleware,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger = get_logger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@asynccontextmanager
|
|
40
|
+
async def lifespan(app: FastAPI):
|
|
41
|
+
"""Application lifespan manager"""
|
|
42
|
+
# Startup
|
|
43
|
+
logger.info("Starting ML API server...")
|
|
44
|
+
|
|
45
|
+
# Initialize database
|
|
46
|
+
init_db()
|
|
47
|
+
logger.info("Database initialized")
|
|
48
|
+
|
|
49
|
+
# Initialize cache
|
|
50
|
+
await init_cache()
|
|
51
|
+
logger.info("Cache initialized")
|
|
52
|
+
|
|
53
|
+
# Initialize ML models
|
|
54
|
+
from mcli.ml.models import load_production_models
|
|
55
|
+
await load_production_models()
|
|
56
|
+
logger.info("ML models loaded")
|
|
57
|
+
|
|
58
|
+
yield
|
|
59
|
+
|
|
60
|
+
# Shutdown
|
|
61
|
+
logger.info("Shutting down ML API server...")
|
|
62
|
+
|
|
63
|
+
# Cleanup cache connections
|
|
64
|
+
from mcli.ml.cache import close_cache
|
|
65
|
+
await close_cache()
|
|
66
|
+
|
|
67
|
+
# Cleanup database connections
|
|
68
|
+
from mcli.ml.database.session import async_engine
|
|
69
|
+
await async_engine.dispose()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create_app() -> FastAPI:
|
|
73
|
+
"""Create FastAPI application with all configurations"""
|
|
74
|
+
|
|
75
|
+
# Setup logging
|
|
76
|
+
setup_logging()
|
|
77
|
+
|
|
78
|
+
# Create FastAPI app
|
|
79
|
+
app = FastAPI(
|
|
80
|
+
title="MCLI ML System API",
|
|
81
|
+
description="ML system for politician trading analysis and stock recommendations",
|
|
82
|
+
version="1.0.0",
|
|
83
|
+
docs_url="/docs" if settings.debug else None,
|
|
84
|
+
redoc_url="/redoc" if settings.debug else None,
|
|
85
|
+
openapi_url="/openapi.json" if settings.debug else None,
|
|
86
|
+
lifespan=lifespan,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Add middlewares
|
|
90
|
+
app.add_middleware(ErrorHandlingMiddleware)
|
|
91
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
92
|
+
app.add_middleware(RateLimitMiddleware, requests_per_minute=settings.api.rate_limit)
|
|
93
|
+
app.add_middleware(
|
|
94
|
+
SessionMiddleware,
|
|
95
|
+
secret_key=settings.api.secret_key,
|
|
96
|
+
session_cookie="ml_session",
|
|
97
|
+
max_age=3600,
|
|
98
|
+
)
|
|
99
|
+
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
|
100
|
+
|
|
101
|
+
# CORS configuration
|
|
102
|
+
app.add_middleware(
|
|
103
|
+
CORSMiddleware,
|
|
104
|
+
allow_origins=settings.security.cors_origins,
|
|
105
|
+
allow_credentials=True,
|
|
106
|
+
allow_methods=["*"],
|
|
107
|
+
allow_headers=["*"],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Trusted host middleware
|
|
111
|
+
if settings.is_production:
|
|
112
|
+
app.add_middleware(
|
|
113
|
+
TrustedHostMiddleware,
|
|
114
|
+
allowed_hosts=["*.mcli-ml.com", "mcli-ml.com"]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Include routers
|
|
118
|
+
app.include_router(auth_router.router, prefix="/api/v1/auth", tags=["Authentication"])
|
|
119
|
+
app.include_router(model_router.router, prefix="/api/v1/models", tags=["Models"])
|
|
120
|
+
app.include_router(prediction_router.router, prefix="/api/v1/predictions", tags=["Predictions"])
|
|
121
|
+
app.include_router(portfolio_router.router, prefix="/api/v1/portfolios", tags=["Portfolios"])
|
|
122
|
+
app.include_router(data_router.router, prefix="/api/v1/data", tags=["Data"])
|
|
123
|
+
app.include_router(trade_router.router, prefix="/api/v1/trades", tags=["Trades"])
|
|
124
|
+
app.include_router(backtest_router.router, prefix="/api/v1/backtests", tags=["Backtesting"])
|
|
125
|
+
app.include_router(monitoring_router.router, prefix="/api/v1/monitoring", tags=["Monitoring"])
|
|
126
|
+
app.include_router(admin_router.router, prefix="/api/v1/admin", tags=["Admin"])
|
|
127
|
+
app.include_router(websocket_router.router, prefix="/ws", tags=["WebSocket"])
|
|
128
|
+
|
|
129
|
+
# Health check endpoint
|
|
130
|
+
@app.get("/health", tags=["Health"])
|
|
131
|
+
async def health_check():
|
|
132
|
+
"""Health check endpoint"""
|
|
133
|
+
return {
|
|
134
|
+
"status": "healthy",
|
|
135
|
+
"environment": settings.environment,
|
|
136
|
+
"version": "1.0.0"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Ready check endpoint
|
|
140
|
+
@app.get("/ready", tags=["Health"])
|
|
141
|
+
async def ready_check():
|
|
142
|
+
"""Readiness check endpoint"""
|
|
143
|
+
from mcli.ml.database.session import check_database_health
|
|
144
|
+
from mcli.ml.cache import check_cache_health
|
|
145
|
+
|
|
146
|
+
db_healthy = await check_database_health()
|
|
147
|
+
cache_healthy = await check_cache_health()
|
|
148
|
+
|
|
149
|
+
if db_healthy and cache_healthy:
|
|
150
|
+
return {"status": "ready", "database": "healthy", "cache": "healthy"}
|
|
151
|
+
else:
|
|
152
|
+
return JSONResponse(
|
|
153
|
+
status_code=503,
|
|
154
|
+
content={
|
|
155
|
+
"status": "not ready",
|
|
156
|
+
"database": "healthy" if db_healthy else "unhealthy",
|
|
157
|
+
"cache": "healthy" if cache_healthy else "unhealthy"
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Metrics endpoint (Prometheus format)
|
|
162
|
+
@app.get("/metrics", tags=["Monitoring"])
|
|
163
|
+
async def metrics():
|
|
164
|
+
"""Prometheus metrics endpoint"""
|
|
165
|
+
from mcli.ml.monitoring.metrics import get_metrics
|
|
166
|
+
return Response(content=get_metrics(), media_type="text/plain")
|
|
167
|
+
|
|
168
|
+
# Root endpoint
|
|
169
|
+
@app.get("/")
|
|
170
|
+
async def root():
|
|
171
|
+
"""Root endpoint"""
|
|
172
|
+
return {
|
|
173
|
+
"message": "MCLI ML System API",
|
|
174
|
+
"version": "1.0.0",
|
|
175
|
+
"docs": "/docs" if settings.debug else None
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Exception handlers
|
|
179
|
+
@app.exception_handler(404)
|
|
180
|
+
async def not_found_handler(request: Request, exc):
|
|
181
|
+
return JSONResponse(
|
|
182
|
+
status_code=404,
|
|
183
|
+
content={"detail": "Resource not found"}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@app.exception_handler(500)
|
|
187
|
+
async def internal_server_error_handler(request: Request, exc):
|
|
188
|
+
logger.error(f"Internal server error: {exc}")
|
|
189
|
+
return JSONResponse(
|
|
190
|
+
status_code=500,
|
|
191
|
+
content={"detail": "Internal server error"}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return app
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_application() -> FastAPI:
|
|
198
|
+
"""Get configured FastAPI application"""
|
|
199
|
+
return create_app()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Create app instance
|
|
203
|
+
app = get_application()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
"""Run the application directly"""
|
|
208
|
+
uvicorn.run(
|
|
209
|
+
"mcli.ml.api.app:app",
|
|
210
|
+
host=settings.api.host,
|
|
211
|
+
port=settings.api.port,
|
|
212
|
+
workers=settings.api.workers,
|
|
213
|
+
reload=settings.debug,
|
|
214
|
+
log_level="debug" if settings.debug else "info",
|
|
215
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Custom middleware for API"""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Callable
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
|
|
9
|
+
from fastapi import Request, Response, HTTPException
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
12
|
+
from starlette.types import ASGIApp
|
|
13
|
+
|
|
14
|
+
from mcli.ml.logging import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
20
|
+
"""Log all incoming requests and responses"""
|
|
21
|
+
|
|
22
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
23
|
+
# Generate request ID
|
|
24
|
+
request_id = str(uuid.uuid4())
|
|
25
|
+
request.state.request_id = request_id
|
|
26
|
+
|
|
27
|
+
# Log request
|
|
28
|
+
start_time = time.time()
|
|
29
|
+
logger.info(f"Request {request_id}: {request.method} {request.url.path}")
|
|
30
|
+
|
|
31
|
+
# Process request
|
|
32
|
+
response = await call_next(request)
|
|
33
|
+
|
|
34
|
+
# Log response
|
|
35
|
+
process_time = time.time() - start_time
|
|
36
|
+
logger.info(
|
|
37
|
+
f"Response {request_id}: status={response.status_code} "
|
|
38
|
+
f"duration={process_time:.3f}s"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Add headers
|
|
42
|
+
response.headers["X-Request-ID"] = request_id
|
|
43
|
+
response.headers["X-Process-Time"] = str(process_time)
|
|
44
|
+
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RateLimitMiddleware(BaseHTTPMiddleware):
|
|
49
|
+
"""Rate limiting middleware"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, app: ASGIApp, requests_per_minute: int = 60):
|
|
52
|
+
super().__init__(app)
|
|
53
|
+
self.requests_per_minute = requests_per_minute
|
|
54
|
+
self.clients = defaultdict(list)
|
|
55
|
+
|
|
56
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
57
|
+
# Skip rate limiting for health checks
|
|
58
|
+
if request.url.path in ["/health", "/ready", "/metrics"]:
|
|
59
|
+
return await call_next(request)
|
|
60
|
+
|
|
61
|
+
# Get client IP
|
|
62
|
+
client_ip = request.client.host
|
|
63
|
+
|
|
64
|
+
# Check rate limit
|
|
65
|
+
now = datetime.utcnow()
|
|
66
|
+
minute_ago = now - timedelta(minutes=1)
|
|
67
|
+
|
|
68
|
+
# Clean old requests
|
|
69
|
+
self.clients[client_ip] = [
|
|
70
|
+
req_time for req_time in self.clients[client_ip]
|
|
71
|
+
if req_time > minute_ago
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Check if limit exceeded
|
|
75
|
+
if len(self.clients[client_ip]) >= self.requests_per_minute:
|
|
76
|
+
logger.warning(f"Rate limit exceeded for {client_ip}")
|
|
77
|
+
return JSONResponse(
|
|
78
|
+
status_code=429,
|
|
79
|
+
content={"detail": "Rate limit exceeded. Please try again later."},
|
|
80
|
+
headers={"Retry-After": "60"}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Record request
|
|
84
|
+
self.clients[client_ip].append(now)
|
|
85
|
+
|
|
86
|
+
# Process request
|
|
87
|
+
return await call_next(request)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ErrorHandlingMiddleware(BaseHTTPMiddleware):
|
|
91
|
+
"""Global error handling middleware"""
|
|
92
|
+
|
|
93
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
94
|
+
try:
|
|
95
|
+
response = await call_next(request)
|
|
96
|
+
return response
|
|
97
|
+
|
|
98
|
+
except HTTPException as e:
|
|
99
|
+
# Let FastAPI handle HTTP exceptions
|
|
100
|
+
raise e
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
# Log unexpected errors
|
|
104
|
+
request_id = getattr(request.state, "request_id", "unknown")
|
|
105
|
+
logger.error(
|
|
106
|
+
f"Unhandled exception in request {request_id}: {str(e)}",
|
|
107
|
+
exc_info=True
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Return generic error response
|
|
111
|
+
return JSONResponse(
|
|
112
|
+
status_code=500,
|
|
113
|
+
content={
|
|
114
|
+
"detail": "An internal error occurred",
|
|
115
|
+
"request_id": request_id
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class CompressionMiddleware(BaseHTTPMiddleware):
|
|
121
|
+
"""Response compression middleware"""
|
|
122
|
+
|
|
123
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
124
|
+
response = await call_next(request)
|
|
125
|
+
|
|
126
|
+
# Check if client accepts gzip
|
|
127
|
+
accept_encoding = request.headers.get("accept-encoding", "")
|
|
128
|
+
if "gzip" in accept_encoding.lower():
|
|
129
|
+
# Response will be compressed by GZipMiddleware
|
|
130
|
+
response.headers["Vary"] = "Accept-Encoding"
|
|
131
|
+
|
|
132
|
+
return response
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class CacheControlMiddleware(BaseHTTPMiddleware):
|
|
136
|
+
"""Add cache control headers"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, app: ASGIApp, max_age: int = 0):
|
|
139
|
+
super().__init__(app)
|
|
140
|
+
self.max_age = max_age
|
|
141
|
+
|
|
142
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
143
|
+
response = await call_next(request)
|
|
144
|
+
|
|
145
|
+
# Add cache control headers based on endpoint
|
|
146
|
+
if request.url.path.startswith("/api/v1/data"):
|
|
147
|
+
# Cache data endpoints
|
|
148
|
+
response.headers["Cache-Control"] = f"public, max-age=300"
|
|
149
|
+
elif request.url.path.startswith("/api/v1/predictions"):
|
|
150
|
+
# Don't cache predictions
|
|
151
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
152
|
+
else:
|
|
153
|
+
# Default cache control
|
|
154
|
+
response.headers["Cache-Control"] = f"public, max-age={self.max_age}"
|
|
155
|
+
|
|
156
|
+
return response
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
160
|
+
"""Add security headers to responses"""
|
|
161
|
+
|
|
162
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
163
|
+
response = await call_next(request)
|
|
164
|
+
|
|
165
|
+
# Add security headers
|
|
166
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
167
|
+
response.headers["X-Frame-Options"] = "DENY"
|
|
168
|
+
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
169
|
+
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
|
170
|
+
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
171
|
+
|
|
172
|
+
# Content Security Policy
|
|
173
|
+
response.headers["Content-Security-Policy"] = (
|
|
174
|
+
"default-src 'self'; "
|
|
175
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
|
176
|
+
"style-src 'self' 'unsafe-inline'; "
|
|
177
|
+
"img-src 'self' data: https:; "
|
|
178
|
+
"font-src 'self' data:; "
|
|
179
|
+
"connect-src 'self' wss: https:;"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return response
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class MetricsMiddleware(BaseHTTPMiddleware):
|
|
186
|
+
"""Collect metrics for monitoring"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, app: ASGIApp):
|
|
189
|
+
super().__init__(app)
|
|
190
|
+
self.request_count = defaultdict(int)
|
|
191
|
+
self.request_duration = defaultdict(list)
|
|
192
|
+
|
|
193
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
194
|
+
# Record metrics
|
|
195
|
+
endpoint = f"{request.method} {request.url.path}"
|
|
196
|
+
start_time = time.time()
|
|
197
|
+
|
|
198
|
+
# Process request
|
|
199
|
+
response = await call_next(request)
|
|
200
|
+
|
|
201
|
+
# Update metrics
|
|
202
|
+
duration = time.time() - start_time
|
|
203
|
+
self.request_count[endpoint] += 1
|
|
204
|
+
self.request_duration[endpoint].append(duration)
|
|
205
|
+
|
|
206
|
+
# Limit history size
|
|
207
|
+
if len(self.request_duration[endpoint]) > 1000:
|
|
208
|
+
self.request_duration[endpoint] = self.request_duration[endpoint][-1000:]
|
|
209
|
+
|
|
210
|
+
return response
|
|
211
|
+
|
|
212
|
+
def get_metrics(self) -> dict:
|
|
213
|
+
"""Get collected metrics"""
|
|
214
|
+
metrics = {}
|
|
215
|
+
for endpoint, count in self.request_count.items():
|
|
216
|
+
durations = self.request_duration[endpoint]
|
|
217
|
+
if durations:
|
|
218
|
+
metrics[endpoint] = {
|
|
219
|
+
"count": count,
|
|
220
|
+
"avg_duration": sum(durations) / len(durations),
|
|
221
|
+
"min_duration": min(durations),
|
|
222
|
+
"max_duration": max(durations),
|
|
223
|
+
}
|
|
224
|
+
return metrics
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Admin API routes"""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from mcli.ml.auth import get_current_active_user, require_role
|
|
5
|
+
from mcli.ml.database.models import User, UserRole
|
|
6
|
+
|
|
7
|
+
router = APIRouter()
|
|
8
|
+
|
|
9
|
+
@router.get("/users")
|
|
10
|
+
async def list_users(current_user: User = Depends(require_role(UserRole.ADMIN))):
|
|
11
|
+
"""List all users"""
|
|
12
|
+
return {"users": []}
|