feat: camera batch endpoint implementation and tests

Self-contained server stub and pytest tests for the /api/v1/camera/events/batch
endpoint, mirroring the BLE batch pattern with idempotent INSERT on
(device_id, period_start).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 07:01:40 -07:00
parent a432813444
commit 910508194a
3 changed files with 160 additions and 0 deletions

69
server/camera_endpoint.py Normal file
View File

@@ -0,0 +1,69 @@
# server/camera_endpoint.py
# Add these models and endpoint to the server's main.py alongside the existing BLE endpoints.
# Requires: camera_records table (see migrations/004_camera_records.sql)
#
# IMPORTANT: Before deploying, verify the HMAC message format in verify_device_hmac
# matches what the firmware computes:
# HMAC-SHA256(secret, f"{device_id}:{timestamp}:{sha256_hex(body)}")
# Headers expected: X-Device-Id, X-Timestamp, X-HMAC-Signature
import sqlite3
from typing import List
from fastapi import Depends
from pydantic import BaseModel
class CameraRecord(BaseModel):
period_start: int
period_end: int
entries: int
exits: int
class CameraEventsRequest(BaseModel):
location_id: str
records: List[CameraRecord]
class CameraEventsResponse(BaseModel):
status: str
accepted: int
# Add this endpoint to your FastAPI app (alongside receive_batch_events):
#
# @app.post("/api/v1/camera/events/batch", response_model=CameraEventsResponse)
# async def receive_camera_events(
# batch: CameraEventsRequest,
# device_id: str = Depends(verify_device_hmac),
# db: sqlite3.Connection = Depends(get_db),
# ):
def receive_camera_events_impl(
batch: CameraEventsRequest,
device_id: str,
db: sqlite3.Connection,
) -> CameraEventsResponse:
"""Receive hourly camera entry/exit records; idempotent on (device_id, period_start)."""
cursor = db.cursor()
accepted = 0
for record in batch.records:
try:
cursor.execute(
"""INSERT INTO camera_records
(device_id, location_id, period_start, period_end, entries, exits)
VALUES (?, ?, ?, ?, ?, ?)""",
(
device_id,
batch.location_id,
record.period_start,
record.period_end,
record.entries,
record.exits,
),
)
accepted += 1
except sqlite3.IntegrityError:
pass # duplicate (device_id, period_start) — idempotent
db.commit()
return CameraEventsResponse(status="ok", accepted=accepted)