Implement basic aircraft management

This commit is contained in:
april 2024-01-09 12:31:04 -06:00
parent b7610e9b6f
commit 04e8c8ca8c
8 changed files with 346 additions and 7 deletions

View File

@ -6,7 +6,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from database.utils import create_admin_user from database.utils import create_admin_user
from routes import users, flights, auth from routes import users, flights, auth, aircraft
logger = logging.getLogger("api") logger = logging.getLogger("api")
@ -31,4 +31,5 @@ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
# Add subroutes # Add subroutes
app.include_router(users.router, tags=["Users"], prefix="/users") app.include_router(users.router, tags=["Users"], prefix="/users")
app.include_router(flights.router, tags=["Flights"], prefix="/flights") app.include_router(flights.router, tags=["Flights"], prefix="/flights")
app.include_router(aircraft.router, tags=["Aircraft"], prefix="/aircraft")
app.include_router(auth.router, tags=["Auth"], prefix="/auth") app.include_router(auth.router, tags=["Auth"], prefix="/auth")

87
api/database/aircraft.py Normal file
View File

@ -0,0 +1,87 @@
from bson import ObjectId
from fastapi import HTTPException
from database.db import aircraft_collection
from database.utils import aircraft_display_helper, aircraft_add_helper
from schemas.aircraft import AircraftDisplaySchema, AircraftCreateSchema
async def retrieve_aircraft(user: str = "") -> list[AircraftDisplaySchema]:
"""
Retrieve a list of aircraft, optionally filtered by user
:param user: User to filter aircraft by
:return: List of aircraft
"""
aircraft = []
if user == "":
async for doc in aircraft_collection.find():
aircraft.append(AircraftDisplaySchema(**aircraft_display_helper(doc)))
else:
async for doc in aircraft_collection.find({"user": ObjectId(user)}):
aircraft.append(AircraftDisplaySchema(**aircraft_display_helper(doc)))
return aircraft
async def retrieve_aircraft_by_id(id: str) -> AircraftDisplaySchema:
"""
Retrieve details about the requested aircraft
:param id: ID of desired aircraft
:return: Aircraft details
"""
aircraft = await aircraft_collection.find_one({"_id": ObjectId(id)})
if aircraft is None:
raise HTTPException(404, "Aircraft not found")
return AircraftDisplaySchema(**aircraft_display_helper(aircraft))
async def insert_aircraft(body: AircraftCreateSchema, id: str) -> ObjectId:
"""
Insert a new aircraft into the database
:param body: Aircraft data
:param id: ID of creating user
:return: ID of inserted aircraft
"""
aircraft = await aircraft_collection.insert_one(aircraft_add_helper(body.model_dump(), id))
return aircraft.inserted_id
async def update_aircraft(body: AircraftCreateSchema, id: str) -> AircraftDisplaySchema:
"""
Update given aircraft in the database
:param body: Updated aircraft data
:param id: ID of aircraft to update
:return: ID of updated aircraft
"""
aircraft = await aircraft_collection.find_one({"_id": ObjectId(id)})
if aircraft is None:
raise HTTPException(404, "Aircraft not found")
updated_aircraft = await aircraft_collection.update_one({"_id": ObjectId(id)}, {"$set": body.model_dump()})
if updated_aircraft is None:
raise HTTPException(500, "Failed to update flight")
return id
async def delete_aircraft(id: str) -> AircraftDisplaySchema:
"""
Delete the given aircraft from the database
:param id: ID of aircraft to delete
:return: Deleted aircraft information
"""
aircraft = await aircraft_collection.find_one({"_id": ObjectId(id)})
if aircraft is None:
raise HTTPException(404, "Aircraft not found")
await aircraft_collection.delete_one({"_id": ObjectId(id)})
return AircraftDisplaySchema(**aircraft_display_helper(aircraft))

View File

@ -24,4 +24,5 @@ except Exception as e:
# Get db collections # Get db collections
user_collection = db_client["user"] user_collection = db_client["user"]
flight_collection = db_client["flight"] flight_collection = db_client["flight"]
aircraft_collection = db_client["aircraft"]
token_collection = db_client["token_blacklist"] token_collection = db_client["token_blacklist"]

View File

@ -88,8 +88,7 @@ async def retrieve_flight(id: str) -> FlightDisplaySchema:
:param id: ID of flight to retrieve :param id: ID of flight to retrieve
:return: Flight information :return: Flight information
""" """
oid = ObjectId(id) flight = await flight_collection.find_one({"_id": ObjectId(id)})
flight = await flight_collection.find_one({"_id": oid})
if flight is None: if flight is None:
raise HTTPException(404, "Flight not found") raise HTTPException(404, "Flight not found")

View File

@ -3,6 +3,7 @@ import logging
from bson import ObjectId from bson import ObjectId
from app.config import get_settings from app.config import get_settings
from schemas.aircraft import AircraftCategory, AircraftClass
from .db import user_collection from .db import user_collection
from routes.utils import get_hashed_password from routes.utils import get_hashed_password
from schemas.user import AuthLevel, UserCreateSchema from schemas.user import AuthLevel, UserCreateSchema
@ -13,6 +14,7 @@ logger = logging.getLogger("api")
def user_helper(user) -> dict: def user_helper(user) -> dict:
""" """
Convert given db response into a format usable by UserDisplaySchema Convert given db response into a format usable by UserDisplaySchema
:param user: Database response :param user: Database response
:return: Usable dict :return: Usable dict
""" """
@ -26,6 +28,7 @@ def user_helper(user) -> dict:
def system_user_helper(user) -> dict: def system_user_helper(user) -> dict:
""" """
Convert given db response to a format usable by UserSystemSchema Convert given db response to a format usable by UserSystemSchema
:param user: Database response :param user: Database response
:return: Usable dict :return: Usable dict
""" """
@ -40,6 +43,7 @@ def system_user_helper(user) -> dict:
def create_user_helper(user) -> dict: def create_user_helper(user) -> dict:
""" """
Convert given db response to a format usable by UserCreateSchema Convert given db response to a format usable by UserCreateSchema
:param user: Database response :param user: Database response
:return: Usable dict :return: Usable dict
""" """
@ -53,6 +57,7 @@ def create_user_helper(user) -> dict:
def flight_display_helper(flight: dict) -> dict: def flight_display_helper(flight: dict) -> dict:
""" """
Convert given db response to a format usable by FlightDisplaySchema Convert given db response to a format usable by FlightDisplaySchema
:param flight: Database response :param flight: Database response
:return: Usable dict :return: Usable dict
""" """
@ -65,6 +70,7 @@ def flight_display_helper(flight: dict) -> dict:
def flight_add_helper(flight: dict, user: str) -> dict: def flight_add_helper(flight: dict, user: str) -> dict:
""" """
Convert given flight schema and user string to a format that can be inserted into the db Convert given flight schema and user string to a format that can be inserted into the db
:param flight: Flight request body :param flight: Flight request body
:param user: User that created flight :param user: User that created flight
:return: Combined dict that can be inserted into db :return: Combined dict that can be inserted into db
@ -73,6 +79,36 @@ def flight_add_helper(flight: dict, user: str) -> dict:
return flight return flight
def aircraft_add_helper(aircraft: dict, user: str) -> dict:
"""
Convert given aircraft dict to a format that can be inserted into the db
:param aircraft: Aircraft request body
:param user: User that created aircraft
:return: Combined dict that can be inserted into db
"""
aircraft["user"] = ObjectId(user)
aircraft["aircraft_category"] = aircraft["aircraft_category"].name
aircraft["aircraft_class"] = aircraft["aircraft_class"].name
return aircraft
def aircraft_display_helper(aircraft: dict) -> dict:
"""
Convert given db response into a format usable by AircraftDisplaySchema
:param aircraft:
:return: USable dict
"""
aircraft["id"] = str(aircraft["_id"])
aircraft["user"] = str(aircraft["user"])
if aircraft["aircraft_category"] is not AircraftCategory:
aircraft["aircraft_category"] = AircraftCategory.__members__.get(aircraft["aircraft_category"])
if aircraft["aircraft_class"] is not AircraftClass:
aircraft["aircraft_class"] = AircraftClass.__members__.get(aircraft["aircraft_class"])
return aircraft
# UTILS # # UTILS #
async def create_admin_user(): async def create_admin_user():

117
api/routes/aircraft.py Normal file
View File

@ -0,0 +1,117 @@
import logging
from fastapi import APIRouter, Depends, HTTPException
from app.deps import get_current_user, admin_required
from database import aircraft as db
from schemas.aircraft import AircraftDisplaySchema, AircraftCreateSchema
from schemas.user import UserDisplaySchema, AuthLevel
router = APIRouter()
logger = logging.getLogger("aircraft")
@router.get('/', summary="Get aircraft created by the currently logged-in user", status_code=200)
async def get_aircraft(user: UserDisplaySchema = Depends(get_current_user)) -> list[AircraftDisplaySchema]:
"""
Get a list of aircraft created by the currently logged-in user
:param user: Current user
:return: List of aircraft
"""
aircraft = await db.retrieve_aircraft(user.id)
return aircraft
@router.get('/all', summary="Get all aircraft created by all users", status_code=200,
dependencies=[Depends(admin_required)], response_model=list[AircraftDisplaySchema])
async def get_all_aircraft() -> list[AircraftDisplaySchema]:
"""
Get a list of all aircraft created by any user
:return: List of aircraft
"""
aircraft = await db.retrieve_aircraft()
return aircraft
@router.get('/{aircraft_id}', summary="Get details of a given aircraft", response_model=AircraftDisplaySchema,
status_code=200)
async def get_aircraft_by_id(aircraft_id: str,
user: UserDisplaySchema = Depends(get_current_user)) -> AircraftDisplaySchema:
"""
Get all details of a given aircraft
:param aircraft_id: ID of requested aircraft
:param user: Currently logged-in user
:return: Aircraft details
"""
aircraft = await db.retrieve_aircraft_by_id(aircraft_id)
if str(aircraft.user) != user.id and AuthLevel(user.level) != AuthLevel.ADMIN:
logger.info("Attempted access to unauthorized aircraft by %s", user.username)
raise HTTPException(403, "Unauthorized access")
return aircraft
@router.post('/', summary="Add an aircraft", status_code=200)
async def add_aircraft(aircraft_body: AircraftCreateSchema,
user: UserDisplaySchema = Depends(get_current_user)) -> dict:
"""
Add an aircraft to the database
:param aircraft_body: Information associated with new aircraft
:param user: Currently logged-in user
:return: Error message if request invalid, else ID of newly created aircraft
"""
aircraft = await db.insert_aircraft(aircraft_body, user.id)
return {"id": str(aircraft)}
@router.put('/{aircraft_id}', summary="Update the given aircraft with new information", status_code=200)
async def update_aircraft(aircraft_id: str, aircraft_body: AircraftCreateSchema,
user: UserDisplaySchema = Depends(get_current_user)) -> dict:
"""
Update the given aircraft with new information
:param aircraft_id: ID of aircraft to update
:param aircraft_body: New aircraft information to update with
:param user: Currently logged-in user
:return: Updated aircraft
"""
aircraft = await get_aircraft_by_id(aircraft_id, user)
if aircraft is None:
raise HTTPException(404, "Aircraft not found")
if str(aircraft.user) != user.id and AuthLevel(user.level) != AuthLevel.ADMIN:
logger.info("Attempted access to unauthorized aircraft by %s", user.username)
raise HTTPException(403, "Unauthorized access")
updated_aircraft_id = await db.update_aircraft(aircraft_body, aircraft_id)
return {"id": str(updated_aircraft_id)}
@router.delete('/{aircraft_id}', summary="Delete the given aircraft", status_code=200,
response_model=AircraftDisplaySchema)
async def delete_aircraft(aircraft_id: str,
user: UserDisplaySchema = Depends(get_current_user)) -> AircraftDisplaySchema:
"""
Delete the given aircraft
:param aircraft_id: ID of aircraft to delete
:param user: Currently logged-in user
:return: 200
"""
aircraft = await get_aircraft_by_id(aircraft_id, user)
if str(aircraft.user) != user.id and AuthLevel(user.level) != AuthLevel.ADMIN:
logger.info("Attempted access to unauthorized aircraft by %s", user.username)
raise HTTPException(403, "Unauthorized access")
deleted = await db.delete_aircraft(aircraft_id)
return deleted

View File

@ -1,14 +1,12 @@
import json
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Dict, Union, List
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from app.deps import get_current_user, admin_required from app.deps import get_current_user, admin_required
from database import flights as db from database import flights as db
from schemas.flight import FlightConciseSchema, FlightDisplaySchema, FlightCreateSchema, FlightByDateSchema
from schemas.flight import FlightConciseSchema, FlightDisplaySchema, FlightCreateSchema, FlightByDateSchema
from schemas.user import UserDisplaySchema, AuthLevel from schemas.user import UserDisplaySchema, AuthLevel
router = APIRouter() router = APIRouter()
@ -27,7 +25,6 @@ async def get_flights(user: UserDisplaySchema = Depends(get_current_user), sort:
:param order: Order of sorting (asc/desc) :param order: Order of sorting (asc/desc)
:return: List of flights :return: List of flights
""" """
# l = get_flight_list(filters=[[{"field": "user", "operator": "eq", "value": user.id}]])
flights = await db.retrieve_flights(user.id, sort, order) flights = await db.retrieve_flights(user.id, sort, order)
return flights return flights

101
api/schemas/aircraft.py Normal file
View File

@ -0,0 +1,101 @@
from enum import Enum
from typing import Annotated
from pydantic import BaseModel, field_validator, Field
from pydantic_core.core_schema import ValidationInfo
from schemas.flight import PyObjectId
class AircraftCategory(Enum):
airplane = "Airplane"
rotorcraft = "Rotorcraft"
powered_lift = "Powered Lift"
glider = "Glider"
lighter_than_air = "Lighter-Than-Air"
ppg = "Powered Parachute"
weight_shift = "Weight-Shift Control"
class AircraftClass(Enum):
# Airplane
sel = "Single-Engine Land"
ses = "Single-Engine Sea"
mel = "Multi-Engine Land"
mes = "Multi-Engine Sea"
# Rotorcraft
helicopter = "Helicopter"
gyroplane = "Gyroplane"
# Powered Lift
powered_lift = "Powered Lift"
# Glider
glider = "Glider"
# Lighther-than-air
airship = "Airship"
balloon = "Balloon"
# Powered Parachute
ppl = "Powered Parachute Land"
pps = "Powered Parachute Sea"
# Weight-Shift
wsl = "Weight-Shift Control Land"
wss = "Weight-Shift Control Sea"
PositiveFloat = Annotated[float, Field(default=0., ge=0)]
class AircraftCreateSchema(BaseModel):
tail_no: str
make: str
model: str
aircraft_category: AircraftCategory
aircraft_class: AircraftClass
hobbs: PositiveFloat
tach: PositiveFloat
@field_validator('aircraft_class')
def validate_class(cls, v: str, info: ValidationInfo, **kwargs):
"""
Dependent field validator for aircraft class. Ensures class corresponds to the correct category
:param v: Value of aircraft_class
:param values: Other values in schema
:param kwargs:
:return: v
"""
if 'aircraft_category' in info.data.keys():
category = info.data['aircraft_category']
if category == AircraftCategory.airplane and v not in [AircraftClass.sel, AircraftClass.mel,
AircraftClass.ses, AircraftClass.mes]:
raise ValueError("Class must be SEL, MEL, SES, or MES for Airplane category")
elif category == AircraftCategory.rotorcraft and v not in [AircraftClass.helicopter,
AircraftClass.gyroplane]:
raise ValueError("Class must be Helicopter or Gyroplane for Rotorcraft category")
elif category == AircraftCategory.powered_lift and not v == AircraftClass.powered_lift:
raise ValueError("Class must be Powered Lift for Powered Lift category")
elif category == AircraftCategory.glider and not v == AircraftClass.glider:
raise ValueError("Class must be Glider for Glider category")
elif category == AircraftCategory.lighter_than_air and v not in [
AircraftClass.airship, AircraftClass.balloon]:
raise ValueError("Class must be Airship or Balloon for Lighter-Than-Air category")
elif category == AircraftCategory.ppg and v not in [AircraftClass.ppl,
AircraftClass.pps]:
raise ValueError("Class must be Powered Parachute Land or "
"Powered Parachute Sea for Powered Parachute category")
elif category == AircraftCategory.weight_shift and v not in [AircraftClass.wsl,
AircraftClass.wss]:
raise ValueError("Class must be Weight-Shift Control Land or Weight-Shift "
"Control Sea for Weight-Shift Control category")
return v
class AircraftDisplaySchema(AircraftCreateSchema):
user: PyObjectId
id: PyObjectId