From ce1f64b9237d0e026eb53e24eacb5922e3948466 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Sat, 27 Jul 2024 11:20:11 +0545 Subject: [PATCH 1/6] feat: Implement create & read operations for drone with schema validation --- src/backend/app/drones/drone_crud.py | 63 ++++++++++++++++++++++++ src/backend/app/drones/drone_routes.py | 64 +++++++++++++++++++++++++ src/backend/app/drones/drone_schemas.py | 21 ++++++++ src/backend/app/main.py | 3 +- 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 src/backend/app/drones/drone_crud.py create mode 100644 src/backend/app/drones/drone_routes.py create mode 100644 src/backend/app/drones/drone_schemas.py diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_crud.py new file mode 100644 index 00000000..3c587bd3 --- /dev/null +++ b/src/backend/app/drones/drone_crud.py @@ -0,0 +1,63 @@ +from app.drones import drone_schemas +from app.models.enums import HTTPStatus +from databases import Database +from loguru import logger as log +from fastapi import HTTPException + +async def get_drone(db: Database, drone_id: int): + """ + Retrieves a drone record from the database. + + Args: + db (Database): The database connection object. + drone_id (int): The ID of the drone to be retrieved. + + Returns: + dict: The drone record if found, otherwise None. + """ + try: + select_query = """ + SELECT * FROM drones + WHERE id = :id + """ + result = await db.fetch_one(select_query, {'id': drone_id}) + return result + + except Exception as e: + log.exception(e) + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed") from e + + +async def create_drone(db: Database, drone_info: drone_schemas.DroneIn): + """ + Creates a new drone record in the database. + + Args: + db (Database): The database connection object. + drone (drone_schemas.DroneIn): The schema object containing drone details. + + Returns: + The ID of the newly created drone record. + """ + try: + insert_query = """ + INSERT INTO drones ( + model, manufacturer, camera_model, sensor_width, sensor_height, + max_battery_health, focal_length, image_width, image_height, + max_altitude, max_speed, weight, created + ) VALUES ( + :model, :manufacturer, :camera_model, :sensor_width, :sensor_height, + :max_battery_health, :focal_length, :image_width, :image_height, + :max_altitude, :max_speed, :weight, CURRENT_TIMESTAMP + ) + RETURNING id + """ + result = await db.execute(insert_query, drone_info.__dict__) + return result + + except Exception as e: + log.exception(e) + raise HTTPException(e) from e + + + diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py new file mode 100644 index 00000000..a5f56be1 --- /dev/null +++ b/src/backend/app/drones/drone_routes.py @@ -0,0 +1,64 @@ +from app.users.user_deps import login_required +from app.users.user_schemas import AuthUser +from app.models.enums import HTTPStatus +from fastapi import APIRouter, Depends, HTTPException +from app.db.database import get_db +from app.config import settings +from app.drones import drone_schemas +from databases import Database +from app.drones import drone_crud + +router = APIRouter( + prefix=f"{settings.API_PREFIX}/drones", + responses={404: {"description": "Not found"}}, +) + + +@router.post("/create_drone", tags=["Drones"]) +async def drone_create( + drone_info: drone_schemas.DroneIn, + db: Database = Depends(get_db), + user_data: AuthUser = Depends(login_required), +): + """ + Creates a new drone record in the database. + + Args: + drone_info (drone_schemas.DroneIn): The schema object containing drone details. + db (Database, optional): The database session object. + user_data (AuthUser, optional): The authenticated user data. + + Returns: + dict: A dictionary containing a success message and the ID of the newly created drone. + """ + drone_id = await drone_crud.create_drone(db, drone_info) + if not drone_id: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Drone creation failed" + ) + return {"message": "Drone created successfully", "drone_id": drone_id} + + +@router.get("/{drone_id}", tags=["Drones"], response_model=drone_schemas.DroneOut) +async def get_drone( + drone_id: int, + db: Database = Depends(get_db), + user_data: AuthUser = Depends(login_required), +): + """ + Retrieves a drone record from the database. + + Args: + drone_id (int): The ID of the drone to be retrieved. + db (Database, optional): The database session object. + user_data (AuthUser, optional): The authenticated user data. + + Returns: + dict: The drone record if found. + """ + drone = await drone_crud.get_drone(db, drone_id) + if not drone: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Drone not found" + ) + return drone diff --git a/src/backend/app/drones/drone_schemas.py b/src/backend/app/drones/drone_schemas.py new file mode 100644 index 00000000..f93c77c4 --- /dev/null +++ b/src/backend/app/drones/drone_schemas.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel + +class DroneBase(BaseModel): + model: str + manufacturer: str + camera_model: str + sensor_width: float + sensor_height: float + max_battery_health: float + focal_length: float + image_width: int + image_height: int + max_altitude: float + max_speed: float + weight: float + +class DroneIn(DroneBase): + pass + +class DroneOut(DroneBase): + id: int diff --git a/src/backend/app/main.py b/src/backend/app/main.py index 64981266..643340a7 100644 --- a/src/backend/app/main.py +++ b/src/backend/app/main.py @@ -10,6 +10,7 @@ from app.config import settings from app.projects import project_routes +from app.drones import drone_routes from app.waypoints import waypoint_routes from app.users import oauth_routes from app.users import user_routes @@ -94,7 +95,7 @@ def get_application() -> FastAPI: allow_headers=["*"], expose_headers=["Content-Disposition"], ) - + _app.include_router(drone_routes.router) _app.include_router(project_routes.router) _app.include_router(waypoint_routes.router) _app.include_router(user_routes.router) From 809c9bd2c8a10b0f1cca2168203c25beb58a7d90 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Sat, 27 Jul 2024 15:03:13 +0545 Subject: [PATCH 2/6] feat: Add delete operation for drone with schema validation --- src/backend/app/drones/drone_crud.py | 24 ++++++++++++++++++++++++ src/backend/app/drones/drone_routes.py | 25 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_crud.py index 3c587bd3..28ff2ad3 100644 --- a/src/backend/app/drones/drone_crud.py +++ b/src/backend/app/drones/drone_crud.py @@ -4,6 +4,30 @@ from loguru import logger as log from fastapi import HTTPException +async def delete_drone(db: Database, drone_id: int) -> bool: + """ + Deletes a drone record from the database. + + Args: + db (Database): The database connection object. + drone_id (int): The ID of the drone to be deleted. + + Returns: + bool: True if the drone was successfully deleted, False otherwise. + """ + try: + delete_query = """ + DELETE FROM drones + WHERE id = :id + """ + result = await db.execute(delete_query, {'id': drone_id}) + return result > 0 + + except Exception as e: + log.exception(e) + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Deletion failed") from e + + async def get_drone(db: Database, drone_id: int): """ Retrieves a drone record from the database. diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index a5f56be1..a53a3621 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -14,6 +14,31 @@ ) +@router.delete("/{drone_id}", tags=["Drones"]) +async def delete_drone( + drone_id: int, + db: Database = Depends(get_db), + user_data: AuthUser = Depends(login_required), +): + """ + Deletes a drone record from the database. + + Args: + drone_id (int): The ID of the drone to be deleted. + db (Database, optional): The database session object. + user_data (AuthUser, optional): The authenticated user data. + + Returns: + dict: A success message if the drone was deleted. + """ + success = await drone_crud.delete_drone(db, drone_id) + if not success: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Drone not found" + ) + return {"message": "Drone deleted successfully"} + + @router.post("/create_drone", tags=["Drones"]) async def drone_create( drone_info: drone_schemas.DroneIn, From 53d8ee01dc69b2fd4ca8430bbb0a853f7fb79465 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Sat, 27 Jul 2024 16:42:35 +0545 Subject: [PATCH 3/6] feat: Add read-all operation for drones with schema validation --- src/backend/app/drones/drone_crud.py | 46 +++++++++++++++++++------ src/backend/app/drones/drone_routes.py | 24 +++++++++++-- src/backend/app/drones/drone_schemas.py | 3 ++ 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_crud.py index 28ff2ad3..2c87dc34 100644 --- a/src/backend/app/drones/drone_crud.py +++ b/src/backend/app/drones/drone_crud.py @@ -4,6 +4,31 @@ from loguru import logger as log from fastapi import HTTPException +from typing import List +from app.drones.drone_schemas import DroneOut + +async def read_all_drones(db: Database) -> List[DroneOut]: + """ + Retrieves all drone records from the database. + + Args: + db (Database): The database connection object. + + Returns: + List[DroneOut]: A list of all drone records. + """ + try: + select_query = """ + SELECT * FROM drones + """ + results = await db.fetch_all(select_query) + return [dict(result) for result in results] # Convert each Record to dict + + except Exception as e: + log.exception(e) + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed") from e + + async def delete_drone(db: Database, drone_id: int) -> bool: """ Deletes a drone record from the database. @@ -20,12 +45,14 @@ async def delete_drone(db: Database, drone_id: int) -> bool: DELETE FROM drones WHERE id = :id """ - result = await db.execute(delete_query, {'id': drone_id}) + result = await db.execute(delete_query, {"id": drone_id}) return result > 0 - + except Exception as e: log.exception(e) - raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Deletion failed") from e + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Deletion failed" + ) from e async def get_drone(db: Database, drone_id: int): @@ -44,12 +71,14 @@ async def get_drone(db: Database, drone_id: int): SELECT * FROM drones WHERE id = :id """ - result = await db.fetch_one(select_query, {'id': drone_id}) + result = await db.fetch_one(select_query, {"id": drone_id}) return result - + except Exception as e: log.exception(e) - raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed") from e + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed" + ) from e async def create_drone(db: Database, drone_info: drone_schemas.DroneIn): @@ -78,10 +107,7 @@ async def create_drone(db: Database, drone_info: drone_schemas.DroneIn): """ result = await db.execute(insert_query, drone_info.__dict__) return result - + except Exception as e: log.exception(e) raise HTTPException(e) from e - - - diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index a53a3621..768d58f2 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -7,12 +7,32 @@ from app.drones import drone_schemas from databases import Database from app.drones import drone_crud +from typing import List + router = APIRouter( prefix=f"{settings.API_PREFIX}/drones", responses={404: {"description": "Not found"}}, ) +@router.get("/", tags=["Drones"], response_model=List[drone_schemas.DroneOut]) +async def read_drones( + db: Database = Depends(get_db), + user_data: AuthUser = Depends(login_required), +): + """ + Retrieves all drone records from the database. + + Args: + db (Database, optional): The database session object. + user_data (AuthUser, optional): The authenticated user data. + + Returns: + List[drone_schemas.DroneOut]: A list of all drone records. + """ + drones = await drone_crud.read_all_drones(db) + return drones + @router.delete("/{drone_id}", tags=["Drones"]) async def delete_drone( @@ -40,7 +60,7 @@ async def delete_drone( @router.post("/create_drone", tags=["Drones"]) -async def drone_create( +async def create_drone( drone_info: drone_schemas.DroneIn, db: Database = Depends(get_db), user_data: AuthUser = Depends(login_required), @@ -65,7 +85,7 @@ async def drone_create( @router.get("/{drone_id}", tags=["Drones"], response_model=drone_schemas.DroneOut) -async def get_drone( +async def read_drone( drone_id: int, db: Database = Depends(get_db), user_data: AuthUser = Depends(login_required), diff --git a/src/backend/app/drones/drone_schemas.py b/src/backend/app/drones/drone_schemas.py index f93c77c4..34f53031 100644 --- a/src/backend/app/drones/drone_schemas.py +++ b/src/backend/app/drones/drone_schemas.py @@ -1,5 +1,6 @@ from pydantic import BaseModel + class DroneBase(BaseModel): model: str manufacturer: str @@ -14,8 +15,10 @@ class DroneBase(BaseModel): max_speed: float weight: float + class DroneIn(DroneBase): pass + class DroneOut(DroneBase): id: int From 3c7942eb078850f7e1434d900e9ef5bc7c992aba Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Sat, 27 Jul 2024 16:47:14 +0545 Subject: [PATCH 4/6] feat: Implement deletion of drone and associated drone flights in one query --- src/backend/app/drones/drone_crud.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_crud.py index 2c87dc34..2cbb97a5 100644 --- a/src/backend/app/drones/drone_crud.py +++ b/src/backend/app/drones/drone_crud.py @@ -31,7 +31,7 @@ async def read_all_drones(db: Database) -> List[DroneOut]: async def delete_drone(db: Database, drone_id: int) -> bool: """ - Deletes a drone record from the database. + Deletes a drone record from the database, along with associated drone flights. Args: db (Database): The database connection object. @@ -42,12 +42,17 @@ async def delete_drone(db: Database, drone_id: int) -> bool: """ try: delete_query = """ + WITH deleted_flights AS ( + DELETE FROM drone_flights + WHERE drone_id = :drone_id + RETURNING drone_id + ) DELETE FROM drones - WHERE id = :id + WHERE id = :drone_id """ - result = await db.execute(delete_query, {"id": drone_id}) + result = await db.execute(delete_query, {"drone_id": drone_id}) return result > 0 - + except Exception as e: log.exception(e) raise HTTPException( From 6f5008793d7f3101ef291ba6595e20bd49843d57 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Sat, 27 Jul 2024 17:22:53 +0545 Subject: [PATCH 5/6] fix(project): deleted task event when project deleted --- src/backend/app/drones/drone_crud.py | 24 +++++++++++++++------- src/backend/app/drones/drone_routes.py | 9 +++----- src/backend/app/projects/project_routes.py | 6 +++++- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_crud.py index 2cbb97a5..56fb6e38 100644 --- a/src/backend/app/drones/drone_crud.py +++ b/src/backend/app/drones/drone_crud.py @@ -3,10 +3,11 @@ from databases import Database from loguru import logger as log from fastapi import HTTPException - +from asyncpg import UniqueViolationError from typing import List from app.drones.drone_schemas import DroneOut + async def read_all_drones(db: Database) -> List[DroneOut]: """ Retrieves all drone records from the database. @@ -22,11 +23,13 @@ async def read_all_drones(db: Database) -> List[DroneOut]: SELECT * FROM drones """ results = await db.fetch_all(select_query) - return [dict(result) for result in results] # Convert each Record to dict - + return results + except Exception as e: log.exception(e) - raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed") from e + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed" + ) from e async def delete_drone(db: Database, drone_id: int) -> bool: @@ -50,9 +53,9 @@ async def delete_drone(db: Database, drone_id: int) -> bool: DELETE FROM drones WHERE id = :drone_id """ - result = await db.execute(delete_query, {"drone_id": drone_id}) - return result > 0 - + await db.execute(delete_query, {"drone_id": drone_id}) + return True + except Exception as e: log.exception(e) raise HTTPException( @@ -113,6 +116,13 @@ async def create_drone(db: Database, drone_info: drone_schemas.DroneIn): result = await db.execute(insert_query, drone_info.__dict__) return result + except UniqueViolationError as e: + log.exception("Unique constraint violation: %s", e) + raise HTTPException( + status_code=HTTPStatus.CONFLICT, + detail="A drone with this model already exists", + ) + except Exception as e: log.exception(e) raise HTTPException(e) from e diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index 768d58f2..a3fc232e 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -15,6 +15,7 @@ responses={404: {"description": "Not found"}}, ) + @router.get("/", tags=["Drones"], response_model=List[drone_schemas.DroneOut]) async def read_drones( db: Database = Depends(get_db), @@ -53,9 +54,7 @@ async def delete_drone( """ success = await drone_crud.delete_drone(db, drone_id) if not success: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Drone not found" - ) + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Drone not found") return {"message": "Drone deleted successfully"} @@ -103,7 +102,5 @@ async def read_drone( """ drone = await drone_crud.get_drone(db, drone_id) if not drone: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Drone not found" - ) + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Drone not found") return drone diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index c59e9e43..757873c5 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -50,7 +50,11 @@ async def delete_project_by_id( ), deleted_tasks AS ( DELETE FROM tasks WHERE project_id = :project_id - RETURNING project_id + RETURNING id + ), deleted_task_events AS ( + DELETE FROM task_events + WHERE project_id = :project_id + RETURNING id ) SELECT id FROM deleted_project """ From 2bd16ca9112b54ff30fe863acf12d822518a037e Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 29 Jul 2024 09:32:20 +0545 Subject: [PATCH 6/6] feat: Implement read, create & delete operation on drone table --- src/backend/app/drones/drone_crud.py | 4 +++- src/backend/app/drones/drone_schemas.py | 9 +++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_crud.py index 56fb6e38..0bb50783 100644 --- a/src/backend/app/drones/drone_crud.py +++ b/src/backend/app/drones/drone_crud.py @@ -125,4 +125,6 @@ async def create_drone(db: Database, drone_info: drone_schemas.DroneIn): except Exception as e: log.exception(e) - raise HTTPException(e) from e + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Drone creation failed" + ) from e diff --git a/src/backend/app/drones/drone_schemas.py b/src/backend/app/drones/drone_schemas.py index 34f53031..f92cb136 100644 --- a/src/backend/app/drones/drone_schemas.py +++ b/src/backend/app/drones/drone_schemas.py @@ -1,7 +1,7 @@ from pydantic import BaseModel -class DroneBase(BaseModel): +class DroneIn(BaseModel): model: str manufacturer: str camera_model: str @@ -16,9 +16,6 @@ class DroneBase(BaseModel): weight: float -class DroneIn(DroneBase): - pass - - -class DroneOut(DroneBase): +class DroneOut(BaseModel): id: int + model: str