added comp_systems and all databases
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
quantum-backend:
|
quantum-backend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ./dockerfile_build
|
||||||
environment:
|
environment:
|
||||||
PORT: 1656
|
PORT: 1656
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
|
|||||||
500
dockerfile_build/src/api_endpoint/machine_api.py
Normal file
500
dockerfile_build/src/api_endpoint/machine_api.py
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
from connections.db import get_db
|
||||||
|
from connections.keycloak import (
|
||||||
|
KeycloakAdminService,
|
||||||
|
get_current_token_payload,
|
||||||
|
get_keycloak_admin,
|
||||||
|
)
|
||||||
|
from crud.machine_crud import (
|
||||||
|
create_or_get_computational_system,
|
||||||
|
delete_computational_system,
|
||||||
|
get_computational_system,
|
||||||
|
get_system_status,
|
||||||
|
get_team_computational_systems,
|
||||||
|
get_user_computational_systems,
|
||||||
|
give_system_to_team,
|
||||||
|
remove_system_from_team,
|
||||||
|
update_computational_system,
|
||||||
|
)
|
||||||
|
from crud.team_crud import check_team_permission, get_team
|
||||||
|
from crud.user_crud import get_or_create_user
|
||||||
|
from fastapi import Depends, Query
|
||||||
|
from fastapi.routing import APIRouter
|
||||||
|
from rest_models.machine_models import (
|
||||||
|
ComputationalSystemCreateRequest,
|
||||||
|
ComputationalSystemCreateResponse,
|
||||||
|
ComputationalSystemData,
|
||||||
|
ComputationalSystemDeleteRequest,
|
||||||
|
ComputationalSystemEditRequest,
|
||||||
|
ComputationalSystemListResponse,
|
||||||
|
GiveSystemToTeamRequest,
|
||||||
|
RemoveSystemFromTeamRequest,
|
||||||
|
SystemStatusResponse,
|
||||||
|
SystemTeamData,
|
||||||
|
SystemWithTeamResponse,
|
||||||
|
)
|
||||||
|
from rest_models.team_models import TeamsShortListResponse
|
||||||
|
from rest_models.user_models import UserResponse
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=ComputationalSystemCreateResponse)
|
||||||
|
async def create_or_get_computational_system_request(
|
||||||
|
create_data: ComputationalSystemCreateRequest,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
payload: dict = Depends(get_current_token_payload),
|
||||||
|
) -> ComputationalSystemCreateResponse:
|
||||||
|
"""Create a new computational system or get existing one by name"""
|
||||||
|
keycloak_id = payload.get("sub")
|
||||||
|
|
||||||
|
if not keycloak_id:
|
||||||
|
raise HTTPException(403, "Permission denied")
|
||||||
|
|
||||||
|
system = await create_or_get_computational_system(
|
||||||
|
db=db,
|
||||||
|
user_id=keycloak_id,
|
||||||
|
system_name=create_data.system_name,
|
||||||
|
max_qubits=create_data.max_qubits,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ComputationalSystemCreateResponse(system_id=system.id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("", response_model=ComputationalSystemEditRequest)
|
||||||
|
async def edit_computational_system_request(
|
||||||
|
edit_data: ComputationalSystemEditRequest,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
payload: dict = Depends(get_current_token_payload),
|
||||||
|
) -> ComputationalSystemEditRequest:
|
||||||
|
"""Update computational system details (owner only)"""
|
||||||
|
keycloak_id = payload.get("sub")
|
||||||
|
|
||||||
|
if not keycloak_id:
|
||||||
|
raise HTTPException(403, "Permission denied")
|
||||||
|
|
||||||
|
# Get system first
|
||||||
|
system = await get_computational_system(db, edit_data.system_id)
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(404, "Computational system not found")
|
||||||
|
|
||||||
|
# Check ownership
|
||||||
|
if system.user_id != keycloak_id:
|
||||||
|
raise HTTPException(403, "Only the system owner can update system details")
|
||||||
|
|
||||||
|
updated_system = await update_computational_system(
|
||||||
|
db=db,
|
||||||
|
system_id=system.id,
|
||||||
|
system_name=edit_data.system_name,
|
||||||
|
max_qubits=edit_data.max_qubits,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ComputationalSystemEditRequest(
|
||||||
|
system_id=updated_system.id,
|
||||||
|
system_name=updated_system.system_name,
|
||||||
|
max_qubits=updated_system.max_qubits,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=ComputationalSystemListResponse)
|
||||||
|
async def get_my_systems_request(
|
||||||
|
page_num: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(8, ge=1, le=100),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
payload: dict = Depends(get_current_token_payload),
|
||||||
|
keycloak_admin: KeycloakAdminService = Depends(get_keycloak_admin),
|
||||||
|
) -> ComputationalSystemListResponse:
|
||||||
|
"""Get all computational systems owned by the current user"""
|
||||||
|
keycloak_id = payload.get("sub")
|
||||||
|
|
||||||
|
if not keycloak_id:
|
||||||
|
raise HTTPException(403, "Permission denied")
|
||||||
|
|
||||||
|
offset = (page_num - 1) * page_size
|
||||||
|
|
||||||
|
systems, total_count = await get_user_computational_systems(
|
||||||
|
db=db, user_id=keycloak_id, offset=offset, limit=page_size
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get current user's full data from Keycloak
|
||||||
|
current_user_data = keycloak_admin.get_user_by_id(keycloak_id)
|
||||||
|
db_user = await get_or_create_user(db, keycloak_id)
|
||||||
|
owner_response = UserResponse(
|
||||||
|
keycloak_id=keycloak_id,
|
||||||
|
email=current_user_data.get("email", "") if current_user_data else "",
|
||||||
|
username=current_user_data.get("username", "") if current_user_data else "",
|
||||||
|
profile_picture_path=db_user.profile_picture_path,
|
||||||
|
created_at=db_user.created_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for system in systems:
|
||||||
|
# Build teams list for this system
|
||||||
|
teams_list = []
|
||||||
|
for team_system in system.team_systems:
|
||||||
|
team = await get_team(db, team_system.team_id)
|
||||||
|
if team:
|
||||||
|
teams_list.append(
|
||||||
|
SystemTeamData(
|
||||||
|
team=TeamsShortListResponse(
|
||||||
|
team_id=team.id,
|
||||||
|
team_name=team.name,
|
||||||
|
),
|
||||||
|
num_qubits=team_system.qubits_given,
|
||||||
|
created_at=team_system.created_at,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
SystemWithTeamResponse(
|
||||||
|
system=ComputationalSystemData(
|
||||||
|
id=system.id,
|
||||||
|
system_name=system.system_name,
|
||||||
|
max_qubits=system.max_qubits,
|
||||||
|
status=system.status.name if system.status else None,
|
||||||
|
last_updated=system.last_updated,
|
||||||
|
owner=owner_response,
|
||||||
|
created_at=system.created_at,
|
||||||
|
),
|
||||||
|
teams=teams_list,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ComputationalSystemListResponse(
|
||||||
|
systems=result,
|
||||||
|
cur_page=page_num,
|
||||||
|
total_systems=total_count,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/team", response_model=ComputationalSystemListResponse)
|
||||||
|
async def get_team_systems_request(
|
||||||
|
team_id: int = Query(),
|
||||||
|
page_num: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(10, ge=1),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
payload: dict = Depends(get_current_token_payload),
|
||||||
|
keycloak_admin: KeycloakAdminService = Depends(get_keycloak_admin),
|
||||||
|
) -> ComputationalSystemListResponse:
|
||||||
|
"""Get all computational systems shared with a team (requires team membership)"""
|
||||||
|
keycloak_id = payload.get("sub")
|
||||||
|
|
||||||
|
if not keycloak_id:
|
||||||
|
raise HTTPException(403, "Permission denied")
|
||||||
|
|
||||||
|
# Check if user has access to this team (any permission works)
|
||||||
|
await check_team_permission([], team_id, db, keycloak_id)
|
||||||
|
|
||||||
|
offset = (page_num - 1) * page_size
|
||||||
|
|
||||||
|
systems, total_count = await get_team_computational_systems(
|
||||||
|
db=db, team_id=team_id, offset=offset, limit=page_size
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get team info
|
||||||
|
team = await get_team(db, team_id)
|
||||||
|
if not team:
|
||||||
|
raise HTTPException(404, "Team not found")
|
||||||
|
|
||||||
|
team_short = TeamsShortListResponse(
|
||||||
|
team_id=team.id,
|
||||||
|
team_name=team.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get owner info for each system
|
||||||
|
owner_ids = {system.user_id for system in systems}
|
||||||
|
keycloak_users = {}
|
||||||
|
for uid in owner_ids:
|
||||||
|
user_data = keycloak_admin.get_user_by_id(uid)
|
||||||
|
if user_data:
|
||||||
|
keycloak_users[uid] = user_data
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for system in systems:
|
||||||
|
owner_data = keycloak_users.get(system.user_id, {})
|
||||||
|
db_user = await get_or_create_user(db, system.user_id)
|
||||||
|
owner_response = UserResponse(
|
||||||
|
keycloak_id=system.user_id,
|
||||||
|
email=owner_data.get("email", ""),
|
||||||
|
username=owner_data.get("username", ""),
|
||||||
|
profile_picture_path=db_user.profile_picture_path,
|
||||||
|
created_at=db_user.created_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find the qubits given to this specific team
|
||||||
|
qubits_given = 0
|
||||||
|
created_at = 0
|
||||||
|
for ts in system.team_systems:
|
||||||
|
if ts.team_id == team_id:
|
||||||
|
qubits_given = ts.qubits_given
|
||||||
|
created_at = ts.created_at
|
||||||
|
break
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
SystemWithTeamResponse(
|
||||||
|
system=ComputationalSystemData(
|
||||||
|
id=system.id,
|
||||||
|
system_name=system.system_name,
|
||||||
|
max_qubits=system.max_qubits,
|
||||||
|
status=system.status.name if system.status else None,
|
||||||
|
last_updated=system.last_updated,
|
||||||
|
owner=owner_response,
|
||||||
|
created_at=system.created_at,
|
||||||
|
),
|
||||||
|
teams=[
|
||||||
|
SystemTeamData(
|
||||||
|
team=team_short, num_qubits=qubits_given, created_at=created_at
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ComputationalSystemListResponse(
|
||||||
|
systems=result,
|
||||||
|
cur_page=page_num,
|
||||||
|
total_systems=total_count,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/system", response_model=SystemWithTeamResponse)
|
||||||
|
async def get_computational_system_request(
|
||||||
|
system_id: int = Query(),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
payload: dict = Depends(get_current_token_payload),
|
||||||
|
keycloak_admin: KeycloakAdminService = Depends(get_keycloak_admin),
|
||||||
|
) -> SystemWithTeamResponse:
|
||||||
|
"""Get computational system by ID with all teams that have access (requires ownership or team access)"""
|
||||||
|
keycloak_id = payload.get("sub")
|
||||||
|
|
||||||
|
if not keycloak_id:
|
||||||
|
raise HTTPException(403, "Permission denied")
|
||||||
|
|
||||||
|
system = await get_computational_system(db, system_id)
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(404, "Computational system not found")
|
||||||
|
|
||||||
|
# Get owner info
|
||||||
|
owner_data = keycloak_admin.get_user_by_id(system.user_id)
|
||||||
|
if not owner_data:
|
||||||
|
raise HTTPException(404, "Owner data not found")
|
||||||
|
db_user = await get_or_create_user(db, system.user_id)
|
||||||
|
owner_response = UserResponse(
|
||||||
|
keycloak_id=system.user_id,
|
||||||
|
email=owner_data.get("email", ""),
|
||||||
|
username=owner_data.get("username", ""),
|
||||||
|
profile_picture_path=db_user.profile_picture_path,
|
||||||
|
created_at=db_user.created_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user owns the system
|
||||||
|
if system.user_id == keycloak_id:
|
||||||
|
# Owner has full access - build complete team list
|
||||||
|
teams_list = []
|
||||||
|
for team_system in system.team_systems:
|
||||||
|
team = await get_team(db, team_system.team_id)
|
||||||
|
if team:
|
||||||
|
teams_list.append(
|
||||||
|
SystemTeamData(
|
||||||
|
team=TeamsShortListResponse(
|
||||||
|
team_id=team.id,
|
||||||
|
team_name=team.name,
|
||||||
|
),
|
||||||
|
num_qubits=team_system.qubits_given,
|
||||||
|
created_at=team_system.created_at,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return SystemWithTeamResponse(
|
||||||
|
system=ComputationalSystemData(
|
||||||
|
id=system.id,
|
||||||
|
system_name=system.system_name,
|
||||||
|
max_qubits=system.max_qubits,
|
||||||
|
status=system.status.name if system.status else None,
|
||||||
|
last_updated=system.last_updated,
|
||||||
|
owner=owner_response,
|
||||||
|
created_at=system.created_at,
|
||||||
|
),
|
||||||
|
teams=teams_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user has access through any team
|
||||||
|
has_access = False
|
||||||
|
accessible_teams = []
|
||||||
|
for team_system in system.team_systems:
|
||||||
|
try:
|
||||||
|
# Check if user is a member of this team
|
||||||
|
await check_team_permission([], team_system.team_id, db, keycloak_id)
|
||||||
|
has_access = True
|
||||||
|
team = await get_team(db, team_system.team_id)
|
||||||
|
if team:
|
||||||
|
accessible_teams.append(
|
||||||
|
SystemTeamData(
|
||||||
|
team=TeamsShortListResponse(
|
||||||
|
team_id=team.id,
|
||||||
|
team_name=team.name,
|
||||||
|
),
|
||||||
|
num_qubits=team_system.qubits_given,
|
||||||
|
created_at=team_system.created_at,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except HTTPException:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not has_access:
|
||||||
|
raise HTTPException(403, "You don't have access to this computational system")
|
||||||
|
|
||||||
|
return SystemWithTeamResponse(
|
||||||
|
system=ComputationalSystemData(
|
||||||
|
id=system.id,
|
||||||
|
system_name=system.system_name,
|
||||||
|
max_qubits=system.max_qubits,
|
||||||
|
status=system.status.name if system.status else None,
|
||||||
|
last_updated=system.last_updated,
|
||||||
|
owner=owner_response,
|
||||||
|
created_at=system.created_at,
|
||||||
|
),
|
||||||
|
teams=accessible_teams,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/status", response_model=SystemStatusResponse)
|
||||||
|
async def get_system_status_request(
|
||||||
|
system_id: int = Query(),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
payload: dict = Depends(get_current_token_payload),
|
||||||
|
) -> SystemStatusResponse:
|
||||||
|
"""Get the status of a computational system (requires ownership or team access)"""
|
||||||
|
keycloak_id = payload.get("sub")
|
||||||
|
|
||||||
|
if not keycloak_id:
|
||||||
|
raise HTTPException(403, "Permission denied")
|
||||||
|
|
||||||
|
system = await get_computational_system(db, system_id)
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(404, "Computational system not found")
|
||||||
|
|
||||||
|
# Check ownership or team access
|
||||||
|
if system.user_id != keycloak_id:
|
||||||
|
has_access = False
|
||||||
|
for team_system in system.team_systems:
|
||||||
|
try:
|
||||||
|
await check_team_permission([], team_system.team_id, db, keycloak_id)
|
||||||
|
has_access = True
|
||||||
|
break
|
||||||
|
except HTTPException:
|
||||||
|
continue
|
||||||
|
if not has_access:
|
||||||
|
raise HTTPException(
|
||||||
|
403, "You don't have access to this computational system"
|
||||||
|
)
|
||||||
|
|
||||||
|
status = await get_system_status(db, system.id)
|
||||||
|
|
||||||
|
return SystemStatusResponse(
|
||||||
|
system_id=system.id,
|
||||||
|
status=status.name,
|
||||||
|
description=status.description,
|
||||||
|
last_updated=system.last_updated,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/team", response_model=GiveSystemToTeamRequest)
|
||||||
|
async def give_system_to_team_request(
|
||||||
|
give_data: GiveSystemToTeamRequest,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
payload: dict = Depends(get_current_token_payload),
|
||||||
|
) -> GiveSystemToTeamRequest:
|
||||||
|
"""Give a team access to a computational system (requires manage_machines permission in the team)"""
|
||||||
|
keycloak_id = payload.get("sub")
|
||||||
|
|
||||||
|
if not keycloak_id:
|
||||||
|
raise HTTPException(403, "Permission denied")
|
||||||
|
|
||||||
|
# Check if user has manage_machines permission in the team
|
||||||
|
await check_team_permission(["manage_machines"], give_data.team_id, db, keycloak_id)
|
||||||
|
|
||||||
|
# Get system and verify ownership
|
||||||
|
system = await get_computational_system(db, give_data.system_id)
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(404, "Computational system not found")
|
||||||
|
|
||||||
|
if system.user_id != keycloak_id:
|
||||||
|
raise HTTPException(403, "Only the system owner can grant team access")
|
||||||
|
|
||||||
|
team_system = await give_system_to_team(
|
||||||
|
db=db,
|
||||||
|
system_id=system.id,
|
||||||
|
team_id=give_data.team_id,
|
||||||
|
qubits_given=give_data.qubits_given,
|
||||||
|
)
|
||||||
|
|
||||||
|
return GiveSystemToTeamRequest(
|
||||||
|
system_id=team_system.system_id,
|
||||||
|
team_id=team_system.team_id,
|
||||||
|
qubits_given=team_system.qubits_given,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/team")
|
||||||
|
async def remove_system_from_team_request(
|
||||||
|
remove_data: RemoveSystemFromTeamRequest,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
payload: dict = Depends(get_current_token_payload),
|
||||||
|
):
|
||||||
|
"""Remove a system's access from a team (requires manage_machines permission in the team)"""
|
||||||
|
keycloak_id = payload.get("sub")
|
||||||
|
|
||||||
|
if not keycloak_id:
|
||||||
|
raise HTTPException(403, "Permission denied")
|
||||||
|
|
||||||
|
# Check if user has manage_machines permission in the team
|
||||||
|
await check_team_permission(
|
||||||
|
["manage_machines"], remove_data.team_id, db, keycloak_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get system and verify ownership
|
||||||
|
system = await get_computational_system(db, remove_data.system_id)
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(404, "Computational system not found")
|
||||||
|
|
||||||
|
deleted = await remove_system_from_team(
|
||||||
|
db=db,
|
||||||
|
system_id=system.id,
|
||||||
|
team_id=remove_data.team_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(404, "Team access not found for this system")
|
||||||
|
|
||||||
|
return {"message": "System access removed from team successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("")
|
||||||
|
async def delete_computational_system_request(
|
||||||
|
delete_data: ComputationalSystemDeleteRequest,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
payload: dict = Depends(get_current_token_payload),
|
||||||
|
):
|
||||||
|
"""Delete a computational system (owner only)"""
|
||||||
|
keycloak_id = payload.get("sub")
|
||||||
|
|
||||||
|
if not keycloak_id:
|
||||||
|
raise HTTPException(403, "Permission denied")
|
||||||
|
|
||||||
|
system = await get_computational_system(db, delete_data.system_id)
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(404, "Computational system not found")
|
||||||
|
|
||||||
|
# Check ownership
|
||||||
|
if system.user_id != keycloak_id:
|
||||||
|
raise HTTPException(403, "Only the system owner can delete the system")
|
||||||
|
|
||||||
|
deleted = await delete_computational_system(db, system.id)
|
||||||
|
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(404, "Computational system not found")
|
||||||
|
|
||||||
|
return {"message": "Computational system deleted successfully"}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import math
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from connections.db import get_db
|
from connections.db import get_db
|
||||||
@@ -13,7 +12,6 @@ from crud.team_crud import (
|
|||||||
create_team,
|
create_team,
|
||||||
delete_team,
|
delete_team,
|
||||||
delete_team_member,
|
delete_team_member,
|
||||||
get_team,
|
|
||||||
get_user_teams,
|
get_user_teams,
|
||||||
update_team,
|
update_team,
|
||||||
)
|
)
|
||||||
@@ -175,6 +173,7 @@ async def get_team_list_request(
|
|||||||
async def get_team_short_list_request(
|
async def get_team_short_list_request(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
payload: dict = Depends(get_current_token_payload),
|
payload: dict = Depends(get_current_token_payload),
|
||||||
|
permission: str | None = None, # Optional query parameter
|
||||||
) -> List[TeamsShortListResponse]:
|
) -> List[TeamsShortListResponse]:
|
||||||
"""Get all teams for the current user with user details from Keycloak"""
|
"""Get all teams for the current user with user details from Keycloak"""
|
||||||
keycloak_id = payload.get("sub")
|
keycloak_id = payload.get("sub")
|
||||||
@@ -182,7 +181,7 @@ async def get_team_short_list_request(
|
|||||||
if not keycloak_id:
|
if not keycloak_id:
|
||||||
raise HTTPException(403, "permission denied")
|
raise HTTPException(403, "permission denied")
|
||||||
|
|
||||||
[teams, count] = await get_user_teams(db, keycloak_id, 0, 256)
|
teams, count = await get_user_teams(db, keycloak_id, 0, 256, permission)
|
||||||
|
|
||||||
return list(TeamsShortListResponse(team_id=i.id, team_name=i.name) for i in teams)
|
return list(TeamsShortListResponse(team_id=i.id, team_name=i.name) for i in teams)
|
||||||
|
|
||||||
54
dockerfile_build/src/app.py
Executable file
54
dockerfile_build/src/app.py
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from api_endpoint.health_api import router as health_router
|
||||||
|
from api_endpoint.machine_api import router as machine_router
|
||||||
|
from api_endpoint.teams_api import router as team_router
|
||||||
|
from api_endpoint.user_api import router as user_router
|
||||||
|
from config.seeding import seed_permissions, seed_system_statuses
|
||||||
|
from connections.db import create_tables, engine
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
# from fastapi.requests import Request
|
||||||
|
from fastapi_cache import FastAPICache
|
||||||
|
from fastapi_cache.backends.redis import RedisBackend
|
||||||
|
from redis import asyncio as aioredis
|
||||||
|
from sqlalchemy.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||||
|
redis = aioredis.from_url("redis://redis:6379")
|
||||||
|
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
|
||||||
|
|
||||||
|
await create_tables()
|
||||||
|
async with AsyncSession(engine) as session:
|
||||||
|
await seed_permissions(session)
|
||||||
|
await seed_system_statuses(session)
|
||||||
|
|
||||||
|
yield
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
|
||||||
|
origins = [
|
||||||
|
"http://localhost",
|
||||||
|
"http://localhost:8001",
|
||||||
|
]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
app.include_router(health_router, prefix="", tags=["Health"])
|
||||||
|
app.include_router(user_router, prefix="/user", tags=["User"])
|
||||||
|
app.include_router(team_router, prefix="/team", tags=["Team"])
|
||||||
|
app.include_router(machine_router, prefix="/machine", tags=["Machine"])
|
||||||
80
dockerfile_build/src/config/seeding.py
Normal file
80
dockerfile_build/src/config/seeding.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from config.logging_config import logger
|
||||||
|
from sql_models.models import Permission, SystemStatus
|
||||||
|
from sqlalchemy.ext.asyncio.session import AsyncSession
|
||||||
|
from sqlalchemy.sql import select
|
||||||
|
|
||||||
|
DEFAULT_PERMISSIONS = [
|
||||||
|
{"id": 1, "name": "edit_team", "description": "Can edit team name and description"},
|
||||||
|
{"id": 2, "name": "delete_team", "description": "Can delete the team"},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "manage_members",
|
||||||
|
"description": "Can add and remove team members",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "manage_machines",
|
||||||
|
"description": "Can add and remove team machines",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "create_experiment",
|
||||||
|
"description": "Can create and run experiments",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
DEFAULT_STATUSES = [
|
||||||
|
{
|
||||||
|
"name": "ONLINE",
|
||||||
|
"description": "System is fully operational and available for use",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OFFLINE",
|
||||||
|
"description": "System is currently offline or undergoing maintenance",
|
||||||
|
},
|
||||||
|
{"name": "BUSY", "description": "System is busy processing other tasks"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def seed_permissions(db: AsyncSession) -> bool:
|
||||||
|
"""Seed default permissions into the database."""
|
||||||
|
try:
|
||||||
|
result = await db.execute(select(Permission).limit(1))
|
||||||
|
if result.scalar_one_or_none():
|
||||||
|
logger.info("Permissions already seeded, skipping...")
|
||||||
|
return False
|
||||||
|
|
||||||
|
permissions = [
|
||||||
|
Permission(name=perm["name"], description=perm["description"])
|
||||||
|
for perm in DEFAULT_PERMISSIONS
|
||||||
|
]
|
||||||
|
db.add_all(permissions)
|
||||||
|
await db.commit()
|
||||||
|
logger.info(f"Seeded {len(permissions)} permissions")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error seeding permissions: {e}")
|
||||||
|
await db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def seed_system_statuses(db: AsyncSession) -> bool:
|
||||||
|
"""Seed default system statuses into the database."""
|
||||||
|
try:
|
||||||
|
result = await db.execute(select(SystemStatus).limit(1))
|
||||||
|
if result.scalar_one_or_none():
|
||||||
|
logger.info("System statuses already seeded, skipping...")
|
||||||
|
return False
|
||||||
|
|
||||||
|
statuses = [
|
||||||
|
SystemStatus(name=status["name"], description=status["description"])
|
||||||
|
for status in DEFAULT_STATUSES
|
||||||
|
]
|
||||||
|
db.add_all(statuses)
|
||||||
|
await db.commit()
|
||||||
|
logger.info(f"Seeded {len(statuses)} system statuses")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error seeding system statuses: {e}")
|
||||||
|
await db.rollback()
|
||||||
|
raise
|
||||||
345
dockerfile_build/src/crud/machine_crud.py
Normal file
345
dockerfile_build/src/crud/machine_crud.py
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from sql_models.models import ComputationalSystem, SystemStatus, Team, TeamSystem, User
|
||||||
|
from sqlalchemy import and_, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from sqlalchemy.sql.expression import func
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
# Helper function to get status by name
|
||||||
|
async def get_status_by_name(db: AsyncSession, status_name: str) -> SystemStatus:
|
||||||
|
"""Get system status by name, raise exception if not found"""
|
||||||
|
result = await db.execute(
|
||||||
|
select(SystemStatus).where(SystemStatus.name == status_name)
|
||||||
|
)
|
||||||
|
status = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not status:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Invalid status name: {status_name}. Valid statuses: ONLINE, OFFLINE, BUSY",
|
||||||
|
)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
async def create_or_get_computational_system(
|
||||||
|
db: AsyncSession,
|
||||||
|
user_id: str,
|
||||||
|
system_name: str,
|
||||||
|
max_qubits: int,
|
||||||
|
) -> ComputationalSystem:
|
||||||
|
"""Create a new computational system or return existing one by name"""
|
||||||
|
|
||||||
|
# Check if system with this name already exists
|
||||||
|
existing_result = await db.execute(
|
||||||
|
select(ComputationalSystem).where(
|
||||||
|
ComputationalSystem.system_name == system_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
existing = existing_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
# Get the status (will raise exception if invalid)
|
||||||
|
status = await get_status_by_name(db, "ONLINE")
|
||||||
|
|
||||||
|
# Create new system
|
||||||
|
system = ComputationalSystem(
|
||||||
|
user_id=user_id,
|
||||||
|
system_name=system_name,
|
||||||
|
status_id=status.id,
|
||||||
|
max_qubits=max_qubits,
|
||||||
|
last_updated=datetime.now(),
|
||||||
|
)
|
||||||
|
db.add(system)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(system, attribute_names=["user", "status"])
|
||||||
|
|
||||||
|
return system
|
||||||
|
|
||||||
|
|
||||||
|
async def update_computational_system(
|
||||||
|
db: AsyncSession,
|
||||||
|
system_id: int,
|
||||||
|
system_name: Optional[str] = None,
|
||||||
|
max_qubits: Optional[int] = None,
|
||||||
|
) -> ComputationalSystem:
|
||||||
|
"""Update a computational system (PUT semantics)"""
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(ComputationalSystem).where(ComputationalSystem.id == system_id)
|
||||||
|
)
|
||||||
|
system = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(status_code=404, detail="Computational system not found")
|
||||||
|
|
||||||
|
if system_name is not None:
|
||||||
|
# Check if new name conflicts with existing system
|
||||||
|
name_check = await db.execute(
|
||||||
|
select(ComputationalSystem).where(
|
||||||
|
and_(
|
||||||
|
ComputationalSystem.system_name == system_name,
|
||||||
|
ComputationalSystem.id != system_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if name_check.scalar_one_or_none():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail=f"System with name '{system_name}' already exists",
|
||||||
|
)
|
||||||
|
system.system_name = system_name
|
||||||
|
|
||||||
|
if max_qubits is not None:
|
||||||
|
system.max_qubits = max_qubits
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(system, attribute_names=["user", "status"])
|
||||||
|
|
||||||
|
return system
|
||||||
|
|
||||||
|
|
||||||
|
async def get_computational_system(
|
||||||
|
db: AsyncSession, system_id: int
|
||||||
|
) -> Optional[ComputationalSystem]:
|
||||||
|
"""Get computational system by ID with relationships loaded"""
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(ComputationalSystem)
|
||||||
|
.where(ComputationalSystem.id == system_id)
|
||||||
|
.options(
|
||||||
|
selectinload(ComputationalSystem.user),
|
||||||
|
selectinload(ComputationalSystem.status),
|
||||||
|
selectinload(ComputationalSystem.team_systems).selectinload(
|
||||||
|
TeamSystem.team
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_computational_systems(
|
||||||
|
db: AsyncSession,
|
||||||
|
user_id: str,
|
||||||
|
offset: int = 0,
|
||||||
|
limit: int = 256,
|
||||||
|
) -> tuple[List[ComputationalSystem], int]:
|
||||||
|
"""Get paginated computational systems owned by a user, optionally filtered by status"""
|
||||||
|
|
||||||
|
# Build query
|
||||||
|
query = select(ComputationalSystem).where(ComputationalSystem.user_id == user_id)
|
||||||
|
|
||||||
|
# Get paginated systems
|
||||||
|
result = await db.execute(
|
||||||
|
query.options(
|
||||||
|
selectinload(ComputationalSystem.status),
|
||||||
|
selectinload(ComputationalSystem.team_systems),
|
||||||
|
)
|
||||||
|
.order_by(ComputationalSystem.last_updated.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
systems = list(result.scalars().all())
|
||||||
|
|
||||||
|
# Get total count
|
||||||
|
count_query = (
|
||||||
|
select(func.count())
|
||||||
|
.select_from(ComputationalSystem)
|
||||||
|
.where(ComputationalSystem.user_id == user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
count_result = await db.execute(count_query)
|
||||||
|
total_count = count_result.scalar()
|
||||||
|
|
||||||
|
return systems, total_count if total_count else 0
|
||||||
|
|
||||||
|
|
||||||
|
async def get_team_computational_systems(
|
||||||
|
db: AsyncSession,
|
||||||
|
team_id: int,
|
||||||
|
offset: int = 0,
|
||||||
|
limit: int = 256,
|
||||||
|
) -> tuple[List[ComputationalSystem], int]:
|
||||||
|
"""Get computational systems shared with a team, optionally filtered by status"""
|
||||||
|
|
||||||
|
# Build query
|
||||||
|
query = (
|
||||||
|
select(ComputationalSystem)
|
||||||
|
.join(TeamSystem)
|
||||||
|
.where(TeamSystem.team_id == team_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get paginated systems
|
||||||
|
result = await db.execute(
|
||||||
|
query.options(
|
||||||
|
selectinload(ComputationalSystem.user),
|
||||||
|
selectinload(ComputationalSystem.status),
|
||||||
|
selectinload(ComputationalSystem.team_systems),
|
||||||
|
)
|
||||||
|
.order_by(ComputationalSystem.last_updated.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
systems = list(result.scalars().all())
|
||||||
|
|
||||||
|
# Get total count
|
||||||
|
count_query = (
|
||||||
|
select(func.count())
|
||||||
|
.select_from(ComputationalSystem)
|
||||||
|
.join(TeamSystem)
|
||||||
|
.where(TeamSystem.team_id == team_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
count_result = await db.execute(count_query)
|
||||||
|
total_count = count_result.scalar()
|
||||||
|
|
||||||
|
return systems, total_count if total_count else 0
|
||||||
|
|
||||||
|
|
||||||
|
async def give_system_to_team(
|
||||||
|
db: AsyncSession,
|
||||||
|
system_id: int,
|
||||||
|
team_id: int,
|
||||||
|
qubits_given: int,
|
||||||
|
) -> TeamSystem:
|
||||||
|
"""Give a team access to a computational system (PUT semantics)"""
|
||||||
|
|
||||||
|
# Verify system exists
|
||||||
|
system_result = await db.execute(
|
||||||
|
select(ComputationalSystem).where(ComputationalSystem.id == system_id)
|
||||||
|
)
|
||||||
|
system = system_result.scalar_one_or_none()
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(status_code=404, detail="Computational system not found")
|
||||||
|
|
||||||
|
# Verify team exists
|
||||||
|
team_result = await db.execute(select(Team).where(Team.id == team_id))
|
||||||
|
team = team_result.scalar_one_or_none()
|
||||||
|
if not team:
|
||||||
|
raise HTTPException(status_code=404, detail="Team not found")
|
||||||
|
|
||||||
|
# Verify qubits_given doesn't exceed system's max_qubits
|
||||||
|
if qubits_given > system.max_qubits:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Cannot give {qubits_given} qubits. System only has {system.max_qubits} qubits available.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if relationship already exists
|
||||||
|
existing_result = await db.execute(
|
||||||
|
select(TeamSystem).where(
|
||||||
|
and_(TeamSystem.system_id == system_id, TeamSystem.team_id == team_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
team_system = existing_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if team_system:
|
||||||
|
# Update existing relationship
|
||||||
|
team_system.qubits_given = qubits_given
|
||||||
|
else:
|
||||||
|
# Create new relationship
|
||||||
|
team_system = TeamSystem(
|
||||||
|
team_id=team_id, system_id=system_id, qubits_given=qubits_given
|
||||||
|
)
|
||||||
|
db.add(team_system)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(team_system, attribute_names=["team", "system"])
|
||||||
|
|
||||||
|
return team_system
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_system_from_team(
|
||||||
|
db: AsyncSession,
|
||||||
|
system_id: int,
|
||||||
|
team_id: int,
|
||||||
|
) -> bool:
|
||||||
|
"""Remove a system's access from a team (DELETE)"""
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(TeamSystem).where(
|
||||||
|
and_(TeamSystem.system_id == system_id, TeamSystem.team_id == team_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
team_system = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not team_system:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await db.delete(team_system)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_computational_system(
|
||||||
|
db: AsyncSession,
|
||||||
|
system_id: int,
|
||||||
|
) -> bool:
|
||||||
|
"""Delete a computational system"""
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(ComputationalSystem).where(ComputationalSystem.id == system_id)
|
||||||
|
)
|
||||||
|
system = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not system:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Cascade will delete TeamSystem entries automatically
|
||||||
|
await db.delete(system)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def get_system_status(
|
||||||
|
db: AsyncSession,
|
||||||
|
system_id: int,
|
||||||
|
) -> SystemStatus:
|
||||||
|
"""Get the status of a computational system"""
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(ComputationalSystem)
|
||||||
|
.where(ComputationalSystem.id == system_id)
|
||||||
|
.options(selectinload(ComputationalSystem.status))
|
||||||
|
)
|
||||||
|
system = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(status_code=404, detail="Computational system not found")
|
||||||
|
|
||||||
|
return system.status
|
||||||
|
|
||||||
|
|
||||||
|
async def update_system_status(
|
||||||
|
db: AsyncSession,
|
||||||
|
system_id: int,
|
||||||
|
status_name: str,
|
||||||
|
) -> ComputationalSystem:
|
||||||
|
|
||||||
|
# Get the system
|
||||||
|
result = await db.execute(
|
||||||
|
select(ComputationalSystem).where(ComputationalSystem.id == system_id)
|
||||||
|
)
|
||||||
|
system = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(status_code=404, detail="Computational system not found")
|
||||||
|
|
||||||
|
# Get the status (will raise exception if invalid)
|
||||||
|
status = await get_status_by_name(db, status_name)
|
||||||
|
|
||||||
|
# Update system
|
||||||
|
system.status_id = status.id
|
||||||
|
system.last_updated = datetime.now()
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(system, attribute_names=["user", "status"])
|
||||||
|
|
||||||
|
return system
|
||||||
@@ -81,16 +81,36 @@ async def get_team(db: AsyncSession, team_id: int) -> Optional[Team]:
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_teams(
|
async def get_user_teams(
|
||||||
db: AsyncSession, user_id: str, offset: int, limit: int
|
db: AsyncSession,
|
||||||
|
user_id: str,
|
||||||
|
offset: int,
|
||||||
|
limit: int,
|
||||||
|
permission: str | None = None,
|
||||||
) -> tuple[List[Team], int | None]:
|
) -> tuple[List[Team], int | None]:
|
||||||
"""Get paginated teams a user belongs to and return total count"""
|
|
||||||
|
# Base query for teams
|
||||||
|
query = select(Team).join(TeamMember).where(TeamMember.user_id == user_id)
|
||||||
|
|
||||||
|
count_query = (
|
||||||
|
select(func.count())
|
||||||
|
.select_from(Team)
|
||||||
|
.join(TeamMember)
|
||||||
|
.where(TeamMember.user_id == user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add permission filter if provided
|
||||||
|
if permission:
|
||||||
|
# Join with permissions table to filter by specific permission
|
||||||
|
query = query.join(TeamMember.permissions).where(Permission.name == permission)
|
||||||
|
count_query = count_query.join(TeamMember.permissions).where(
|
||||||
|
Permission.name == permission
|
||||||
|
)
|
||||||
|
|
||||||
# Get paginated teams
|
# Get paginated teams
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Team)
|
query.options(
|
||||||
.join(TeamMember)
|
selectinload(Team.team_memberships).selectinload(TeamMember.permissions)
|
||||||
.where(TeamMember.user_id == user_id)
|
)
|
||||||
.options(selectinload(Team.team_memberships))
|
|
||||||
.order_by(Team.created_at.desc())
|
.order_by(Team.created_at.desc())
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
@@ -98,12 +118,7 @@ async def get_user_teams(
|
|||||||
teams = list(result.scalars().all())
|
teams = list(result.scalars().all())
|
||||||
|
|
||||||
# Get total count of teams for this user
|
# Get total count of teams for this user
|
||||||
count_result = await db.execute(
|
count_result = await db.execute(count_query)
|
||||||
select(func.count())
|
|
||||||
.select_from(Team)
|
|
||||||
.join(TeamMember)
|
|
||||||
.where(TeamMember.user_id == user_id)
|
|
||||||
)
|
|
||||||
total_count = count_result.scalar()
|
total_count = count_result.scalar()
|
||||||
|
|
||||||
return teams, total_count
|
return teams, total_count
|
||||||
@@ -162,7 +177,7 @@ async def get_team_members(db: AsyncSession, team_id: int) -> List[TeamMember]:
|
|||||||
return list(result.scalars().all())
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
async def delete_team_member(db: AsyncSession, team_id: int, user_id: str) -> None:
|
async def delete_team_member(db: AsyncSession, team_id: int, user_id: str) -> bool:
|
||||||
"""Remove a user from a team"""
|
"""Remove a user from a team"""
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(TeamMember).where(
|
select(TeamMember).where(
|
||||||
@@ -174,8 +189,8 @@ async def delete_team_member(db: AsyncSession, team_id: int, user_id: str) -> No
|
|||||||
if team_member:
|
if team_member:
|
||||||
await db.delete(team_member)
|
await db.delete(team_member)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return 1
|
return True
|
||||||
return 0
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def update_team_member_permissions(
|
async def update_team_member_permissions(
|
||||||
74
dockerfile_build/src/rest_models/machine_models.py
Normal file
74
dockerfile_build/src/rest_models/machine_models.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from rest_models.team_models import (
|
||||||
|
TeamsShortListResponse,
|
||||||
|
)
|
||||||
|
from rest_models.user_models import UserResponse
|
||||||
|
|
||||||
|
|
||||||
|
class ComputationalSystemCreateRequest(BaseModel):
|
||||||
|
system_name: str
|
||||||
|
max_qubits: int
|
||||||
|
status_name: Optional[str] = "ONLINE"
|
||||||
|
|
||||||
|
|
||||||
|
class ComputationalSystemCreateResponse(BaseModel):
|
||||||
|
system_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class ComputationalSystemEditRequest(BaseModel):
|
||||||
|
system_id: int
|
||||||
|
system_name: Optional[str] = None
|
||||||
|
max_qubits: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ComputationalSystemData(BaseModel):
|
||||||
|
id: int
|
||||||
|
system_name: str
|
||||||
|
max_qubits: int
|
||||||
|
status: Optional[str] = None
|
||||||
|
last_updated: datetime
|
||||||
|
owner: UserResponse
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class SystemTeamData(BaseModel):
|
||||||
|
team: TeamsShortListResponse
|
||||||
|
num_qubits: int
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class SystemWithTeamResponse(BaseModel):
|
||||||
|
system: ComputationalSystemData
|
||||||
|
teams: List[SystemTeamData]
|
||||||
|
|
||||||
|
|
||||||
|
class ComputationalSystemListResponse(BaseModel):
|
||||||
|
systems: List[SystemWithTeamResponse]
|
||||||
|
cur_page: int
|
||||||
|
total_systems: int
|
||||||
|
page_size: int
|
||||||
|
|
||||||
|
|
||||||
|
class GiveSystemToTeamRequest(BaseModel):
|
||||||
|
system_id: int
|
||||||
|
team_id: int
|
||||||
|
qubits_given: int
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveSystemFromTeamRequest(BaseModel):
|
||||||
|
system_id: int
|
||||||
|
team_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class SystemStatusResponse(BaseModel):
|
||||||
|
system_id: int
|
||||||
|
status: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
last_updated: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ComputationalSystemDeleteRequest(BaseModel):
|
||||||
|
system_id: int
|
||||||
317
dockerfile_build/src/sql_models/models.py
Normal file
317
dockerfile_build/src/sql_models/models.py
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from connections.db import Base
|
||||||
|
from sqlalchemy import Boolean, DateTime, Integer, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.orm.properties import ForeignKey
|
||||||
|
from sqlalchemy.sql.schema import Column, Table
|
||||||
|
from typing_extensions import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Team(Base):
|
||||||
|
__tablename__ = "teams"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
|
description: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
||||||
|
|
||||||
|
# 1:m relationship with creator
|
||||||
|
creator_id: Mapped[str] = mapped_column(
|
||||||
|
String(36), ForeignKey("users.keycloak_id"), nullable=False
|
||||||
|
)
|
||||||
|
creator: Mapped["User"] = relationship(
|
||||||
|
"User",
|
||||||
|
foreign_keys=[creator_id],
|
||||||
|
back_populates="created_teams",
|
||||||
|
lazy="selectin", # Added
|
||||||
|
)
|
||||||
|
|
||||||
|
# Team memberships
|
||||||
|
team_memberships: Mapped[List["TeamMember"]] = relationship(
|
||||||
|
"TeamMember",
|
||||||
|
back_populates="team",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="selectin", # Added
|
||||||
|
)
|
||||||
|
|
||||||
|
# Many-to-many with ComputationalSystem through TeamSystem
|
||||||
|
team_systems: Mapped[List["TeamSystem"]] = relationship(
|
||||||
|
"TeamSystem",
|
||||||
|
back_populates="team",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(Base):
|
||||||
|
__tablename__ = "permissions"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||||
|
description: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
team_member_permissions = Table(
|
||||||
|
"team_member_permissions",
|
||||||
|
Base.metadata,
|
||||||
|
Column("team_member_id", Integer, ForeignKey("team_members.id"), primary_key=True),
|
||||||
|
Column("permission_id", Integer, ForeignKey("permissions.id"), primary_key=True),
|
||||||
|
Column("granted_at", DateTime, default=datetime.now),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMember(Base):
|
||||||
|
__tablename__ = "team_members"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
team_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("teams.id"), nullable=False
|
||||||
|
)
|
||||||
|
user_id: Mapped[str] = mapped_column(
|
||||||
|
String(36), ForeignKey("users.keycloak_id"), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Basic info without hardcoded permission levels
|
||||||
|
joined_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
# Many-to-many with Permission
|
||||||
|
permissions: Mapped[List["Permission"]] = relationship(
|
||||||
|
"Permission",
|
||||||
|
secondary=team_member_permissions,
|
||||||
|
lazy="selectin", # Added - critical for many-to-many
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
team: Mapped["Team"] = relationship(
|
||||||
|
"Team",
|
||||||
|
back_populates="team_memberships",
|
||||||
|
lazy="selectin", # Added
|
||||||
|
)
|
||||||
|
user: Mapped["User"] = relationship(
|
||||||
|
"User",
|
||||||
|
back_populates="team_memberships",
|
||||||
|
foreign_keys=[user_id], # Added foreign_keys to be explicit
|
||||||
|
lazy="selectin", # Added
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemStatus(Base):
|
||||||
|
__tablename__ = "system_statuses"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||||
|
description: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ComputationalSystem(Base):
|
||||||
|
__tablename__ = "computational_systems"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id: Mapped[str] = mapped_column(
|
||||||
|
String(36), ForeignKey("users.keycloak_id"), nullable=False
|
||||||
|
)
|
||||||
|
system_name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
||||||
|
status_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("system_statuses.id"), nullable=False
|
||||||
|
)
|
||||||
|
max_qubits: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
last_updated: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime, default=datetime.now, onupdate=datetime.now
|
||||||
|
)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user: Mapped["User"] = relationship(
|
||||||
|
"User",
|
||||||
|
back_populates="computational_systems",
|
||||||
|
foreign_keys=[user_id],
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
status: Mapped["SystemStatus"] = relationship(
|
||||||
|
"SystemStatus",
|
||||||
|
foreign_keys=[status_id],
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
team_systems: Mapped[List["TeamSystem"]] = relationship(
|
||||||
|
"TeamSystem",
|
||||||
|
back_populates="system",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Junction table for Team - ComputationalSystem many-to-many relationship
|
||||||
|
class TeamSystem(Base):
|
||||||
|
"""Association table linking teams with computational systems"""
|
||||||
|
|
||||||
|
__tablename__ = "team_systems"
|
||||||
|
|
||||||
|
team_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("teams.id"), primary_key=True
|
||||||
|
)
|
||||||
|
system_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("computational_systems.id"), primary_key=True
|
||||||
|
)
|
||||||
|
qubits_given: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
team: Mapped["Team"] = relationship(
|
||||||
|
"Team", back_populates="team_systems", lazy="selectin"
|
||||||
|
)
|
||||||
|
system: Mapped["ComputationalSystem"] = relationship(
|
||||||
|
"ComputationalSystem", back_populates="team_systems", lazy="selectin"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
keycloak_id: Mapped[str] = mapped_column(
|
||||||
|
String(36), primary_key=True, index=True, nullable=False
|
||||||
|
)
|
||||||
|
profile_picture_path: Mapped[str] = mapped_column(String(500), nullable=True)
|
||||||
|
is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
# Teams this user created
|
||||||
|
created_teams: Mapped[List["Team"]] = relationship(
|
||||||
|
"Team",
|
||||||
|
foreign_keys=[Team.creator_id],
|
||||||
|
back_populates="creator",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Team memberships
|
||||||
|
team_memberships: Mapped[List["TeamMember"]] = relationship(
|
||||||
|
"TeamMember",
|
||||||
|
back_populates="user",
|
||||||
|
foreign_keys=[TeamMember.user_id],
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
computational_systems: Mapped[List["ComputationalSystem"]] = relationship(
|
||||||
|
"ComputationalSystem",
|
||||||
|
back_populates="user",
|
||||||
|
foreign_keys=[ComputationalSystem.user_id],
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Add these new classes before your existing User class
|
||||||
|
|
||||||
|
|
||||||
|
class ExperimentType(Base):
|
||||||
|
__tablename__ = "experiment_types"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True)
|
||||||
|
description: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
||||||
|
file_frontend: Mapped[str] = mapped_column(
|
||||||
|
String(255), nullable=False
|
||||||
|
) # Path to frontend file
|
||||||
|
server_path: Mapped[str] = mapped_column(String(255), nullable=False) # Server path
|
||||||
|
file_comp_system: Mapped[str] = mapped_column(
|
||||||
|
String(255), nullable=False
|
||||||
|
) # Path to computational system file
|
||||||
|
|
||||||
|
|
||||||
|
class Experiment(Base):
|
||||||
|
__tablename__ = "experiments"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
team_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("teams.id"), nullable=False
|
||||||
|
)
|
||||||
|
experiment_type_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("experiment_types.id"), nullable=False
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||||
|
description: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
team: Mapped["Team"] = relationship("Team", foreign_keys=[team_id], lazy="selectin")
|
||||||
|
experiment_type: Mapped["ExperimentType"] = relationship(
|
||||||
|
"ExperimentType",
|
||||||
|
foreign_keys=[experiment_type_id],
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
instances: Mapped[List["Instance"]] = relationship(
|
||||||
|
"Instance",
|
||||||
|
back_populates="experiment",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SimulationStatus(Base):
|
||||||
|
__tablename__ = "simulation_statuses"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||||
|
description: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SimulationResult(Base):
|
||||||
|
__tablename__ = "simulation_results"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
comp_system_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("computational_systems.id"), nullable=False
|
||||||
|
)
|
||||||
|
simulation_result_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, nullable=False
|
||||||
|
) # FK to another microservice
|
||||||
|
status_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("simulation_statuses.id"), nullable=False
|
||||||
|
)
|
||||||
|
started_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime, nullable=False, default=datetime.now
|
||||||
|
)
|
||||||
|
ended_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
computational_system: Mapped["ComputationalSystem"] = relationship(
|
||||||
|
"ComputationalSystem", foreign_keys=[comp_system_id], lazy="selectin"
|
||||||
|
)
|
||||||
|
status: Mapped["SimulationStatus"] = relationship(
|
||||||
|
"SimulationStatus",
|
||||||
|
foreign_keys=[status_id],
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Instance(Base):
|
||||||
|
__tablename__ = "instances"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
experiment_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("experiments.id"), nullable=False
|
||||||
|
)
|
||||||
|
simulation_result_id: Mapped[Optional[int]] = mapped_column(
|
||||||
|
Integer, ForeignKey("simulation_results.id"), nullable=True
|
||||||
|
)
|
||||||
|
instance_data_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, nullable=False
|
||||||
|
) # FK to another microservice
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||||
|
description: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
experiment: Mapped["Experiment"] = relationship(
|
||||||
|
"Experiment",
|
||||||
|
back_populates="instances",
|
||||||
|
foreign_keys=[experiment_id],
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
simulation_result: Mapped[Optional["SimulationResult"]] = relationship(
|
||||||
|
"SimulationResult",
|
||||||
|
foreign_keys=[simulation_result_id],
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
107
src/app.py
107
src/app.py
@@ -1,107 +0,0 @@
|
|||||||
from collections.abc import AsyncIterator
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
from api_endpoint.health_api import router as health_router
|
|
||||||
from api_endpoint.teams_api import router as team_router
|
|
||||||
from api_endpoint.user_api import router as user_router
|
|
||||||
from config.logging_config import logger
|
|
||||||
from connections.db import create_tables, engine
|
|
||||||
from fastapi import FastAPI
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
# from fastapi.requests import Request
|
|
||||||
from fastapi_cache import FastAPICache
|
|
||||||
from fastapi_cache.backends.redis import RedisBackend
|
|
||||||
from redis import asyncio as aioredis
|
|
||||||
from sql_models.models import Permission
|
|
||||||
from sqlalchemy.ext.asyncio.session import AsyncSession
|
|
||||||
from sqlalchemy.sql import select
|
|
||||||
|
|
||||||
# Define your permissions with IDs for consistency
|
|
||||||
DEFAULT_PERMISSIONS = [
|
|
||||||
{"id": 1, "name": "edit_team", "description": "Can edit team name and description"},
|
|
||||||
{"id": 2, "name": "delete_team", "description": "Can delete the team"},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"name": "manage_members",
|
|
||||||
"description": "Can add and remove team members",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"name": "manage_machines",
|
|
||||||
"description": "Can add and remove team machines",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"name": "create_experiment",
|
|
||||||
"description": "Can create and run experiments",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def seed_permissions(db: AsyncSession) -> bool:
|
|
||||||
"""
|
|
||||||
Seed default permissions into the database.
|
|
||||||
Returns True if seeded, False if already existed.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Check if permissions already exist
|
|
||||||
result = await db.execute(select(Permission).limit(1))
|
|
||||||
existing = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
logger.info("Permissions already seeded, skipping...")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Create permissions
|
|
||||||
permissions = [
|
|
||||||
Permission(name=perm["name"], description=perm["description"])
|
|
||||||
for perm in DEFAULT_PERMISSIONS
|
|
||||||
]
|
|
||||||
|
|
||||||
db.add_all(permissions)
|
|
||||||
await db.commit()
|
|
||||||
|
|
||||||
logger.info(f"Seeded {len(permissions)} permissions")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error seeding permissions: {e}")
|
|
||||||
await db.rollback()
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
|
||||||
redis = aioredis.from_url("redis://redis:6379")
|
|
||||||
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
|
|
||||||
|
|
||||||
await create_tables()
|
|
||||||
async with AsyncSession(engine) as session:
|
|
||||||
await seed_permissions(session)
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
await engine.dispose()
|
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
|
||||||
|
|
||||||
|
|
||||||
origins = [
|
|
||||||
"http://localhost",
|
|
||||||
"http://localhost:8001",
|
|
||||||
]
|
|
||||||
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=origins,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include routers
|
|
||||||
app.include_router(health_router, prefix="", tags=["Health"])
|
|
||||||
app.include_router(user_router, prefix="/user", tags=["User"])
|
|
||||||
app.include_router(team_router, prefix="/team", tags=["Team"])
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from connections.db import Base
|
|
||||||
from sqlalchemy import Boolean, DateTime, Integer, String
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
from sqlalchemy.orm.properties import ForeignKey
|
|
||||||
from sqlalchemy.sql.schema import Column, Table
|
|
||||||
from typing_extensions import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class Team(Base):
|
|
||||||
__tablename__ = "teams"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
||||||
description: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
|
||||||
|
|
||||||
# 1:m relationship with creator
|
|
||||||
creator_id: Mapped[str] = mapped_column(
|
|
||||||
String(36), ForeignKey("users.keycloak_id"), nullable=False
|
|
||||||
)
|
|
||||||
creator: Mapped["User"] = relationship(
|
|
||||||
"User",
|
|
||||||
foreign_keys=[creator_id],
|
|
||||||
back_populates="created_teams",
|
|
||||||
lazy="selectin", # Added
|
|
||||||
)
|
|
||||||
|
|
||||||
# Team memberships
|
|
||||||
team_memberships: Mapped[List["TeamMember"]] = relationship(
|
|
||||||
"TeamMember",
|
|
||||||
back_populates="team",
|
|
||||||
cascade="all, delete-orphan",
|
|
||||||
lazy="selectin", # Added
|
|
||||||
)
|
|
||||||
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
|
||||||
|
|
||||||
|
|
||||||
class Permission(Base):
|
|
||||||
__tablename__ = "permissions"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
|
||||||
description: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
team_member_permissions = Table(
|
|
||||||
"team_member_permissions",
|
|
||||||
Base.metadata,
|
|
||||||
Column("team_member_id", Integer, ForeignKey("team_members.id"), primary_key=True),
|
|
||||||
Column("permission_id", Integer, ForeignKey("permissions.id"), primary_key=True),
|
|
||||||
Column("granted_at", DateTime, default=datetime.now),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TeamMember(Base):
|
|
||||||
__tablename__ = "team_members"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
team_id: Mapped[int] = mapped_column(
|
|
||||||
Integer, ForeignKey("teams.id"), nullable=False
|
|
||||||
)
|
|
||||||
user_id: Mapped[str] = mapped_column(
|
|
||||||
String(36), ForeignKey("users.keycloak_id"), nullable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Basic info without hardcoded permission levels
|
|
||||||
joined_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
|
||||||
|
|
||||||
# Many-to-many with Permission
|
|
||||||
permissions: Mapped[List["Permission"]] = relationship(
|
|
||||||
"Permission",
|
|
||||||
secondary=team_member_permissions,
|
|
||||||
lazy="selectin", # Added - critical for many-to-many
|
|
||||||
)
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
team: Mapped["Team"] = relationship(
|
|
||||||
"Team",
|
|
||||||
back_populates="team_memberships",
|
|
||||||
lazy="selectin", # Added
|
|
||||||
)
|
|
||||||
user: Mapped["User"] = relationship(
|
|
||||||
"User",
|
|
||||||
back_populates="team_memberships",
|
|
||||||
foreign_keys=[user_id], # Added foreign_keys to be explicit
|
|
||||||
lazy="selectin", # Added
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
|
||||||
__tablename__ = "users"
|
|
||||||
|
|
||||||
keycloak_id: Mapped[str] = mapped_column(
|
|
||||||
String(36), primary_key=True, index=True, nullable=False
|
|
||||||
)
|
|
||||||
profile_picture_path: Mapped[str] = mapped_column(String(500), nullable=True)
|
|
||||||
is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
|
||||||
|
|
||||||
# Teams this user created
|
|
||||||
created_teams: Mapped[List["Team"]] = relationship(
|
|
||||||
"Team",
|
|
||||||
foreign_keys=[Team.creator_id],
|
|
||||||
back_populates="creator",
|
|
||||||
lazy="selectin", # Added
|
|
||||||
)
|
|
||||||
|
|
||||||
# Team memberships
|
|
||||||
team_memberships: Mapped[List["TeamMember"]] = relationship(
|
|
||||||
"TeamMember",
|
|
||||||
back_populates="user",
|
|
||||||
foreign_keys=[TeamMember.user_id],
|
|
||||||
lazy="selectin", # Added
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user