Skip to content

feat(alerts/export): expand CSV with sequence + camera context (#591)#593

Open
Acruve15 wants to merge 10 commits into
mainfrom
alexis/csv-export-sequences-cameras
Open

feat(alerts/export): expand CSV with sequence + camera context (#591)#593
Acruve15 wants to merge 10 commits into
mainfrom
alexis/csv-export-sequences-cameras

Conversation

@Acruve15
Copy link
Copy Markdown
Collaborator

Summary

Departures from the issue spec

Both decided after looking at how the file actually gets used (Excel/Sheets pivots). Happy to revert either if @MateoLostanlen prefers the original shape.

  • Long format instead of |-separated multi-value cells. The issue proposed id1|id2|id3 packed cells positionally aligned across sequence_* and camera_* columns. That shape is fragile (a | in a camera_name silently breaks parsing) and kills Excel's filter + pivot on sequence_label / camera_name. One row per (alert, sequence) is the spreadsheet-native way to encode a 1:N relation; the dropped sequence_count / camera_count columns are trivial COUNTIFs.
  • Two column renames to make the meaning explicit:
    • alert_lat / alert_lonalert_triangulated_lat / alert_triangulated_lon (pre-answers the "why is it empty?" question raised in the issue thread).
    • sequence_azimuthsequence_triangulated_azimuth (the Sequence model also has camera_azimuth, the cone center; the prefix removes the ambiguity).

Schema

Each row joins one alert with one of its sequences. Alert-level cells repeat across the alert's rows; sequences within an alert are sorted ASC by started_at.

Section Columns
Alert alert_id, alert_started_at_date, alert_started_at_time, alert_last_seen_at, alert_duration_seconds, alert_triangulated_lat, alert_triangulated_lon, organization_id
Sequence sequence_id, sequence_started_at, sequence_last_seen_at, sequence_triangulated_azimuth, sequence_label, pose_id
Camera camera_id, camera_name

sequence_label maps AnnotationType to a 3-bucket label: WILDFIRE_SMOKE → wildfire, OTHER_SMOKE / OTHER → other, None → unknown.

Tests

Split into two clearly-labeled sections:

Unit tests for _iter_alerts_csv (sync, in-memory Alert / Sequence, no DB / HTTP / auth):

  • header-only when no alerts
  • null lat / lon → empty cells
  • one row per sequence, ASC by started_at
  • wildfire label mapping, parametrized over all 4 AnnotationType | None cases (catches the previously-uncovered AnnotationType.OTHER mapping)
  • camera_name resolved per sequence from camera_names_by_id

Integration tests for GET /alerts/export (route → JWT → DB):

  • happy path (content-type, attachment disposition, full first-row dict equality)
  • date window narrows correctly
  • org isolation (non-admin user; cross-org alerts hidden)
  • to_date < from_date → 422
  • no token → 401

Test plan

  • CI runs pytest
  • ruff 0.11.9 check and ruff 0.11.9 format --check pass locally (repo pins 0.11.9 in pre-commit)
  • Manual curl against a dev stack with a valid token; open the CSV in Sheets and confirm filter / pivot work on sequence_label and camera_name

Acruve15 added 8 commits May 14, 2026 19:50
Long format: one row per (alert, sequence). Alerts with no sequences
still emit one row with empty sequence/camera cells.

New columns: alert_started_at_date, alert_started_at_time,
alert_duration_seconds, alert_triangulated_lat/lon, organization_id,
sequence_id/started_at/last_seen_at/triangulated_azimuth/label, pose_id,
camera_id, camera_name. is_wildfire is collapsed to wildfire / other /
unknown via a dict mapper.

Tests will be updated in the next commit.
Only camera_name is read in the CSV; selecting (Camera.id, Camera.name)
and returning Dict[int, str] avoids loading columns we ignore.
Alerts are born from sequences and delete_alert wipes AlertSequence rows
before the alert itself, so an alert with 0 sequences shouldn't exist
outside a brief delete window. The defensive branch was dead code; if
such an alert ever appears in production it's a data-integrity issue
worth noticing rather than papering over with blank cells.
Update the seven existing export tests to the 16-column header, swap
the positional CSV parser for csv.DictReader so assertions look up by
column name. Each existing test now attaches a sequence to its alerts
via a new focused _attach_sequence helper (alerts always have ≥1
sequence in production).

Three new tests cover the long-format semantics:
- emits_one_row_per_sequence: 3 sequences -> 3 rows, ASC by started_at
- wildfire_label_mapping: WILDFIRE_SMOKE/OTHER_SMOKE/None -> wildfire/other/unknown
- camera_name_resolution: rows pick the correct camera_name
Import _ALERT_EXPORT_COLUMNS from the endpoint module instead of
maintaining a copy. In happy_path, replace nine per-cell asserts with a
single dict equality on the first row (pytest renders the diff cleanly,
and the equality implicitly covers the full column set). Keep the
explicit header assertion only in empty_range, where it's the test's
raison d'etre.
Scratch drafts and other ephemeral notes live under tmp/ during local
work and shouldn't ship in PR diffs.
Replace the per-test boilerplate (datetime anchor, JWT token build, the
HTTP request + status + parse triplet) with three fixtures and one
helper:

- export_base_dt: anchor datetime shared by 7 tests
- org1_admin_auth / org1_agent_auth: token headers shared by 8 tests
- _get_export: wraps the GET + 200 assert + CSV parse used by 6 tests

Parametrize test_alerts_export_wildfire_label_mapping over all four
(is_wildfire, expected_label) pairs. This also closes a coverage hole:
the previous version exercised WILDFIRE_SMOKE / OTHER_SMOKE / None but
not the AnnotationType.OTHER -> "other" mapping.
… tests

Five of the export tests were exercising _iter_alerts_csv behavior
(header, null coords, row-per-sequence ordering, wildfire label,
camera-name lookup) through the full HTTP route + JWT + DB stack.
Move them to plain unit tests that call _iter_alerts_csv directly:

- no async, no detection_session, no async_client, no fixtures
- Alert / Sequence instances built with small in-memory helpers
- failures point at the serializer, not the route

Keep five integration tests that genuinely exercise route wiring
(happy path with content headers, SQL date filter, JWT org scoping,
422 validation, 401 auth required).
@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

❌ Patch coverage is 93.75000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.10%. Comparing base (3a69b32) to head (c162dd0).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/app/api/api_v1/endpoints/alerts.py 93.75% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #593      +/-   ##
==========================================
- Coverage   90.11%   90.10%   -0.01%     
==========================================
  Files          55       55              
  Lines        2498     2517      +19     
==========================================
+ Hits         2251     2268      +17     
- Misses        247      249       +2     
Flag Coverage Δ
backend 90.09% <93.75%> (-0.01%) ⬇️
client 90.40% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The iterator was building alert-level cells, sequence-level cells (with
wildfire label and camera-name lookups), and running the CSV streaming
buffer in one body. Extract _alert_cells and _sequence_cells as pure
helpers next to the data they shape, factor the buffer drain into a
local closure, and leave _iter_alerts_csv as the orchestrator it should
be: header, iterate, drain.
@Acruve15 Acruve15 requested review from MateoLostanlen and fe51 May 15, 2026 10:29
@Acruve15 Acruve15 self-assigned this May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expand alerts CSV export with sequence and camera context

1 participant