Repository navigation
Conversation
Add GET /alerts/all/fromdate/count-per-day so that a month calendar gets
its daily alert counts in one request instead of one per day.
- from_date / to_date (inclusive), at most 62 days: a month calendar shows
at most 6 weeks, and this bounds the per-day risk lookups.
- tz: IANA timezone used to split the days, UTC by default to match
/alerts/all/fromdate.
- Same alerts as /alerts/all/fromdate/count: organization scoping, and the
risk filter of each day (fetched concurrently; days sharing the same
scores are counted by a single query), or the risk_score override.
- Returns [{"date": ..., "count": ...}] for the days with alerts only.
Closes pyronear#719
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #724 +/- ##
==========================================
+ Coverage 94.87% 94.96% +0.08%
==========================================
Files 60 60
Lines 3278 3335 +57
==========================================
+ Hits 3110 3167 +57
Misses 168 168
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
MateoLostanlen
left a comment
There was a problem hiding this comment.
Thanks, the approach matches what we agreed in #719. A few things before merging:
Blocking
-
The 62-day cap is checked after the day list is built.
daysis built for the whole range first, sofrom_date=0001-01-01&to_date=9999-12-31creates ~3.65M dates before returning 422. Could you check(to_date - from_date).days + 1 > MAX_COUNT_PER_DAY_PERIODbefore building the list? -
No index-friendly filter on
started_at. The queries only filter ondate(timezone(tz, timezone('UTC', started_at))) IN (...), so each one scans all of the organization's alerts (all alerts for admins). FWI classes change most days, so grouping by identical scores rarely helps: a month can mean ~30 sequential queries, each a full scan. AddingAlert.started_at >= start AND < end(the period bounds converted fromtzto UTC in Python) would fix it. -
Up to 62 concurrent risk API calls. Each call opens a new
httpx.AsyncClient, and a failure or timeout returns{}, so that day is counted with no risk filter and no error. The calendar can then disagree with/all/fromdate/countfor that day. A semaphore and one shared client (as in #708) would make this safer.
Non-blocking
tzis checked with Python'szoneinfo, but the conversion runs in Postgres, which uses its own timezone database. A name Python accepts but Postgres doesn't would give a 500 instead of a 422.- Risk scores are also fetched for days after today. For the current month's calendar that can be ~30 wasted calls; we could clip the period to today in
tz. - The sequence filter (
AlertSequencejoin +max_conf_filter_clause) is copied from_build_alerts_from_date_stmt. Since the goal is the same alerts as/all/fromdate/count, a small shared helper would stop the two from drifting apart. - Telemetry is sent before validation, so rejected requests are counted too.
raise HTTPException(...)in theexceptshould befrom None(ruff B904,"B"is enabled).- Nit:
_user_authin the tests does what the existing tests do inline withpytest.get_token(...). Could we use one style or the other in this file?
…er-day # Conflicts: # src/tests/endpoints/test_alerts.py
- Check the period length on the dates, before listing the days. - Filter started_at on UTC ranges: day boundaries are computed in Python with zoneinfo and days are bucketed with a CASE, so Postgres no longer converts timezones (index-friendly, consistent tz validation). - Clip the period to today in tz: no risk lookup for future days. - RiskService: shared httpx client closed in the app lifespan, and a semaphore bounding concurrent calls. - Share the sequence filter of /all/fromdate in _filter_alerts_by_sequences. - Send telemetry after validation, chain the tz exception with from None, inline get_token in tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select started_at on the plain UTC range and count the alerts per local day with zoneinfo, instead of a CASE over every day boundary: simpler to read, and the timezone is only handled by zoneinfo. Also make the period length check read as a number of days. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks a lot for the thorough review, and for catching these: some of them were basic slips on my side. All points are addressed: Blocking
Non-blocking
I also merged |
MateoLostanlen
left a comment
There was a problem hiding this comment.
Thanks, all points are addressed, and the Python-side bucketing is a nice way to settle the zoneinfo/Postgres mismatch. Approving.
On your questions:
- Index: yes, please add it in this PR, on
(organization_id, started_at). The table has no index at all today, so without it the range filter is still a full scan. - Risk API range endpoint: not yet, only
/scores/{day}. Tracked in MateoLostanlen/pyro-risk-api#2 (all the more since/scores/{day}computes missing days on the fly and can hit the 5s timeout on a month never queried before), then we can switch this endpoint to a single call in a follow-up. - Fail-open: keep it, consistent with
/all/fromdate.
The triage job failure is the labeler lacking write permission on fork PRs, unrelated to this change.
MateoLostanlen
left a comment
There was a problem hiding this comment.
Thanks, all points are addressed, and the Python-side bucketing is a nice way to settle the zoneinfo/Postgres mismatch. One change left before merging, see the index below.
On your questions:
- Index: yes, please add it in this PR, on
(organization_id, started_at). The table has no index at all today, so without it the range filter is still a full scan. - Risk API range endpoint: not yet, only
/scores/{day}. Tracked in MateoLostanlen/pyro-risk-api#2 (all the more since/scores/{day}computes missing days on the fly and can hit the 5s timeout on a month never queried before), then we can switch this endpoint to a single call in a follow-up. - Fail-open: keep it, consistent with
/all/fromdate.
The triage job failure is the labeler lacking write permission on fork PRs, unrelated to this change.
Posted with the wrong state, the index is a change request. Re-posted below.
What
Adds
GET /alerts/all/fromdate/count-per-dayso that the History calendar (pyronear/new-pyro-platform#308) gets its daily alert counts in one request instead of one per day.How (as agreed in #719)
from_date/to_date(inclusive), at most 62 days: a month calendar shows at most 6 weeks, and this bounds the per-day risk lookups. Inverted or longer periods → 422.tz: IANA timezone used to split the days,UTCby default to match/alerts/all/fromdate. Unknown timezone → 422. (tzon/all/fromdateis left out of scope, as discussed.)/alerts/all/fromdate/count: organization scoping (admins see all), and the risk filter of each day, or therisk_scoreoverride. The per-day risk scores are fetched concurrently, and days sharing the same scores are counted by a single grouped query (a single query when the risk API is not configured).[{"date": "2026-10-23", "count": 2}, …], only days with alerts.Tests
/all/fromdate/count, and atzcase where 23:30 UTC falls on the next day in Paris.risk_scoreoverride skipping the risk API.Full suite, ruff and ty pass locally. Also tried in pyro-envdev against the demo alerts.
Note
The response field is
date, consistent withfrom_date/to_date; in the schema it is typeddt.datesince a field nameddatewould otherwise shadow the type for Pydantic.Closes #719
Generated with Claude Code