diff --git a/src/app/api/api_v1/endpoints/cameras.py b/src/app/api/api_v1/endpoints/cameras.py index d291ac71..045bb4b4 100644 --- a/src/app/api/api_v1/endpoints/cameras.py +++ b/src/app/api/api_v1/endpoints/cameras.py @@ -16,6 +16,7 @@ from app.crud.crud_pose import PoseCRUD from app.models import Camera, Role, UserRole from app.schemas.cameras import ( + CameraAngleOfView, CameraCreate, CameraDeviceConfig, CameraEdit, @@ -199,6 +200,24 @@ async def update_camera_name( return CameraOut(**camera.model_dump()) +@router.patch( + "/{camera_id}/angle_of_view", status_code=status.HTTP_200_OK, summary="Update the angle of view of a camera" +) +async def update_camera_angle_of_view( + payload: CameraAngleOfView, + camera_id: int = Path(..., gt=0), + cameras: CameraCRUD = Depends(get_camera_crud), + token_payload: TokenPayload = Security(get_jwt, scopes=[UserRole.ADMIN]), +) -> CameraOut: + telemetry_client.capture( + token_payload.sub, + event="cameras-update-angle-of-view", + properties={"camera_id": camera_id, "angle_of_view": payload.angle_of_view}, + ) + camera = await cameras.update(camera_id, payload) + return CameraOut(**camera.model_dump()) + + @router.patch( "/{camera_id}/trustable", status_code=status.HTTP_200_OK, summary="Update the trustable status of a camera" ) diff --git a/src/app/crud/crud_camera.py b/src/app/crud/crud_camera.py index 79ffef73..a7e9d196 100644 --- a/src/app/crud/crud_camera.py +++ b/src/app/crud/crud_camera.py @@ -8,6 +8,7 @@ from app.crud.base import BaseCRUD from app.models import Camera from app.schemas.cameras import ( + CameraAngleOfView, CameraCreate, CameraDeviceConfig, CameraEdit, @@ -20,7 +21,11 @@ class CameraCRUD( - BaseCRUD[Camera, CameraCreate, LastActive | CameraEdit | CameraName | CameraDeviceConfig | CameraTrustable] + BaseCRUD[ + Camera, + CameraCreate, + LastActive | CameraEdit | CameraName | CameraAngleOfView | CameraDeviceConfig | CameraTrustable, + ] ): def __init__(self, session: AsyncSession) -> None: super().__init__(session, Camera) diff --git a/src/app/schemas/cameras.py b/src/app/schemas/cameras.py index 6b39d001..c4405549 100644 --- a/src/app/schemas/cameras.py +++ b/src/app/schemas/cameras.py @@ -58,6 +58,16 @@ class CameraName(BaseModel): name: ResourceName = Field(..., description="name of the camera") +class CameraAngleOfView(BaseModel): + angle_of_view: float = Field( + ..., + gt=0, + le=360, + description="angle between left and right camera view", + json_schema_extra={"examples": [51.36]}, + ) + + class CameraTrustable(BaseModel): is_trustable: bool = Field(..., description="whether the detection from this camera can be trusted") diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index 5d58b1cf..bb8175bb 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -616,6 +616,49 @@ async def test_update_camera_name( assert all(response.json()[k] == v for k, v in payload.items()) +@pytest.mark.parametrize( + ("user_idx", "cam_id", "payload", "status_code", "status_detail"), + [ + (None, 1, {"angle_of_view": 51.36}, 401, "Not authenticated"), + (0, 1, {"angle_of_view": 0}, 422, None), + (0, 1, {"angle_of_view": 360.5}, 422, None), + (0, 1, {}, 422, None), + (0, 999, {"angle_of_view": 51.36}, 404, "Table Camera has no corresponding entry."), + (0, 1, {"angle_of_view": 51.36}, 200, None), + (0, 2, {"angle_of_view": 360}, 200, None), + (1, 1, {"angle_of_view": 51.36}, 403, "Incompatible token scope."), + (2, 2, {"angle_of_view": 51.36}, 403, "Incompatible token scope."), + ], +) +@pytest.mark.asyncio +async def test_update_camera_angle_of_view( + async_client: AsyncClient, + camera_session: AsyncSession, + user_idx: Union[int, None], + cam_id: int, + payload: Dict[str, Any], + status_code: int, + status_detail: Union[str, None], +): + auth = None + if isinstance(user_idx, int): + auth = pytest.get_token( + pytest.user_table[user_idx]["id"], + pytest.user_table[user_idx]["role"].split(), + pytest.user_table[user_idx]["organization_id"], + ) + + response = await async_client.patch(f"/cameras/{cam_id}/angle_of_view", json=payload, headers=auth) + assert response.status_code == status_code, print(response.__dict__) + if isinstance(status_detail, str): + assert response.json()["detail"] == status_detail + if response.status_code // 100 == 2: + assert response.json()["angle_of_view"] == payload["angle_of_view"] + # Only the angle of view changes + before = next(cam for cam in pytest.camera_table if cam["id"] == cam_id) + assert all(response.json()[k] == before[k] for k in ("name", "lat", "lon", "elevation")) + + @pytest.mark.parametrize("name_length", [2, 3, 51, 100, 101]) @pytest.mark.asyncio async def test_camera_name_create_rename_and_read_round_trip(async_client, camera_session, name_length):