From 388b5ccb027a30d6d8e2597ca02bf49404e055e3 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 17:07:50 +0545 Subject: [PATCH 01/10] feat: DbDrone schema with crud functions --- src/backend/app/drones/drone_schemas.py | 97 +++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/backend/app/drones/drone_schemas.py b/src/backend/app/drones/drone_schemas.py index f92cb136..181b33f0 100644 --- a/src/backend/app/drones/drone_schemas.py +++ b/src/backend/app/drones/drone_schemas.py @@ -1,4 +1,8 @@ from pydantic import BaseModel +from fastapi import HTTPException +from app.models.enums import HTTPStatus +from psycopg import Connection +from psycopg.rows import class_row class DroneIn(BaseModel): @@ -19,3 +23,96 @@ class DroneIn(BaseModel): class DroneOut(BaseModel): id: int model: str + + +class DbDrone(BaseModel): + id: int + model: str + manufacturer: str + camera_model: str + sensor_width: float + sensor_height: float + max_battery_health: int + focal_length: float + image_width: int + image_height: int + max_altitude: int + max_speed: float + weight: int + + @staticmethod + async def one(db: Connection, drone_id: int): + """Get a single project by it's ID, including tasks and task count.""" + async with db.cursor(row_factory=class_row(DbDrone)) as cur: + await cur.execute( + """ + SELECT * FROM drones d + WHERE + d.id = %(drone_id)s + GROUP BY + p.id; + """, + {"drone_id": drone_id}, + ) + drone = await cur.fetchone() + + if not drone: + raise KeyError(f"Drone {drone_id} not found") + + return drone + + @staticmethod + async def all(db: Connection): + """Get all projects, including tasks and task count.""" + async with db.cursor(row_factory=class_row(DbDrone)) as cur: + await cur.execute( + """ + SELECT * FROM drones d + GROUP BY d.id; + """ + ) + drones = await cur.fetchall() + + if not drones: + raise KeyError("No drones found") + return drones + + @staticmethod + async def create(db: Connection, drone: DroneIn): + """Create a single drone.""" + # NOTE we first check if a drone with this model name exists + async with db.cursor() as cur: + sql = """ + SELECT EXISTS ( + SELECT 1 + FROM drones + WHERE LOWER(model) = %(model_name)s + ) + """ + await cur.execute(sql, {"model_name": drone.model.lower()}) + project_exists = await cur.fetchone() + if project_exists[0]: + msg = f"Drone ({drone.model}) already exists!" + raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=msg) + + # If drone with the same model does not already exists, add a new one. + model_dump = drone.model_dump() + columns = ", ".join(model_dump.keys()) + value_placeholders = ", ".join(f"%({key})s" for key in model_dump.keys()) + + sql = f""" + INSERT INTO drones ({columns}, created) + VALUES ({value_placeholders}, NOW()) + RETURNING id; + """ + + async with db.cursor() as cur: + await cur.execute(sql, model_dump) + new_drone_id = await cur.fetchone() + + if not new_drone_id: + msg = f"Unknown SQL error for data: {model_dump}" + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=msg + ) + return new_drone_id[0] From 326da39a4b6acd9f4a21c797efc059842b3781b3 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 17:13:45 +0545 Subject: [PATCH 02/10] drone crud with psycopg and pydantic schemas --- src/backend/app/drones/drone_routes.py | 59 +++++++++----------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index 4fee8b4f..0ac631fb 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -8,35 +8,38 @@ from app.drones import drone_schemas from psycopg import Connection from app.drones import drone_crud -from typing import List router = APIRouter( prefix=f"{settings.API_PREFIX}/drones", + tags=["Drones"], responses={404: {"description": "Not found"}}, ) -@router.get("/", tags=["Drones"], response_model=List[drone_schemas.DroneOut]) +@router.get("/", response_model=list[drone_schemas.DroneOut]) async def read_drones( db: Annotated[Connection, Depends(database.get_db)], - user_data: Annotated[AuthUser, Depends(login_required)], ): - """ - Retrieves all drone records from the database. + """Get all drones.""" + try: + return await drone_schemas.DbDrone.all(db) + except KeyError as e: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) from e - 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.post("/create_drone") +async def create_drone( + drone_info: drone_schemas.DroneIn, + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], +): + """Create a new drone in database""" + drone_id = await drone_schemas.DbDrone.create(db, drone_info) + return {"message": "Drone created successfully", "drone_id": drone_id} -@router.delete("/{drone_id}", tags=["Drones"]) +@router.delete("/{drone_id}") async def delete_drone( drone_id: int, db: Annotated[Connection, Depends(database.get_db)], @@ -53,37 +56,15 @@ async def delete_drone( Returns: dict: A success message if the drone was deleted. """ + + # TODO: Check user role, Admin can only do this + 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 create_drone( - drone_info: drone_schemas.DroneIn, - db: Annotated[Connection, Depends(database.get_db)], - user_data: Annotated[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 read_drone( drone_id: int, From 52c863e15c6a12ccc5c46fb41754b8c4ef75dc18 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 17:17:17 +0545 Subject: [PATCH 03/10] remove create_drone function from drones --- src/backend/app/drones/drone_crud.py | 42 ---------------------------- 1 file changed, 42 deletions(-) diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_crud.py index 1494091f..b99c875f 100644 --- a/src/backend/app/drones/drone_crud.py +++ b/src/backend/app/drones/drone_crud.py @@ -1,4 +1,3 @@ -from app.drones import drone_schemas from app.models.enums import HTTPStatus from loguru import logger as log from fastapi import HTTPException @@ -88,44 +87,3 @@ async def get_drone(db: Connection, drone_id: int): raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed" ) from e - - -async def create_drone(db: Connection, 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 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( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Drone creation failed" - ) from e From e402893b1263b2aade50802f916103674da7d159 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 17:25:03 +0545 Subject: [PATCH 04/10] feat: drone deps to get one drone --- src/backend/app/drones/drone_deps.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/backend/app/drones/drone_deps.py diff --git a/src/backend/app/drones/drone_deps.py b/src/backend/app/drones/drone_deps.py new file mode 100644 index 00000000..bb42dc0e --- /dev/null +++ b/src/backend/app/drones/drone_deps.py @@ -0,0 +1,20 @@ +from typing import Annotated +from fastapi import Depends, HTTPException, Path +from psycopg import Connection +from app.db import database +from app.drones.drone_schemas import DbDrone +from app.models.enums import HTTPStatus + + +async def get_drone_by_id( + project_id: Annotated[ + id, + Path(description="Drone ID."), + ], + db: Annotated[Connection, Depends(database.get_db)], +) -> DbDrone: + """Get a single project by id.""" + try: + return await DbDrone.one(db, project_id) + except KeyError as e: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) from e From 04e92842e596f26d55e60f56115ca3f2709cc8c1 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:04:02 +0545 Subject: [PATCH 05/10] fix: drone schemas to delete drone --- src/backend/app/drones/drone_deps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/app/drones/drone_deps.py b/src/backend/app/drones/drone_deps.py index bb42dc0e..7060acfa 100644 --- a/src/backend/app/drones/drone_deps.py +++ b/src/backend/app/drones/drone_deps.py @@ -7,14 +7,14 @@ async def get_drone_by_id( - project_id: Annotated[ - id, + drone_id: Annotated[ + int, Path(description="Drone ID."), ], db: Annotated[Connection, Depends(database.get_db)], ) -> DbDrone: """Get a single project by id.""" try: - return await DbDrone.one(db, project_id) + return await DbDrone.one(db, drone_id) except KeyError as e: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) from e From 538a0f42c538911ec3854ca6cd1cf0602c497c5a Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:04:43 +0545 Subject: [PATCH 06/10] delete drone schemas --- src/backend/app/drones/drone_schemas.py | 31 +++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/backend/app/drones/drone_schemas.py b/src/backend/app/drones/drone_schemas.py index 181b33f0..fecbdedc 100644 --- a/src/backend/app/drones/drone_schemas.py +++ b/src/backend/app/drones/drone_schemas.py @@ -42,15 +42,13 @@ class DbDrone(BaseModel): @staticmethod async def one(db: Connection, drone_id: int): - """Get a single project by it's ID, including tasks and task count.""" + """Get a single drone by it's ID""" + print("drone_id = ", drone_id) async with db.cursor(row_factory=class_row(DbDrone)) as cur: await cur.execute( """ - SELECT * FROM drones d - WHERE - d.id = %(drone_id)s - GROUP BY - p.id; + SELECT * FROM drones + WHERE id = %(drone_id)s; """, {"drone_id": drone_id}, ) @@ -63,7 +61,7 @@ async def one(db: Connection, drone_id: int): @staticmethod async def all(db: Connection): - """Get all projects, including tasks and task count.""" + """Get all drones""" async with db.cursor(row_factory=class_row(DbDrone)) as cur: await cur.execute( """ @@ -77,6 +75,25 @@ async def all(db: Connection): raise KeyError("No drones found") return drones + @staticmethod + async def delete(db: Connection, drone_id: int): + """Delete a single drone by its ID.""" + async with db.cursor() as cur: + await cur.execute( + """ + DELETE FROM drones + WHERE id = %(drone_id)s + RETURNING id; + """, + {"drone_id": drone_id}, + ) + deleted_drone_id = await cur.fetchone() + + if not deleted_drone_id: + raise KeyError(f"Drone {drone_id} not found or could not be deleted") + + return deleted_drone_id[0] + @staticmethod async def create(db: Connection, drone: DroneIn): """Create a single drone.""" From 11f0a4ef5f4b400e1747567cfed7ea34355ae9a6 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:05:41 +0545 Subject: [PATCH 07/10] update: drone cruds --- src/backend/app/drones/drone_routes.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index 0ac631fb..137bc16d 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException from app.db import database from app.config import settings -from app.drones import drone_schemas +from app.drones import drone_schemas, drone_deps from psycopg import Connection from app.drones import drone_crud @@ -41,7 +41,7 @@ async def create_drone( @router.delete("/{drone_id}") async def delete_drone( - drone_id: int, + drone: Annotated[drone_schemas.DbDrone, Depends(drone_deps.get_drone_by_id)], db: Annotated[Connection, Depends(database.get_db)], user_data: Annotated[AuthUser, Depends(login_required)], ): @@ -57,12 +57,10 @@ async def delete_drone( dict: A success message if the drone was deleted. """ - # TODO: Check user role, Admin can only do this - - 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"} + # TODO: Check user role, Admin can only do this. + # After user roles introduction + drone_id = await drone_schemas.DbDrone.delete(db, drone.id) + return {"message": f"Drone successfully deleted {drone_id}"} @router.get("/{drone_id}", tags=["Drones"], response_model=drone_schemas.DroneOut) From 62ff03bb6682c9a13c842737660e1fb9b6139c59 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:10:14 +0545 Subject: [PATCH 08/10] get a single drone --- src/backend/app/drones/drone_routes.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index 137bc16d..0f3611da 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -7,7 +7,6 @@ from app.config import settings from app.drones import drone_schemas, drone_deps from psycopg import Connection -from app.drones import drone_crud router = APIRouter( @@ -63,9 +62,9 @@ async def delete_drone( return {"message": f"Drone successfully deleted {drone_id}"} -@router.get("/{drone_id}", tags=["Drones"], response_model=drone_schemas.DroneOut) +@router.get("/{drone_id}", response_model=drone_schemas.DroneOut) async def read_drone( - drone_id: int, + drone: Annotated[drone_schemas.DbDrone, Depends(drone_deps.get_drone_by_id)], db: Annotated[Connection, Depends(database.get_db)], user_data: Annotated[AuthUser, Depends(login_required)], ): @@ -80,7 +79,4 @@ async def read_drone( 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 From 81b6d7d679833fd32b80348d09f7a1cae3b1a111 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:12:50 +0545 Subject: [PATCH 09/10] update: drone retrieve api output model --- src/backend/app/drones/drone_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index 0f3611da..8da0e4f9 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -62,7 +62,7 @@ async def delete_drone( return {"message": f"Drone successfully deleted {drone_id}"} -@router.get("/{drone_id}", response_model=drone_schemas.DroneOut) +@router.get("/{drone_id}", response_model=drone_schemas.DbDrone) async def read_drone( drone: Annotated[drone_schemas.DbDrone, Depends(drone_deps.get_drone_by_id)], db: Annotated[Connection, Depends(database.get_db)], From 2685d9b2b0482c28aeec3c4676bb7e650a40e7b6 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:17:41 +0545 Subject: [PATCH 10/10] remove redundancy in drone schemas --- src/backend/app/drones/drone_schemas.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/backend/app/drones/drone_schemas.py b/src/backend/app/drones/drone_schemas.py index fecbdedc..0abaec2e 100644 --- a/src/backend/app/drones/drone_schemas.py +++ b/src/backend/app/drones/drone_schemas.py @@ -5,7 +5,7 @@ from psycopg.rows import class_row -class DroneIn(BaseModel): +class BaseDrone(BaseModel): model: str manufacturer: str camera_model: str @@ -20,25 +20,17 @@ class DroneIn(BaseModel): weight: float +class DroneIn(BaseDrone): + """Model for drone creation""" + + class DroneOut(BaseModel): id: int model: str -class DbDrone(BaseModel): +class DbDrone(BaseDrone): id: int - model: str - manufacturer: str - camera_model: str - sensor_width: float - sensor_height: float - max_battery_health: int - focal_length: float - image_width: int - image_height: int - max_altitude: int - max_speed: float - weight: int @staticmethod async def one(db: Connection, drone_id: int):