pylantir 0.1.3__py3-none-any.whl → 0.2.1__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.
pylantir/api_server.py ADDED
@@ -0,0 +1,769 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Author: Milton Camacho
5
+ Date: 2025-11-18
6
+ FastAPI application for Pylantir worklist management.
7
+
8
+ Provides RESTful API endpoints for:
9
+ - GET /worklist: Retrieve worklist items with optional filtering
10
+ - POST /worklist: Create new worklist items
11
+ - PUT /worklist/{id}: Update existing worklist items
12
+ - DELETE /worklist/{id}: Delete worklist items
13
+ - GET /users: List users (admin only)
14
+ - POST /users: Create users (admin only)
15
+ - PUT /users/{id}: Update users (admin only)
16
+ - DELETE /users/{id}: Delete users (admin only)
17
+ """
18
+
19
+ import logging
20
+ from typing import List, Optional, Dict, Any
21
+ from datetime import datetime, timedelta
22
+
23
+ try:
24
+ from fastapi import FastAPI, HTTPException, Depends, status, Query
25
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
26
+ from fastapi.middleware.cors import CORSMiddleware
27
+ from pydantic import BaseModel, validator
28
+ except ImportError:
29
+ raise ImportError(
30
+ "FastAPI dependencies not installed. Install with: pip install pylantir[api]"
31
+ )
32
+
33
+ from sqlalchemy.orm import Session
34
+ from sqlalchemy import and_, or_
35
+ import os
36
+ import json
37
+
38
+ from .db_setup import get_api_db
39
+ from .db_concurrency import ConcurrencyManager, safe_database_transaction, DatabaseBusyError
40
+ from .auth_db_setup import get_auth_db, init_auth_database, create_initial_admin_user
41
+ from .models import WorklistItem
42
+ from .auth_models import User, UserRole
43
+ from .auth_utils import (
44
+ authenticate_user,
45
+ create_access_token,
46
+ verify_token,
47
+ get_password_hash,
48
+ AuthenticationError,
49
+ AuthorizationError
50
+ )
51
+
52
+ lgr = logging.getLogger(__name__)
53
+
54
+
55
+ def get_cors_config():
56
+ """
57
+ Get CORS configuration from environment variables set by CLI.
58
+
59
+ Returns:
60
+ dict: CORS configuration with defaults for security
61
+ """
62
+ # Default secure CORS configuration
63
+ cors_config = {
64
+ "allow_origins": ["http://localhost:3000", "http://localhost:8080"],
65
+ "allow_credentials": True,
66
+ "allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
67
+ "allow_headers": ["*"],
68
+ }
69
+
70
+ # Load from environment variables if set by CLI
71
+ cors_origins = os.getenv("CORS_ALLOWED_ORIGINS")
72
+ if cors_origins:
73
+ try:
74
+ # Parse JSON array from environment variable
75
+ cors_config["allow_origins"] = json.loads(cors_origins)
76
+ except json.JSONDecodeError:
77
+ lgr.warning(f"Invalid CORS origins format: {cors_origins}, using defaults")
78
+
79
+ cors_credentials = os.getenv("CORS_ALLOW_CREDENTIALS")
80
+ if cors_credentials:
81
+ cors_config["allow_credentials"] = cors_credentials.lower() in ("true", "1")
82
+
83
+ cors_methods = os.getenv("CORS_ALLOW_METHODS")
84
+ if cors_methods:
85
+ try:
86
+ cors_config["allow_methods"] = json.loads(cors_methods)
87
+ except json.JSONDecodeError:
88
+ lgr.warning(f"Invalid CORS methods format: {cors_methods}, using defaults")
89
+
90
+ cors_headers = os.getenv("CORS_ALLOW_HEADERS")
91
+ if cors_headers:
92
+ try:
93
+ cors_config["allow_headers"] = json.loads(cors_headers)
94
+ except json.JSONDecodeError:
95
+ lgr.warning(f"Invalid CORS headers format: {cors_headers}, using defaults")
96
+
97
+ return cors_config
98
+
99
+
100
+ # Initialize FastAPI app
101
+ app = FastAPI(
102
+ title="Pylantir API",
103
+ description="RESTful API for DICOM Modality Worklist Management",
104
+ version="0.2.0",
105
+ docs_url="/docs",
106
+ redoc_url="/redoc"
107
+ )
108
+
109
+ # CORS middleware with configurable origins
110
+ cors_config = get_cors_config()
111
+ app.add_middleware(
112
+ CORSMiddleware,
113
+ allow_origins=cors_config["allow_origins"],
114
+ allow_credentials=cors_config["allow_credentials"],
115
+ allow_methods=cors_config["allow_methods"],
116
+ allow_headers=cors_config["allow_headers"],
117
+ )
118
+
119
+ lgr.info(f"CORS configured with origins: {cors_config['allow_origins']}")
120
+
121
+ # Security
122
+ security = HTTPBearer()
123
+
124
+
125
+ # Pydantic models for API
126
+ class WorklistItemResponse(BaseModel):
127
+ """Response model for worklist items."""
128
+ id: int
129
+ study_instance_uid: Optional[str]
130
+ patient_name: Optional[str]
131
+ patient_id: Optional[str]
132
+ patient_birth_date: Optional[str]
133
+ patient_sex: Optional[str]
134
+ patient_weight_lb: Optional[str]
135
+ accession_number: Optional[str]
136
+ referring_physician_name: Optional[str]
137
+ modality: Optional[str]
138
+ study_description: Optional[str]
139
+ scheduled_station_aetitle: Optional[str]
140
+ scheduled_start_date: Optional[str]
141
+ scheduled_start_time: Optional[str]
142
+ performing_physician: Optional[str]
143
+ procedure_description: Optional[str]
144
+ protocol_name: Optional[str]
145
+ station_name: Optional[str]
146
+ performed_procedure_step_status: Optional[str]
147
+
148
+ class Config:
149
+ from_attributes = True
150
+
151
+
152
+ class WorklistItemCreate(BaseModel):
153
+ """Model for creating worklist items."""
154
+ study_instance_uid: Optional[str] = None
155
+ patient_name: str
156
+ patient_id: str
157
+ patient_birth_date: Optional[str] = None
158
+ patient_sex: Optional[str] = None
159
+ patient_weight_lb: Optional[str] = "100"
160
+ accession_number: Optional[str] = None
161
+ referring_physician_name: Optional[str] = None
162
+ modality: Optional[str] = None
163
+ study_description: Optional[str] = None
164
+ scheduled_station_aetitle: Optional[str] = None
165
+ scheduled_start_date: Optional[str] = None
166
+ scheduled_start_time: Optional[str] = None
167
+ performing_physician: Optional[str] = None
168
+ procedure_description: Optional[str] = None
169
+ protocol_name: Optional[str] = None
170
+ station_name: Optional[str] = None
171
+ performed_procedure_step_status: str = "SCHEDULED"
172
+
173
+ @validator('performed_procedure_step_status')
174
+ def validate_status(cls, v):
175
+ allowed_statuses = ['SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'DISCONTINUED']
176
+ if v not in allowed_statuses:
177
+ raise ValueError(f'Status must be one of: {allowed_statuses}')
178
+ return v
179
+
180
+
181
+ class WorklistItemUpdate(BaseModel):
182
+ """Model for updating worklist items."""
183
+ patient_name: Optional[str] = None
184
+ patient_birth_date: Optional[str] = None
185
+ patient_sex: Optional[str] = None
186
+ patient_weight_lb: Optional[str] = None
187
+ referring_physician_name: Optional[str] = None
188
+ modality: Optional[str] = None
189
+ study_description: Optional[str] = None
190
+ scheduled_station_aetitle: Optional[str] = None
191
+ scheduled_start_date: Optional[str] = None
192
+ scheduled_start_time: Optional[str] = None
193
+ performing_physician: Optional[str] = None
194
+ procedure_description: Optional[str] = None
195
+ protocol_name: Optional[str] = None
196
+ station_name: Optional[str] = None
197
+ performed_procedure_step_status: Optional[str] = None
198
+
199
+ @validator('performed_procedure_step_status')
200
+ def validate_status(cls, v):
201
+ if v is not None:
202
+ allowed_statuses = ['SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'DISCONTINUED']
203
+ if v not in allowed_statuses:
204
+ raise ValueError(f'Status must be one of: {allowed_statuses}')
205
+ return v
206
+
207
+
208
+ class UserResponse(BaseModel):
209
+ """Response model for users."""
210
+ id: int
211
+ username: str
212
+ email: Optional[str]
213
+ full_name: Optional[str]
214
+ role: str
215
+ is_active: bool
216
+ created_at: datetime
217
+ last_login: Optional[datetime]
218
+
219
+ class Config:
220
+ from_attributes = True
221
+
222
+
223
+ class UserCreate(BaseModel):
224
+ """Model for creating users."""
225
+ username: str
226
+ email: Optional[str] = None
227
+ full_name: Optional[str] = None
228
+ password: str
229
+ role: str = "read"
230
+
231
+ @validator('role')
232
+ def validate_role(cls, v):
233
+ allowed_roles = ['admin', 'write', 'read']
234
+ if v not in allowed_roles:
235
+ raise ValueError(f'Role must be one of: {allowed_roles}')
236
+ return v
237
+
238
+
239
+ class UserUpdate(BaseModel):
240
+ """Model for updating users."""
241
+ email: Optional[str] = None
242
+ full_name: Optional[str] = None
243
+ password: Optional[str] = None
244
+ role: Optional[str] = None
245
+ is_active: Optional[bool] = None
246
+
247
+ @validator('role')
248
+ def validate_role(cls, v):
249
+ if v is not None:
250
+ allowed_roles = ['admin', 'write', 'read']
251
+ if v not in allowed_roles:
252
+ raise ValueError(f'Role must be one of: {allowed_roles}')
253
+ return v
254
+
255
+
256
+ class Token(BaseModel):
257
+ """Token response model."""
258
+ access_token: str
259
+ token_type: str
260
+
261
+
262
+ class LoginRequest(BaseModel):
263
+ """Login request model."""
264
+ username: str
265
+ password: str
266
+ access_token_expire_minutes: Optional[int] = None
267
+
268
+ @validator('access_token_expire_minutes')
269
+ def validate_expiry_minutes(cls, v):
270
+ if v is not None and v <= 0:
271
+ raise ValueError('access_token_expire_minutes must be a positive integer')
272
+ return v
273
+
274
+
275
+ # Authentication dependency
276
+ async def get_current_user(
277
+ credentials: HTTPAuthorizationCredentials = Depends(security),
278
+ auth_db: Session = Depends(get_auth_db)
279
+ ) -> User:
280
+ """
281
+ Get current authenticated user from JWT token.
282
+
283
+ Args:
284
+ credentials: HTTP Authorization credentials
285
+ auth_db: Authentication database session
286
+
287
+ Returns:
288
+ User: Authenticated user object
289
+
290
+ Raises:
291
+ HTTPException: If authentication fails
292
+ """
293
+ try:
294
+ token = credentials.credentials
295
+ payload = verify_token(token)
296
+
297
+ if payload is None:
298
+ raise HTTPException(
299
+ status_code=status.HTTP_401_UNAUTHORIZED,
300
+ detail="Invalid authentication token",
301
+ headers={"WWW-Authenticate": "Bearer"},
302
+ )
303
+
304
+ username = payload.get("sub")
305
+ if username is None:
306
+ raise HTTPException(
307
+ status_code=status.HTTP_401_UNAUTHORIZED,
308
+ detail="Invalid token payload",
309
+ headers={"WWW-Authenticate": "Bearer"},
310
+ )
311
+
312
+ user = auth_db.query(User).filter(User.username == username).first()
313
+ if user is None or not user.is_active:
314
+ raise HTTPException(
315
+ status_code=status.HTTP_401_UNAUTHORIZED,
316
+ detail="User not found or inactive",
317
+ headers={"WWW-Authenticate": "Bearer"},
318
+ )
319
+
320
+ return user
321
+
322
+ except HTTPException:
323
+ raise
324
+ except Exception as e:
325
+ lgr.error(f"Authentication error: {e}")
326
+ raise HTTPException(
327
+ status_code=status.HTTP_401_UNAUTHORIZED,
328
+ detail="Authentication failed",
329
+ headers={"WWW-Authenticate": "Bearer"},
330
+ )
331
+
332
+
333
+ def require_permission(action: str, resource: str = "worklist"):
334
+ """
335
+ Dependency factory for permission checking.
336
+
337
+ Args:
338
+ action: Required action permission
339
+ resource: Resource type
340
+
341
+ Returns:
342
+ Dependency function
343
+ """
344
+ def permission_checker(current_user: User = Depends(get_current_user)) -> User:
345
+ if not current_user.has_permission(action, resource):
346
+ raise HTTPException(
347
+ status_code=status.HTTP_403_FORBIDDEN,
348
+ detail=f"Insufficient permissions for {action} on {resource}"
349
+ )
350
+ return current_user
351
+
352
+ return permission_checker
353
+
354
+
355
+ # Authentication endpoints
356
+ @app.post("/auth/login", response_model=Token)
357
+ async def login(
358
+ login_data: LoginRequest,
359
+ auth_db: Session = Depends(get_auth_db)
360
+ ):
361
+ """Authenticate user and return access token."""
362
+ try:
363
+ user = authenticate_user(auth_db, login_data.username, login_data.password)
364
+
365
+ if not user:
366
+ raise HTTPException(
367
+ status_code=status.HTTP_401_UNAUTHORIZED,
368
+ detail="Invalid username or password",
369
+ headers={"WWW-Authenticate": "Bearer"},
370
+ )
371
+
372
+ expires_delta = None
373
+ if login_data.access_token_expire_minutes is not None:
374
+ expires_delta = timedelta(minutes=login_data.access_token_expire_minutes)
375
+
376
+ access_token = create_access_token(
377
+ data={"sub": user.username},
378
+ expires_delta=expires_delta
379
+ )
380
+
381
+ lgr.info(f"User {user.username} logged in successfully")
382
+
383
+ return {
384
+ "access_token": access_token,
385
+ "token_type": "bearer"
386
+ }
387
+
388
+ except HTTPException:
389
+ raise
390
+ except Exception as e:
391
+ lgr.error(f"Login error: {e}")
392
+ raise HTTPException(
393
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
394
+ detail="Login failed"
395
+ )
396
+
397
+
398
+ # Worklist endpoints
399
+ @app.get("/worklist", response_model=List[WorklistItemResponse])
400
+ async def get_worklist_items(
401
+ status: Optional[List[str]] = Query(
402
+ default=["SCHEDULED", "IN_PROGRESS"],
403
+ description="Filter by procedure status (SCHEDULED, IN_PROGRESS, COMPLETED, DISCONTINUED)"
404
+ ),
405
+ limit: int = Query(default=100, le=1000, description="Maximum number of items to return"),
406
+ offset: int = Query(default=0, ge=0, description="Number of items to skip"),
407
+ patient_id: Optional[str] = Query(default=None, description="Filter by patient ID"),
408
+ modality: Optional[str] = Query(default=None, description="Filter by modality"),
409
+ current_user: User = Depends(require_permission("read", "worklist")),
410
+ db: Session = Depends(get_api_db)
411
+ ):
412
+ """
413
+ Get worklist items with optional filtering.
414
+
415
+ Requires: read permission on worklist
416
+ """
417
+ try:
418
+ query = db.query(WorklistItem)
419
+
420
+ # Apply filters
421
+ if status:
422
+ query = query.filter(WorklistItem.performed_procedure_step_status.in_(status))
423
+
424
+ if patient_id:
425
+ query = query.filter(WorklistItem.patient_id.ilike(f"%{patient_id}%"))
426
+
427
+ if modality:
428
+ query = query.filter(WorklistItem.modality == modality)
429
+
430
+ # Apply pagination
431
+ items = query.offset(offset).limit(limit).all()
432
+
433
+ lgr.info(f"User {current_user.username} retrieved {len(items)} worklist items")
434
+
435
+ return items
436
+
437
+ except Exception as e:
438
+ lgr.error(f"Error retrieving worklist items: {e}")
439
+ raise HTTPException(
440
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
441
+ detail="Failed to retrieve worklist items"
442
+ )
443
+
444
+
445
+ @app.post("/worklist", response_model=WorklistItemResponse)
446
+ async def create_worklist_item(
447
+ item_data: WorklistItemCreate,
448
+ current_user: User = Depends(require_permission("create", "worklist")),
449
+ db: Session = Depends(get_api_db)
450
+ ):
451
+ """
452
+ Create a new worklist item.
453
+
454
+ Requires: write permission on worklist
455
+ """
456
+ try:
457
+ # Generate study_instance_uid if not provided
458
+ if not item_data.study_instance_uid:
459
+ import uuid
460
+ item_data.study_instance_uid = f"1.2.840.10008.3.1.2.3.4.{uuid.uuid4().int}"
461
+
462
+ # Create database object
463
+ db_item = WorklistItem(**item_data.dict())
464
+
465
+ db.add(db_item)
466
+ db.commit()
467
+ db.refresh(db_item)
468
+
469
+ lgr.info(f"User {current_user.username} created worklist item {db_item.id}")
470
+
471
+ return db_item
472
+
473
+ except Exception as e:
474
+ lgr.error(f"Error creating worklist item: {e}")
475
+ db.rollback()
476
+ raise HTTPException(
477
+ status_code=500,
478
+ detail="Failed to create worklist item"
479
+ )
480
+
481
+
482
+ @app.put("/worklist/{item_id}", response_model=WorklistItemResponse)
483
+ async def update_worklist_item(
484
+ item_id: int,
485
+ item_data: WorklistItemUpdate,
486
+ current_user: User = Depends(require_permission("update", "worklist")),
487
+ db: Session = Depends(get_api_db)
488
+ ):
489
+ """
490
+ Update an existing worklist item.
491
+
492
+ Requires: write permission on worklist
493
+ """
494
+ try:
495
+ db_item = db.query(WorklistItem).filter(WorklistItem.id == item_id).first()
496
+
497
+ if not db_item:
498
+ raise HTTPException(
499
+ status_code=404,
500
+ detail=f"Worklist item {item_id} not found"
501
+ )
502
+
503
+ # Update fields that are provided
504
+ update_data = item_data.dict(exclude_unset=True)
505
+ for field, value in update_data.items():
506
+ setattr(db_item, field, value)
507
+
508
+ db.commit()
509
+ db.refresh(db_item)
510
+
511
+ lgr.info(f"User {current_user.username} updated worklist item {item_id}")
512
+
513
+ return db_item
514
+
515
+ except HTTPException:
516
+ raise
517
+ except Exception as e:
518
+ lgr.error(f"Error updating worklist item {item_id}: {e}")
519
+ db.rollback()
520
+ raise HTTPException(
521
+ status_code=500,
522
+ detail="Failed to update worklist item"
523
+ )
524
+
525
+
526
+ @app.delete("/worklist/{item_id}")
527
+ async def delete_worklist_item(
528
+ item_id: int,
529
+ current_user: User = Depends(require_permission("delete", "worklist")),
530
+ db: Session = Depends(get_api_db)
531
+ ):
532
+ """
533
+ Delete a worklist item.
534
+
535
+ Requires: write permission on worklist
536
+ """
537
+ try:
538
+ db_item = db.query(WorklistItem).filter(WorklistItem.id == item_id).first()
539
+
540
+ if not db_item:
541
+ raise HTTPException(
542
+ status_code=404,
543
+ detail=f"Worklist item {item_id} not found"
544
+ )
545
+
546
+ db.delete(db_item)
547
+ db.commit()
548
+
549
+ lgr.info(f"User {current_user.username} deleted worklist item {item_id}")
550
+
551
+ return {"message": f"Worklist item {item_id} deleted successfully"}
552
+
553
+ except HTTPException:
554
+ raise
555
+ except Exception as e:
556
+ lgr.error(f"Error deleting worklist item {item_id}: {e}")
557
+ db.rollback()
558
+ raise HTTPException(
559
+ status_code=500,
560
+ detail="Failed to delete worklist item"
561
+ )
562
+
563
+
564
+ # User management endpoints (admin only)
565
+ @app.get("/users", response_model=List[UserResponse])
566
+ async def get_users(
567
+ current_user: User = Depends(require_permission("read", "users")),
568
+ auth_db: Session = Depends(get_auth_db)
569
+ ):
570
+ """
571
+ Get list of all users.
572
+
573
+ Requires: admin role
574
+ """
575
+ try:
576
+ users = auth_db.query(User).all()
577
+
578
+ lgr.info(f"Admin {current_user.username} retrieved user list")
579
+
580
+ return users
581
+
582
+ except Exception as e:
583
+ lgr.error(f"Error retrieving users: {e}")
584
+ raise HTTPException(
585
+ status_code=500,
586
+ detail="Failed to retrieve users"
587
+ )
588
+
589
+
590
+ @app.post("/users", response_model=UserResponse)
591
+ async def create_user(
592
+ user_data: UserCreate,
593
+ current_user: User = Depends(require_permission("create", "users")),
594
+ auth_db: Session = Depends(get_auth_db)
595
+ ):
596
+ """
597
+ Create a new user.
598
+
599
+ Requires: admin role
600
+ """
601
+ try:
602
+ # Check if username already exists
603
+ existing_user = auth_db.query(User).filter(User.username == user_data.username).first()
604
+ if existing_user:
605
+ raise HTTPException(
606
+ status_code=400,
607
+ detail="Username already exists"
608
+ )
609
+
610
+ # Hash password
611
+ hashed_password = get_password_hash(user_data.password)
612
+
613
+ # Create user
614
+ db_user = User(
615
+ username=user_data.username,
616
+ email=user_data.email,
617
+ full_name=user_data.full_name,
618
+ hashed_password=hashed_password,
619
+ role=UserRole(user_data.role),
620
+ is_active=True,
621
+ created_at=datetime.utcnow(),
622
+ created_by=current_user.id
623
+ )
624
+
625
+ auth_db.add(db_user)
626
+ auth_db.commit()
627
+ auth_db.refresh(db_user)
628
+
629
+ lgr.info(f"Admin {current_user.username} created user {db_user.username}")
630
+
631
+ return db_user
632
+
633
+ except HTTPException:
634
+ raise
635
+ except Exception as e:
636
+ lgr.error(f"Error creating user: {e}")
637
+ auth_db.rollback()
638
+ raise HTTPException(
639
+ status_code=500,
640
+ detail="Failed to create user"
641
+ )
642
+
643
+
644
+ @app.put("/users/{user_id}", response_model=UserResponse)
645
+ async def update_user(
646
+ user_id: int,
647
+ user_data: UserUpdate,
648
+ current_user: User = Depends(require_permission("update", "users")),
649
+ auth_db: Session = Depends(get_auth_db)
650
+ ):
651
+ """
652
+ Update an existing user.
653
+
654
+ Requires: admin role
655
+ """
656
+ try:
657
+ db_user = auth_db.query(User).filter(User.id == user_id).first()
658
+
659
+ if not db_user:
660
+ raise HTTPException(
661
+ status_code=404,
662
+ detail=f"User {user_id} not found"
663
+ )
664
+
665
+ # Update fields that are provided
666
+ update_data = user_data.dict(exclude_unset=True)
667
+
668
+ # Handle password hashing separately
669
+ if 'password' in update_data:
670
+ update_data['hashed_password'] = get_password_hash(update_data.pop('password'))
671
+
672
+ # Handle role conversion
673
+ if 'role' in update_data:
674
+ update_data['role'] = UserRole(update_data['role'])
675
+
676
+ for field, value in update_data.items():
677
+ setattr(db_user, field, value)
678
+
679
+ auth_db.commit()
680
+ auth_db.refresh(db_user)
681
+
682
+ lgr.info(f"Admin {current_user.username} updated user {db_user.username}")
683
+
684
+ return db_user
685
+
686
+ except HTTPException:
687
+ raise
688
+ except Exception as e:
689
+ lgr.error(f"Error updating user {user_id}: {e}")
690
+ auth_db.rollback()
691
+ raise HTTPException(
692
+ status_code=500,
693
+ detail="Failed to update user"
694
+ )
695
+
696
+
697
+ @app.delete("/users/{user_id}")
698
+ async def delete_user(
699
+ user_id: int,
700
+ current_user: User = Depends(require_permission("delete", "users")),
701
+ auth_db: Session = Depends(get_auth_db)
702
+ ):
703
+ """
704
+ Delete a user.
705
+
706
+ Requires: admin role
707
+ """
708
+ try:
709
+ if user_id == current_user.id:
710
+ raise HTTPException(
711
+ status_code=400,
712
+ detail="Cannot delete your own account"
713
+ )
714
+
715
+ db_user = auth_db.query(User).filter(User.id == user_id).first()
716
+
717
+ if not db_user:
718
+ raise HTTPException(
719
+ status_code=404,
720
+ detail=f"User {user_id} not found"
721
+ )
722
+
723
+ auth_db.delete(db_user)
724
+ auth_db.commit()
725
+
726
+ lgr.info(f"Admin {current_user.username} deleted user {db_user.username}")
727
+
728
+ return {"message": f"User {db_user.username} deleted successfully"}
729
+
730
+ except HTTPException:
731
+ raise
732
+ except Exception as e:
733
+ lgr.error(f"Error deleting user {user_id}: {e}")
734
+ auth_db.rollback()
735
+ raise HTTPException(
736
+ status_code=500,
737
+ detail="Failed to delete user"
738
+ )
739
+
740
+
741
+ # Health check endpoint
742
+ @app.get("/health")
743
+ async def health_check():
744
+ """Health check endpoint."""
745
+ return {"status": "healthy", "timestamp": datetime.utcnow()}
746
+
747
+
748
+ # Initialize authentication database on startup
749
+ @app.on_event("startup")
750
+ async def startup_event():
751
+ """Initialize authentication system on startup."""
752
+ try:
753
+ # Try to load configuration to get users_db_path
754
+ # This will work when started via CLI, but fallback gracefully for direct API startup
755
+ import os
756
+ users_db_path = os.getenv("USERS_DB_PATH") # Set by CLI when config is loaded
757
+
758
+ init_auth_database(users_db_path)
759
+ create_initial_admin_user(users_db_path)
760
+ lgr.info("Pylantir API server started successfully")
761
+ except Exception as e:
762
+ lgr.error(f"Failed to initialize API server: {e}")
763
+ raise
764
+
765
+
766
+ if __name__ == "__main__":
767
+ import uvicorn
768
+ lgr.info("Starting Pylantir API server...")
769
+ uvicorn.run(app, host="0.0.0.0", port=8000)