Skip to content

Commit ff50e90

Browse files
userapi and unfinished pytests
2 parents 4af5a8d + 9ed8a93 commit ff50e90

File tree

7 files changed

+342
-1
lines changed

7 files changed

+342
-1
lines changed
Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,67 @@
1-
from fastapi import APIRouter
1+
from fastapi import APIRouter, HTTPException, status
2+
3+
from gs.backend.api.v1.aro.models.requests import ChangeUserInfo, CreateUser, GetUserData
4+
from gs.backend.data.data_wrappers.aro_wrapper.aro_user_data_wrapper import add_user, get_user_by_id, modify_user
25

36
aro_user_router = APIRouter(tags=["ARO", "User Information"])
7+
8+
9+
@aro_user_router.post("/create_user/", status_code=status.HTTP_200_OK)
10+
def create_user(payload: CreateUser) -> dict:
11+
"""
12+
:param payload: Payload of type CreateUser, contains first_name, last_name,
13+
call_sign, email, phone numer
14+
all of type str
15+
"""
16+
try:
17+
user = add_user(
18+
call_sign=payload.call_sign,
19+
email=payload.email,
20+
f_name=payload.first_name,
21+
l_name=payload.last_name,
22+
phone_number=payload.phone_number,
23+
)
24+
except ValueError as e:
25+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid input: {e}") from e
26+
27+
return {"user": user}
28+
29+
30+
@aro_user_router.get("/", status_code=status.HTTP_200_OK)
31+
def get_user(payload: GetUserData) -> dict:
32+
"""
33+
:param payload: Payload of type GetUserData, contains user_id of type str
34+
"""
35+
try:
36+
user_id = payload.user_id
37+
38+
except KeyError as e:
39+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Missing user_id") from e
40+
try:
41+
user = get_user_by_id(user_id)
42+
return {"user": user}
43+
44+
except ValueError as e:
45+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, details=f"User does not exist, {e}") from e
46+
47+
48+
@aro_user_router.put("/", status_code=status.HTTP_200_OK)
49+
def change_user_info(payload: ChangeUserInfo) -> dict:
50+
"""
51+
:params payload: Payload of type ChangeUserInfo, contains user_id of type UUID, first_name,
52+
last_name, call_sign
53+
"""
54+
try:
55+
user_id = payload.user_id
56+
57+
except KeyError as e:
58+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Missing user_id") from e
59+
60+
allowed_fields = {"first_name", "last_name", "call_sign"}
61+
update_fields = {k: v for k, v in payload.model_dump().items() if v is not None and k in allowed_fields}
62+
63+
try:
64+
return modify_user(userid=user_id, **update_fields)
65+
66+
except ValueError as e:
67+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from uuid import UUID
2+
3+
from pydantic import BaseModel
4+
5+
6+
class CreateUser(BaseModel):
7+
"""
8+
Base model for create user, used for the create user endpoint
9+
"""
10+
11+
first_name: str
12+
last_name: str
13+
call_sign: str
14+
email: str
15+
phone_number: str
16+
17+
18+
class ChangeUserInfo(BaseModel):
19+
"""
20+
Base model for changing user info, used for the change user info endpoint
21+
"""
22+
23+
user_id: UUID
24+
first_name: str | None = None
25+
last_name: str | None = None
26+
call_sign: str | None = None
27+
28+
29+
class GetUserData(BaseModel):
30+
"""
31+
Base model for getting user info, used by the get user info endpoint
32+
"""
33+
34+
user_id: UUID

gs/backend/data/data_wrappers/aro_wrapper/aro_user_data_wrapper.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Any
12
from uuid import UUID
23

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

6465
return get_all_users()
66+
67+
68+
# deletes the user with given id and returns the remaining users
69+
def get_user_by_id(userid: UUID) -> list[AROUsers]:
70+
"""
71+
Use the user.id to delete a user from table
72+
73+
:param userid: identifier unique to the user
74+
"""
75+
with get_db_session() as session:
76+
user = session.get(AROUsers, userid)
77+
78+
if user:
79+
return user
80+
else:
81+
raise ValueError("User ID does not exist")
82+
83+
84+
def modify_user(userid: UUID, **kwargs: dict[str, Any]) -> AROUsers:
85+
"""
86+
Modifies the target user
87+
88+
:param userid: identifier unique to the user
89+
"""
90+
with get_db_session() as session:
91+
user = session.get(AROUsers, userid)
92+
if not user:
93+
raise ValueError("User does not exist based on ID")
94+
95+
for field, value in kwargs.items():
96+
setattr(user, field, value)
97+
98+
session.commit()
99+
session.refresh(user)
100+
return user
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from enum import StrEnum, auto
2+
3+
4+
class StateMachineStates(StrEnum):
5+
"""
6+
The states of the ground station state machine
7+
Defaults to disconnected
8+
"""
9+
10+
# UPLINK STATES
11+
DISCONNECTED = auto()
12+
ATTEMPTING_CONNECTION = auto()
13+
AWAITING_ACK = auto()
14+
UPLINKING = auto()
15+
DOWNLINKING = auto()
16+
17+
# DISCONNECT STATES
18+
AWAITING_DISCONNECT = auto()
19+
SEND_DISCONNECT_ACK = auto()
20+
21+
# EMERGENCY STATES
22+
ENTERING_EMERGENCY = auto()
23+
AWAITING_CONNECTION = auto()
24+
SEND_CONNECTION_ACK = auto()
25+
EMERGENCY_UPLINK = auto()
26+
27+
SERVER_SIDE_ERROR = auto()
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from gs.backend.state_machine.state_enums import StateMachineStates
2+
from gs.backend.state_machine.state_transition_enums import StateTransition
3+
4+
5+
class StateMachine:
6+
"""
7+
Ground station state machine.
8+
"""
9+
10+
def __init__(self, default_state: StateMachineStates) -> None:
11+
"""
12+
Initialize the state machine and sets the default state and default transitional state
13+
14+
:params default_state: default state which must come from one of the enums in StateMachineStates
15+
"""
16+
self.state = default_state
17+
self.transitional_state: StateTransition = StateTransition.NO_TRANSITION_TRIGGERED
18+
19+
def switch_state(self, transitional_state: StateTransition) -> None:
20+
"""
21+
:params transitional_state: The transitional state of the state machine, used to update state
22+
"""
23+
self.transitional_state = transitional_state
24+
25+
match self.state:
26+
case StateMachineStates.DISCONNECTED:
27+
match self.transitional_state:
28+
case StateTransition.ENTER_EMERGENCY:
29+
self.state = StateMachineStates.ENTERING_EMERGENCY
30+
case StateTransition.BEGIN_UPLINK:
31+
self.state = StateMachineStates.ATTEMPTING_CONNECTION
32+
case _:
33+
self.state = StateMachineStates.SERVER_SIDE_ERROR
34+
35+
case StateMachineStates.ATTEMPTING_CONNECTION:
36+
match self.transitional_state:
37+
case StateTransition.CONNECTION_ESTABLISHED:
38+
self.state = StateMachineStates.AWAITING_ACK
39+
case StateTransition.ERROR:
40+
self.state = StateMachineStates.DISCONNECTED
41+
case _:
42+
self.state = StateMachineStates.SERVER_SIDE_ERROR
43+
44+
case StateMachineStates.AWAITING_ACK:
45+
match self.transitional_state:
46+
case StateTransition.ACK_RECEIVED:
47+
self.state = StateMachineStates.UPLINKING
48+
case StateTransition.ERROR:
49+
self.state = StateMachineStates.DISCONNECTED
50+
case _:
51+
self.state = StateMachineStates.SERVER_SIDE_ERROR
52+
53+
case StateMachineStates.UPLINKING:
54+
match self.transitional_state:
55+
case StateTransition.UPLINK_FINISHED:
56+
self.state = StateMachineStates.DOWNLINKING
57+
case StateTransition.DISCONNECTING:
58+
self.state = StateMachineStates.AWAITING_DISCONNECT
59+
case StateTransition.ERROR:
60+
self.state = StateMachineStates.DISCONNECTED
61+
case _:
62+
self.state = StateMachineStates.SERVER_SIDE_ERROR
63+
64+
case StateMachineStates.AWAITING_DISCONNECT:
65+
match self.transitional_state:
66+
case StateTransition.DISCONNECT_CMD_RECEIVED:
67+
self.state = StateMachineStates.SEND_DISCONNECT_ACK
68+
case StateTransition.ERROR:
69+
self.state = StateMachineStates.DISCONNECTED
70+
case _:
71+
self.state = StateMachineStates.SERVER_SIDE_ERROR
72+
73+
case StateMachineStates.SEND_DISCONNECT_ACK:
74+
match self.transitional_state:
75+
case StateTransition.DISCONNECT_COMPLETE:
76+
self.state = StateMachineStates.DISCONNECTED
77+
case StateTransition.ERROR:
78+
self.state = StateMachineStates.DISCONNECTED
79+
case _:
80+
self.state = StateMachineStates.SERVER_SIDE_ERROR
81+
82+
case StateMachineStates.DOWNLINKING:
83+
match self.transitional_state:
84+
case StateTransition.DOWNLINKING_FINISHED:
85+
self.state = StateMachineStates.UPLINKING
86+
case StateTransition.ERROR:
87+
self.state = StateMachineStates.DISCONNECTED
88+
case _:
89+
self.state = StateMachineStates.SERVER_SIDE_ERROR
90+
91+
case StateMachineStates.ENTERING_EMERGENCY:
92+
match self.transitional_state:
93+
case StateTransition.EMERGENCY_INITIATED:
94+
self.state = StateMachineStates.AWAITING_CONNECTION
95+
case StateTransition.ERROR:
96+
self.state = StateMachineStates.DISCONNECTED
97+
case _:
98+
self.state = StateMachineStates.SERVER_SIDE_ERROR
99+
100+
case StateMachineStates.AWAITING_CONNECTION:
101+
match self.transitional_state:
102+
case StateTransition.CONNECTION_RECEIVED:
103+
self.state = StateMachineStates.SEND_CONNECTION_ACK
104+
case StateTransition.ERROR:
105+
self.state = StateMachineStates.DISCONNECTED
106+
case _:
107+
self.state = StateMachineStates.SERVER_SIDE_ERROR
108+
109+
case StateMachineStates.SEND_CONNECTION_ACK:
110+
match self.transitional_state:
111+
case StateTransition.CONNECTION_ACK_SENT:
112+
self.state = StateMachineStates.EMERGENCY_UPLINK
113+
case StateTransition.ERROR:
114+
self.state = StateMachineStates.DISCONNECTED
115+
case _:
116+
self.state = StateMachineStates.SERVER_SIDE_ERROR
117+
118+
case StateMachineStates.EMERGENCY_UPLINK:
119+
match self.transitional_state:
120+
case StateTransition.EMERGENCY_UPLINK_FINISHED:
121+
self.state = StateMachineStates.DISCONNECTED
122+
case StateTransition.ERROR:
123+
self.state = StateMachineStates.DISCONNECTED
124+
case _:
125+
self.state = StateMachineStates.SERVER_SIDE_ERROR
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from enum import StrEnum, auto
2+
3+
4+
class StateTransition(StrEnum):
5+
"""
6+
The transition states of the ground station state machine
7+
"""
8+
9+
# TODO rename / clarify the specific transition states
10+
ERROR = auto()
11+
12+
# UPLINK TRANSITION STATES
13+
BEGIN_UPLINK = auto()
14+
CONNECTION_ESTABLISHED = auto()
15+
ACK_RECEIVED = auto()
16+
17+
# UPLINK / DOWNLINK TRANSITION STATES
18+
UPLINK_FINISHED = auto()
19+
DOWNLINKING_FINISHED = auto()
20+
21+
# DISCONNECT TRANSITION STATES
22+
DISCONNECTING = auto()
23+
DISCONNECT_CMD_RECEIVED = auto()
24+
DISCONNECT_COMPLETE = auto()
25+
26+
# EMERGENCY TRANSITION STATES
27+
ENTER_EMERGENCY = auto()
28+
EMERGENCY_INITIATED = auto()
29+
CONNECTION_RECEIVED = auto()
30+
CONNECTION_ACK_SENT = auto()
31+
EMERGENCY_UPLINK_FINISHED = auto()
32+
33+
NO_TRANSITION_TRIGGERED = auto() # NOT USED IN STATE MACHINE
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from fastapi.testclient import TestClient
2+
from gs.backend.main import app
3+
4+
5+
def test_create_user():
6+
with TestClient(app) as client:
7+
json_obj = {
8+
"call_sign": "KEVWAN",
9+
"email": "kevwan19@gmail.com",
10+
"first_name": "kevin",
11+
"last_name": "wan",
12+
"phone_number": "18008888888",
13+
}
14+
15+
res = client.post("/api/v1/aro/user/create_user/", json=json_obj)
16+
assert res.status_code == 200
17+
user = res.model_dump().get("user")
18+
assert user.call_sign == "KEVWAN"
19+
assert user.email == "kevwan19@gmail.com"
20+
assert user.first_name == "kevin"
21+
assert user.last_name == "wan"
22+
assert user.phone_number == "18008888888"

0 commit comments

Comments
 (0)