mcli-framework 7.1.0__py3-none-any.whl → 7.1.2__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 (94) hide show
  1. mcli/app/completion_cmd.py +59 -49
  2. mcli/app/completion_helpers.py +60 -138
  3. mcli/app/logs_cmd.py +46 -13
  4. mcli/app/main.py +17 -14
  5. mcli/app/model_cmd.py +19 -4
  6. mcli/chat/chat.py +3 -2
  7. mcli/lib/search/cached_vectorizer.py +1 -0
  8. mcli/lib/services/data_pipeline.py +12 -5
  9. mcli/lib/services/lsh_client.py +69 -58
  10. mcli/ml/api/app.py +28 -36
  11. mcli/ml/api/middleware.py +8 -16
  12. mcli/ml/api/routers/admin_router.py +3 -1
  13. mcli/ml/api/routers/auth_router.py +32 -56
  14. mcli/ml/api/routers/backtest_router.py +3 -1
  15. mcli/ml/api/routers/data_router.py +3 -1
  16. mcli/ml/api/routers/model_router.py +35 -74
  17. mcli/ml/api/routers/monitoring_router.py +3 -1
  18. mcli/ml/api/routers/portfolio_router.py +3 -1
  19. mcli/ml/api/routers/prediction_router.py +60 -65
  20. mcli/ml/api/routers/trade_router.py +6 -2
  21. mcli/ml/api/routers/websocket_router.py +12 -9
  22. mcli/ml/api/schemas.py +10 -2
  23. mcli/ml/auth/auth_manager.py +49 -114
  24. mcli/ml/auth/models.py +30 -15
  25. mcli/ml/auth/permissions.py +12 -19
  26. mcli/ml/backtesting/backtest_engine.py +134 -108
  27. mcli/ml/backtesting/performance_metrics.py +142 -108
  28. mcli/ml/cache.py +12 -18
  29. mcli/ml/cli/main.py +37 -23
  30. mcli/ml/config/settings.py +29 -12
  31. mcli/ml/dashboard/app.py +122 -130
  32. mcli/ml/dashboard/app_integrated.py +283 -152
  33. mcli/ml/dashboard/app_supabase.py +176 -108
  34. mcli/ml/dashboard/app_training.py +212 -206
  35. mcli/ml/dashboard/cli.py +14 -5
  36. mcli/ml/data_ingestion/api_connectors.py +51 -81
  37. mcli/ml/data_ingestion/data_pipeline.py +127 -125
  38. mcli/ml/data_ingestion/stream_processor.py +72 -80
  39. mcli/ml/database/migrations/env.py +3 -2
  40. mcli/ml/database/models.py +112 -79
  41. mcli/ml/database/session.py +6 -5
  42. mcli/ml/experimentation/ab_testing.py +149 -99
  43. mcli/ml/features/ensemble_features.py +9 -8
  44. mcli/ml/features/political_features.py +6 -5
  45. mcli/ml/features/recommendation_engine.py +15 -14
  46. mcli/ml/features/stock_features.py +7 -6
  47. mcli/ml/features/test_feature_engineering.py +8 -7
  48. mcli/ml/logging.py +10 -15
  49. mcli/ml/mlops/data_versioning.py +57 -64
  50. mcli/ml/mlops/experiment_tracker.py +49 -41
  51. mcli/ml/mlops/model_serving.py +59 -62
  52. mcli/ml/mlops/pipeline_orchestrator.py +203 -149
  53. mcli/ml/models/base_models.py +8 -7
  54. mcli/ml/models/ensemble_models.py +6 -5
  55. mcli/ml/models/recommendation_models.py +7 -6
  56. mcli/ml/models/test_models.py +18 -14
  57. mcli/ml/monitoring/drift_detection.py +95 -74
  58. mcli/ml/monitoring/metrics.py +10 -22
  59. mcli/ml/optimization/portfolio_optimizer.py +172 -132
  60. mcli/ml/predictions/prediction_engine.py +235 -0
  61. mcli/ml/preprocessing/data_cleaners.py +6 -5
  62. mcli/ml/preprocessing/feature_extractors.py +7 -6
  63. mcli/ml/preprocessing/ml_pipeline.py +3 -2
  64. mcli/ml/preprocessing/politician_trading_preprocessor.py +11 -10
  65. mcli/ml/preprocessing/test_preprocessing.py +4 -4
  66. mcli/ml/scripts/populate_sample_data.py +36 -16
  67. mcli/ml/tasks.py +82 -83
  68. mcli/ml/tests/test_integration.py +86 -76
  69. mcli/ml/tests/test_training_dashboard.py +169 -142
  70. mcli/mygroup/test_cmd.py +2 -1
  71. mcli/self/self_cmd.py +38 -18
  72. mcli/self/test_cmd.py +2 -1
  73. mcli/workflow/dashboard/dashboard_cmd.py +13 -6
  74. mcli/workflow/lsh_integration.py +46 -58
  75. mcli/workflow/politician_trading/commands.py +576 -427
  76. mcli/workflow/politician_trading/config.py +7 -7
  77. mcli/workflow/politician_trading/connectivity.py +35 -33
  78. mcli/workflow/politician_trading/data_sources.py +72 -71
  79. mcli/workflow/politician_trading/database.py +18 -16
  80. mcli/workflow/politician_trading/demo.py +4 -3
  81. mcli/workflow/politician_trading/models.py +5 -5
  82. mcli/workflow/politician_trading/monitoring.py +13 -13
  83. mcli/workflow/politician_trading/scrapers.py +332 -224
  84. mcli/workflow/politician_trading/scrapers_california.py +116 -94
  85. mcli/workflow/politician_trading/scrapers_eu.py +70 -71
  86. mcli/workflow/politician_trading/scrapers_uk.py +118 -90
  87. mcli/workflow/politician_trading/scrapers_us_states.py +125 -92
  88. mcli/workflow/politician_trading/workflow.py +98 -71
  89. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/METADATA +2 -2
  90. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/RECORD +94 -93
  91. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/WHEEL +0 -0
  92. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/entry_points.txt +0 -0
  93. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/licenses/LICENSE +0 -0
  94. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/top_level.txt +0 -0
@@ -2,21 +2,21 @@
2
2
 
3
3
  import secrets
4
4
  from datetime import datetime, timedelta
5
- from typing import Optional, Union, Dict, Any
5
+ from typing import Any, Dict, Optional, Union
6
6
  from uuid import UUID
7
7
 
8
8
  import bcrypt
9
9
  import jwt
10
- from fastapi import Depends, HTTPException, status, Request
11
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
12
- from sqlalchemy.orm import Session
10
+ from fastapi import Depends, HTTPException, Request, status
11
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
13
12
  from sqlalchemy import select
13
+ from sqlalchemy.orm import Session
14
14
 
15
15
  from mcli.ml.config import settings
16
16
  from mcli.ml.database.models import User, UserRole
17
17
  from mcli.ml.database.session import get_db
18
- from .models import UserCreate, UserLogin, TokenResponse, TokenData, UserResponse
19
18
 
19
+ from .models import TokenData, TokenResponse, UserCreate, UserLogin, UserResponse
20
20
 
21
21
  # Security scheme
22
22
  security = HTTPBearer()
@@ -34,22 +34,15 @@ class AuthManager:
34
34
  def hash_password(self, password: str) -> str:
35
35
  """Hash a password using bcrypt"""
36
36
  salt = bcrypt.gensalt()
37
- hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
38
- return hashed.decode('utf-8')
37
+ hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
38
+ return hashed.decode("utf-8")
39
39
 
40
40
  def verify_password(self, plain_password: str, hashed_password: str) -> bool:
41
41
  """Verify a password against a hash"""
42
- return bcrypt.checkpw(
43
- plain_password.encode('utf-8'),
44
- hashed_password.encode('utf-8')
45
- )
42
+ return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
46
43
 
47
44
  def create_access_token(
48
- self,
49
- user_id: str,
50
- username: str,
51
- role: str,
52
- expires_delta: Optional[timedelta] = None
45
+ self, user_id: str, username: str, role: str, expires_delta: Optional[timedelta] = None
53
46
  ) -> str:
54
47
  """Create a JWT access token"""
55
48
  if expires_delta:
@@ -69,11 +62,7 @@ class AuthManager:
69
62
  encoded_jwt = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
70
63
  return encoded_jwt
71
64
 
72
- def create_refresh_token(
73
- self,
74
- user_id: str,
75
- expires_delta: Optional[timedelta] = None
76
- ) -> str:
65
+ def create_refresh_token(self, user_id: str, expires_delta: Optional[timedelta] = None) -> str:
77
66
  """Create a refresh token"""
78
67
  if expires_delta:
79
68
  expire = datetime.utcnow() + expires_delta
@@ -94,11 +83,7 @@ class AuthManager:
94
83
  def verify_token(self, token: str) -> Optional[TokenData]:
95
84
  """Verify and decode a JWT token"""
96
85
  try:
97
- payload = jwt.decode(
98
- token,
99
- self.secret_key,
100
- algorithms=[self.algorithm]
101
- )
86
+ payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
102
87
 
103
88
  token_data = TokenData(
104
89
  sub=payload.get("sub"),
@@ -124,28 +109,23 @@ class AuthManager:
124
109
  headers={"WWW-Authenticate": "Bearer"},
125
110
  )
126
111
 
127
- async def register_user(
128
- self,
129
- user_data: UserCreate,
130
- db: Session
131
- ) -> User:
112
+ async def register_user(self, user_data: UserCreate, db: Session) -> User:
132
113
  """Register a new user"""
133
114
  # 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()
115
+ existing_user = (
116
+ db.query(User)
117
+ .filter((User.username == user_data.username) | (User.email == user_data.email))
118
+ .first()
119
+ )
138
120
 
139
121
  if existing_user:
140
122
  if existing_user.username == user_data.username:
141
123
  raise HTTPException(
142
- status_code=status.HTTP_400_BAD_REQUEST,
143
- detail="Username already registered"
124
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Username already registered"
144
125
  )
145
126
  else:
146
127
  raise HTTPException(
147
- status_code=status.HTTP_400_BAD_REQUEST,
148
- detail="Email already registered"
128
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered"
149
129
  )
150
130
 
151
131
  # Create new user
@@ -168,15 +148,9 @@ class AuthManager:
168
148
 
169
149
  return new_user
170
150
 
171
- async def authenticate_user(
172
- self,
173
- login_data: UserLogin,
174
- db: Session
175
- ) -> Optional[User]:
151
+ async def authenticate_user(self, login_data: UserLogin, db: Session) -> Optional[User]:
176
152
  """Authenticate a user"""
177
- user = db.query(User).filter(
178
- User.username == login_data.username
179
- ).first()
153
+ user = db.query(User).filter(User.username == login_data.username).first()
180
154
 
181
155
  if not user:
182
156
  return None
@@ -190,11 +164,7 @@ class AuthManager:
190
164
 
191
165
  return user
192
166
 
193
- async def login(
194
- self,
195
- login_data: UserLogin,
196
- db: Session
197
- ) -> TokenResponse:
167
+ async def login(self, login_data: UserLogin, db: Session) -> TokenResponse:
198
168
  """Login user and return tokens"""
199
169
  user = await self.authenticate_user(login_data, db)
200
170
 
@@ -207,15 +177,12 @@ class AuthManager:
207
177
 
208
178
  if not user.is_active:
209
179
  raise HTTPException(
210
- status_code=status.HTTP_403_FORBIDDEN,
211
- detail="User account is disabled"
180
+ status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled"
212
181
  )
213
182
 
214
183
  # Create tokens
215
184
  access_token = self.create_access_token(
216
- user_id=str(user.id),
217
- username=user.username,
218
- role=user.role.value
185
+ user_id=str(user.id), username=user.username, role=user.role.value
219
186
  )
220
187
 
221
188
  refresh_token = self.create_refresh_token(user_id=str(user.id))
@@ -224,26 +191,17 @@ class AuthManager:
224
191
  access_token=access_token,
225
192
  refresh_token=refresh_token,
226
193
  expires_in=self.access_token_expire_minutes * 60,
227
- user=UserResponse.from_orm(user)
194
+ user=UserResponse.from_orm(user),
228
195
  )
229
196
 
230
- async def refresh_access_token(
231
- self,
232
- refresh_token: str,
233
- db: Session
234
- ) -> TokenResponse:
197
+ async def refresh_access_token(self, refresh_token: str, db: Session) -> TokenResponse:
235
198
  """Refresh access token using refresh token"""
236
199
  try:
237
- payload = jwt.decode(
238
- refresh_token,
239
- self.secret_key,
240
- algorithms=[self.algorithm]
241
- )
200
+ payload = jwt.decode(refresh_token, self.secret_key, algorithms=[self.algorithm])
242
201
 
243
202
  if payload.get("type") != "refresh":
244
203
  raise HTTPException(
245
- status_code=status.HTTP_401_UNAUTHORIZED,
246
- detail="Invalid refresh token"
204
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
247
205
  )
248
206
 
249
207
  user_id = payload.get("sub")
@@ -251,39 +209,34 @@ class AuthManager:
251
209
 
252
210
  if not user or not user.is_active:
253
211
  raise HTTPException(
254
- status_code=status.HTTP_401_UNAUTHORIZED,
255
- detail="User not found or disabled"
212
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or disabled"
256
213
  )
257
214
 
258
215
  # Create new access token
259
216
  access_token = self.create_access_token(
260
- user_id=str(user.id),
261
- username=user.username,
262
- role=user.role.value
217
+ user_id=str(user.id), username=user.username, role=user.role.value
263
218
  )
264
219
 
265
220
  return TokenResponse(
266
221
  access_token=access_token,
267
222
  refresh_token=refresh_token, # Return same refresh token
268
223
  expires_in=self.access_token_expire_minutes * 60,
269
- user=UserResponse.from_orm(user)
224
+ user=UserResponse.from_orm(user),
270
225
  )
271
226
 
272
227
  except jwt.ExpiredSignatureError:
273
228
  raise HTTPException(
274
- status_code=status.HTTP_401_UNAUTHORIZED,
275
- detail="Refresh token has expired"
229
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Refresh token has expired"
276
230
  )
277
231
  except jwt.JWTError:
278
232
  raise HTTPException(
279
- status_code=status.HTTP_401_UNAUTHORIZED,
280
- detail="Invalid refresh token"
233
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
281
234
  )
282
235
 
283
236
  async def get_current_user(
284
237
  self,
285
238
  credentials: HTTPAuthorizationCredentials = Depends(security),
286
- db: Session = Depends(get_db)
239
+ db: Session = Depends(get_db),
287
240
  ) -> User:
288
241
  """Get current authenticated user from JWT token"""
289
242
  token = credentials.credentials
@@ -293,28 +246,22 @@ class AuthManager:
293
246
  user = db.query(User).filter(User.id == token_data.sub).first()
294
247
 
295
248
  if not user:
296
- raise HTTPException(
297
- status_code=status.HTTP_404_NOT_FOUND,
298
- detail="User not found"
299
- )
249
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
300
250
 
301
251
  if not user.is_active:
302
252
  raise HTTPException(
303
- status_code=status.HTTP_403_FORBIDDEN,
304
- detail="User account is disabled"
253
+ status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled"
305
254
  )
306
255
 
307
256
  return user
308
257
 
309
258
  def require_role(self, *allowed_roles: UserRole):
310
259
  """Decorator/dependency to require specific roles"""
311
- async def role_checker(
312
- current_user: User = Depends(self.get_current_user)
313
- ) -> User:
260
+
261
+ async def role_checker(current_user: User = Depends(self.get_current_user)) -> User:
314
262
  if current_user.role not in allowed_roles:
315
263
  raise HTTPException(
316
- status_code=status.HTTP_403_FORBIDDEN,
317
- detail="Insufficient permissions"
264
+ status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
318
265
  )
319
266
  return current_user
320
267
 
@@ -333,34 +280,25 @@ get_current_user = auth_manager.get_current_user
333
280
  require_role = auth_manager.require_role
334
281
 
335
282
 
336
- async def get_current_active_user(
337
- current_user: User = Depends(get_current_user)
338
- ) -> User:
283
+ async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
339
284
  """Get current active user"""
340
285
  if not current_user.is_active:
341
- raise HTTPException(
342
- status_code=status.HTTP_403_FORBIDDEN,
343
- detail="Inactive user"
344
- )
286
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user")
345
287
  return current_user
346
288
 
347
289
 
348
- async def get_admin_user(
349
- current_user: User = Depends(get_current_user)
350
- ) -> User:
290
+ async def get_admin_user(current_user: User = Depends(get_current_user)) -> User:
351
291
  """Get current admin user"""
352
292
  if current_user.role != UserRole.ADMIN:
353
- raise HTTPException(
354
- status_code=status.HTTP_403_FORBIDDEN,
355
- detail="Admin access required"
356
- )
293
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
357
294
  return current_user
358
295
 
359
296
 
297
+ import asyncio
298
+
360
299
  # Rate limiting
361
300
  from collections import defaultdict
362
301
  from datetime import datetime, timedelta
363
- import asyncio
364
302
 
365
303
 
366
304
  class RateLimiter:
@@ -379,8 +317,7 @@ class RateLimiter:
379
317
 
380
318
  # Clean old requests
381
319
  self.clients[client_id] = [
382
- req_time for req_time in self.clients[client_id]
383
- if req_time > minute_ago
320
+ req_time for req_time in self.clients[client_id] if req_time > minute_ago
384
321
  ]
385
322
 
386
323
  # Check limit
@@ -400,8 +337,7 @@ class RateLimiter:
400
337
 
401
338
  for client_id in list(self.clients.keys()):
402
339
  self.clients[client_id] = [
403
- req_time for req_time in self.clients[client_id]
404
- if req_time > window_start
340
+ req_time for req_time in self.clients[client_id] if req_time > window_start
405
341
  ]
406
342
 
407
343
  if not self.clients[client_id]:
@@ -418,8 +354,7 @@ async def check_rate_limit(request: Request):
418
354
 
419
355
  if not await rate_limiter.check_rate_limit(client_ip):
420
356
  raise HTTPException(
421
- status_code=status.HTTP_429_TOO_MANY_REQUESTS,
422
- detail="Rate limit exceeded"
357
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Rate limit exceeded"
423
358
  )
424
359
 
425
- return True
360
+ return True
mcli/ml/auth/models.py CHANGED
@@ -1,41 +1,45 @@
1
1
  """Authentication data models"""
2
2
 
3
3
  from datetime import datetime
4
- from typing import Optional, List
5
- from pydantic import BaseModel, EmailStr, Field, validator
4
+ from typing import List, Optional
6
5
  from uuid import UUID
7
6
 
7
+ from pydantic import BaseModel, EmailStr, Field, validator
8
+
8
9
 
9
10
  class UserCreate(BaseModel):
10
11
  """User registration model"""
12
+
11
13
  username: str = Field(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9_-]+$")
12
14
  email: EmailStr
13
15
  password: str = Field(..., min_length=8, max_length=100)
14
16
  first_name: Optional[str] = Field(None, max_length=50)
15
17
  last_name: Optional[str] = Field(None, max_length=50)
16
18
 
17
- @validator('password')
19
+ @validator("password")
18
20
  def validate_password(cls, v):
19
21
  """Ensure password meets security requirements"""
20
22
  if len(v) < 8:
21
- raise ValueError('Password must be at least 8 characters long')
23
+ raise ValueError("Password must be at least 8 characters long")
22
24
  if not any(char.isdigit() for char in v):
23
- raise ValueError('Password must contain at least one digit')
25
+ raise ValueError("Password must contain at least one digit")
24
26
  if not any(char.isupper() for char in v):
25
- raise ValueError('Password must contain at least one uppercase letter')
27
+ raise ValueError("Password must contain at least one uppercase letter")
26
28
  if not any(char.islower() for char in v):
27
- raise ValueError('Password must contain at least one lowercase letter')
29
+ raise ValueError("Password must contain at least one lowercase letter")
28
30
  return v
29
31
 
30
32
 
31
33
  class UserLogin(BaseModel):
32
34
  """User login model"""
35
+
33
36
  username: str
34
37
  password: str
35
38
 
36
39
 
37
40
  class UserResponse(BaseModel):
38
41
  """User response model"""
42
+
39
43
  id: UUID
40
44
  username: str
41
45
  email: str
@@ -53,6 +57,7 @@ class UserResponse(BaseModel):
53
57
 
54
58
  class TokenResponse(BaseModel):
55
59
  """JWT token response"""
60
+
56
61
  access_token: str
57
62
  token_type: str = "Bearer"
58
63
  expires_in: int
@@ -62,6 +67,7 @@ class TokenResponse(BaseModel):
62
67
 
63
68
  class TokenData(BaseModel):
64
69
  """JWT token payload"""
70
+
65
71
  sub: str # User ID
66
72
  username: str
67
73
  role: str
@@ -72,45 +78,50 @@ class TokenData(BaseModel):
72
78
 
73
79
  class PasswordReset(BaseModel):
74
80
  """Password reset request"""
81
+
75
82
  email: EmailStr
76
83
 
77
84
 
78
85
  class PasswordResetConfirm(BaseModel):
79
86
  """Password reset confirmation"""
87
+
80
88
  token: str
81
89
  new_password: str = Field(..., min_length=8, max_length=100)
82
90
 
83
91
 
84
92
  class PasswordChange(BaseModel):
85
93
  """Password change request"""
94
+
86
95
  current_password: str
87
96
  new_password: str = Field(..., min_length=8, max_length=100)
88
97
 
89
- @validator('new_password')
98
+ @validator("new_password")
90
99
  def validate_password(cls, v, values):
91
100
  """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')
101
+ if "current_password" in values and v == values["current_password"]:
102
+ raise ValueError("New password must be different from current password")
94
103
 
95
104
  if len(v) < 8:
96
- raise ValueError('Password must be at least 8 characters long')
105
+ raise ValueError("Password must be at least 8 characters long")
97
106
  if not any(char.isdigit() for char in v):
98
- raise ValueError('Password must contain at least one digit')
107
+ raise ValueError("Password must contain at least one digit")
99
108
  if not any(char.isupper() for char in v):
100
- raise ValueError('Password must contain at least one uppercase letter')
109
+ raise ValueError("Password must contain at least one uppercase letter")
101
110
  if not any(char.islower() for char in v):
102
- raise ValueError('Password must contain at least one lowercase letter')
111
+ raise ValueError("Password must contain at least one lowercase letter")
103
112
  return v
104
113
 
105
114
 
106
115
  class APIKeyCreate(BaseModel):
107
116
  """API key creation model"""
117
+
108
118
  name: str = Field(..., min_length=1, max_length=100)
109
119
  expires_at: Optional[datetime] = None
110
120
 
111
121
 
112
122
  class APIKeyResponse(BaseModel):
113
123
  """API key response"""
124
+
114
125
  key: str
115
126
  name: str
116
127
  created_at: datetime
@@ -120,6 +131,7 @@ class APIKeyResponse(BaseModel):
120
131
 
121
132
  class UserUpdate(BaseModel):
122
133
  """User update model"""
134
+
123
135
  email: Optional[EmailStr] = None
124
136
  first_name: Optional[str] = Field(None, max_length=50)
125
137
  last_name: Optional[str] = Field(None, max_length=50)
@@ -129,6 +141,7 @@ class UserUpdate(BaseModel):
129
141
 
130
142
  class UserPermissions(BaseModel):
131
143
  """User permissions model"""
144
+
132
145
  user_id: UUID
133
146
  permissions: List[str]
134
147
  roles: List[str]
@@ -136,6 +149,7 @@ class UserPermissions(BaseModel):
136
149
 
137
150
  class SessionInfo(BaseModel):
138
151
  """Session information"""
152
+
139
153
  session_id: str
140
154
  user_id: UUID
141
155
  ip_address: str
@@ -147,8 +161,9 @@ class SessionInfo(BaseModel):
147
161
 
148
162
  class LoginAttempt(BaseModel):
149
163
  """Login attempt tracking"""
164
+
150
165
  username: str
151
166
  ip_address: str
152
167
  success: bool
153
168
  timestamp: datetime
154
- failure_reason: Optional[str] = None
169
+ failure_reason: Optional[str] = None
@@ -2,8 +2,8 @@
2
2
 
3
3
  from datetime import datetime
4
4
  from enum import Enum
5
- from typing import List, Set, Dict, Any
6
5
  from functools import wraps
6
+ from typing import Any, Dict, List, Set
7
7
 
8
8
  from fastapi import HTTPException, status
9
9
  from sqlalchemy.orm import Session
@@ -60,7 +60,6 @@ class Permission(Enum):
60
60
  # Role-based permission mapping
61
61
  ROLE_PERMISSIONS: Dict[UserRole, Set[Permission]] = {
62
62
  UserRole.ADMIN: set(Permission), # Admin has all permissions
63
-
64
63
  UserRole.ANALYST: {
65
64
  Permission.MODEL_VIEW,
66
65
  Permission.MODEL_CREATE,
@@ -76,7 +75,6 @@ ROLE_PERMISSIONS: Dict[UserRole, Set[Permission]] = {
76
75
  Permission.DATA_EDIT,
77
76
  Permission.SYSTEM_STATUS,
78
77
  },
79
-
80
78
  UserRole.USER: {
81
79
  Permission.MODEL_VIEW,
82
80
  Permission.PREDICTION_VIEW,
@@ -88,7 +86,6 @@ ROLE_PERMISSIONS: Dict[UserRole, Set[Permission]] = {
88
86
  Permission.DATA_VIEW,
89
87
  Permission.SYSTEM_STATUS,
90
88
  },
91
-
92
89
  UserRole.VIEWER: {
93
90
  Permission.MODEL_VIEW,
94
91
  Permission.PREDICTION_VIEW,
@@ -109,8 +106,7 @@ def check_permission(user: User, permission: Permission) -> None:
109
106
  """Check permission and raise exception if not allowed"""
110
107
  if not has_permission(user, permission):
111
108
  raise HTTPException(
112
- status_code=status.HTTP_403_FORBIDDEN,
113
- detail=f"Permission denied: {permission.value}"
109
+ status_code=status.HTTP_403_FORBIDDEN, detail=f"Permission denied: {permission.value}"
114
110
  )
115
111
 
116
112
 
@@ -121,12 +117,11 @@ def require_permission(permission: Permission):
121
117
  @wraps(func)
122
118
  async def wrapper(*args, **kwargs):
123
119
  # Extract user from kwargs (assumes it's passed as current_user)
124
- current_user = kwargs.get('current_user')
120
+ current_user = kwargs.get("current_user")
125
121
 
126
122
  if not current_user:
127
123
  raise HTTPException(
128
- status_code=status.HTTP_401_UNAUTHORIZED,
129
- detail="Authentication required"
124
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required"
130
125
  )
131
126
 
132
127
  check_permission(current_user, permission)
@@ -143,18 +138,17 @@ def require_any_permission(*permissions: Permission):
143
138
  def decorator(func):
144
139
  @wraps(func)
145
140
  async def wrapper(*args, **kwargs):
146
- current_user = kwargs.get('current_user')
141
+ current_user = kwargs.get("current_user")
147
142
 
148
143
  if not current_user:
149
144
  raise HTTPException(
150
- status_code=status.HTTP_401_UNAUTHORIZED,
151
- detail="Authentication required"
145
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required"
152
146
  )
153
147
 
154
148
  if not any(has_permission(current_user, p) for p in permissions):
155
149
  raise HTTPException(
156
150
  status_code=status.HTTP_403_FORBIDDEN,
157
- detail=f"Permission denied. Required: {[p.value for p in permissions]}"
151
+ detail=f"Permission denied. Required: {[p.value for p in permissions]}",
158
152
  )
159
153
 
160
154
  return await func(*args, **kwargs)
@@ -170,12 +164,11 @@ def require_all_permissions(*permissions: Permission):
170
164
  def decorator(func):
171
165
  @wraps(func)
172
166
  async def wrapper(*args, **kwargs):
173
- current_user = kwargs.get('current_user')
167
+ current_user = kwargs.get("current_user")
174
168
 
175
169
  if not current_user:
176
170
  raise HTTPException(
177
- status_code=status.HTTP_401_UNAUTHORIZED,
178
- detail="Authentication required"
171
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required"
179
172
  )
180
173
 
181
174
  for permission in permissions:
@@ -257,7 +250,7 @@ class AuditLogger:
257
250
  action: str,
258
251
  success: bool,
259
252
  details: Dict[str, Any] = None,
260
- db: Session = None
253
+ db: Session = None,
261
254
  ):
262
255
  """Log access attempt"""
263
256
  log_entry = {
@@ -267,7 +260,7 @@ class AuditLogger:
267
260
  "action": action,
268
261
  "success": success,
269
262
  "timestamp": datetime.utcnow(),
270
- "details": details or {}
263
+ "details": details or {},
271
264
  }
272
265
 
273
266
  # In production, save to database or logging service
@@ -299,4 +292,4 @@ class PermissionGroup:
299
292
  Permission.DATA_EDIT,
300
293
  }
301
294
 
302
- ADMIN = set(Permission) # All permissions
295
+ ADMIN = set(Permission) # All permissions