From 629568d0bc292a858a4fa48788822e923a8f1447 Mon Sep 17 00:00:00 2001 From: Pablo Uribe Date: Sat, 8 Aug 2026 21:30:57 +0200 Subject: [PATCH 1/6] sequence azimuth schema --- src/app/schemas/sequences.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/schemas/sequences.py b/src/app/schemas/sequences.py index a037784c..28f0a028 100644 --- a/src/app/schemas/sequences.py +++ b/src/app/schemas/sequences.py @@ -10,7 +10,7 @@ from app.models import AnnotationType, Sequence -__all__ = ["SequenceLabel", "SequenceRead", "SequenceUpdate"] +__all__ = ["SequenceAzimuth", "SequenceLabel", "SequenceRead", "SequenceUpdate"] # Accesses @@ -30,3 +30,7 @@ class SequenceRead(Sequence): validation_lease_until: Union[datetime, None] = Field(None, exclude=True) validation_status: Union[str, None] = Field(None, exclude=True) validation_attempts: int = Field(0, exclude=True) + + +class SequenceAzimuth(BaseModel): + sequence_azimuth: float = Field(..., ge=0, lt=360) From 2dd79b2f73614bdbaae7c236637b0e869b8e83d0 Mon Sep 17 00:00:00 2001 From: Pablo Uribe Date: Sat, 8 Aug 2026 21:32:05 +0200 Subject: [PATCH 2/6] sequence crud update schemas --- src/app/crud/crud_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/crud/crud_sequence.py b/src/app/crud/crud_sequence.py index 8e8a70b6..0f383337 100644 --- a/src/app/crud/crud_sequence.py +++ b/src/app/crud/crud_sequence.py @@ -15,14 +15,14 @@ from app.core.time import utcnow from app.crud.base import BaseCRUD from app.models import TERMINAL_VALIDATION_STATUSES, VALIDATION_FAILED, Detection, Sequence -from app.schemas.sequences import SequenceLabel, SequenceUpdate +from app.schemas.sequences import SequenceAzimuth, SequenceLabel, SequenceUpdate __all__ = ["SequenceCRUD"] logger = logging.getLogger("uvicorn.error") -class SequenceCRUD(BaseCRUD[Sequence, Sequence, Union[SequenceUpdate, SequenceLabel]]): +class SequenceCRUD(BaseCRUD[Sequence, Sequence, Union[SequenceAzimuth, SequenceUpdate, SequenceLabel]]): def __init__(self, session: AsyncSession) -> None: super().__init__(session, Sequence) From 80ea27098c4abdfc3f82bba0b2686d9542dcdac9 Mon Sep 17 00:00:00 2001 From: Pablo Uribe Date: Sat, 8 Aug 2026 21:32:46 +0200 Subject: [PATCH 3/6] refine sequences azimuth endpoint --- src/app/api/api_v1/endpoints/sequences.py | 44 ++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/app/api/api_v1/endpoints/sequences.py b/src/app/api/api_v1/endpoints/sequences.py index fd66d9cf..823de24e 100644 --- a/src/app/api/api_v1/endpoints/sequences.py +++ b/src/app/api/api_v1/endpoints/sequences.py @@ -19,7 +19,7 @@ from app.schemas.alerts import AlertCreate from app.schemas.detections import DetectionRead, DetectionSequence, DetectionWithUrl from app.schemas.login import TokenPayload -from app.schemas.sequences import SequenceLabel, SequenceRead +from app.schemas.sequences import SequenceAzimuth, SequenceLabel, SequenceRead from app.services.alerts import refresh_alert_state from app.services.risk import FwiClass, risk_service from app.services.sequence_confidence import max_conf_filter_clause @@ -278,3 +278,45 @@ async def label_sequence( await session.commit() return updated + + +@router.patch( + "/sequences/{sequence_id}/azimuth", status_code=status.HTTP_200_OK, summary="Refine that azimuth of the sequence" +) +async def refine_azimuth( + payload: SequenceAzimuth, + sequence_id: int = Path(..., gt=0), + cameras: CameraCRUD = Depends(get_camera_crud), + sequences: SequenceCRUD = Depends(get_sequence_crud), + alerts: AlertCRUD = Depends(get_alert_crud), + session: AsyncSession = Depends(get_session), + token_payload: TokenPayload = Security(get_jwt, scopes=[UserRole.ADMIN, UserRole.USER]), +) -> Sequence: + + telemetry_client.capture(token_payload.sub, event="azimuth-refine", properties={"sequence_id": sequence_id}) + + # Fetch the sequence: + sequence = cast(Sequence, await sequences.get(sequence_id, strict=True)) + + # Non-admins are scoped to their own organization: + if not token_payload.is_admin: + await verify_org_rights(token_payload.organization_id, sequence.camera_id, cameras) + + # Persist the new azimuth: + updated = await sequences.update(sequence_id, payload) + + # Re-run the same attach/reconcile flow: + # Imported lazily to avoid a services -> endpoints import at module load. + from app.api.api_v1.endpoints.detections import _attach_sequence_to_alert + + camera = cast(Camera, await cameras.get(sequence.camera_id, strict=True)) + # Recompute triangulation: + _ = _attach_sequence_to_alert(sequence_=updated, camera=camera, cameras=cameras, sequences=sequences, alerts=alerts) + + # Refresh every previously- and newly-linked alert: + alert_ids_res = await session.exec(select(AlertSequence.alert_id).where(AlertSequence.sequence_id == sequence_id)) + alert_ids = list(alert_ids_res.all()) + for aid in alert_ids: + await refresh_alert_state(aid, session, alerts) + + return updated From 17b7f30d43d00809b5cbb520d35c12ab850b2223 Mon Sep 17 00:00:00 2001 From: Pablo Uribe Date: Tue, 11 Aug 2026 18:17:54 +0200 Subject: [PATCH 4/6] test refine azimuth init --- src/tests/endpoints/test_sequences.py | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/tests/endpoints/test_sequences.py b/src/tests/endpoints/test_sequences.py index 1cb5d5f7..1f5b72a4 100644 --- a/src/tests/endpoints/test_sequences.py +++ b/src/tests/endpoints/test_sequences.py @@ -224,6 +224,35 @@ async def test_label_sequence( **payload, } +@pytest.mark.parametrize( + ("user_idx", "sequence_id", "payload", "status_code", "status_detail"), + [ + (None, 1, {"sequence_azimuth": 100}, 401, "Not authenticated"), + (0, 0, {"sequence_azimuth": 100}, 422, None), + ], +) +@pytest.mark.asyncio +async def test_refine_azimuth( + async_client: AsyncClient, + sequence_session: AsyncSession, + user_idx: Union[int, None], + sequence_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"/sequences/{sequence_id}/azimuth", json=payload, headers=auth) + assert response.status_code == status_code, print(response.__dict__) + if isinstance(status_detail, str): + assert response.json()["detail"] == status_detail @pytest.mark.parametrize( ("user_idx", "from_date", "status_code", "status_detail", "expected_result"), From 920fa92addd88e21478c8a948fa726d27cc8233b Mon Sep 17 00:00:00 2001 From: Pablo Uribe Date: Wed, 12 Aug 2026 18:18:51 +0200 Subject: [PATCH 5/6] endpoint url correction --- src/app/api/api_v1/endpoints/sequences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/api_v1/endpoints/sequences.py b/src/app/api/api_v1/endpoints/sequences.py index 823de24e..7907525b 100644 --- a/src/app/api/api_v1/endpoints/sequences.py +++ b/src/app/api/api_v1/endpoints/sequences.py @@ -281,7 +281,7 @@ async def label_sequence( @router.patch( - "/sequences/{sequence_id}/azimuth", status_code=status.HTTP_200_OK, summary="Refine that azimuth of the sequence" + "/{sequence_id}/azimuth", status_code=status.HTTP_200_OK, summary="Refine the azimuth of the sequence" ) async def refine_azimuth( payload: SequenceAzimuth, From c37d1a0b01607aad53ec61f6f04adc22ef883278 Mon Sep 17 00:00:00 2001 From: Pablo Uribe Date: Wed, 12 Aug 2026 18:20:16 +0200 Subject: [PATCH 6/6] 400 tests --- src/tests/endpoints/test_sequences.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tests/endpoints/test_sequences.py b/src/tests/endpoints/test_sequences.py index 1f5b72a4..4c81787b 100644 --- a/src/tests/endpoints/test_sequences.py +++ b/src/tests/endpoints/test_sequences.py @@ -227,8 +227,12 @@ async def test_label_sequence( @pytest.mark.parametrize( ("user_idx", "sequence_id", "payload", "status_code", "status_detail"), [ - (None, 1, {"sequence_azimuth": 100}, 401, "Not authenticated"), - (0, 0, {"sequence_azimuth": 100}, 422, None), + (None, 1, {"sequence_azimuth": 100}, 401, "Not authenticated"), # No user auth + (0, 0, {"sequence_azimuth": 100}, 422, None), # Sequence id should be >0 + (0, 0, {"sequence_azimuth": 700}, 422, None), # Sequence azimuth should be in range [0, 360) + (0, 99, {"sequence_azimuth": 100}, 404, None), # Inexsisting Sequence + (1, 1, {"sequence_azimuth": 100}, 403, None), # Agent does not have permission + (2, 1, {"sequence_azimuth": 100}, 403, "Access forbidden."), # Non admin action on out of scope organisation ], ) @pytest.mark.asyncio