Increase ruff coverage (#23644)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / Jetson Jetpack 6 (push) Waiting to run

* Pin ruff

* Add python upgrade fixes

This enables python upgrade checks in ruff to look for deprecated types and patterns. This namely fixes:
- usage of deprecated `Typing` which is now built in
- some specific exceptions which are caught and have new aliases

Some specific UP checks were also ignored as they are stylistic / unimportant and likely to cause bugs

* Remove async blocking calls

Use asyncio.to_thread on two remaining blocking calls to fix hanging event thread loop. Enable this specific rule to block it in the future.

* Use proper logging mechanism

* Correctly format logs

* Raise with context

When raising an exception include the from context to improve debugging

* Cleanup
This commit is contained in:
Nicolas Mowen
2026-07-06 12:28:02 -05:00
committed by GitHub
parent 455b8687e8
commit 4ee12e6237
169 changed files with 1053 additions and 1150 deletions
+2 -2
View File
@@ -66,7 +66,7 @@ class DebugReplayJob(Job):
replay_camera_name: str = ""
start_ts: float = 0.0
end_ts: float = 0.0
current_step: Optional[str] = None
current_step: str | None = None
progress_percent: float = 0.0
def to_dict(self) -> dict[str, Any]:
@@ -255,7 +255,7 @@ class DebugReplayJobRunner(threading.Thread):
frigate_config: FrigateConfig,
config_publisher: CameraConfigUpdatePublisher,
replay_manager: "DebugReplayManager",
publisher: Optional[JobStatePublisher] = None,
publisher: JobStatePublisher | None = None,
) -> None:
super().__init__(daemon=True, name=f"debug_replay_{job.id}")
self.job = job
+12 -11
View File
@@ -4,10 +4,11 @@ import logging
import os
import threading
import time
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from queue import Full, Queue
from typing import Any, Callable, Optional
from typing import Any
from peewee import DoesNotExist
@@ -47,16 +48,16 @@ class ExportJob(Job):
job_type: str = "export"
camera: str = ""
name: Optional[str] = None
image_path: Optional[str] = None
export_case_id: Optional[str] = None
name: str | None = None
image_path: str | None = None
export_case_id: str | None = None
request_start_time: float = 0.0
request_end_time: float = 0.0
playback_source: str = PlaybackSourceEnum.recordings.value
ffmpeg_input_args: Optional[str] = None
ffmpeg_output_args: Optional[str] = None
ffmpeg_input_args: str | None = None
ffmpeg_output_args: str | None = None
cpu_fallback: bool = False
chapters: Optional[ChaptersEnum] = None
chapters: ChaptersEnum | None = None
current_step: str = "queued"
progress_percent: float = 0.0
@@ -149,7 +150,7 @@ class ExportJobManager:
config: FrigateConfig,
max_concurrent: int,
max_queued: int = MAX_QUEUED_EXPORT_JOBS,
publisher: Optional[JobStatePublisher] = None,
publisher: JobStatePublisher | None = None,
) -> None:
self.config = config
self.max_concurrent = max(1, max_concurrent)
@@ -268,7 +269,7 @@ class ExportJobManager:
return job.id
def get_job(self, job_id: str) -> Optional[ExportJob]:
def get_job(self, job_id: str) -> ExportJob | None:
"""Get a job by ID."""
with self.lock:
return self.jobs.get(job_id)
@@ -379,7 +380,7 @@ class ExportJobManager:
self._schedule_job_cleanup(job.id)
_job_manager: Optional[ExportJobManager] = None
_job_manager: ExportJobManager | None = None
_job_manager_lock = threading.Lock()
@@ -485,7 +486,7 @@ def start_export_job(config: FrigateConfig, job: ExportJob) -> str:
return get_export_job_manager(config).enqueue(job)
def get_export_job(config: FrigateConfig, job_id: str) -> Optional[ExportJob]:
def get_export_job(config: FrigateConfig, job_id: str) -> ExportJob | None:
"""Get a queued or completed export job by ID."""
return get_export_job_manager(config).get_job(job_id)
+5 -5
View File
@@ -1,7 +1,7 @@
"""Generic base class for long-running background jobs."""
from dataclasses import asdict, dataclass, field
from typing import Any, Optional
from typing import Any
@dataclass
@@ -11,10 +11,10 @@ class Job:
id: str = field(default_factory=lambda: __import__("uuid").uuid4().__str__()[:12])
job_type: str = "" # Must be set by subclasses
status: str = "queued" # queued, running, success, failed, cancelled
results: Optional[dict[str, Any]] = None
start_time: Optional[float] = None
end_time: Optional[float] = None
error_message: Optional[str] = None
results: dict[str, Any] | None = None
start_time: float | None = None
end_time: float | None = None
error_message: str | None = None
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for WebSocket transmission."""
+4 -5
View File
@@ -1,14 +1,13 @@
"""Generic job management for long-running background tasks."""
import threading
from typing import Optional
from frigate.jobs.job import Job
from frigate.types import JobStatusTypesEnum
# Global state and locks for enforcing single concurrent job per job type
_job_locks: dict[str, threading.Lock] = {}
_current_jobs: dict[str, Optional[Job]] = {}
_current_jobs: dict[str, Job | None] = {}
# Keep completed jobs for retrieval, keyed by (job_type, job_id)
_completed_jobs: dict[tuple[str, str], Job] = {}
@@ -35,7 +34,7 @@ def set_current_job(job: Job) -> None:
_current_jobs[job.job_type] = job
def clear_current_job(job_type: str, job_id: Optional[str] = None) -> None:
def clear_current_job(job_type: str, job_id: str | None = None) -> None:
"""Clear the current job for a given job type, optionally checking the ID."""
lock = _get_lock(job_type)
with lock:
@@ -45,14 +44,14 @@ def clear_current_job(job_type: str, job_id: Optional[str] = None) -> None:
_current_jobs[job_type] = None
def get_current_job(job_type: str) -> Optional[Job]:
def get_current_job(job_type: str) -> Job | None:
"""Get the current running/queued job for a given job type, if any."""
lock = _get_lock(job_type)
with lock:
return _current_jobs.get(job_type)
def get_job_by_id(job_type: str, job_id: str) -> Optional[Job]:
def get_job_by_id(job_type: str, job_id: str) -> Job | None:
"""Get job by ID. Checks current job first, then completed jobs."""
lock = _get_lock(job_type)
with lock:
+8 -8
View File
@@ -5,7 +5,7 @@ import os
import threading
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, cast
from typing import cast
from frigate.comms.inter_process import InterProcessRequestor
from frigate.const import CONFIG_DIR, UPDATE_JOB_STATE
@@ -87,7 +87,7 @@ class MediaSyncRunner(threading.Thread):
self._broadcast_status()
except Exception as e:
logger.error(f"Media sync job {self.job.id} failed: {e}", exc_info=True)
logger.exception(f"Media sync job {self.job.id} failed: {e}")
self.job.status = JobStatusTypesEnum.failed
self.job.error_message = str(e)
self.job.end_time = datetime.now().timestamp()
@@ -110,10 +110,10 @@ class MediaSyncRunner(threading.Thread):
def start_media_sync_job(
dry_run: bool = False,
media_types: Optional[list[str]] = None,
media_types: list[str] | None = None,
force: bool = False,
verbose: bool = False,
) -> Optional[str]:
) -> str | None:
"""Start a new media sync job if none is currently running.
Returns job ID on success, None if job already running.
@@ -144,11 +144,11 @@ def start_media_sync_job(
return job.id
def get_current_media_sync_job() -> Optional[MediaSyncJob]:
def get_current_media_sync_job() -> MediaSyncJob | None:
"""Get the current running/queued media sync job, if any."""
return cast(Optional[MediaSyncJob], get_current_job("media_sync"))
return cast(MediaSyncJob | None, get_current_job("media_sync"))
def get_media_sync_job_by_id(job_id: str) -> Optional[MediaSyncJob]:
def get_media_sync_job_by_id(job_id: str) -> MediaSyncJob | None:
"""Get media sync job by ID. Currently only tracks the current job."""
return cast(Optional[MediaSyncJob], get_job_by_id("media_sync", job_id))
return cast(MediaSyncJob | None, get_job_by_id("media_sync", job_id))
+6 -6
View File
@@ -8,7 +8,7 @@ from collections.abc import Callable, Generator, Iterable
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import Any, Optional, cast
from typing import Any, cast
import cv2
import numpy as np
@@ -102,11 +102,11 @@ class MotionSearchJob(Job):
total_frames_processed: int = 0
# Live progress (ride the existing to_dict() websocket broadcast)
scanning_timestamp: Optional[float] = None
scanning_timestamp: float | None = None
progress: float = 0.0
# Metrics for observability
metrics: Optional[MotionSearchMetrics] = None
metrics: MotionSearchMetrics | None = None
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for WebSocket transmission."""
@@ -911,14 +911,14 @@ def start_motion_search_job(
return job.id
def get_motion_search_job(job_id: str) -> Optional[MotionSearchJob]:
def get_motion_search_job(job_id: str) -> MotionSearchJob | None:
"""Get a motion search job by ID."""
with _jobs_lock:
job_entry = _motion_search_jobs.get(job_id)
if job_entry:
return job_entry[0]
# Check completed jobs via manager
return cast(Optional[MotionSearchJob], get_job_by_id("motion_search", job_id))
return cast(MotionSearchJob | None, get_job_by_id("motion_search", job_id))
def cancel_motion_search_job(job_id: str) -> bool:
@@ -942,7 +942,7 @@ def cancel_motion_search_job(job_id: str) -> bool:
job_payload = job.to_dict()
logger.info("Cancelled motion search job %s", job_id)
requestor: Optional[InterProcessRequestor] = None
requestor: InterProcessRequestor | None = None
try:
requestor = InterProcessRequestor()
requestor.send_data(UPDATE_JOB_STATE, job_payload)
+4 -4
View File
@@ -8,7 +8,7 @@ import threading
import time
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import Any, Optional
from typing import Any
import cv2
@@ -360,8 +360,8 @@ class VLMWatchRunner(threading.Thread):
# Module-level singleton (only one watch job at a time)
_current_job: Optional[VLMWatchJob] = None
_cancel_event: Optional[threading.Event] = None
_current_job: VLMWatchJob | None = None
_cancel_event: threading.Event | None = None
_job_lock = threading.Lock()
@@ -444,6 +444,6 @@ def stop_vlm_watch_job() -> bool:
return True
def get_vlm_watch_job() -> Optional[VLMWatchJob]:
def get_vlm_watch_job() -> VLMWatchJob | None:
"""Return the current (or most recent) VLM watch job."""
return _current_job