Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion gs/backend/api/v1/aro/endpoints/user.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException, status

from gs.backend.api.v1.aro.models.requests import ChangeUserInfo, CreateUser, GetUserData
from gs.backend.data.data_wrappers.aro_wrapper.aro_user_data_wrapper import add_user, get_user_by_id, modify_user
from gs.backend.data.tables.aro_user_tables import AROUsers

aro_user_router = APIRouter(tags=["ARO", "User Information"])


@aro_user_router.post("/create_user/", status_code=status.HTTP_200_OK)
def create_user(payload: CreateUser) -> dict[str, AROUsers]:
"""
:param payload: Payload of type CreateUser, contains first_name, last_name,
call_sign, email, phone numer
all of type str
"""
try:
user = add_user(
call_sign=payload.call_sign,
email=payload.email,
f_name=payload.first_name,
l_name=payload.last_name,
phone_number=payload.phone_number,
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid input: {e}") from e

return {"user": user}


@aro_user_router.get("/", status_code=status.HTTP_200_OK)
def get_user(payload: GetUserData) -> dict[str, AROUsers]:
"""
:param payload: Payload of type GetUserData, contains user_id of type str
"""
try:
user_id = payload.user_id

except KeyError as e:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Missing user_id") from e
try:
user = get_user_by_id(user_id)
return {"user": user}

except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User does not exist, {e}") from e


@aro_user_router.put("/", status_code=status.HTTP_200_OK)
def change_user_info(payload: ChangeUserInfo) -> dict[str, AROUsers]:
"""
:params payload: Payload of type ChangeUserInfo, contains user_id of type UUID, first_name,
last_name, call_sign
"""
try:
user_id = payload.user_id

except KeyError as e:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Missing user_id") from e

allowed_fields = {"first_name", "last_name", "call_sign"}
update_fields = {k: v for k, v in payload.model_dump().items() if v is not None and k in allowed_fields}

try:
return {"data": modify_user(userid=user_id, **update_fields)}

except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
34 changes: 34 additions & 0 deletions gs/backend/api/v1/aro/models/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from uuid import UUID

from pydantic import BaseModel


class CreateUser(BaseModel):
"""
Base model for create user, used for the create user endpoint
"""

first_name: str
last_name: str
call_sign: str
email: str
phone_number: str


class ChangeUserInfo(BaseModel):
"""
Base model for changing user info, used for the change user info endpoint
"""

user_id: UUID
first_name: str | None = None
last_name: str | None = None
call_sign: str | None = None


class GetUserData(BaseModel):
"""
Base model for getting user info, used by the get user info endpoint
"""

user_id: UUID
36 changes: 36 additions & 0 deletions gs/backend/data/data_wrappers/aro_wrapper/aro_user_data_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Any
from uuid import UUID

from sqlmodel import select
Expand Down Expand Up @@ -62,3 +63,38 @@ def delete_user_by_id(userid: UUID) -> list[AROUsers]:
raise ValueError("User ID does not exist")

return get_all_users()


# gets the user with given id
def get_user_by_id(userid: UUID) -> AROUsers:
"""
Use the user.id to delete a user from table

:param userid: identifier unique to the user
"""
with get_db_session() as session:
user = session.get(AROUsers, userid)

if user:
return user
else:
raise ValueError("User ID does not exist")


def modify_user(userid: UUID, **kwargs: dict[str, Any]) -> AROUsers:
"""
Modifies the target user

:param userid: identifier unique to the user
"""
with get_db_session() as session:
user = session.get(AROUsers, userid)
if not user:
raise ValueError("User does not exist based on ID")

for field, value in kwargs.items():
setattr(user, field, value)

session.commit()
session.refresh(user)
return user
27 changes: 27 additions & 0 deletions gs/backend/state_machine/state_enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from enum import StrEnum, auto


class StateMachineStates(StrEnum):
"""
The states of the ground station state machine
Defaults to disconnected
"""

# UPLINK STATES
DISCONNECTED = auto()
ATTEMPTING_CONNECTION = auto()
AWAITING_ACK = auto()
UPLINKING = auto()
DOWNLINKING = auto()

# DISCONNECT STATES
AWAITING_DISCONNECT = auto()
SEND_DISCONNECT_ACK = auto()

# EMERGENCY STATES
ENTERING_EMERGENCY = auto()
AWAITING_CONNECTION = auto()
SEND_CONNECTION_ACK = auto()
EMERGENCY_UPLINK = auto()

SERVER_SIDE_ERROR = auto()
125 changes: 125 additions & 0 deletions gs/backend/state_machine/state_machine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from gs.backend.state_machine.state_enums import StateMachineStates
from gs.backend.state_machine.state_transition_enums import StateTransition


class StateMachine:
"""
Ground station state machine.
"""

def __init__(self, default_state: StateMachineStates) -> None:
"""
Initialize the state machine and sets the default state and default transitional state

:params default_state: default state which must come from one of the enums in StateMachineStates
"""
self.state = default_state
self.transitional_state: StateTransition = StateTransition.NO_TRANSITION_TRIGGERED

def switch_state(self, transitional_state: StateTransition) -> None:
"""
:params transitional_state: The transitional state of the state machine, used to update state
"""
self.transitional_state = transitional_state

match self.state:
case StateMachineStates.DISCONNECTED:
match self.transitional_state:
case StateTransition.ENTER_EMERGENCY:
self.state = StateMachineStates.ENTERING_EMERGENCY
case StateTransition.BEGIN_UPLINK:
self.state = StateMachineStates.ATTEMPTING_CONNECTION
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR

case StateMachineStates.ATTEMPTING_CONNECTION:
match self.transitional_state:
case StateTransition.CONNECTION_ESTABLISHED:
self.state = StateMachineStates.AWAITING_ACK
case StateTransition.ERROR:
self.state = StateMachineStates.DISCONNECTED
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR

case StateMachineStates.AWAITING_ACK:
match self.transitional_state:
case StateTransition.ACK_RECEIVED:
self.state = StateMachineStates.UPLINKING
case StateTransition.ERROR:
self.state = StateMachineStates.DISCONNECTED
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR

case StateMachineStates.UPLINKING:
match self.transitional_state:
case StateTransition.UPLINK_FINISHED:
self.state = StateMachineStates.DOWNLINKING
case StateTransition.DISCONNECTING:
self.state = StateMachineStates.AWAITING_DISCONNECT
case StateTransition.ERROR:
self.state = StateMachineStates.DISCONNECTED
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR

case StateMachineStates.AWAITING_DISCONNECT:
match self.transitional_state:
case StateTransition.DISCONNECT_CMD_RECEIVED:
self.state = StateMachineStates.SEND_DISCONNECT_ACK
case StateTransition.ERROR:
self.state = StateMachineStates.DISCONNECTED
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR

case StateMachineStates.SEND_DISCONNECT_ACK:
match self.transitional_state:
case StateTransition.DISCONNECT_COMPLETE:
self.state = StateMachineStates.DISCONNECTED
case StateTransition.ERROR:
self.state = StateMachineStates.DISCONNECTED
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR

case StateMachineStates.DOWNLINKING:
match self.transitional_state:
case StateTransition.DOWNLINKING_FINISHED:
self.state = StateMachineStates.UPLINKING
case StateTransition.ERROR:
self.state = StateMachineStates.DISCONNECTED
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR

case StateMachineStates.ENTERING_EMERGENCY:
match self.transitional_state:
case StateTransition.EMERGENCY_INITIATED:
self.state = StateMachineStates.AWAITING_CONNECTION
case StateTransition.ERROR:
self.state = StateMachineStates.DISCONNECTED
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR

case StateMachineStates.AWAITING_CONNECTION:
match self.transitional_state:
case StateTransition.CONNECTION_RECEIVED:
self.state = StateMachineStates.SEND_CONNECTION_ACK
case StateTransition.ERROR:
self.state = StateMachineStates.DISCONNECTED
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR

case StateMachineStates.SEND_CONNECTION_ACK:
match self.transitional_state:
case StateTransition.CONNECTION_ACK_SENT:
self.state = StateMachineStates.EMERGENCY_UPLINK
case StateTransition.ERROR:
self.state = StateMachineStates.DISCONNECTED
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR

case StateMachineStates.EMERGENCY_UPLINK:
match self.transitional_state:
case StateTransition.EMERGENCY_UPLINK_FINISHED:
self.state = StateMachineStates.DISCONNECTED
case StateTransition.ERROR:
self.state = StateMachineStates.DISCONNECTED
case _:
self.state = StateMachineStates.SERVER_SIDE_ERROR
33 changes: 33 additions & 0 deletions gs/backend/state_machine/state_transition_enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from enum import StrEnum, auto


class StateTransition(StrEnum):
"""
The transition states of the ground station state machine
"""

# TODO rename / clarify the specific transition states
ERROR = auto()

# UPLINK TRANSITION STATES
BEGIN_UPLINK = auto()
CONNECTION_ESTABLISHED = auto()
ACK_RECEIVED = auto()

# UPLINK / DOWNLINK TRANSITION STATES
UPLINK_FINISHED = auto()
DOWNLINKING_FINISHED = auto()

# DISCONNECT TRANSITION STATES
DISCONNECTING = auto()
DISCONNECT_CMD_RECEIVED = auto()
DISCONNECT_COMPLETE = auto()

# EMERGENCY TRANSITION STATES
ENTER_EMERGENCY = auto()
EMERGENCY_INITIATED = auto()
CONNECTION_RECEIVED = auto()
CONNECTION_ACK_SENT = auto()
EMERGENCY_UPLINK_FINISHED = auto()

NO_TRANSITION_TRIGGERED = auto() # NOT USED IN STATE MACHINE
22 changes: 22 additions & 0 deletions python_test/test_user_api_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from fastapi.testclient import TestClient
from gs.backend.main import app


def test_create_user():
with TestClient(app) as client:
json_obj = {
"call_sign": "KEVWAN",
"email": "kevwan19@gmail.com",
"first_name": "kevin",
"last_name": "wan",
"phone_number": "18008888888",
}

res = client.post("/api/v1/aro/user/create_user/", json=json_obj)
assert res.status_code == 200
user = res.model_dump().get("user")
assert user.call_sign == "KEVWAN"
assert user.email == "kevwan19@gmail.com"
assert user.first_name == "kevin"
assert user.last_name == "wan"
assert user.phone_number == "18008888888"
Loading