From 3931ab74a8f676011437360859e52036d37cf5a8 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 14 Sep 2026 07:52:23 -0500 Subject: [PATCH] fix mypy errors from types-peewee 4.0 (#24323) types-peewee 4.0 types model fields precisely, so 17 `type: ignore` comments and 2 `cast(str, ...)` calls are no longer needed. Its stubs type `.namedtuples()` and `.dicts()` queries as returning model instances, so the review cleanup reads namedtuple fields by name and the storage usage query casts its dict rows. `start_time` is declared `DateTimeField` but stores unix timestamps, so two reads cast it like `debug_replay.py` already does. `Export` gets an annotation for the `export_case_id` attribute peewee adds at runtime. --- docker/main/requirements-dev.txt | 2 +- .../data_processing/post/object_descriptions.py | 2 +- .../data_processing/post/review_descriptions.py | 5 +++-- frigate/data_processing/post/semantic_trigger.py | 2 +- frigate/events/cleanup.py | 10 +++++----- frigate/jobs/debug_replay.py | 4 ++-- frigate/models.py | 2 ++ frigate/notices/registry.py | 6 +++--- frigate/record/cleanup.py | 11 ++++++----- frigate/record/export.py | 7 +++++-- frigate/storage.py | 7 +++++-- frigate/track/object_processing.py | 14 +++++++------- 12 files changed, 41 insertions(+), 31 deletions(-) diff --git a/docker/main/requirements-dev.txt b/docker/main/requirements-dev.txt index df5818fe0d..85b054bdca 100644 --- a/docker/main/requirements-dev.txt +++ b/docker/main/requirements-dev.txt @@ -1,4 +1,4 @@ ruff == 0.15.20 # types -types-peewee == 3.17.* +types-peewee == 4.0.* diff --git a/frigate/data_processing/post/object_descriptions.py b/frigate/data_processing/post/object_descriptions.py index 122f5a9fbe..388a3ffcff 100644 --- a/frigate/data_processing/post/object_descriptions.py +++ b/frigate/data_processing/post/object_descriptions.py @@ -253,7 +253,7 @@ class ObjectDescriptionProcessor(PostProcessorApi): # Crop snapshot based on region # provide full image if region doesn't exist (manual events) height, width = img.shape[:2] - x1_rel, y1_rel, width_rel, height_rel = event.data.get( # type: ignore[attr-defined] + x1_rel, y1_rel, width_rel, height_rel = event.data.get( "region", [0, 0, 1, 1] ) x1, y1 = int(x1_rel * width), int(y1_rel * height) diff --git a/frigate/data_processing/post/review_descriptions.py b/frigate/data_processing/post/review_descriptions.py index 300b862cab..fdf6bd3799 100644 --- a/frigate/data_processing/post/review_descriptions.py +++ b/frigate/data_processing/post/review_descriptions.py @@ -8,7 +8,7 @@ import os import shutil import threading from pathlib import Path -from typing import Any +from typing import Any, cast import cv2 from peewee import DoesNotExist @@ -528,7 +528,8 @@ class ReviewDescriptionProcessor(PostProcessorApi): .get() ) - time_in_segment = ts - recording.start_time + # start_time is a DateTimeField holding a unix timestamp + time_in_segment = ts - cast(float, recording.start_time) return get_image_from_recording( self.config.ffmpeg, recording.path, diff --git a/frigate/data_processing/post/semantic_trigger.py b/frigate/data_processing/post/semantic_trigger.py index e2b305ea2d..5a2dcfa8cd 100644 --- a/frigate/data_processing/post/semantic_trigger.py +++ b/frigate/data_processing/post/semantic_trigger.py @@ -237,7 +237,7 @@ class SemanticTriggerProcessor(PostProcessorApi): return # Skip the event if not an object - if event.data.get("type") != "object": # type: ignore[attr-defined] + if event.data.get("type") != "object": return thumbnail_bytes = get_event_thumbnail_bytes(event) diff --git a/frigate/events/cleanup.py b/frigate/events/cleanup.py index b7c98bfdac..d4bee61a05 100644 --- a/frigate/events/cleanup.py +++ b/frigate/events/cleanup.py @@ -37,7 +37,7 @@ class EventCleanup(threading.Thread): if self.removed_camera_labels is None: self.removed_camera_labels = list( Event.select(Event.label) - .where(Event.camera.not_in(self.camera_keys)) # type: ignore[arg-type,call-arg,misc] + .where(Event.camera.not_in(self.camera_keys)) .distinct() .execute() ) @@ -89,7 +89,7 @@ class EventCleanup(threading.Thread): Event.thumbnail, ) .where( - Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc] + Event.camera.not_in(self.camera_keys), Event.start_time < expire_after, Event.label == event.label, Event.retain_indefinitely == False, @@ -111,7 +111,7 @@ class EventCleanup(threading.Thread): # update the clips attribute for the db entry query = Event.select(Event.id).where( - Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc] + Event.camera.not_in(self.camera_keys), Event.start_time < expire_after, Event.label == event.label, Event.retain_indefinitely == False, @@ -218,7 +218,7 @@ class EventCleanup(threading.Thread): Event.camera, ) .where( - Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc] + Event.camera.not_in(self.camera_keys), Event.start_time < expire_after, Event.retain_indefinitely == False, ) @@ -249,7 +249,7 @@ class EventCleanup(threading.Thread): # update the clips attribute for the db entry query = Event.select(Event.id).where( - Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc] + Event.camera.not_in(self.camera_keys), Event.start_time < expire_after, Event.retain_indefinitely == False, ) diff --git a/frigate/jobs/debug_replay.py b/frigate/jobs/debug_replay.py index 393211ea99..a530392bd7 100644 --- a/frigate/jobs/debug_replay.py +++ b/frigate/jobs/debug_replay.py @@ -216,11 +216,11 @@ class ExportDebugReplaySource(DebugReplaySource): """ def __init__(self, export: Export, duration: float) -> None: - self._camera = cast(str, export.camera) + self._camera = export.camera # Export.date is declared DateTimeField but Frigate writes raw unix # timestamps to the column. self._start_ts = float(cast(Any, export.date)) - self._video_path = cast(str, export.video_path) + self._video_path = export.video_path self._duration = duration @property diff --git a/frigate/models.py b/frigate/models.py index e9d98046c9..7346a1fc95 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -109,6 +109,8 @@ class Export(Model): backref="exports", column_name="export_case_id", ) + # peewee adds this accessor for the export_case column at runtime + export_case_id: str | None class ReviewSegment(Model): diff --git a/frigate/notices/registry.py b/frigate/notices/registry.py index fcd2d6672f..cb8f304f47 100644 --- a/frigate/notices/registry.py +++ b/frigate/notices/registry.py @@ -186,7 +186,7 @@ class NoticeRegistry: deleted = ( Notice.delete() .where( - Notice.kind.in_(camera_kinds), # type: ignore[call-arg, arg-type, misc] + Notice.kind.in_(camera_kinds), Notice.scope == camera, ) .execute() @@ -257,7 +257,7 @@ class NoticeRegistry: """Dismissed config and stream check rows, newest first.""" rows = ( Notice.select() - .where(Notice.kind.in_(list(CHECK_KINDS))) # type: ignore[call-arg, arg-type, misc] + .where(Notice.kind.in_(list(CHECK_KINDS))) .order_by(Notice.dismissed_at.desc()) ) return [{"id": row.id, "dismissed_at": row.dismissed_at} for row in rows] @@ -351,7 +351,7 @@ class NoticeRegistry: ) Notice.delete().where( Notice.kind == kind, - Notice.id.not_in(newest), # type: ignore[call-arg, misc] + Notice.id.not_in(newest), ).execute() def _bump_occurrences(self, kind: str, count: int, now: float) -> None: diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py index c71b366d36..bc30f398ce 100644 --- a/frigate/record/cleanup.py +++ b/frigate/record/cleanup.py @@ -5,6 +5,7 @@ import itertools import logging import os import threading +from collections.abc import Iterable from multiprocessing.synchronize import Event as MpEvent from pathlib import Path from typing import Any @@ -28,7 +29,7 @@ logger = logging.getLogger(__name__) def _filter_reviews_for_pass( - reviews: list[Any], + reviews: Iterable[Any], now: datetime.datetime, alerts_days: float, detections_days: float, @@ -121,14 +122,14 @@ class RecordingCleanup(threading.Thread): ) maybe_empty_dirs = set() - thumbs_to_delete = list(map(lambda x: x[1], expired_reviews)) - for thumb_path in thumbs_to_delete: - thumb_path = Path(thumb_path) + thumbs_to_delete = list(map(lambda x: x.thumb_path, expired_reviews)) + for thumb in thumbs_to_delete: + thumb_path = Path(thumb) thumb_path.unlink(missing_ok=True) maybe_empty_dirs.add(thumb_path.parent) max_deletes = 100000 - deleted_reviews_list = list(map(lambda x: x[0], expired_reviews)) + deleted_reviews_list = list(map(lambda x: x.id, expired_reviews)) for i in range(0, len(deleted_reviews_list), max_deletes): ReviewSegment.delete().where( ReviewSegment.id << deleted_reviews_list[i : i + max_deletes] diff --git a/frigate/record/export.py b/frigate/record/export.py index 57f7a06086..c563920cde 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -14,7 +14,7 @@ from collections.abc import Callable from dataclasses import dataclass from enum import Enum from pathlib import Path -from typing import Any +from typing import Any, cast import pytz # type: ignore[import-untyped] from pathvalidate import sanitize_filename @@ -1055,7 +1055,10 @@ class RecordingExporter(threading.Thread): except DoesNotExist: return "" - diff = max(0.0, float(self.start_time) - float(preview.start_time)) + # start_time is a DateTimeField holding a unix timestamp + diff = max( + 0.0, float(self.start_time) - float(cast(Any, preview.start_time)) + ) ffmpeg_cmd = [ "/usr/lib/ffmpeg/8.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support "-hide_banner", diff --git a/frigate/storage.py b/frigate/storage.py index 1487e0bd14..5050875f47 100644 --- a/frigate/storage.py +++ b/frigate/storage.py @@ -3,8 +3,10 @@ import logging import shutil import threading +from collections.abc import Iterable from multiprocessing.synchronize import Event as MpEvent from pathlib import Path +from typing import Any, cast from peewee import SQL, Case, fn @@ -191,14 +193,15 @@ class StorageMaintainer(threading.Thread): stream_usages = { row["stream_type"]: row["usage"] or 0 - for row in ( + for row in cast( + Iterable[dict[str, Any]], Recordings.select( Recordings.stream_type, fn.SUM(Recordings.segment_size).alias("usage"), ) .where(Recordings.camera == camera, Recordings.segment_size != 0) .group_by(Recordings.stream_type) - .dicts() + .dicts(), ) } stream_bandwidths = self.camera_storage_stats.get(camera, {}).get( diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index 5382ef3a9a..8593fb7cdc 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -406,12 +406,12 @@ class TrackedObjectProcessor(threading.Thread): tracked_obj.obj_data["sub_label"] = (sub_label, score) if event: - event.sub_label = sub_label # type: ignore[assignment] + event.sub_label = sub_label data = event.data if sub_label is None: - data["sub_label_score"] = None # type: ignore[index] + data["sub_label_score"] = None elif score is not None: - data["sub_label_score"] = score # type: ignore[index] + data["sub_label_score"] = score event.data = data event.save() @@ -440,7 +440,7 @@ class TrackedObjectProcessor(threading.Thread): objects_list = [] sub_labels = set() events = Event.select(Event.id, Event.label, Event.sub_label).where( - Event.id.in_(detection_ids) # type: ignore[call-arg, misc] + Event.id.in_(detection_ids) ) for det_event in events: if det_event.sub_label: @@ -506,11 +506,11 @@ class TrackedObjectProcessor(threading.Thread): if event: data = event.data - data[field_name] = field_value # type: ignore[index] + data[field_name] = field_value if field_value is None: - data[f"{field_name}_score"] = None # type: ignore[index] + data[f"{field_name}_score"] = None elif score is not None: - data[f"{field_name}_score"] = score # type: ignore[index] + data[f"{field_name}_score"] = score event.data = data event.save()