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,425 @@
1
+ """Authentication manager with JWT support"""
2
+
3
+ import secrets
4
+ from datetime import datetime, timedelta
5
+ from typing import Optional, Union, Dict, Any
6
+ from uuid import UUID
7
+
8
+ import bcrypt
9
+ import jwt
10
+ from fastapi import Depends, HTTPException, status, Request
11
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
12
+ from sqlalchemy.orm import Session
13
+ from sqlalchemy import select
14
+
15
+ from mcli.ml.config import settings
16
+ from mcli.ml.database.models import User, UserRole
17
+ from mcli.ml.database.session import get_db
18
+ from .models import UserCreate, UserLogin, TokenResponse, TokenData, UserResponse
19
+
20
+
21
+ # Security scheme
22
+ security = HTTPBearer()
23
+
24
+
25
+ class AuthManager:
26
+ """Authentication and authorization manager"""
27
+
28
+ def __init__(self):
29
+ self.secret_key = settings.api.secret_key
30
+ self.algorithm = settings.api.algorithm
31
+ self.access_token_expire_minutes = settings.api.access_token_expire_minutes
32
+ self.refresh_token_expire_days = 7
33
+
34
+ def hash_password(self, password: str) -> str:
35
+ """Hash a password using bcrypt"""
36
+ salt = bcrypt.gensalt()
37
+ hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
38
+ return hashed.decode('utf-8')
39
+
40
+ def verify_password(self, plain_password: str, hashed_password: str) -> bool:
41
+ """Verify a password against a hash"""
42
+ return bcrypt.checkpw(
43
+ plain_password.encode('utf-8'),
44
+ hashed_password.encode('utf-8')
45
+ )
46
+
47
+ def create_access_token(
48
+ self,
49
+ user_id: str,
50
+ username: str,
51
+ role: str,
52
+ expires_delta: Optional[timedelta] = None
53
+ ) -> str:
54
+ """Create a JWT access token"""
55
+ if expires_delta:
56
+ expire = datetime.utcnow() + expires_delta
57
+ else:
58
+ expire = datetime.utcnow() + timedelta(minutes=self.access_token_expire_minutes)
59
+
60
+ payload = {
61
+ "sub": user_id,
62
+ "username": username,
63
+ "role": role,
64
+ "exp": expire,
65
+ "iat": datetime.utcnow(),
66
+ "jti": secrets.token_urlsafe(32), # JWT ID for token revocation
67
+ }
68
+
69
+ encoded_jwt = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
70
+ return encoded_jwt
71
+
72
+ def create_refresh_token(
73
+ self,
74
+ user_id: str,
75
+ expires_delta: Optional[timedelta] = None
76
+ ) -> str:
77
+ """Create a refresh token"""
78
+ if expires_delta:
79
+ expire = datetime.utcnow() + expires_delta
80
+ else:
81
+ expire = datetime.utcnow() + timedelta(days=self.refresh_token_expire_days)
82
+
83
+ payload = {
84
+ "sub": user_id,
85
+ "type": "refresh",
86
+ "exp": expire,
87
+ "iat": datetime.utcnow(),
88
+ "jti": secrets.token_urlsafe(32),
89
+ }
90
+
91
+ encoded_jwt = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
92
+ return encoded_jwt
93
+
94
+ def verify_token(self, token: str) -> Optional[TokenData]:
95
+ """Verify and decode a JWT token"""
96
+ try:
97
+ payload = jwt.decode(
98
+ token,
99
+ self.secret_key,
100
+ algorithms=[self.algorithm]
101
+ )
102
+
103
+ token_data = TokenData(
104
+ sub=payload.get("sub"),
105
+ username=payload.get("username"),
106
+ role=payload.get("role"),
107
+ exp=datetime.fromtimestamp(payload.get("exp")),
108
+ iat=datetime.fromtimestamp(payload.get("iat")),
109
+ jti=payload.get("jti"),
110
+ )
111
+
112
+ return token_data
113
+
114
+ except jwt.ExpiredSignatureError:
115
+ raise HTTPException(
116
+ status_code=status.HTTP_401_UNAUTHORIZED,
117
+ detail="Token has expired",
118
+ headers={"WWW-Authenticate": "Bearer"},
119
+ )
120
+ except jwt.JWTError:
121
+ raise HTTPException(
122
+ status_code=status.HTTP_401_UNAUTHORIZED,
123
+ detail="Could not validate credentials",
124
+ headers={"WWW-Authenticate": "Bearer"},
125
+ )
126
+
127
+ async def register_user(
128
+ self,
129
+ user_data: UserCreate,
130
+ db: Session
131
+ ) -> User:
132
+ """Register a new user"""
133
+ # Check if user already exists
134
+ existing_user = db.query(User).filter(
135
+ (User.username == user_data.username) |
136
+ (User.email == user_data.email)
137
+ ).first()
138
+
139
+ if existing_user:
140
+ if existing_user.username == user_data.username:
141
+ raise HTTPException(
142
+ status_code=status.HTTP_400_BAD_REQUEST,
143
+ detail="Username already registered"
144
+ )
145
+ else:
146
+ raise HTTPException(
147
+ status_code=status.HTTP_400_BAD_REQUEST,
148
+ detail="Email already registered"
149
+ )
150
+
151
+ # Create new user
152
+ hashed_password = self.hash_password(user_data.password)
153
+
154
+ new_user = User(
155
+ username=user_data.username,
156
+ email=user_data.email,
157
+ password_hash=hashed_password,
158
+ first_name=user_data.first_name,
159
+ last_name=user_data.last_name,
160
+ role=UserRole.USER,
161
+ is_active=True,
162
+ is_verified=False,
163
+ )
164
+
165
+ db.add(new_user)
166
+ db.commit()
167
+ db.refresh(new_user)
168
+
169
+ return new_user
170
+
171
+ async def authenticate_user(
172
+ self,
173
+ login_data: UserLogin,
174
+ db: Session
175
+ ) -> Optional[User]:
176
+ """Authenticate a user"""
177
+ user = db.query(User).filter(
178
+ User.username == login_data.username
179
+ ).first()
180
+
181
+ if not user:
182
+ return None
183
+
184
+ if not self.verify_password(login_data.password, user.password_hash):
185
+ return None
186
+
187
+ # Update last login
188
+ user.last_login_at = datetime.utcnow()
189
+ db.commit()
190
+
191
+ return user
192
+
193
+ async def login(
194
+ self,
195
+ login_data: UserLogin,
196
+ db: Session
197
+ ) -> TokenResponse:
198
+ """Login user and return tokens"""
199
+ user = await self.authenticate_user(login_data, db)
200
+
201
+ if not user:
202
+ raise HTTPException(
203
+ status_code=status.HTTP_401_UNAUTHORIZED,
204
+ detail="Invalid username or password",
205
+ headers={"WWW-Authenticate": "Bearer"},
206
+ )
207
+
208
+ if not user.is_active:
209
+ raise HTTPException(
210
+ status_code=status.HTTP_403_FORBIDDEN,
211
+ detail="User account is disabled"
212
+ )
213
+
214
+ # Create tokens
215
+ access_token = self.create_access_token(
216
+ user_id=str(user.id),
217
+ username=user.username,
218
+ role=user.role.value
219
+ )
220
+
221
+ refresh_token = self.create_refresh_token(user_id=str(user.id))
222
+
223
+ return TokenResponse(
224
+ access_token=access_token,
225
+ refresh_token=refresh_token,
226
+ expires_in=self.access_token_expire_minutes * 60,
227
+ user=UserResponse.from_orm(user)
228
+ )
229
+
230
+ async def refresh_access_token(
231
+ self,
232
+ refresh_token: str,
233
+ db: Session
234
+ ) -> TokenResponse:
235
+ """Refresh access token using refresh token"""
236
+ try:
237
+ payload = jwt.decode(
238
+ refresh_token,
239
+ self.secret_key,
240
+ algorithms=[self.algorithm]
241
+ )
242
+
243
+ if payload.get("type") != "refresh":
244
+ raise HTTPException(
245
+ status_code=status.HTTP_401_UNAUTHORIZED,
246
+ detail="Invalid refresh token"
247
+ )
248
+
249
+ user_id = payload.get("sub")
250
+ user = db.query(User).filter(User.id == user_id).first()
251
+
252
+ if not user or not user.is_active:
253
+ raise HTTPException(
254
+ status_code=status.HTTP_401_UNAUTHORIZED,
255
+ detail="User not found or disabled"
256
+ )
257
+
258
+ # Create new access token
259
+ access_token = self.create_access_token(
260
+ user_id=str(user.id),
261
+ username=user.username,
262
+ role=user.role.value
263
+ )
264
+
265
+ return TokenResponse(
266
+ access_token=access_token,
267
+ refresh_token=refresh_token, # Return same refresh token
268
+ expires_in=self.access_token_expire_minutes * 60,
269
+ user=UserResponse.from_orm(user)
270
+ )
271
+
272
+ except jwt.ExpiredSignatureError:
273
+ raise HTTPException(
274
+ status_code=status.HTTP_401_UNAUTHORIZED,
275
+ detail="Refresh token has expired"
276
+ )
277
+ except jwt.JWTError:
278
+ raise HTTPException(
279
+ status_code=status.HTTP_401_UNAUTHORIZED,
280
+ detail="Invalid refresh token"
281
+ )
282
+
283
+ async def get_current_user(
284
+ self,
285
+ credentials: HTTPAuthorizationCredentials = Depends(security),
286
+ db: Session = Depends(get_db)
287
+ ) -> User:
288
+ """Get current authenticated user from JWT token"""
289
+ token = credentials.credentials
290
+
291
+ token_data = self.verify_token(token)
292
+
293
+ user = db.query(User).filter(User.id == token_data.sub).first()
294
+
295
+ if not user:
296
+ raise HTTPException(
297
+ status_code=status.HTTP_404_NOT_FOUND,
298
+ detail="User not found"
299
+ )
300
+
301
+ if not user.is_active:
302
+ raise HTTPException(
303
+ status_code=status.HTTP_403_FORBIDDEN,
304
+ detail="User account is disabled"
305
+ )
306
+
307
+ return user
308
+
309
+ def require_role(self, *allowed_roles: UserRole):
310
+ """Decorator/dependency to require specific roles"""
311
+ async def role_checker(
312
+ current_user: User = Depends(self.get_current_user)
313
+ ) -> User:
314
+ if current_user.role not in allowed_roles:
315
+ raise HTTPException(
316
+ status_code=status.HTTP_403_FORBIDDEN,
317
+ detail="Insufficient permissions"
318
+ )
319
+ return current_user
320
+
321
+ return role_checker
322
+
323
+
324
+ # Global auth manager instance
325
+ auth_manager = AuthManager()
326
+
327
+ # Convenience functions
328
+ hash_password = auth_manager.hash_password
329
+ verify_password = auth_manager.verify_password
330
+ create_access_token = auth_manager.create_access_token
331
+ verify_access_token = auth_manager.verify_token
332
+ get_current_user = auth_manager.get_current_user
333
+ require_role = auth_manager.require_role
334
+
335
+
336
+ async def get_current_active_user(
337
+ current_user: User = Depends(get_current_user)
338
+ ) -> User:
339
+ """Get current active user"""
340
+ if not current_user.is_active:
341
+ raise HTTPException(
342
+ status_code=status.HTTP_403_FORBIDDEN,
343
+ detail="Inactive user"
344
+ )
345
+ return current_user
346
+
347
+
348
+ async def get_admin_user(
349
+ current_user: User = Depends(get_current_user)
350
+ ) -> User:
351
+ """Get current admin user"""
352
+ if current_user.role != UserRole.ADMIN:
353
+ raise HTTPException(
354
+ status_code=status.HTTP_403_FORBIDDEN,
355
+ detail="Admin access required"
356
+ )
357
+ return current_user
358
+
359
+
360
+ # Rate limiting
361
+ from collections import defaultdict
362
+ from datetime import datetime, timedelta
363
+ import asyncio
364
+
365
+
366
+ class RateLimiter:
367
+ """Simple rate limiter"""
368
+
369
+ def __init__(self, requests: int = 100, window: int = 60):
370
+ self.requests = requests
371
+ self.window = window
372
+ self.clients = defaultdict(list)
373
+ self._cleanup_task = None
374
+
375
+ async def check_rate_limit(self, client_id: str) -> bool:
376
+ """Check if client has exceeded rate limit"""
377
+ now = datetime.utcnow()
378
+ minute_ago = now - timedelta(seconds=self.window)
379
+
380
+ # Clean old requests
381
+ self.clients[client_id] = [
382
+ req_time for req_time in self.clients[client_id]
383
+ if req_time > minute_ago
384
+ ]
385
+
386
+ # Check limit
387
+ if len(self.clients[client_id]) >= self.requests:
388
+ return False
389
+
390
+ # Add current request
391
+ self.clients[client_id].append(now)
392
+ return True
393
+
394
+ async def cleanup(self):
395
+ """Periodic cleanup of old entries"""
396
+ while True:
397
+ await asyncio.sleep(300) # Clean every 5 minutes
398
+ now = datetime.utcnow()
399
+ window_start = now - timedelta(seconds=self.window)
400
+
401
+ for client_id in list(self.clients.keys()):
402
+ self.clients[client_id] = [
403
+ req_time for req_time in self.clients[client_id]
404
+ if req_time > window_start
405
+ ]
406
+
407
+ if not self.clients[client_id]:
408
+ del self.clients[client_id]
409
+
410
+
411
+ # Global rate limiter
412
+ rate_limiter = RateLimiter(requests=settings.api.rate_limit, window=60)
413
+
414
+
415
+ async def check_rate_limit(request: Request):
416
+ """FastAPI dependency to check rate limit"""
417
+ client_ip = request.client.host
418
+
419
+ if not await rate_limiter.check_rate_limit(client_ip):
420
+ raise HTTPException(
421
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
422
+ detail="Rate limit exceeded"
423
+ )
424
+
425
+ return True
mcli/ml/auth/models.py ADDED
@@ -0,0 +1,154 @@
1
+ """Authentication data models"""
2
+
3
+ from datetime import datetime
4
+ from typing import Optional, List
5
+ from pydantic import BaseModel, EmailStr, Field, validator
6
+ from uuid import UUID
7
+
8
+
9
+ class UserCreate(BaseModel):
10
+ """User registration model"""
11
+ username: str = Field(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9_-]+$")
12
+ email: EmailStr
13
+ password: str = Field(..., min_length=8, max_length=100)
14
+ first_name: Optional[str] = Field(None, max_length=50)
15
+ last_name: Optional[str] = Field(None, max_length=50)
16
+
17
+ @validator('password')
18
+ def validate_password(cls, v):
19
+ """Ensure password meets security requirements"""
20
+ if len(v) < 8:
21
+ raise ValueError('Password must be at least 8 characters long')
22
+ if not any(char.isdigit() for char in v):
23
+ raise ValueError('Password must contain at least one digit')
24
+ if not any(char.isupper() for char in v):
25
+ raise ValueError('Password must contain at least one uppercase letter')
26
+ if not any(char.islower() for char in v):
27
+ raise ValueError('Password must contain at least one lowercase letter')
28
+ return v
29
+
30
+
31
+ class UserLogin(BaseModel):
32
+ """User login model"""
33
+ username: str
34
+ password: str
35
+
36
+
37
+ class UserResponse(BaseModel):
38
+ """User response model"""
39
+ id: UUID
40
+ username: str
41
+ email: str
42
+ first_name: Optional[str]
43
+ last_name: Optional[str]
44
+ role: str
45
+ is_active: bool
46
+ is_verified: bool
47
+ created_at: datetime
48
+ last_login_at: Optional[datetime]
49
+
50
+ class Config:
51
+ orm_mode = True
52
+
53
+
54
+ class TokenResponse(BaseModel):
55
+ """JWT token response"""
56
+ access_token: str
57
+ token_type: str = "Bearer"
58
+ expires_in: int
59
+ refresh_token: Optional[str] = None
60
+ user: UserResponse
61
+
62
+
63
+ class TokenData(BaseModel):
64
+ """JWT token payload"""
65
+ sub: str # User ID
66
+ username: str
67
+ role: str
68
+ exp: datetime
69
+ iat: datetime
70
+ jti: Optional[str] = None # JWT ID for token revocation
71
+
72
+
73
+ class PasswordReset(BaseModel):
74
+ """Password reset request"""
75
+ email: EmailStr
76
+
77
+
78
+ class PasswordResetConfirm(BaseModel):
79
+ """Password reset confirmation"""
80
+ token: str
81
+ new_password: str = Field(..., min_length=8, max_length=100)
82
+
83
+
84
+ class PasswordChange(BaseModel):
85
+ """Password change request"""
86
+ current_password: str
87
+ new_password: str = Field(..., min_length=8, max_length=100)
88
+
89
+ @validator('new_password')
90
+ def validate_password(cls, v, values):
91
+ """Ensure new password is different and meets requirements"""
92
+ if 'current_password' in values and v == values['current_password']:
93
+ raise ValueError('New password must be different from current password')
94
+
95
+ if len(v) < 8:
96
+ raise ValueError('Password must be at least 8 characters long')
97
+ if not any(char.isdigit() for char in v):
98
+ raise ValueError('Password must contain at least one digit')
99
+ if not any(char.isupper() for char in v):
100
+ raise ValueError('Password must contain at least one uppercase letter')
101
+ if not any(char.islower() for char in v):
102
+ raise ValueError('Password must contain at least one lowercase letter')
103
+ return v
104
+
105
+
106
+ class APIKeyCreate(BaseModel):
107
+ """API key creation model"""
108
+ name: str = Field(..., min_length=1, max_length=100)
109
+ expires_at: Optional[datetime] = None
110
+
111
+
112
+ class APIKeyResponse(BaseModel):
113
+ """API key response"""
114
+ key: str
115
+ name: str
116
+ created_at: datetime
117
+ expires_at: Optional[datetime]
118
+ last_used_at: Optional[datetime]
119
+
120
+
121
+ class UserUpdate(BaseModel):
122
+ """User update model"""
123
+ email: Optional[EmailStr] = None
124
+ first_name: Optional[str] = Field(None, max_length=50)
125
+ last_name: Optional[str] = Field(None, max_length=50)
126
+ is_active: Optional[bool] = None
127
+ role: Optional[str] = None
128
+
129
+
130
+ class UserPermissions(BaseModel):
131
+ """User permissions model"""
132
+ user_id: UUID
133
+ permissions: List[str]
134
+ roles: List[str]
135
+
136
+
137
+ class SessionInfo(BaseModel):
138
+ """Session information"""
139
+ session_id: str
140
+ user_id: UUID
141
+ ip_address: str
142
+ user_agent: str
143
+ created_at: datetime
144
+ expires_at: datetime
145
+ is_active: bool
146
+
147
+
148
+ class LoginAttempt(BaseModel):
149
+ """Login attempt tracking"""
150
+ username: str
151
+ ip_address: str
152
+ success: bool
153
+ timestamp: datetime
154
+ failure_reason: Optional[str] = None