diff --git a/docker/main/requirements-dev.txt b/docker/main/requirements-dev.txt index ac9d35758..df5818fe0 100644 --- a/docker/main/requirements-dev.txt +++ b/docker/main/requirements-dev.txt @@ -1,4 +1,4 @@ -ruff +ruff == 0.15.20 # types types-peewee == 3.17.* diff --git a/docker/rockchip/conv2rknn.py b/docker/rockchip/conv2rknn.py index 4880d9868..700f35689 100644 --- a/docker/rockchip/conv2rknn.py +++ b/docker/rockchip/conv2rknn.py @@ -11,10 +11,10 @@ except FileNotFoundError: pass try: - with open("/config/conv2rknn.yaml", "r") as config_file: + with open("/config/conv2rknn.yaml") as config_file: configuration = yaml.safe_load(config_file) except FileNotFoundError: - raise Exception("Please place a config file at /config/conv2rknn.yaml") + raise Exception("Please place a config file at /config/conv2rknn.yaml") from None if configuration["config"] != None: rknn_config = configuration["config"] @@ -31,7 +31,7 @@ if "soc" not in configuration: with open("/proc/device-tree/compatible") as file: soc = file.read().split(",")[-1].strip("\x00") except FileNotFoundError: - raise Exception("Make sure to run docker in privileged mode.") + raise Exception("Make sure to run docker in privileged mode.") from None configuration["soc"] = [ soc, diff --git a/frigate/__main__.py b/frigate/__main__.py index f3181e494..df27b42a2 100644 --- a/frigate/__main__.py +++ b/frigate/__main__.py @@ -4,7 +4,6 @@ import multiprocessing as mp import signal import sys import threading -from typing import Union import ruamel.yaml from pydantic import ValidationError @@ -54,7 +53,7 @@ def main() -> None: print("*************************************************************\n") # Attempt to get the original config file for line number tracking config_path = find_config_file() - with open(config_path, "r") as f: + with open(config_path) as f: yaml_config = ruamel.yaml.YAML() yaml_config.preserve_quotes = True full_config = yaml_config.load(f) @@ -68,7 +67,7 @@ def main() -> None: try: for i, part in enumerate(error_path): - key: Union[int, str] = ( + key: int | str = ( int(part) if isinstance(part, str) and part.isdigit() else part ) diff --git a/frigate/api/app.py b/frigate/api/app.py index 8ed365fce..ac839158e 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -12,7 +12,7 @@ from datetime import datetime, timedelta from functools import reduce from io import StringIO from pathlib import Path as FilePath -from typing import Any, Dict, List, Optional +from typing import Any import aiofiles import ruamel.yaml @@ -113,7 +113,7 @@ def version(): @router.get("/stats", dependencies=[Depends(allow_any_authenticated())]) def stats( request: Request, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): stats_data = request.app.stats_emitter.get_latest_stats() @@ -164,7 +164,7 @@ def metrics(request: Request): # Retrieve the latest statistics and update the Prometheus metrics stats = request.app.stats_emitter.get_latest_stats() # query DB for count of events by camera, label - event_counts: List[Dict[str, Any]] = ( + event_counts: list[dict[str, Any]] = ( Event.select(Event.camera, Event.label, fn.Count()) .group_by(Event.camera, Event.label) .dicts() @@ -250,7 +250,7 @@ async def genai_probe(body: GenAIProbeBody): asyncio.to_thread(client.list_models), timeout=_PROBE_OUTER_TIMEOUT_SECONDS, ) - except asyncio.TimeoutError: + except TimeoutError: return JSONResponse( content={"success": False, "message": "Probe timed out"}, ) @@ -374,7 +374,7 @@ def config(request: Request): if model_path: model_json_path = FilePath(model_path).with_suffix(".json") try: - with open(model_json_path, "r") as f: + with open(model_json_path) as f: model_plus_data = json.load(f) config["model"]["plus"] = model_plus_data except FileNotFoundError: @@ -502,7 +502,7 @@ def config_raw(): status_code=404, ) - with open(config_file, "r") as f: + with open(config_file) as f: raw_config = f.read() f.close() @@ -807,7 +807,7 @@ def config_set(request: Request, body: AppConfigSetBody): try: with lock: - with open(config_file, "r") as f: + with open(config_file) as f: old_raw_config = f.read() try: @@ -854,7 +854,7 @@ def config_set(request: Request, body: AppConfigSetBody): update_yaml_file_bulk(config_file, updates) # validate the updated config - with open(config_file, "r") as f: + with open(config_file) as f: new_raw_config = f.read() try: @@ -1028,16 +1028,16 @@ def nvinfo(): ) async def logs( service: str = Path(enum=["frigate", "nginx", "go2rtc"]), - download: Optional[str] = None, - stream: Optional[bool] = False, - start: Optional[int] = 0, - end: Optional[int] = None, + download: str | None = None, + stream: bool | None = False, + start: int | None = 0, + end: int | None = None, ): """Get logs for the requested service (frigate/nginx/go2rtc)""" def download_logs(service_location: str): try: - file = open(service_location, "r") + file = open(service_location) contents = file.read() file.close() return JSONResponse(jsonable_encoder(contents)) @@ -1052,7 +1052,7 @@ async def logs( """Asynchronously stream log lines.""" buffer = "" try: - async with aiofiles.open(file_path, "r") as file: + async with aiofiles.open(file_path) as file: await file.seek(0, 2) while True: line = await file.readline() @@ -1090,7 +1090,7 @@ async def logs( # For full logs initially try: - async with aiofiles.open(service_location, "r") as file: + async with aiofiles.open(service_location) as file: contents = await file.read() total_lines, log_lines = process_logs(contents, service, start, end) @@ -1231,7 +1231,7 @@ def get_media_sync_status(job_id: str): @router.get("/labels", dependencies=[Depends(allow_any_authenticated())]) def get_labels( camera: str = "", - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): try: if camera: @@ -1263,8 +1263,8 @@ def get_labels( @router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())]) def get_sub_labels( - split_joined: Optional[int] = None, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + split_joined: int | None = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): try: events = ( @@ -1351,8 +1351,8 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False): "/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())] ) def get_recognized_license_plates( - split_joined: Optional[int] = None, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + split_joined: int | None = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): try: query = ( @@ -1393,8 +1393,8 @@ def get_recognized_license_plates( def timeline( camera: str = "all", limit: int = 100, - source_id: Optional[str] = None, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + source_id: str | None = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): clauses = [] @@ -1408,20 +1408,20 @@ def timeline( ] if camera != "all": - clauses.append((Timeline.camera == camera)) + clauses.append(Timeline.camera == camera) if source_id: source_ids = [sid.strip() for sid in source_id.split(",")] if len(source_ids) == 1: - clauses.append((Timeline.source_id == source_ids[0])) + clauses.append(Timeline.source_id == source_ids[0]) else: - clauses.append((Timeline.source_id.in_(source_ids))) + clauses.append(Timeline.source_id.in_(source_ids)) # Enforce per-camera access control - clauses.append((Timeline.camera << allowed_cameras)) + clauses.append(Timeline.camera << allowed_cameras) if len(clauses) == 0: - clauses.append((True)) + clauses.append(True) timeline = ( Timeline.select(*selected_columns) @@ -1437,7 +1437,7 @@ def timeline( @router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())]) def hourly_timeline( params: AppTimelineHourlyQueryParameters = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """Get hourly summary for timeline.""" cameras = params.cameras @@ -1454,23 +1454,23 @@ def hourly_timeline( if cameras != "all": camera_list = cameras.split(",") - clauses.append((Timeline.camera << camera_list)) + clauses.append(Timeline.camera << camera_list) # Enforce per-camera access control - clauses.append((Timeline.camera << allowed_cameras)) + clauses.append(Timeline.camera << allowed_cameras) if labels != "all": label_list = labels.split(",") - clauses.append((Timeline.data["label"] << label_list)) + clauses.append(Timeline.data["label"] << label_list) if before: - clauses.append((Timeline.timestamp < before)) + clauses.append(Timeline.timestamp < before) if after: - clauses.append((Timeline.timestamp > after)) + clauses.append(Timeline.timestamp > after) if len(clauses) == 0: - clauses.append((True)) + clauses.append(True) timeline = ( Timeline.select( diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 4ceac139f..a6a4c65b6 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -11,7 +11,6 @@ import secrets import time from datetime import datetime from pathlib import Path -from typing import List, Optional from urllib.parse import parse_qs, urlparse from fastapi import APIRouter, Depends, HTTPException, Request, Response @@ -390,7 +389,7 @@ def verify_password(password, password_hash): return secrets.compare_digest(password_hash, compare_hash) -def validate_password_strength(password: str) -> tuple[bool, Optional[str]]: +def validate_password_strength(password: str) -> tuple[bool, str | None]: """ Validate password strength. @@ -445,7 +444,7 @@ async def get_current_user(request: Request): return {"username": username, "role": role} -def require_role(required_roles: List[str]): +def require_role(required_roles: list[str]): async def role_checker(request: Request): proxy_config: ProxyConfig = request.app.frigate_config.proxy config_roles = list(request.app.frigate_config.auth.roles.keys()) @@ -1083,7 +1082,7 @@ async def update_role( async def require_camera_access( - camera_name: Optional[str] = None, + camera_name: str | None = None, request: Request = None, ): """Dependency to enforce camera access based on user role.""" @@ -1148,8 +1147,8 @@ GO2RTC_STREAM_PROXY_PATHS = frozenset( def deny_response_for_go2rtc_stream( - original_url: Optional[str], role: Optional[str], request: Request -) -> Optional[int]: + original_url: str | None, role: str | None, request: Request +) -> int | None: """Block role-restricted users from go2rtc live streams they cannot access. Returns 403 when any `src` stream named in `original_url` resolves to a @@ -1194,7 +1193,7 @@ def deny_response_for_go2rtc_stream( async def require_go2rtc_stream_access( - stream_name: Optional[str] = None, + stream_name: str | None = None, request: Request = None, ): """Dependency to enforce go2rtc stream access based on owning camera access.""" diff --git a/frigate/api/camera.py b/frigate/api/camera.py index 0b01ada1d..a86c35883 100644 --- a/frigate/api/camera.py +++ b/frigate/api/camera.py @@ -74,7 +74,7 @@ def _is_valid_host(host: str) -> bool: @router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())]) async def go2rtc_streams(request: Request): - r = requests.get("http://127.0.0.1:1984/api/streams") + r = await asyncio.to_thread(requests.get, "http://127.0.0.1:1984/api/streams") if not r.ok: logger.error("Failed to fetch streams from go2rtc") return JSONResponse( @@ -1187,14 +1187,14 @@ async def delete_camera( try: with lock: - with open(config_file, "r") as f: + with open(config_file) as f: old_raw_config = f.read() try: yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) - with open(config_file, "r") as f: + with open(config_file) as f: data = yaml.load(f) # Remove camera from config @@ -1223,7 +1223,7 @@ async def delete_camera( with open(config_file, "w") as f: yaml.dump(data, f) - with open(config_file, "r") as f: + with open(config_file) as f: new_raw_config = f.read() try: @@ -1285,7 +1285,8 @@ async def delete_camera( # Best-effort go2rtc stream removal try: - requests.delete( + await asyncio.to_thread( + requests.delete, "http://127.0.0.1:1984/api/streams", params={"src": camera_name}, timeout=5, diff --git a/frigate/api/chat.py b/frigate/api/chat.py index f523d9991..ae6ff7a94 100644 --- a/frigate/api/chat.py +++ b/frigate/api/chat.py @@ -7,7 +7,7 @@ import operator import time from datetime import datetime from functools import reduce -from typing import Any, Optional +from typing import Any import cv2 from fastapi import APIRouter, Body, Depends, HTTPException, Request @@ -201,7 +201,7 @@ async def _execute_search_objects( # Return it as-is for the LLM return response except Exception as e: - logger.error(f"Error executing search_objects: {e}", exc_info=True) + logger.exception(f"Error executing search_objects: {e}") return JSONResponse( content={ "success": False, @@ -611,7 +611,7 @@ async def _execute_get_live_context( return result except Exception as e: - logger.error(f"Error executing get_live_context: {e}", exc_info=True) + logger.exception(f"Error executing get_live_context: {e}") return { "error": "Error getting live context", } @@ -621,7 +621,7 @@ async def _get_live_frame_image_url( request: Request, camera: str, allowed_cameras: list[str], -) -> Optional[str]: +) -> str | None: """ Fetch the current live frame for a camera as a base64 data URL. @@ -801,7 +801,7 @@ async def _execute_start_camera_watch( zones=zones, ) except RuntimeError as e: - logger.error("Failed to start VLM watch job: %s", e, exc_info=True) + logger.exception("Failed to start VLM watch job: %s", e) return {"error": "Failed to start VLM watch job."} return { @@ -979,7 +979,7 @@ def _execute_get_recap( return {"events": events} except Exception as e: - logger.error("Error executing get_recap: %s", e, exc_info=True) + logger.exception("Error executing get_recap: %s", e) return {"error": "Failed to fetch recap data."} @@ -1072,13 +1072,12 @@ async def _execute_pending_tools( } ) except Exception as e: - logger.error( + logger.exception( "Error executing tool %s (id: %s): %s. Arguments: %s", tool_name, tool_call_id, e, json.dumps(tool_args), - exc_info=True, ) error_content = json.dumps({"error": f"Tool execution failed: {str(e)}"}) tool_calls_out.append( @@ -1186,7 +1185,7 @@ async def chat_completion( async def stream_body_llm(): nonlocal conversation, stream_iterations - def _emit_chain(extra: Optional[list[dict[str, Any]]] = None): + def _emit_chain(extra: list[dict[str, Any]] | None = None): # Return the full conversation (including the system message) so # the client persists and replays it verbatim next turn. chain = conversation + (extra or []) @@ -1414,7 +1413,7 @@ async def chat_completion( ) except Exception as e: - logger.error(f"Error in chat completion: {e}", exc_info=True) + logger.exception(f"Error in chat completion: {e}") return JSONResponse( content={ "error": "An error occurred while processing your request.", @@ -1477,7 +1476,7 @@ async def start_vlm_monitor( username=request.headers.get("remote-user", ""), ) except RuntimeError as e: - logger.error("Failed to start VLM watch job: %s", e, exc_info=True) + logger.exception("Failed to start VLM watch job: %s", e) return JSONResponse( content={"success": False, "message": "Failed to start VLM watch job."}, status_code=409, diff --git a/frigate/api/chat_util.py b/frigate/api/chat_util.py index 743c38e57..a2f29c75b 100644 --- a/frigate/api/chat_util.py +++ b/frigate/api/chat_util.py @@ -9,8 +9,9 @@ loop state — all inputs and outputs are plain data. import logging import math import time +from collections.abc import Generator from datetime import datetime -from typing import Any, Dict, Generator, List, Optional +from typing import Any from frigate.embeddings.util import ZScoreNormalization from frigate.models import Event @@ -30,7 +31,7 @@ def chunk_content(content: str, chunk_size: int = 80) -> Generator[str, None, No if not content: return words = content.split(" ") - current: List[str] = [] + current: list[str] = [] current_len = 0 for w in words: current.append(w) @@ -44,8 +45,8 @@ def chunk_content(content: str, chunk_size: int = 80) -> Generator[str, None, No def format_events_with_local_time( - events_list: List[Dict[str, Any]], -) -> List[Dict[str, Any]]: + events_list: list[dict[str, Any]], +) -> list[dict[str, Any]]: """Add human-readable local start/end times to each event for the LLM.""" result = [] for evt in events_list: @@ -84,9 +85,9 @@ def distance_to_score(distance: float, stats: ZScoreNormalization) -> float: def fuse_scores( - visual_score: Optional[float], - description_score: Optional[float], -) -> Optional[float]: + visual_score: float | None, + description_score: float | None, +) -> float | None: """Weighted fusion of visual and description similarity scores. If one side is missing (e.g., no description embedding for this event), @@ -102,7 +103,7 @@ def fuse_scores( return VISUAL_WEIGHT * visual_score + DESCRIPTION_WEIGHT * description_score -def parse_iso_to_timestamp(value: Optional[str]) -> Optional[float]: +def parse_iso_to_timestamp(value: str | None) -> float | None: """Parse an ISO-8601 string as server-local time -> unix timestamp. Mirrors the parsing _execute_search_objects uses so both tools accept the @@ -119,9 +120,9 @@ def parse_iso_to_timestamp(value: Optional[str]) -> Optional[float]: return None -def hydrate_event(event: Event, score: Optional[float] = None) -> Dict[str, Any]: +def hydrate_event(event: Event, score: float | None = None) -> dict[str, Any]: """Convert an Event row into the dict shape returned by find_similar_objects.""" - data: Dict[str, Any] = { + data: dict[str, Any] = { "id": event.id, "camera": event.camera, "label": event.label, diff --git a/frigate/api/defs/query/app_query_parameters.py b/frigate/api/defs/query/app_query_parameters.py index e182a6afd..626d39679 100644 --- a/frigate/api/defs/query/app_query_parameters.py +++ b/frigate/api/defs/query/app_query_parameters.py @@ -1,12 +1,10 @@ -from typing import Optional - from pydantic import BaseModel class AppTimelineHourlyQueryParameters(BaseModel): - cameras: Optional[str] = "all" - labels: Optional[str] = "all" - after: Optional[float] = None - before: Optional[float] = None - limit: Optional[int] = 200 - timezone: Optional[str] = "utc" + cameras: str | None = "all" + labels: str | None = "all" + after: float | None = None + before: float | None = None + limit: int | None = 200 + timezone: str | None = "utc" diff --git a/frigate/api/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py index 8e5a5391a..06d0dfc3a 100644 --- a/frigate/api/defs/query/events_query_parameters.py +++ b/frigate/api/defs/query/events_query_parameters.py @@ -1,28 +1,26 @@ -from typing import Optional - from pydantic import BaseModel, Field DEFAULT_TIME_RANGE = "00:00,24:00" class EventsQueryParams(BaseModel): - camera: Optional[str] = "all" - cameras: Optional[str] = "all" - label: Optional[str] = "all" - labels: Optional[str] = "all" - sub_label: Optional[str] = "all" - sub_labels: Optional[str] = "all" - attributes: Optional[str] = "all" - zone: Optional[str] = "all" - zones: Optional[str] = "all" - limit: Optional[int] = 100 - after: Optional[float] = None - before: Optional[float] = None - time_range: Optional[str] = DEFAULT_TIME_RANGE - has_clip: Optional[int] = None - has_snapshot: Optional[int] = None - in_progress: Optional[int] = None - include_thumbnails: Optional[int] = Field( + camera: str | None = "all" + cameras: str | None = "all" + label: str | None = "all" + labels: str | None = "all" + sub_label: str | None = "all" + sub_labels: str | None = "all" + attributes: str | None = "all" + zone: str | None = "all" + zones: str | None = "all" + limit: int | None = 100 + after: float | None = None + before: float | None = None + time_range: str | None = DEFAULT_TIME_RANGE + has_clip: int | None = None + has_snapshot: int | None = None + in_progress: int | None = None + include_thumbnails: int | None = Field( 1, description=( "Deprecated. Thumbnail data is no longer included in the response. " @@ -30,25 +28,25 @@ class EventsQueryParams(BaseModel): ), deprecated=True, ) - favorites: Optional[int] = None - min_score: Optional[float] = None - max_score: Optional[float] = None - min_speed: Optional[float] = None - max_speed: Optional[float] = None - recognized_license_plate: Optional[str] = "all" - is_submitted: Optional[int] = None - min_length: Optional[float] = None - max_length: Optional[float] = None - event_id: Optional[str] = None - sort: Optional[str] = None - timezone: Optional[str] = "utc" + favorites: int | None = None + min_score: float | None = None + max_score: float | None = None + min_speed: float | None = None + max_speed: float | None = None + recognized_license_plate: str | None = "all" + is_submitted: int | None = None + min_length: float | None = None + max_length: float | None = None + event_id: str | None = None + sort: str | None = None + timezone: str | None = "utc" class EventsSearchQueryParams(BaseModel): - query: Optional[str] = None - event_id: Optional[str] = None - search_type: Optional[str] = "thumbnail" - include_thumbnails: Optional[int] = Field( + query: str | None = None + event_id: str | None = None + search_type: str | None = "thumbnail" + include_thumbnails: int | None = Field( 1, description=( "Deprecated. Thumbnail data is no longer included in the response. " @@ -56,28 +54,28 @@ class EventsSearchQueryParams(BaseModel): ), deprecated=True, ) - limit: Optional[int] = 50 - cameras: Optional[str] = "all" - labels: Optional[str] = "all" - sub_labels: Optional[str] = "all" - attributes: Optional[str] = "all" - zones: Optional[str] = "all" - after: Optional[float] = None - before: Optional[float] = None - time_range: Optional[str] = DEFAULT_TIME_RANGE - has_clip: Optional[bool] = None - has_snapshot: Optional[bool] = None - is_submitted: Optional[bool] = None - timezone: Optional[str] = "utc" - min_score: Optional[float] = None - max_score: Optional[float] = None - min_speed: Optional[float] = None - max_speed: Optional[float] = None - recognized_license_plate: Optional[str] = "all" - sort: Optional[str] = None + limit: int | None = 50 + cameras: str | None = "all" + labels: str | None = "all" + sub_labels: str | None = "all" + attributes: str | None = "all" + zones: str | None = "all" + after: float | None = None + before: float | None = None + time_range: str | None = DEFAULT_TIME_RANGE + has_clip: bool | None = None + has_snapshot: bool | None = None + is_submitted: bool | None = None + timezone: str | None = "utc" + min_score: float | None = None + max_score: float | None = None + min_speed: float | None = None + max_speed: float | None = None + recognized_license_plate: str | None = "all" + sort: str | None = None class EventsSummaryQueryParams(BaseModel): - timezone: Optional[str] = "utc" - has_clip: Optional[int] = None - has_snapshot: Optional[int] = None + timezone: str | None = "utc" + has_clip: int | None = None + has_snapshot: int | None = None diff --git a/frigate/api/defs/query/media_query_parameters.py b/frigate/api/defs/query/media_query_parameters.py index 2d4fae176..f11568597 100644 --- a/frigate/api/defs/query/media_query_parameters.py +++ b/frigate/api/defs/query/media_query_parameters.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional from pydantic import BaseModel @@ -17,33 +16,33 @@ class Extension(str, Enum): class MediaLatestFrameQueryParams(BaseModel): - bbox: Optional[int] = None - timestamp: Optional[int] = None - zones: Optional[int] = None - mask: Optional[int] = None - motion: Optional[int] = None - paths: Optional[int] = None - regions: Optional[int] = None - quality: Optional[int] = 70 - height: Optional[int] = None - store: Optional[int] = None + bbox: int | None = None + timestamp: int | None = None + zones: int | None = None + mask: int | None = None + motion: int | None = None + paths: int | None = None + regions: int | None = None + quality: int | None = 70 + height: int | None = None + store: int | None = None class MediaEventsSnapshotQueryParams(BaseModel): - download: Optional[bool] = False - timestamp: Optional[int] = None - bbox: Optional[int] = None - crop: Optional[int] = None - height: Optional[int] = None - quality: Optional[int] = None + download: bool | None = False + timestamp: int | None = None + bbox: int | None = None + crop: int | None = None + height: int | None = None + quality: int | None = None class MediaMjpegFeedQueryParams(BaseModel): fps: int = 3 height: int = 360 - bbox: Optional[int] = None - timestamp: Optional[int] = None - zones: Optional[int] = None - mask: Optional[int] = None - motion: Optional[int] = None - regions: Optional[int] = None + bbox: int | None = None + timestamp: int | None = None + zones: int | None = None + mask: int | None = None + motion: int | None = None + regions: int | None = None diff --git a/frigate/api/defs/query/recordings_query_parameters.py b/frigate/api/defs/query/recordings_query_parameters.py index d4f1b0a7b..770da96bb 100644 --- a/frigate/api/defs/query/recordings_query_parameters.py +++ b/frigate/api/defs/query/recordings_query_parameters.py @@ -1,21 +1,19 @@ -from typing import Optional, Union - from pydantic import BaseModel from pydantic.json_schema import SkipJsonSchema class MediaRecordingsSummaryQueryParams(BaseModel): timezone: str = "utc" - cameras: Optional[str] = "all" + cameras: str | None = "all" class MediaRecordingsAvailabilityQueryParams(BaseModel): cameras: str = "all" - before: Union[float, SkipJsonSchema[None]] = None - after: Union[float, SkipJsonSchema[None]] = None + before: float | SkipJsonSchema[None] = None + after: float | SkipJsonSchema[None] = None scale: int = 30 class RecordingsDeleteQueryParams(BaseModel): - keep: Optional[str] = None - cameras: Optional[str] = "all" + keep: str | None = None + cameras: str | None = "all" diff --git a/frigate/api/defs/query/regenerate_query_parameters.py b/frigate/api/defs/query/regenerate_query_parameters.py index af50ada2c..20d1016c4 100644 --- a/frigate/api/defs/query/regenerate_query_parameters.py +++ b/frigate/api/defs/query/regenerate_query_parameters.py @@ -1,13 +1,11 @@ -from typing import Optional - from pydantic import BaseModel, Field from frigate.events.types import RegenerateDescriptionEnum class RegenerateQueryParameters(BaseModel): - source: Optional[RegenerateDescriptionEnum] = RegenerateDescriptionEnum.thumbnails - force: Optional[bool] = Field( + source: RegenerateDescriptionEnum | None = RegenerateDescriptionEnum.thumbnails + force: bool | None = Field( default=False, description="Force (re)generating the description even if GenAI is disabled for this camera.", ) diff --git a/frigate/api/defs/query/review_query_parameters.py b/frigate/api/defs/query/review_query_parameters.py index 6d01d824d..b16146a9b 100644 --- a/frigate/api/defs/query/review_query_parameters.py +++ b/frigate/api/defs/query/review_query_parameters.py @@ -1,5 +1,3 @@ -from typing import Union - from pydantic import BaseModel from pydantic.json_schema import SkipJsonSchema @@ -10,11 +8,11 @@ class ReviewQueryParams(BaseModel): cameras: str = "all" labels: str = "all" zones: str = "all" - reviewed: Union[int, SkipJsonSchema[None]] = None - limit: Union[int, SkipJsonSchema[None]] = None - severity: Union[SeverityEnum, SkipJsonSchema[None]] = None - before: Union[float, SkipJsonSchema[None]] = None - after: Union[float, SkipJsonSchema[None]] = None + reviewed: int | SkipJsonSchema[None] = None + limit: int | SkipJsonSchema[None] = None + severity: SeverityEnum | SkipJsonSchema[None] = None + before: float | SkipJsonSchema[None] = None + after: float | SkipJsonSchema[None] = None class ReviewSummaryQueryParams(BaseModel): @@ -26,6 +24,6 @@ class ReviewSummaryQueryParams(BaseModel): class ReviewActivityMotionQueryParams(BaseModel): cameras: str = "all" - before: Union[float, SkipJsonSchema[None]] = None - after: Union[float, SkipJsonSchema[None]] = None + before: float | SkipJsonSchema[None] = None + after: float | SkipJsonSchema[None] = None scale: int = 30 diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py index 2c37f6ae4..b0b85c7ab 100644 --- a/frigate/api/defs/request/app_body.py +++ b/frigate/api/defs/request/app_body.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional +from typing import Any from pydantic import BaseModel, Field @@ -8,26 +8,26 @@ from frigate.config import GenAIProviderEnum class AppConfigSetBody(BaseModel): requires_restart: int = 1 update_topic: str | None = None - config_data: Optional[Dict[str, Any]] = None + config_data: dict[str, Any] | None = None skip_save: bool = False class GenAIProbeBody(BaseModel): provider: GenAIProviderEnum - api_key: Optional[str] = None - base_url: Optional[str] = None - provider_options: Dict[str, Any] = Field(default_factory=dict) + api_key: str | None = None + base_url: str | None = None + provider_options: dict[str, Any] = Field(default_factory=dict) class AppPutPasswordBody(BaseModel): password: str - old_password: Optional[str] = None + old_password: str | None = None class AppPostUsersBody(BaseModel): username: str password: str - role: Optional[str] = "viewer" + role: str | None = "viewer" class AppPostLoginBody(BaseModel): @@ -47,7 +47,7 @@ class MediaSyncBody(BaseModel): dry_run: bool = Field( default=True, description="If True, only report orphans without deleting them" ) - media_types: List[str] = Field( + media_types: list[str] = Field( default=["all"], description="Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails', 'review_thumbnails', 'previews', 'exports', 'recordings'", ) diff --git a/frigate/api/defs/request/batch_export_body.py b/frigate/api/defs/request/batch_export_body.py index c0863c885..24078bcc7 100644 --- a/frigate/api/defs/request/batch_export_body.py +++ b/frigate/api/defs/request/batch_export_body.py @@ -1,5 +1,3 @@ -from typing import List, Optional - from pydantic import BaseModel, Field, model_validator MAX_BATCH_EXPORT_ITEMS = 50 @@ -9,18 +7,18 @@ class BatchExportItem(BaseModel): camera: str = Field(title="Camera name") start_time: float = Field(title="Start time") end_time: float = Field(title="End time") - image_path: Optional[str] = Field( + image_path: str | None = Field( default=None, title="Existing thumbnail path", description="Optional existing image to use as the export thumbnail", ) - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( default=None, title="Friendly name", max_length=256, description="Optional friendly name for this specific export item", ) - client_item_id: Optional[str] = Field( + client_item_id: str | None = Field( default=None, title="Client item ID", max_length=128, @@ -29,13 +27,13 @@ class BatchExportItem(BaseModel): class BatchExportBody(BaseModel): - items: List[BatchExportItem] = Field( + items: list[BatchExportItem] = Field( title="Items", min_length=1, max_length=MAX_BATCH_EXPORT_ITEMS, description="List of export items. Each item has its own camera and time range.", ) - export_case_id: Optional[str] = Field( + export_case_id: str | None = Field( default=None, title="Export case ID", max_length=30, @@ -44,13 +42,13 @@ class BatchExportBody(BaseModel): "existing case is temporarily admin-only until case-level ACLs exist." ), ) - new_case_name: Optional[str] = Field( + new_case_name: str | None = Field( default=None, title="New case name", max_length=100, description="Name of a new export case to create when export_case_id is omitted", ) - new_case_description: Optional[str] = Field( + new_case_description: str | None = Field( default=None, title="New case description", description="Optional description for a newly created export case", diff --git a/frigate/api/defs/request/chat_body.py b/frigate/api/defs/request/chat_body.py index 04b168b9f..5ca674fbc 100644 --- a/frigate/api/defs/request/chat_body.py +++ b/frigate/api/defs/request/chat_body.py @@ -1,6 +1,6 @@ """Chat API request models.""" -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, Field @@ -11,7 +11,7 @@ class ChatMessage(BaseModel): role: str = Field( description="Message role: 'user', 'assistant', 'system', or 'tool'" ) - content: Optional[Any] = Field( + content: Any | None = Field( default=None, description=( "Message content. Usually a string, but may be a multimodal content " @@ -19,13 +19,13 @@ class ChatMessage(BaseModel): "request tool calls." ), ) - tool_call_id: Optional[str] = Field( + tool_call_id: str | None = Field( default=None, description="For tool messages, the ID of the tool call" ) - name: Optional[str] = Field( + name: str | None = Field( default=None, description="For tool messages, the tool name" ) - tool_calls: Optional[list[dict[str, Any]]] = Field( + tool_calls: list[dict[str, Any]] | None = Field( default=None, description=( "For assistant messages replayed from prior turns, the OpenAI-format " @@ -52,7 +52,7 @@ class ChatCompletionRequest(BaseModel): default=False, description="If true, stream the final assistant response in the body as newline-delimited JSON.", ) - enable_thinking: Optional[bool] = Field( + enable_thinking: bool | None = Field( default=None, description=( "Per-request thinking toggle. None means use the provider default. " diff --git a/frigate/api/defs/request/classification_body.py b/frigate/api/defs/request/classification_body.py index fb6a7dd0f..2aa90f458 100644 --- a/frigate/api/defs/request/classification_body.py +++ b/frigate/api/defs/request/classification_body.py @@ -1,5 +1,3 @@ -from typing import Dict, List, Tuple - from pydantic import BaseModel, Field @@ -12,14 +10,14 @@ class AudioTranscriptionBody(BaseModel): class DeleteFaceImagesBody(BaseModel): - ids: List[str] = Field( + ids: list[str] = Field( description="List of image filenames to delete from the face folder" ) class GenerateStateExamplesBody(BaseModel): model_name: str = Field(description="Name of the classification model") - cameras: Dict[str, Tuple[float, float, float, float]] = Field( + cameras: dict[str, tuple[float, float, float, float]] = Field( description="Dictionary mapping camera names to normalized crop coordinates in [x1, y1, x2, y2] format (values 0-1)" ) diff --git a/frigate/api/defs/request/events_body.py b/frigate/api/defs/request/events_body.py index d844c31ca..c7920ae89 100644 --- a/frigate/api/defs/request/events_body.py +++ b/frigate/api/defs/request/events_body.py @@ -1,5 +1,3 @@ -from typing import List, Optional, Union - from pydantic import BaseModel, Field from frigate.config.classification import TriggerType @@ -7,49 +5,47 @@ from frigate.config.classification import TriggerType class EventsSubLabelBody(BaseModel): subLabel: str = Field(title="Sub label", max_length=100) - subLabelScore: Optional[float] = Field( + subLabelScore: float | None = Field( title="Score for sub label", default=None, gt=0.0, le=1.0 ) - camera: Optional[str] = Field( - title="Camera this object is detected on.", default=None - ) + camera: str | None = Field(title="Camera this object is detected on.", default=None) class EventsLPRBody(BaseModel): recognizedLicensePlate: str = Field( title="Recognized License Plate", max_length=100 ) - recognizedLicensePlateScore: Optional[float] = Field( + recognizedLicensePlateScore: float | None = Field( title="Score for recognized license plate", default=None, gt=0.0, le=1.0 ) class EventsAttributesBody(BaseModel): - attributes: List[str] = Field( + attributes: list[str] = Field( title="Selected classification attributes for the event", default_factory=list, ) class EventsDescriptionBody(BaseModel): - description: Union[str, None] = Field(title="The description of the event") + description: str | None = Field(title="The description of the event") class EventsCreateBody(BaseModel): - sub_label: Optional[str] = None - score: Optional[float] = 0 - duration: Optional[int] = 30 - include_recording: Optional[bool] = True - draw: Optional[dict] = {} - pre_capture: Optional[int] = None + sub_label: str | None = None + score: float | None = 0 + duration: int | None = 30 + include_recording: bool | None = True + draw: dict | None = {} + pre_capture: int | None = None class EventsEndBody(BaseModel): - end_time: Optional[float] = None + end_time: float | None = None class EventsDeleteBody(BaseModel): - event_ids: List[str] = Field(title="The event IDs to delete") + event_ids: list[str] = Field(title="The event IDs to delete") class SubmitPlusBody(BaseModel): diff --git a/frigate/api/defs/request/export_bulk_body.py b/frigate/api/defs/request/export_bulk_body.py index 004c67d90..07283f81d 100644 --- a/frigate/api/defs/request/export_bulk_body.py +++ b/frigate/api/defs/request/export_bulk_body.py @@ -1,7 +1,5 @@ """Request bodies for bulk export operations.""" -from typing import Optional - from pydantic import BaseModel, Field, conlist, constr @@ -17,7 +15,7 @@ class ExportBulkReassignBody(BaseModel): # List of export IDs with at least one element and each element with at least one char ids: conlist(constr(min_length=1), min_length=1) - export_case_id: Optional[str] = Field( + export_case_id: str | None = Field( default=None, max_length=30, description="Case ID to assign to, or null to unassign from current case", diff --git a/frigate/api/defs/request/export_case_body.py b/frigate/api/defs/request/export_case_body.py index 66cba58ea..c7b436f14 100644 --- a/frigate/api/defs/request/export_case_body.py +++ b/frigate/api/defs/request/export_case_body.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel, Field @@ -7,7 +5,7 @@ class ExportCaseCreateBody(BaseModel): """Request body for creating a new export case.""" name: str = Field(max_length=100, description="Friendly name of the export case") - description: Optional[str] = Field( + description: str | None = Field( default=None, description="Optional description of the export case" ) @@ -15,11 +13,11 @@ class ExportCaseCreateBody(BaseModel): class ExportCaseUpdateBody(BaseModel): """Request body for updating an existing export case.""" - name: Optional[str] = Field( + name: str | None = Field( default=None, max_length=100, description="Updated friendly name of the export case", ) - description: Optional[str] = Field( + description: str | None = Field( default=None, description="Updated description of the export case" ) diff --git a/frigate/api/defs/request/export_recordings_body.py b/frigate/api/defs/request/export_recordings_body.py index b035203a2..beb8f3996 100644 --- a/frigate/api/defs/request/export_recordings_body.py +++ b/frigate/api/defs/request/export_recordings_body.py @@ -1,5 +1,3 @@ -from typing import Optional, Union - from pydantic import BaseModel, Field from pydantic.json_schema import SkipJsonSchema @@ -13,15 +11,15 @@ class ExportRecordingsBody(BaseModel): source: PlaybackSourceEnum = Field( default=PlaybackSourceEnum.recordings, title="Playback source" ) - name: Optional[str] = Field(title="Friendly name", default=None, max_length=256) - image_path: Union[str, SkipJsonSchema[None]] = None - export_case_id: Optional[str] = Field( + name: str | None = Field(title="Friendly name", default=None, max_length=256) + image_path: str | SkipJsonSchema[None] = None + export_case_id: str | None = Field( default=None, title="Export case ID", max_length=30, description="ID of the export case to assign this export to", ) - chapters: Optional[ChaptersEnum] = Field( + chapters: ChaptersEnum | None = Field( default=None, title="Chapter mode", description=( @@ -36,19 +34,19 @@ class ExportRecordingsCustomBody(BaseModel): default=PlaybackSourceEnum.recordings, title="Playback source" ) name: str = Field(title="Friendly name", default=None, max_length=256) - image_path: Union[str, SkipJsonSchema[None]] = None - export_case_id: Optional[str] = Field( + image_path: str | SkipJsonSchema[None] = None + export_case_id: str | None = Field( default=None, title="Export case ID", max_length=30, description="ID of the export case to assign this export to", ) - ffmpeg_input_args: Optional[str] = Field( + ffmpeg_input_args: str | None = Field( default=None, title="FFmpeg input arguments", description="Custom FFmpeg input arguments. If not provided, defaults to timelapse input args.", ) - ffmpeg_output_args: Optional[str] = Field( + ffmpeg_output_args: str | None = Field( default=None, title="FFmpeg output arguments", description="Custom FFmpeg output arguments. If not provided, defaults to timelapse output args.", diff --git a/frigate/api/defs/response/chat_response.py b/frigate/api/defs/response/chat_response.py index 59c8549e7..3007c4c0f 100644 --- a/frigate/api/defs/response/chat_response.py +++ b/frigate/api/defs/response/chat_response.py @@ -1,6 +1,6 @@ """Chat API response models.""" -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, Field @@ -17,14 +17,14 @@ class ChatMessageResponse(BaseModel): """A message in the chat response.""" role: str = Field(description="Message role") - content: Optional[str] = Field( + content: str | None = Field( default=None, description="Message content (None if tool calls present)" ) - reasoning: Optional[str] = Field( + reasoning: str | None = Field( default=None, description="Separated reasoning/thinking trace if the model emitted one", ) - tool_calls: Optional[list[ToolCallInvocation]] = Field( + tool_calls: list[ToolCallInvocation] | None = Field( default=None, description="Tool calls if LLM wants to call tools" ) diff --git a/frigate/api/defs/response/classification_response.py b/frigate/api/defs/response/classification_response.py index 92d354f24..09cdfbc17 100644 --- a/frigate/api/defs/response/classification_response.py +++ b/frigate/api/defs/response/classification_response.py @@ -1,9 +1,7 @@ -from typing import Dict, List, Optional - from pydantic import BaseModel, Field, RootModel -class FacesResponse(RootModel[Dict[str, List[str]]]): +class FacesResponse(RootModel[dict[str, list[str]]]): """Response model for the get_faces endpoint. Returns a mapping of face names to lists of image filenames. @@ -17,7 +15,7 @@ class FacesResponse(RootModel[Dict[str, List[str]]]): } """ - root: Dict[str, List[str]] = Field( + root: dict[str, list[str]] = Field( default_factory=dict, description="Dictionary mapping face names to lists of image filenames", ) @@ -30,9 +28,9 @@ class FaceRecognitionResponse(BaseModel): """ success: bool = Field(description="Whether the face recognition was successful") - score: Optional[float] = Field( + score: float | None = Field( default=None, description="Confidence score of the recognition (0-1)" ) - face_name: Optional[str] = Field( + face_name: str | None = Field( default=None, description="The recognized face name if successful" ) diff --git a/frigate/api/defs/response/event_response.py b/frigate/api/defs/response/event_response.py index 083849706..a366af8d1 100644 --- a/frigate/api/defs/response/event_response.py +++ b/frigate/api/defs/response/event_response.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, ConfigDict @@ -6,20 +6,20 @@ from pydantic import BaseModel, ConfigDict class EventResponse(BaseModel): id: str label: str - sub_label: Optional[str] + sub_label: str | None camera: str start_time: float - end_time: Optional[float] - false_positive: Optional[bool] + end_time: float | None + false_positive: bool | None zones: list[str] - thumbnail: Optional[str] + thumbnail: str | None has_clip: bool has_snapshot: bool retain_indefinitely: bool - plus_id: Optional[str] - model_hash: Optional[str] - detector_type: Optional[str] - model_type: Optional[str] + plus_id: str | None + model_hash: str | None + detector_type: str | None + model_type: str | None data: dict[str, Any] model_config = ConfigDict(protected_namespaces=()) diff --git a/frigate/api/defs/response/export_case_response.py b/frigate/api/defs/response/export_case_response.py index 713e16683..199d5b6b7 100644 --- a/frigate/api/defs/response/export_case_response.py +++ b/frigate/api/defs/response/export_case_response.py @@ -1,5 +1,3 @@ -from typing import List, Optional - from pydantic import BaseModel, Field @@ -8,7 +6,7 @@ class ExportCaseModel(BaseModel): id: str = Field(description="Unique identifier for the export case") name: str = Field(description="Friendly name of the export case") - description: Optional[str] = Field( + description: str | None = Field( default=None, description="Optional description of the export case" ) created_at: float = Field( @@ -19,4 +17,4 @@ class ExportCaseModel(BaseModel): ) -ExportCasesResponse = List[ExportCaseModel] +ExportCasesResponse = list[ExportCaseModel] diff --git a/frigate/api/defs/response/export_response.py b/frigate/api/defs/response/export_response.py index 10b4a7e64..5e900599d 100644 --- a/frigate/api/defs/response/export_response.py +++ b/frigate/api/defs/response/export_response.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional +from typing import Any from pydantic import BaseModel, Field @@ -15,7 +15,7 @@ class ExportModel(BaseModel): in_progress: bool = Field( description="Whether the export is currently being processed" ) - export_case_id: Optional[str] = Field( + export_case_id: str | None = Field( default=None, description="ID of the export case this export belongs to" ) @@ -25,10 +25,10 @@ class StartExportResponse(BaseModel): success: bool = Field(description="Whether the export was started successfully") message: str = Field(description="Status or error message") - export_id: Optional[str] = Field( + export_id: str | None = Field( default=None, description="The export ID if successfully started" ) - status: Optional[str] = Field( + status: str | None = Field( default=None, description="Queue status for the export job", ) @@ -38,24 +38,24 @@ class BatchExportResultModel(BaseModel): """Per-item result for a batch export request.""" camera: str = Field(description="Camera name for this export attempt") - export_id: Optional[str] = Field( + export_id: str | None = Field( default=None, description="The export ID when the export was successfully queued", ) success: bool = Field(description="Whether the export was successfully queued") - status: Optional[str] = Field( + status: str | None = Field( default=None, description="Queue status for this camera export", ) - error: Optional[str] = Field( + error: str | None = Field( default=None, description="Validation or queueing error for this item, if any", ) - item_index: Optional[int] = Field( + item_index: int | None = Field( default=None, description="Zero-based index of this result within the request items list", ) - client_item_id: Optional[str] = Field( + client_item_id: str | None = Field( default=None, description="Opaque client-supplied item identifier echoed from the request", ) @@ -64,12 +64,12 @@ class BatchExportResultModel(BaseModel): class BatchExportResponse(BaseModel): """Response model for starting an export batch.""" - export_case_id: Optional[str] = Field( + export_case_id: str | None = Field( default=None, description="Export case ID associated with the batch", ) - export_ids: List[str] = Field(description="Export IDs successfully queued") - results: List[BatchExportResultModel] = Field( + export_ids: list[str] = Field(description="Export IDs successfully queued") + results: list[BatchExportResultModel] = Field( description="Per-item batch export results" ) @@ -81,29 +81,29 @@ class ExportJobModel(BaseModel): job_type: str = Field(description="Job type") status: str = Field(description="Current job status") camera: str = Field(description="Camera associated with this export job") - name: Optional[str] = Field( + name: str | None = Field( default=None, description="Friendly name for the export", ) - export_case_id: Optional[str] = Field( + export_case_id: str | None = Field( default=None, description="ID of the export case this export belongs to", ) request_start_time: float = Field(description="Requested export start time") request_end_time: float = Field(description="Requested export end time") - start_time: Optional[float] = Field( + start_time: float | None = Field( default=None, description="Unix timestamp when execution started", ) - end_time: Optional[float] = Field( + end_time: float | None = Field( default=None, description="Unix timestamp when execution completed", ) - error_message: Optional[str] = Field( + error_message: str | None = Field( default=None, description="Error message for failed jobs", ) - results: Optional[dict[str, Any]] = Field( + results: dict[str, Any] | None = Field( default=None, description="Result metadata for completed jobs", ) @@ -117,7 +117,7 @@ class ExportJobModel(BaseModel): ) -ExportJobsResponse = List[ExportJobModel] +ExportJobsResponse = list[ExportJobModel] -ExportsResponse = List[ExportModel] +ExportsResponse = list[ExportModel] diff --git a/frigate/api/defs/response/preview_response.py b/frigate/api/defs/response/preview_response.py index d320a865d..70d2cac17 100644 --- a/frigate/api/defs/response/preview_response.py +++ b/frigate/api/defs/response/preview_response.py @@ -1,5 +1,3 @@ -from typing import List - from pydantic import BaseModel, Field @@ -13,5 +11,5 @@ class PreviewModel(BaseModel): end: float = Field(description="Unix timestamp when the preview ends") -PreviewsResponse = List[PreviewModel] -PreviewFramesResponse = List[str] +PreviewsResponse = list[PreviewModel] +PreviewFramesResponse = list[str] diff --git a/frigate/api/defs/response/review_response.py b/frigate/api/defs/response/review_response.py index b2fed3b1a..a0b755bd6 100644 --- a/frigate/api/defs/response/review_response.py +++ b/frigate/api/defs/response/review_response.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import Dict from pydantic import BaseModel, Json @@ -34,7 +33,7 @@ class DayReview(BaseModel): class ReviewSummaryResponse(BaseModel): last24Hours: Last24HoursReview - root: Dict[str, DayReview] + root: dict[str, DayReview] class ReviewActivityMotionResponse(BaseModel): diff --git a/frigate/api/event.py b/frigate/api/event.py index fc7c58c37..d37a547ba 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -10,7 +10,6 @@ import random import string from functools import reduce from pathlib import Path -from typing import List from urllib.parse import unquote import numpy as np @@ -97,7 +96,7 @@ def _build_attribute_filter_clause(attributes: str): ) def events( params: EventsQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): camera = params.camera cameras = params.cameras @@ -171,7 +170,7 @@ def events( ] if camera != "all": - clauses.append((Event.camera == camera)) + clauses.append(Event.camera == camera) if cameras != "all": requested = set(cameras.split(",")) @@ -181,11 +180,11 @@ def events( camera_list = list(filtered) else: camera_list = allowed_cameras - clauses.append((Event.camera << camera_list)) + clauses.append(Event.camera << camera_list) if labels != "all": label_list = labels.split(",") - clauses.append((Event.label << label_list)) + clauses.append(Event.label << label_list) if sub_labels != "all": # use matching so joined sub labels are included @@ -196,24 +195,24 @@ def events( if "None" in filtered_sub_labels: filtered_sub_labels.remove("None") - sub_label_clauses.append((Event.sub_label.is_null())) + sub_label_clauses.append(Event.sub_label.is_null()) for label in filtered_sub_labels: lowered = label.lower() sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) == lowered) + fn.LOWER(Event.sub_label.cast("text")) == lowered ) # include exact matches (case-insensitive) # include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII) sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*") + fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*" ) sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*") + fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*" ) sub_label_clause = reduce(operator.or_, sub_label_clauses) - clauses.append((sub_label_clause)) + clauses.append(sub_label_clause) if attributes != "all": # Custom classification results are stored as data[model_name] = result_value @@ -257,19 +256,19 @@ def events( if "None" in filtered_zones: filtered_zones.remove("None") - zone_clauses.append((Event.zones.length() == 0)) + zone_clauses.append(Event.zones.length() == 0) for zone in filtered_zones: - zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*')) + zone_clauses.append(Event.zones.cast("text") % f'*"{zone}"*') zone_clause = reduce(operator.or_, zone_clauses) - clauses.append((zone_clause)) + clauses.append(zone_clause) if after: - clauses.append((Event.start_time > after)) + clauses.append(Event.start_time > after) if before: - clauses.append((Event.start_time < before)) + clauses.append(Event.start_time < before) if time_range != DEFAULT_TIME_RANGE: # get timezone arg to ensure browser times are used @@ -289,62 +288,60 @@ def events( # should use or operator if time_after > time_before: clauses.append( - ( - reduce( - operator.or_, - [(start_hour_fun > time_after), (start_hour_fun < time_before)], - ) + reduce( + operator.or_, + [(start_hour_fun > time_after), (start_hour_fun < time_before)], ) ) # all other cases should be and operator else: - clauses.append((start_hour_fun > time_after)) - clauses.append((start_hour_fun < time_before)) + clauses.append(start_hour_fun > time_after) + clauses.append(start_hour_fun < time_before) if has_clip is not None: - clauses.append((Event.has_clip == has_clip)) + clauses.append(Event.has_clip == has_clip) if has_snapshot is not None: - clauses.append((Event.has_snapshot == has_snapshot)) + clauses.append(Event.has_snapshot == has_snapshot) if in_progress is not None: - clauses.append((Event.end_time.is_null(in_progress))) + clauses.append(Event.end_time.is_null(in_progress)) if include_thumbnails: selected_columns.append(Event.thumbnail) if favorites: - clauses.append((Event.retain_indefinitely == favorites)) + clauses.append(Event.retain_indefinitely == favorites) if max_score is not None: - clauses.append((Event.data["score"] <= max_score)) + clauses.append(Event.data["score"] <= max_score) if min_score is not None: - clauses.append((Event.data["score"] >= min_score)) + clauses.append(Event.data["score"] >= min_score) if max_speed is not None: - clauses.append((Event.data["average_estimated_speed"] <= max_speed)) + clauses.append(Event.data["average_estimated_speed"] <= max_speed) if min_speed is not None: - clauses.append((Event.data["average_estimated_speed"] >= min_speed)) + clauses.append(Event.data["average_estimated_speed"] >= min_speed) if min_length is not None: - clauses.append(((Event.end_time - Event.start_time) >= min_length)) + clauses.append((Event.end_time - Event.start_time) >= min_length) if max_length is not None: - clauses.append(((Event.end_time - Event.start_time) <= max_length)) + clauses.append((Event.end_time - Event.start_time) <= max_length) if is_submitted is not None: if is_submitted == 0: - clauses.append((Event.plus_id.is_null())) + clauses.append(Event.plus_id.is_null()) elif is_submitted > 0: - clauses.append((Event.plus_id != "")) + clauses.append(Event.plus_id != "") if event_id is not None: - clauses.append((Event.id == event_id)) + clauses.append(Event.id == event_id) if len(clauses) == 0: - clauses.append((True)) + clauses.append(True) if sort: if sort == "score_asc": @@ -387,7 +384,7 @@ def events( ) def events_explore( limit: int = 10, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): # get distinct labels for all events distinct_labels = ( @@ -515,7 +512,7 @@ async def event_ids(ids: str, request: Request): def events_search( request: Request, params: EventsSearchQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): query = params.query search_type = params.search_type @@ -595,12 +592,12 @@ def events_search( filtered = requested.intersection(allowed_cameras) if not filtered: return JSONResponse(content=[]) - event_filters.append((Event.camera << list(filtered))) + event_filters.append(Event.camera << list(filtered)) else: - event_filters.append((Event.camera << allowed_cameras)) + event_filters.append(Event.camera << allowed_cameras) if labels != "all": - event_filters.append((Event.label << labels.split(","))) + event_filters.append(Event.label << labels.split(",")) if sub_labels != "all": # use matching so joined sub labels are included @@ -611,23 +608,23 @@ def events_search( if "None" in filtered_sub_labels: filtered_sub_labels.remove("None") - sub_label_clauses.append((Event.sub_label.is_null())) + sub_label_clauses.append(Event.sub_label.is_null()) for label in filtered_sub_labels: lowered = label.lower() sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) == lowered) + fn.LOWER(Event.sub_label.cast("text")) == lowered ) # include exact matches (case-insensitive) # include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII) sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*") + fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*" ) sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*") + fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*" ) - event_filters.append((reduce(operator.or_, sub_label_clauses))) + event_filters.append(reduce(operator.or_, sub_label_clauses)) if attributes != "all": # Custom classification results are stored as data[model_name] = result_value @@ -641,12 +638,12 @@ def events_search( if "None" in filtered_zones: filtered_zones.remove("None") - zone_clauses.append((Event.zones.length() == 0)) + zone_clauses.append(Event.zones.length() == 0) for zone in filtered_zones: - zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*')) + zone_clauses.append(Event.zones.cast("text") % f'*"{zone}"*') - event_filters.append((reduce(operator.or_, zone_clauses))) + event_filters.append(reduce(operator.or_, zone_clauses)) if recognized_license_plate != "all": filtered_recognized_license_plates = recognized_license_plate.split(",") @@ -674,43 +671,43 @@ def events_search( ) recognized_license_plate_clause = reduce(operator.or_, clauses_for_plates) - event_filters.append((recognized_license_plate_clause)) + event_filters.append(recognized_license_plate_clause) if after: - event_filters.append((Event.start_time > after)) + event_filters.append(Event.start_time > after) if before: - event_filters.append((Event.start_time < before)) + event_filters.append(Event.start_time < before) if has_clip is not None: - event_filters.append((Event.has_clip == has_clip)) + event_filters.append(Event.has_clip == has_clip) if has_snapshot is not None: - event_filters.append((Event.has_snapshot == has_snapshot)) + event_filters.append(Event.has_snapshot == has_snapshot) if is_submitted is not None: if is_submitted == 0: - event_filters.append((Event.plus_id.is_null())) + event_filters.append(Event.plus_id.is_null()) elif is_submitted > 0: - event_filters.append((Event.plus_id != "")) + event_filters.append(Event.plus_id != "") if min_score is not None and max_score is not None: - event_filters.append((Event.data["score"].between(min_score, max_score))) + event_filters.append(Event.data["score"].between(min_score, max_score)) else: if min_score is not None: - event_filters.append((Event.data["score"] >= min_score)) + event_filters.append(Event.data["score"] >= min_score) if max_score is not None: - event_filters.append((Event.data["score"] <= max_score)) + event_filters.append(Event.data["score"] <= max_score) if min_speed is not None and max_speed is not None: event_filters.append( - (Event.data["average_estimated_speed"].between(min_speed, max_speed)) + Event.data["average_estimated_speed"].between(min_speed, max_speed) ) else: if min_speed is not None: - event_filters.append((Event.data["average_estimated_speed"] >= min_speed)) + event_filters.append(Event.data["average_estimated_speed"] >= min_speed) if max_speed is not None: - event_filters.append((Event.data["average_estimated_speed"] <= max_speed)) + event_filters.append(Event.data["average_estimated_speed"] <= max_speed) if time_range != DEFAULT_TIME_RANGE: tz_name = params.timezone @@ -728,17 +725,15 @@ def events_search( # should use or operator if time_after > time_before: event_filters.append( - ( - reduce( - operator.or_, - [(start_hour_fun > time_after), (start_hour_fun < time_before)], - ) + reduce( + operator.or_, + [(start_hour_fun > time_after), (start_hour_fun < time_before)], ) ) # all other cases should be and operator else: - event_filters.append((start_hour_fun > time_after)) - event_filters.append((start_hour_fun < time_before)) + event_filters.append(start_hour_fun > time_after) + event_filters.append(start_hour_fun < time_before) # Perform semantic search search_results = {} @@ -894,7 +889,7 @@ def events_search( @router.get("/events/summary", dependencies=[Depends(allow_any_authenticated())]) def events_summary( params: EventsSummaryQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): tz_name = params.timezone has_clip = params.has_clip @@ -903,13 +898,13 @@ def events_summary( clauses = [] if has_clip is not None: - clauses.append((Event.has_clip == has_clip)) + clauses.append(Event.has_clip == has_clip) if has_snapshot is not None: - clauses.append((Event.has_snapshot == has_snapshot)) + clauses.append(Event.has_snapshot == has_snapshot) if len(clauses) == 0: - clauses.append((True)) + clauses.append(True) time_range_query = ( Event.select( diff --git a/frigate/api/export.py b/frigate/api/export.py index 1e92374c0..741a0bb5d 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -7,8 +7,8 @@ import string import time import zipfile from collections import deque +from collections.abc import Iterator from pathlib import Path -from typing import Iterator, List, Optional import psutil from fastapi import APIRouter, Depends, Query, Request @@ -89,7 +89,7 @@ def _generate_export_id(camera_name: str) -> str: def _create_export_case_record( name: str, - description: Optional[str], + description: str | None, ) -> ExportCase: now = datetime.datetime.fromtimestamp(time.time()) return ExportCase.create( @@ -101,7 +101,7 @@ def _create_export_case_record( ) -def _validate_camera_name(request: Request, camera_name: str) -> Optional[JSONResponse]: +def _validate_camera_name(request: Request, camera_name: str) -> JSONResponse | None: if camera_name and request.app.frigate_config.cameras.get(camera_name): return None @@ -111,7 +111,7 @@ def _validate_camera_name(request: Request, camera_name: str) -> Optional[JSONRe ) -def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONResponse]: +def _validate_export_case(export_case_id: str | None) -> JSONResponse | None: if export_case_id is None: return None @@ -127,8 +127,8 @@ def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONRespons def _sanitize_existing_image( - image_path: Optional[str], -) -> tuple[Optional[str], Optional[JSONResponse]]: + image_path: str | None, +) -> tuple[str | None, JSONResponse | None]: # sanitize_filepath normalizes "\" to "/" but leaves ".." intact, so a path # like "clips\..\..\etc/passwd" passes the CLIPS_DIR prefix check yet still # escapes the directory once resolved. A valid snapshot path never uses "..". @@ -154,7 +154,7 @@ def _validate_export_source( start_time: float, end_time: float, playback_source: PlaybackSourceEnum, -) -> Optional[str]: +) -> str | None: if playback_source == PlaybackSourceEnum.recordings: recordings_count = ( Recordings.select() @@ -257,14 +257,14 @@ def _build_export_job( camera_name: str, start_time: float, end_time: float, - friendly_name: Optional[str], - existing_image: Optional[str], + friendly_name: str | None, + existing_image: str | None, playback_source: PlaybackSourceEnum, - export_case_id: Optional[str], - ffmpeg_input_args: Optional[str] = None, - ffmpeg_output_args: Optional[str] = None, + export_case_id: 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, ) -> ExportJob: return ExportJob( id=_generate_export_id(camera_name), @@ -302,11 +302,11 @@ def _export_case_to_dict(case: ExportCase) -> dict[str, object]: Returns a list of exports ordered by date (most recent first).""", ) def get_exports( - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), - export_case_id: Optional[str] = None, - cameras: Optional[str] = Query(default="all"), - start_date: Optional[float] = None, - end_date: Optional[float] = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), + export_case_id: str | None = None, + cameras: str | None = Query(default="all"), + start_date: float | None = None, + end_date: float | None = None, ): query = Export.select().where(Export.camera << allowed_cameras) @@ -422,7 +422,7 @@ def _unique_archive_name(export: Export, used: set[str]) -> str: return candidate -def _stream_case_archive(exports: List[Export]) -> Iterator[bytes]: +def _stream_case_archive(exports: list[Export]) -> Iterator[bytes]: """Yield bytes of a zip archive built from the given exports' mp4 files.""" buffer = _StreamingZipBuffer() used_names: set[str] = set() @@ -466,7 +466,7 @@ def _stream_case_archive(exports: List[Export]) -> Iterator[bytes]: ) def download_export_case( case_id: str, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): try: case = ExportCase.get(ExportCase.id == case_id) @@ -580,7 +580,7 @@ def delete_export_case(case_id: str, request: Request, delete_exports: bool = Fa ) def get_active_export_jobs( request: Request, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): jobs = list_active_export_jobs(request.app.frigate_config) return JSONResponse( @@ -622,7 +622,7 @@ async def get_export_job_status(export_id: str, request: Request): def export_recordings_batch( request: Request, body: BatchExportBody, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), current_user: dict = Depends(get_current_user), ): if isinstance(current_user, JSONResponse): @@ -662,7 +662,7 @@ def export_recordings_batch( # Sanitize each item's image_path up front. A bad path in any item # kills the whole request, consistent with single-export behavior. - sanitized_images: list[Optional[str]] = [] + sanitized_images: list[str | None] = [] for item in body.items: existing_image, image_validation_error = _sanitize_existing_image( item.image_path @@ -713,7 +713,7 @@ def export_recordings_batch( export_case_id = export_case.id export_ids: list[str] = [] - results: list[dict[str, Optional[str] | bool | int]] = [] + results: list[dict[str, str | None | bool | int]] = [] for index, item in enumerate(body.items): if index in item_errors: results.append( diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 3f8d8a7a5..5e8323b58 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -1,7 +1,6 @@ import asyncio import logging import re -from typing import Optional from fastapi import Depends, FastAPI, Request from fastapi.responses import JSONResponse @@ -64,7 +63,7 @@ class RemoteUserPlugin(Plugin): def create_fastapi_app( frigate_config: FrigateConfig, database: SqliteQueueDatabase, - embeddings: Optional[EmbeddingsContext], + embeddings: EmbeddingsContext | None, detected_frames_processor, storage_maintainer: StorageMaintainer, onvif: OnvifController, @@ -72,8 +71,8 @@ def create_fastapi_app( event_metadata_updater: EventMetadataPublisher, config_publisher: CameraConfigUpdatePublisher, replay_manager: DebugReplayManager, - dispatcher: Optional[Dispatcher] = None, - profile_manager: Optional[ProfileManager] = None, + dispatcher: Dispatcher | None = None, + profile_manager: ProfileManager | None = None, enforce_default_admin: bool = True, ): logger.info("Starting FastAPI app") diff --git a/frigate/api/media.py b/frigate/api/media.py index 4e01bbebe..8d56475c5 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -7,7 +7,7 @@ import math import os import subprocess as sp import time -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path as FilePath from typing import Any from urllib.parse import unquote @@ -314,10 +314,8 @@ async def get_snapshot_from_recording( Recordings.start_time, ) .where( - ( - (frame_time >= Recordings.start_time) - & (frame_time <= Recordings.end_time) - ) + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) ) .where(Recordings.camera == camera_name) .order_by(Recordings.start_time.desc()) @@ -335,10 +333,8 @@ async def get_snapshot_from_recording( Recordings.start_time, ) .where( - ( - (frame_time >= Recordings.start_time) - & (frame_time <= Recordings.end_time) - ) + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) ) .where(Recordings.camera == camera_name) .order_by(Recordings.start_time.desc()) @@ -398,10 +394,7 @@ async def submit_recording_snapshot_to_plus( Recordings.start_time, ) .where( - ( - (frame_time >= Recordings.start_time) - & (frame_time <= Recordings.end_time) - ) + (frame_time >= Recordings.start_time) & (frame_time <= Recordings.end_time) ) .where(Recordings.camera == camera_name) .order_by(Recordings.start_time.desc()) @@ -719,7 +712,7 @@ async def vod_hour( ): parts = year_month.split("-") start_date = ( - datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=timezone.utc) + datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC) - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() ) end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) diff --git a/frigate/api/media_auth.py b/frigate/api/media_auth.py index cc06eb75c..0630e3604 100644 --- a/frigate/api/media_auth.py +++ b/frigate/api/media_auth.py @@ -14,7 +14,6 @@ from __future__ import annotations import logging import os from enum import Enum -from typing import Optional from urllib.parse import unquote, urlparse from peewee import DoesNotExist @@ -42,7 +41,7 @@ class MediaAuthResolution(str, Enum): UNKNOWN = "unknown" -def extract_path(original_url: Optional[str]) -> Optional[str]: +def extract_path(original_url: str | None) -> str | None: """Return the decoded path component of nginx's `X-Original-URL` header. nginx forwards the *raw* request URI (with `..` segments intact) via @@ -72,8 +71,8 @@ def extract_path(original_url: Optional[str]) -> Optional[str]: def resolve_media_uri( - uri: str, frigate_config: Optional[FrigateConfig] = None -) -> tuple[MediaAuthResolution, Optional[str]]: + uri: str, frigate_config: FrigateConfig | None = None +) -> tuple[MediaAuthResolution, str | None]: """Classify a URI and return the owning camera if applicable. `frigate_config` is used to disambiguate clip/review filenames whose @@ -100,7 +99,7 @@ def resolve_media_uri( def _resolve_recording( parts: list[str], -) -> tuple[MediaAuthResolution, Optional[str]]: +) -> tuple[MediaAuthResolution, str | None]: # /recordings → neutral # /recordings/{date} → neutral # /recordings/{date}/{hour} → multi-camera listing @@ -113,8 +112,8 @@ def _resolve_recording( def _resolve_clip( - parts: list[str], frigate_config: Optional[FrigateConfig] -) -> tuple[MediaAuthResolution, Optional[str]]: + parts: list[str], frigate_config: FrigateConfig | None +) -> tuple[MediaAuthResolution, str | None]: # /clips → multi-camera listing # /clips/thumbs/{cam}/... → camera # /clips/previews/{cam}/... → camera @@ -159,8 +158,8 @@ def _resolve_clip( def _longest_prefix_camera( - stem: str, frigate_config: Optional[FrigateConfig] -) -> Optional[str]: + stem: str, frigate_config: FrigateConfig | None +) -> str | None: if frigate_config is None: return None for cam in sorted(frigate_config.cameras.keys(), key=len, reverse=True): @@ -170,8 +169,8 @@ def _longest_prefix_camera( def _camera_from_clip_filename( - filename: str, frigate_config: Optional[FrigateConfig] -) -> Optional[str]: + filename: str, frigate_config: FrigateConfig | None +) -> str | None: """Match a flat clip filename `{camera}-{event_id}[-clean].{ext}` against configured camera names. Longest-prefix wins so camera names containing hyphens (e.g. `front-door`) resolve correctly. @@ -182,8 +181,8 @@ def _camera_from_clip_filename( def _camera_from_thumb_filename( - filename: str, frigate_config: Optional[FrigateConfig] -) -> Optional[str]: + filename: str, frigate_config: FrigateConfig | None +) -> str | None: """Match a review thumbnail filename `thumb-{camera}-{review_id}.webp`.""" if not filename.startswith("thumb-"): return None @@ -194,7 +193,7 @@ def _camera_from_thumb_filename( def _resolve_export( parts: list[str], -) -> tuple[MediaAuthResolution, Optional[str]]: +) -> tuple[MediaAuthResolution, str | None]: # /exports → multi-camera listing # /exports/{filename}.mp4 → camera (DB lookup by exact path) if len(parts) == 1: @@ -240,8 +239,8 @@ def is_role_restricted(role: str, frigate_config: FrigateConfig) -> bool: def deny_response_for_media_uri( - original_url: Optional[str], role: Optional[str], frigate_config: FrigateConfig -) -> Optional[int]: + original_url: str | None, role: str | None, frigate_config: FrigateConfig +) -> int | None: """Decide whether the current role should be blocked from `original_url`. Returns an HTTP status code (403) when access should be denied, or `None` diff --git a/frigate/api/motion_search.py b/frigate/api/motion_search.py index 378e3469b..8a5f17545 100644 --- a/frigate/api/motion_search.py +++ b/frigate/api/motion_search.py @@ -1,7 +1,7 @@ """Motion search API for detecting changes within a region of interest.""" import logging -from typing import Any, List, Optional +from typing import Any from fastapi import APIRouter, Depends, Request from fastapi.responses import JSONResponse @@ -26,7 +26,7 @@ class MotionSearchRequest(BaseModel): start_time: float = Field(description="Start timestamp for the search range") end_time: float = Field(description="End timestamp for the search range") - polygon_points: List[List[float]] = Field( + polygon_points: list[list[float]] = Field( description="List of [x, y] normalized coordinates (0-1) defining the ROI polygon" ) threshold: int = Field( @@ -87,12 +87,12 @@ class MotionSearchStatusResponse(BaseModel): success: bool message: str status: str # "queued", "running", "success", "failed", or "cancelled" - results: Optional[List[MotionSearchResult]] = None - total_frames_processed: Optional[int] = None - error_message: Optional[str] = None - metrics: Optional[MotionSearchMetricsResponse] = None - scanning_timestamp: Optional[float] = None - progress: Optional[float] = None + results: list[MotionSearchResult] | None = None + total_frames_processed: int | None = None + error_message: str | None = None + metrics: MotionSearchMetricsResponse | None = None + scanning_timestamp: float | None = None + progress: float | None = None @router.post( diff --git a/frigate/api/preview.py b/frigate/api/preview.py index c4d86130e..104759143 100644 --- a/frigate/api/preview.py +++ b/frigate/api/preview.py @@ -4,7 +4,7 @@ import bisect import logging import os import threading -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import pytz from fastapi import APIRouter, Depends, HTTPException @@ -125,7 +125,7 @@ def preview_hour( """Get all mp4 previews relevant for time period given the timezone""" parts = year_month.split("-") start_date = ( - datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=timezone.utc) + datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC) - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() ) end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) diff --git a/frigate/api/record.py b/frigate/api/record.py index 5fc3950e2..5db257f48 100644 --- a/frigate/api/record.py +++ b/frigate/api/record.py @@ -5,7 +5,6 @@ import logging from datetime import datetime, timedelta from functools import reduce from pathlib import Path -from typing import List from urllib.parse import unquote from fastapi import APIRouter, Depends, Request @@ -63,7 +62,7 @@ def get_recordings_storage_usage(request: Request): def all_recordings_summary( request: Request, params: MediaRecordingsSummaryQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """Returns true/false by day indicating if recordings exist""" @@ -263,7 +262,7 @@ async def recordings( async def no_recordings( request: Request, params: MediaRecordingsAvailabilityQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """Get time ranges with no recordings.""" cameras = params.cameras @@ -365,7 +364,7 @@ async def delete_recordings( start: float = PathParam(..., description="Start timestamp (unix)"), end: float = PathParam(..., description="End timestamp (unix)"), params: RecordingsDeleteQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """Delete recordings in the specified time range.""" if start >= end: diff --git a/frigate/api/review.py b/frigate/api/review.py index ccf0be9d1..08705e3f9 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -4,7 +4,6 @@ import datetime import logging from functools import reduce from pathlib import Path -from typing import List import pandas as pd from fastapi import APIRouter, Request @@ -51,7 +50,7 @@ router = APIRouter(tags=[Tags.review]) async def review( params: ReviewQueryParams = Depends(), current_user: dict = Depends(get_current_user), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): if isinstance(current_user, JSONResponse): return current_user @@ -83,7 +82,7 @@ async def review( camera_list = list(filtered) else: camera_list = allowed_cameras - clauses.append((ReviewSegment.camera << camera_list)) + clauses.append(ReviewSegment.camera << camera_list) if labels != "all": # use matching so segments with multiple labels @@ -106,12 +105,12 @@ async def review( for zone in filtered_zones: zone_clauses.append( - (ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*') + ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*' ) clauses.append(reduce(operator.or_, zone_clauses)) if severity: - clauses.append((ReviewSegment.severity == severity)) + clauses.append(ReviewSegment.severity == severity) # Join with UserReviewStatus to get per-user review status review_query = ( @@ -204,7 +203,7 @@ async def review_ids(request: Request, ids: str): async def review_summary( params: ReviewSummaryQueryParams = Depends(), current_user: dict = Depends(get_current_user), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): if isinstance(current_user, JSONResponse): return current_user @@ -227,7 +226,7 @@ async def review_summary( camera_list = list(filtered) else: camera_list = allowed_cameras - clauses.append((ReviewSegment.camera << camera_list)) + clauses.append(ReviewSegment.camera << camera_list) if labels != "all": # use matching so segments with multiple labels @@ -328,7 +327,7 @@ async def review_summary( camera_list = list(filtered) else: camera_list = allowed_cameras - clauses.append((ReviewSegment.camera << camera_list)) + clauses.append(ReviewSegment.camera << camera_list) if labels != "all": # use matching so segments with multiple labels @@ -584,7 +583,7 @@ def delete_reviews(body: ReviewModifyMultipleBody): ) def motion_activity( params: ReviewActivityMotionQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """Get motion and audio activity.""" cameras = params.cameras @@ -597,7 +596,7 @@ def motion_activity( scale = params.scale clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)] - clauses.append((Recordings.motion > 0)) + clauses.append(Recordings.motion > 0) if cameras != "all": requested = set(cameras.split(",")) @@ -608,7 +607,7 @@ def motion_activity( else: camera_list = list(allowed_cameras) - clauses.append((Recordings.camera << camera_list)) + clauses.append(Recordings.camera << camera_list) data: list[Recordings] = ( Recordings.select( diff --git a/frigate/app.py b/frigate/app.py index 785dc2447..b0e29eab2 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -4,11 +4,11 @@ import multiprocessing as mp import os import secrets import shutil +from collections.abc import Callable from multiprocessing import Queue from multiprocessing.managers import DictProxy, SyncManager from multiprocessing.synchronize import Event as MpEvent from pathlib import Path -from typing import Callable, Optional import psutil import uvicorn @@ -95,7 +95,7 @@ class FrigateApp: self, config: FrigateConfig, manager: SyncManager, stop_event: MpEvent ) -> None: self.metrics_manager = manager - self.audio_process: Optional[mp.Process] = None + self.audio_process: mp.Process | None = None self.stop_event = stop_event self.detection_queue: Queue = mp.Queue() self.detectors: dict[str, ObjectDetectProcess] = {} @@ -120,8 +120,8 @@ class FrigateApp: ) self.ptz_metrics: dict[str, PTZMetrics] = {} self.processes: dict[str, int] = {} - self.embeddings: Optional[EmbeddingsContext] = None - self.profile_manager: Optional[ProfileManager] = None + self.embeddings: EmbeddingsContext | None = None + self.profile_manager: ProfileManager | None = None self.config = config def ensure_dirs(self) -> None: diff --git a/frigate/camera/activity_manager.py b/frigate/camera/activity_manager.py index 3952c8b43..bd3474b1a 100644 --- a/frigate/camera/activity_manager.py +++ b/frigate/camera/activity_manager.py @@ -6,7 +6,8 @@ import logging import random import string from collections import Counter -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from frigate.comms.event_metadata_updater import ( EventMetadataPublisher, diff --git a/frigate/camera/state.py b/frigate/camera/state.py index f35a3eaa5..c0d0f8b7f 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -5,7 +5,8 @@ import logging import os import threading from collections import defaultdict -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import cv2 import numpy as np diff --git a/frigate/comms/base_communicator.py b/frigate/comms/base_communicator.py index 5dfbf1115..090552a5b 100644 --- a/frigate/comms/base_communicator.py +++ b/frigate/comms/base_communicator.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod -from typing import Any, Callable +from collections.abc import Callable +from typing import Any class Communicator(ABC): diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 4c6dc6ef9..089da5df3 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -3,8 +3,8 @@ import datetime import json import logging -from collections.abc import Iterable -from typing import Any, Callable, Optional, cast +from collections.abc import Callable, Iterable +from typing import Any, cast from frigate.camera import PTZMetrics from frigate.camera.activity_manager import AudioActivityManager, CameraActivityManager @@ -97,7 +97,7 @@ class Dispatcher: "notifications": self._on_global_notification_command, "profile": self._on_profile_command, } - self.profile_manager: Optional[ProfileManager] = None + self.profile_manager: ProfileManager | None = None for comm in self.comms: comm.subscribe(self._receive) @@ -106,7 +106,7 @@ class Dispatcher: (comm for comm in communicators if isinstance(comm, WebPushClient)), None ) - def _receive(self, topic: str, payload: Any) -> Optional[Any]: + def _receive(self, topic: str, payload: Any) -> Any | None: """Handle receiving of payload from communicators.""" def handle_camera_command( diff --git a/frigate/comms/embeddings_updater.py b/frigate/comms/embeddings_updater.py index f7fd9c2bf..cd83709f0 100644 --- a/frigate/comms/embeddings_updater.py +++ b/frigate/comms/embeddings_updater.py @@ -1,8 +1,9 @@ """Facilitates communication between processes.""" import logging +from collections.abc import Callable from enum import Enum -from typing import Any, Callable +from typing import Any import zmq diff --git a/frigate/comms/inter_process.py b/frigate/comms/inter_process.py index 5e76da5eb..6897b12eb 100644 --- a/frigate/comms/inter_process.py +++ b/frigate/comms/inter_process.py @@ -3,8 +3,9 @@ import logging import multiprocessing as mp import threading +from collections.abc import Callable from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Callable +from typing import Any import zmq diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py index b02dbffb0..979fc6d6c 100644 --- a/frigate/comms/mqtt.py +++ b/frigate/comms/mqtt.py @@ -1,6 +1,7 @@ import logging import threading -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import paho.mqtt.client as mqtt from paho.mqtt.enums import CallbackAPIVersion @@ -214,8 +215,8 @@ class MqttClient(Communicator): logger.error("Unable to connect to MQTT server: MQTT Not authorized") else: logger.error( - "Unable to connect to MQTT server: Connection refused. Error code: " - + reason_code.getName() + "Unable to connect to MQTT server: Connection refused. Error code: %s", + reason_code.getName(), ) self.connected = True diff --git a/frigate/comms/runtime_state.py b/frigate/comms/runtime_state.py index 5066ed399..222d488ec 100644 --- a/frigate/comms/runtime_state.py +++ b/frigate/comms/runtime_state.py @@ -146,7 +146,7 @@ class RuntimeStatePersistence: if not os.path.exists(self._path): return {} try: - with open(self._path, "r") as f: + with open(self._path) as f: data = json.load(f) except (OSError, json.JSONDecodeError): logger.exception( diff --git a/frigate/comms/webpush.py b/frigate/comms/webpush.py index e35b64762..b548446c2 100644 --- a/frigate/comms/webpush.py +++ b/frigate/comms/webpush.py @@ -6,9 +6,10 @@ import logging import os import queue import threading +from collections.abc import Callable from dataclasses import dataclass from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Callable +from typing import Any from py_vapid import Vapid01 from pywebpush import WebPusher diff --git a/frigate/comms/ws.py b/frigate/comms/ws.py index f3d323442..ac047b053 100644 --- a/frigate/comms/ws.py +++ b/frigate/comms/ws.py @@ -4,7 +4,8 @@ import errno import json import logging import threading -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from wsgiref.simple_server import make_server from ws4py.server.wsgirefserver import ( diff --git a/frigate/config/auth.py b/frigate/config/auth.py index fccbfbaf2..04beeb775 100644 --- a/frigate/config/auth.py +++ b/frigate/config/auth.py @@ -1,5 +1,3 @@ -from typing import Dict, List, Optional - from pydantic import Field, field_validator, model_validator from .base import FrigateBaseModel @@ -41,7 +39,7 @@ class AuthConfig(FrigateBaseModel): description="When a session is within this many seconds of expiring, refresh it back to full length.", ge=30, ) - failed_login_rate_limit: Optional[str] = Field( + failed_login_rate_limit: str | None = Field( default=None, title="Failed login limits", description="Rate limiting rules for failed login attempts to reduce brute-force attacks.", @@ -57,12 +55,12 @@ class AuthConfig(FrigateBaseModel): title="Hash iterations", description="Number of PBKDF2-SHA256 iterations to use when hashing user passwords.", ) - roles: Dict[str, List[str]] = Field( + roles: dict[str, list[str]] = Field( default_factory=dict, title="Role mappings", description="Map roles to camera lists. An empty list grants access to all cameras for the role.", ) - admin_first_time_login: Optional[bool] = Field( + admin_first_time_login: bool | None = Field( default=False, title="First-time admin flag", description=( @@ -72,7 +70,7 @@ class AuthConfig(FrigateBaseModel): @field_validator("roles") @classmethod - def validate_roles(cls, v: Dict[str, List[str]]) -> Dict[str, List[str]]: + def validate_roles(cls, v: dict[str, list[str]]) -> dict[str, list[str]]: # Ensure role names are valid (alphanumeric with underscores) for role in v.keys(): if not role.replace("_", "").isalnum(): diff --git a/frigate/config/camera/audio.py b/frigate/config/camera/audio.py index 59a5180be..813c2988d 100644 --- a/frigate/config/camera/audio.py +++ b/frigate/config/camera/audio.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field from frigate.const import AUDIO_MIN_CONFIDENCE @@ -43,12 +41,12 @@ class AudioConfig(FrigateBaseModel): title="Listen types", description="List of audio event types to detect (for example: bark, fire_alarm, speech, yell).", ) - filters: Optional[dict[str, AudioFilterConfig]] = Field( + filters: dict[str, AudioFilterConfig] | None = Field( None, title="Audio filters", description="Per-audio-type filter settings such as confidence thresholds used to reduce false positives.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( None, title="Original audio state", description="Indicates whether audio detection was originally enabled in the static config file.", diff --git a/frigate/config/camera/birdseye.py b/frigate/config/camera/birdseye.py index 32aa66a98..b51c73d3c 100644 --- a/frigate/config/camera/birdseye.py +++ b/frigate/config/camera/birdseye.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional from pydantic import BaseModel, Field @@ -35,7 +34,7 @@ class BirdseyeLayoutConfig(FrigateBaseModel): ge=1.0, le=5.0, ) - max_cameras: Optional[int] = Field( + max_cameras: int | None = Field( default=None, title="Max cameras", description="Maximum number of cameras to display at once in Birdseye; shows the most recent cameras.", diff --git a/frigate/config/camera/camera.py b/frigate/config/camera/camera.py index 247b1d116..b9d2cef72 100644 --- a/frigate/config/camera/camera.py +++ b/frigate/config/camera/camera.py @@ -1,6 +1,5 @@ import os from enum import Enum -from typing import Optional from pydantic import Field, PrivateAttr, model_validator @@ -51,14 +50,14 @@ class CameraTypeEnum(str, Enum): class CameraConfig(FrigateBaseModel): - name: Optional[str] = Field( + name: str | None = Field( None, title="Camera name", description="Camera name is required", pattern=REGEX_CAMERA_NAME, ) - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( None, title="Friendly name", description="Camera friendly name used in the Frigate UI", @@ -180,7 +179,7 @@ class CameraConfig(FrigateBaseModel): title="Camera UI", description="Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.", ) - webui_url: Optional[str] = Field( + webui_url: str | None = Field( None, title="Camera URL", description="URL to visit the camera directly from system page", @@ -196,7 +195,7 @@ class CameraConfig(FrigateBaseModel): title="Zones", description="Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original camera state", description="Keep track of original state of camera.", diff --git a/frigate/config/camera/detect.py b/frigate/config/camera/detect.py index 71dbc3292..d093ed986 100644 --- a/frigate/config/camera/detect.py +++ b/frigate/config/camera/detect.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field, model_validator from ..base import FrigateBaseModel @@ -8,7 +6,7 @@ __all__ = ["DetectConfig", "StationaryConfig", "StationaryMaxFramesConfig"] class StationaryMaxFramesConfig(FrigateBaseModel): - default: Optional[int] = Field( + default: int | None = Field( default=None, title="Default max frames", description="Default maximum frames to track a stationary object before stopping.", @@ -22,13 +20,13 @@ class StationaryMaxFramesConfig(FrigateBaseModel): class StationaryConfig(FrigateBaseModel): - interval: Optional[int] = Field( + interval: int | None = Field( default=None, title="Stationary interval", description="How often (in frames) to run a detection check to confirm a stationary object.", gt=0, ) - threshold: Optional[int] = Field( + threshold: int | None = Field( default=None, title="Stationary threshold", description="Number of frames with no position change required to mark an object as stationary.", @@ -52,12 +50,12 @@ class DetectConfig(FrigateBaseModel): title="Enable object detection", description="Enable or disable object detection for all cameras; can be overridden per-camera.", ) - height: Optional[int] = Field( + height: int | None = Field( default=None, title="Detect height", description="Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", ) - width: Optional[int] = Field( + width: int | None = Field( default=None, title="Detect width", description="Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", @@ -67,13 +65,13 @@ class DetectConfig(FrigateBaseModel): title="Detect FPS", description="Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects).", ) - min_initialized: Optional[int] = Field( + min_initialized: int | None = Field( default=None, title="Minimum initialization frames", description="Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2.", ge=2, ) - max_disappeared: Optional[int] = Field( + max_disappeared: int | None = Field( default=None, title="Maximum disappeared frames", description="Number of frames without a detection before a tracked object is considered gone.", diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index 7e63c4a1e..ad7cbc8aa 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Union from pydantic import Field, field_validator @@ -33,12 +32,12 @@ DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [ class FfmpegOutputArgsConfig(FrigateBaseModel): - detect: Union[str, list[str]] = Field( + detect: str | list[str] = Field( default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT, title="Detect output arguments", description="Default output arguments for detect role streams.", ) - record: Union[str, list[str]] = Field( + record: str | list[str] = Field( default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT, title="Record output arguments", description="Default output arguments for record role streams.", @@ -51,17 +50,17 @@ class FfmpegConfig(FrigateBaseModel): title="FFmpeg path", description='Path to the FFmpeg binary to use or a version alias ("7.0" or "8.0").', ) - global_args: Union[str, list[str]] = Field( + global_args: str | list[str] = Field( default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="FFmpeg global arguments", description="Global arguments passed to FFmpeg processes.", ) - hwaccel_args: Union[str, list[str]] = Field( + hwaccel_args: str | list[str] = Field( default="auto", title="Hardware acceleration arguments", description="Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.", ) - input_args: Union[str, list[str]] = Field( + input_args: str | list[str] = Field( default=FFMPEG_INPUT_ARGS_DEFAULT, title="Input arguments", description="Input arguments applied to FFmpeg input streams.", @@ -112,17 +111,17 @@ class CameraInput(FrigateBaseModel): title="Input roles", description="Roles for this input stream.", ) - global_args: Union[str, list[str]] = Field( + global_args: str | list[str] = Field( default_factory=list, title="FFmpeg global arguments", description="FFmpeg global arguments for this input stream.", ) - hwaccel_args: Union[str, list[str]] = Field( + hwaccel_args: str | list[str] = Field( default_factory=list, title="Hardware acceleration arguments", description="Hardware acceleration arguments for this input stream.", ) - input_args: Union[str, list[str]] = Field( + input_args: str | list[str] = Field( default_factory=list, title="Input arguments", description="Input arguments specific to this stream.", diff --git a/frigate/config/camera/genai.py b/frigate/config/camera/genai.py index 5b9475572..82358252f 100644 --- a/frigate/config/camera/genai.py +++ b/frigate/config/camera/genai.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any, Optional +from typing import Any from pydantic import Field @@ -26,12 +26,12 @@ class GenAIRoleEnum(str, Enum): class GenAIConfig(FrigateBaseModel): """Primary GenAI Config to define GenAI Provider.""" - api_key: Optional[EnvString] = Field( + api_key: EnvString | None = Field( default=None, title="API key", description="API key required by some providers (can also be set via environment variables).", ) - base_url: Optional[str] = Field( + base_url: str | None = Field( default=None, title="Base URL", description="Base URL for self-hosted or compatible providers (for example an Ollama instance).", diff --git a/frigate/config/camera/live.py b/frigate/config/camera/live.py index 54b5a2bfd..90a7c8e4c 100644 --- a/frigate/config/camera/live.py +++ b/frigate/config/camera/live.py @@ -1,5 +1,3 @@ -from typing import Dict - from pydantic import Field from ..base import FrigateBaseModel @@ -8,7 +6,7 @@ __all__ = ["CameraLiveConfig"] class CameraLiveConfig(FrigateBaseModel): - streams: Dict[str, str] = Field( + streams: dict[str, str] = Field( default_factory=list, title="Live stream names", description="Mapping of configured stream names to restream/go2rtc names used for live playback.", diff --git a/frigate/config/camera/mask.py b/frigate/config/camera/mask.py index dbe0f063c..ecfdb418b 100644 --- a/frigate/config/camera/mask.py +++ b/frigate/config/camera/mask.py @@ -1,6 +1,6 @@ """Mask configuration for motion and object masks.""" -from typing import Any, Optional, Union +from typing import Any from pydantic import Field, field_serializer @@ -12,7 +12,7 @@ __all__ = ["MotionMaskConfig", "ObjectMaskConfig"] class MotionMaskConfig(FrigateBaseModel): """Configuration for a single motion mask.""" - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( default=None, title="Friendly name", description="A friendly name for this motion mask used in the Frigate UI", @@ -22,13 +22,13 @@ class MotionMaskConfig(FrigateBaseModel): title="Enabled", description="Enable or disable this motion mask", ) - coordinates: Union[str, list[str]] = Field( + coordinates: str | list[str] = Field( default="", title="Coordinates", description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", ) - raw_coordinates: Union[str, list[str]] = "" - enabled_in_config: Optional[bool] = Field( + raw_coordinates: str | list[str] = "" + enabled_in_config: bool | None = Field( default=None, title="Keep track of original state of motion mask." ) @@ -50,7 +50,7 @@ class MotionMaskConfig(FrigateBaseModel): class ObjectMaskConfig(FrigateBaseModel): """Configuration for a single object mask.""" - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( default=None, title="Friendly name", description="A friendly name for this object mask used in the Frigate UI", @@ -60,13 +60,13 @@ class ObjectMaskConfig(FrigateBaseModel): title="Enabled", description="Enable or disable this object mask", ) - coordinates: Union[str, list[str]] = Field( + coordinates: str | list[str] = Field( default="", title="Coordinates", description="Ordered x,y coordinates defining the object mask polygon used to include/exclude areas.", ) - raw_coordinates: Union[str, list[str]] = "" - enabled_in_config: Optional[bool] = Field( + raw_coordinates: str | list[str] = "" + enabled_in_config: bool | None = Field( default=None, title="Keep track of original state of object mask." ) diff --git a/frigate/config/camera/motion.py b/frigate/config/camera/motion.py index ebba8613c..c44c1453f 100644 --- a/frigate/config/camera/motion.py +++ b/frigate/config/camera/motion.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from pydantic import Field, field_serializer @@ -28,7 +28,7 @@ class MotionConfig(FrigateBaseModel): ge=0.3, le=1.0, ) - skip_motion_threshold: Optional[float] = Field( + skip_motion_threshold: float | None = Field( default=None, title="Skip motion threshold", description="If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.", @@ -40,7 +40,7 @@ class MotionConfig(FrigateBaseModel): title="Improve contrast", description="Apply contrast improvement to frames before motion analysis to help detection.", ) - contour_area: Optional[int] = Field( + contour_area: int | None = Field( default=10, title="Contour area", description="Minimum contour area in pixels required for a motion contour to be counted.", @@ -55,12 +55,12 @@ class MotionConfig(FrigateBaseModel): title="Frame alpha", description="Alpha value used when blending frames for motion preprocessing.", ) - frame_height: Optional[int] = Field( + frame_height: int | None = Field( default=100, title="Frame height", description="Height in pixels to scale frames to when computing motion.", ) - mask: dict[str, Optional[MotionMaskConfig]] = Field( + mask: dict[str, MotionMaskConfig | None] = Field( default_factory=dict, title="Mask coordinates", description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", @@ -70,12 +70,12 @@ class MotionConfig(FrigateBaseModel): title="MQTT off delay", description="Seconds to wait after last motion before publishing an MQTT 'off' state.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original motion state", description="Indicates whether motion detection was enabled in the original static configuration.", ) - raw_mask: dict[str, Optional[MotionMaskConfig]] = Field( + raw_mask: dict[str, MotionMaskConfig | None] = Field( default_factory=dict, exclude=True ) diff --git a/frigate/config/camera/notification.py b/frigate/config/camera/notification.py index dabf94675..7f5968193 100644 --- a/frigate/config/camera/notification.py +++ b/frigate/config/camera/notification.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field from ..base import FrigateBaseModel @@ -13,7 +11,7 @@ class NotificationConfig(FrigateBaseModel): title="Enable notifications", description="Enable or disable notifications for all cameras; can be overridden per-camera.", ) - email: Optional[str] = Field( + email: str | None = Field( default=None, title="Notification email", description="Email address used for push notifications or required by certain notification providers.", @@ -24,7 +22,7 @@ class NotificationConfig(FrigateBaseModel): title="Cooldown period", description="Cooldown (seconds) between notifications to avoid spamming recipients.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original notifications state", description="Indicates whether notifications were enabled in the original static configuration.", diff --git a/frigate/config/camera/objects.py b/frigate/config/camera/objects.py index e93778f23..6b6759edf 100644 --- a/frigate/config/camera/objects.py +++ b/frigate/config/camera/objects.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Union +from typing import Any from pydantic import Field, PrivateAttr, field_serializer, field_validator @@ -12,12 +12,12 @@ DEFAULT_TRACKED_OBJECTS = ["person"] class FilterConfig(FrigateBaseModel): - min_area: Union[int, float] = Field( + min_area: int | float = Field( default=0, title="Minimum object area", description="Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", ) - max_area: Union[int, float] = Field( + max_area: int | float = Field( default=24000000, title="Maximum object area", description="Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", @@ -42,12 +42,12 @@ class FilterConfig(FrigateBaseModel): title="Minimum confidence", description="Minimum single-frame detection confidence required for the object to be counted.", ) - mask: dict[str, Optional[ObjectMaskConfig]] = Field( + mask: dict[str, ObjectMaskConfig | None] = Field( default_factory=dict, title="Filter mask", description="Polygon coordinates defining where this filter applies within the frame.", ) - raw_mask: dict[str, Optional[ObjectMaskConfig]] = Field( + raw_mask: dict[str, ObjectMaskConfig | None] = Field( default_factory=dict, exclude=True ) @@ -68,7 +68,7 @@ class GenAIObjectTriggerConfig(FrigateBaseModel): title="Send on end", description="Send a request to GenAI when the tracked object ends.", ) - after_significant_updates: Optional[int] = Field( + after_significant_updates: int | None = Field( default=None, title="Early GenAI trigger", description="Send a request to GenAI after a specified number of significant updates for the tracked object.", @@ -98,12 +98,12 @@ class GenAIObjectConfig(FrigateBaseModel): description="Per-object prompts to customize GenAI outputs for specific labels.", ) - objects: Union[str, list[str]] = Field( + objects: str | list[str] = Field( default_factory=list, title="GenAI objects", description="List of object labels to send to GenAI by default.", ) - required_zones: Union[str, list[str]] = Field( + required_zones: str | list[str] = Field( default_factory=list, title="Required zones", description="Zones that must be entered for objects to qualify for GenAI description generation.", @@ -118,7 +118,7 @@ class GenAIObjectConfig(FrigateBaseModel): title="GenAI triggers", description="Defines when frames should be sent to GenAI (on end, after updates, etc.).", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original GenAI state", description="Indicates whether GenAI was enabled in the original static config.", @@ -144,12 +144,12 @@ class ObjectConfig(FrigateBaseModel): title="Object filters", description="Filters applied to detected objects to reduce false positives (area, ratio, confidence).", ) - mask: dict[str, Optional[ObjectMaskConfig]] = Field( + mask: dict[str, ObjectMaskConfig | None] = Field( default_factory=dict, title="Object mask", description="Mask polygon used to prevent object detection in specified areas.", ) - raw_mask: dict[str, Optional[ObjectMaskConfig]] = Field( + raw_mask: dict[str, ObjectMaskConfig | None] = Field( default_factory=dict, exclude=True ) genai: GenAIObjectConfig = Field( diff --git a/frigate/config/camera/onvif.py b/frigate/config/camera/onvif.py index 836dec6aa..01c943281 100644 --- a/frigate/config/camera/onvif.py +++ b/frigate/config/camera/onvif.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional, Union from pydantic import Field, field_validator @@ -59,12 +58,12 @@ class PtzAutotrackConfig(FrigateBaseModel): title="Return timeout", description="Wait this many seconds after losing tracking before returning camera to preset position.", ) - movement_weights: Optional[Union[str, list[str]]] = Field( + movement_weights: str | list[str] | None = Field( default_factory=list, title="Movement weights", description="Calibration values automatically generated by camera calibration. Do not modify manually.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original autotrack state", description="Internal field to track whether autotracking was enabled in configuration.", @@ -102,12 +101,12 @@ class OnvifConfig(FrigateBaseModel): title="ONVIF port", description="Port number for the ONVIF service.", ) - user: Optional[EnvString] = Field( + user: EnvString | None = Field( default=None, title="ONVIF username", description="Username for ONVIF authentication; some devices require admin user for ONVIF.", ) - password: Optional[EnvString] = Field( + password: EnvString | None = Field( default=None, title="ONVIF password", description="Password for ONVIF authentication.", @@ -117,7 +116,7 @@ class OnvifConfig(FrigateBaseModel): title="Disable TLS verify", description="Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).", ) - profile: Optional[str] = Field( + profile: str | None = Field( default=None, title="ONVIF profile", description="Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically.", diff --git a/frigate/config/camera/profile.py b/frigate/config/camera/profile.py index 6a52a9ad6..e3014aa4e 100644 --- a/frigate/config/camera/profile.py +++ b/frigate/config/camera/profile.py @@ -1,7 +1,5 @@ """Camera profile configuration for named config overrides.""" -from typing import Optional - from ..base import FrigateBaseModel from ..classification import ( CameraFaceRecognitionConfig, @@ -29,16 +27,16 @@ class CameraProfileConfig(FrigateBaseModel): explicitly-set fields are used as overrides via exclude_unset. """ - enabled: Optional[bool] = None - audio: Optional[AudioConfig] = None - birdseye: Optional[BirdseyeCameraConfig] = None - detect: Optional[DetectConfig] = None - face_recognition: Optional[CameraFaceRecognitionConfig] = None - lpr: Optional[CameraLicensePlateRecognitionConfig] = None - motion: Optional[MotionConfig] = None - notifications: Optional[NotificationConfig] = None - objects: Optional[ObjectConfig] = None - record: Optional[RecordConfig] = None - review: Optional[ReviewConfig] = None - snapshots: Optional[SnapshotsConfig] = None - zones: Optional[dict[str, ZoneConfig]] = None + enabled: bool | None = None + audio: AudioConfig | None = None + birdseye: BirdseyeCameraConfig | None = None + detect: DetectConfig | None = None + face_recognition: CameraFaceRecognitionConfig | None = None + lpr: CameraLicensePlateRecognitionConfig | None = None + motion: MotionConfig | None = None + notifications: NotificationConfig | None = None + objects: ObjectConfig | None = None + record: RecordConfig | None = None + review: ReviewConfig | None = None + snapshots: SnapshotsConfig | None = None + zones: dict[str, ZoneConfig] | None = None diff --git a/frigate/config/camera/record.py b/frigate/config/camera/record.py index 441a58537..44a71c9cb 100644 --- a/frigate/config/camera/record.py +++ b/frigate/config/camera/record.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional, Union from pydantic import Field @@ -94,7 +93,7 @@ class ChaptersEnum(str, Enum): class RecordExportConfig(FrigateBaseModel): - hwaccel_args: Union[str, list[str]] = Field( + hwaccel_args: str | list[str] = Field( default="auto", title="Export hwaccel args", description="Hardware acceleration args to use for export/transcode operations.", @@ -152,7 +151,7 @@ class RecordConfig(FrigateBaseModel): title="Preview config", description="Settings controlling the quality of recording previews shown in the UI.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original recording state", description="Indicates whether recording was enabled in the original static configuration.", diff --git a/frigate/config/camera/review.py b/frigate/config/camera/review.py index fbe24c98c..f267f46ea 100644 --- a/frigate/config/camera/review.py +++ b/frigate/config/camera/review.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional, Union from pydantic import Field, field_validator @@ -32,13 +31,13 @@ class AlertsConfig(FrigateBaseModel): title="Alert labels", description="List of object labels that qualify as alerts (for example: car, person).", ) - required_zones: Union[str, list[str]] = Field( + required_zones: str | list[str] = Field( default_factory=list, title="Required zones", description="Zones that an object must enter to be considered an alert; leave empty to allow any zone.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original alerts state", description="Tracks whether alerts were originally enabled in the static configuration.", @@ -67,12 +66,12 @@ class DetectionsConfig(FrigateBaseModel): description="Enable or disable detection events for all cameras; can be overridden per-camera.", ) - labels: Optional[list[str]] = Field( + labels: list[str] | None = Field( default=None, title="Detection labels", description="List of object labels that qualify as detection events.", ) - required_zones: Union[str, list[str]] = Field( + required_zones: str | list[str] = Field( default_factory=list, title="Required zones", description="Zones that an object must enter to be considered a detection; leave empty to allow any zone.", @@ -83,7 +82,7 @@ class DetectionsConfig(FrigateBaseModel): description="Seconds to wait after no detection-causing activity before cutting off a detection.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original detections state", description="Tracks whether detections were originally enabled in the static configuration.", @@ -129,7 +128,7 @@ class GenAIReviewConfig(FrigateBaseModel): title="Save thumbnails", description="Save thumbnails that are sent to the GenAI provider for debugging and review.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original GenAI state", description="Tracks whether GenAI review was originally enabled in the static configuration.", diff --git a/frigate/config/camera/snapshots.py b/frigate/config/camera/snapshots.py index b6bccfc40..7f5590a06 100644 --- a/frigate/config/camera/snapshots.py +++ b/frigate/config/camera/snapshots.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field from ..base import FrigateBaseModel @@ -46,7 +44,7 @@ class SnapshotsConfig(FrigateBaseModel): title="Required zones", description="Zones an object must enter for a snapshot to be saved.", ) - height: Optional[int] = Field( + height: int | None = Field( default=None, title="Snapshot height", description="Height (pixels) to resize snapshots from API to; leave empty to preserve original size.", diff --git a/frigate/config/camera/timestamp.py b/frigate/config/camera/timestamp.py index 48ec8240b..1245c311b 100644 --- a/frigate/config/camera/timestamp.py +++ b/frigate/config/camera/timestamp.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional from pydantic import Field @@ -76,7 +75,7 @@ class TimestampStyleConfig(FrigateBaseModel): title="Timestamp thickness", description="Line thickness of the timestamp text.", ) - effect: Optional[TimestampEffectEnum] = Field( + effect: TimestampEffectEnum | None = Field( default=None, title="Timestamp effect", description="Visual effect for the timestamp text (none, solid, shadow).", diff --git a/frigate/config/camera/zone.py b/frigate/config/camera/zone.py index e4737f8dc..24c62e0ff 100644 --- a/frigate/config/camera/zone.py +++ b/frigate/config/camera/zone.py @@ -1,6 +1,5 @@ # this uses the base model because the color is an extra attribute import logging -from typing import Optional, Union import numpy as np from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator @@ -13,7 +12,7 @@ logger = logging.getLogger(__name__) class ZoneConfig(BaseModel): - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( None, title="Zone name", description="A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used.", @@ -23,7 +22,7 @@ class ZoneConfig(BaseModel): title="Enabled", description="Enable or disable this zone. Disabled zones are ignored at runtime.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Keep track of original state of zone." ) filters: dict[str, FilterConfig] = Field( @@ -31,11 +30,11 @@ class ZoneConfig(BaseModel): title="Zone filters", description="Filters to apply to objects within this zone. Used to reduce false positives or restrict which objects are considered present in the zone.", ) - coordinates: Union[str, list[str]] = Field( + coordinates: str | list[str] = Field( title="Coordinates", description="Polygon coordinates that define the zone area. Can be a comma-separated string or a list of coordinate strings. Coordinates should be relative (0-1) or absolute (legacy).", ) - distances: Optional[Union[str, list[str]]] = Field( + distances: str | list[str] | None = Field( default_factory=list, title="Real-world distances", description="Optional real-world distances for each side of the zone quadrilateral, used for speed or distance calculations. Must have exactly 4 values if set.", @@ -52,18 +51,18 @@ class ZoneConfig(BaseModel): title="Loitering seconds", description="Number of seconds an object must remain in the zone to be considered as loitering. Set to 0 to disable loitering detection.", ) - speed_threshold: Optional[float] = Field( + speed_threshold: float | None = Field( default=None, ge=0.1, title="Minimum speed", description="Minimum speed (in real-world units if distances are set) required for an object to be considered present in the zone. Used for speed-based zone triggers.", ) - objects: Union[str, list[str]] = Field( + objects: str | list[str] = Field( default_factory=list, title="Trigger objects", description="List of object types (from labelmap) that can trigger this zone. Can be a string or a list of strings. If empty, all objects are considered.", ) - _color: Optional[tuple[int, int, int]] = PrivateAttr() + _color: tuple[int, int, int] | None = PrivateAttr() _contour: np.ndarray = PrivateAttr() @property @@ -147,7 +146,7 @@ class ZoneConfig(BaseModel): except ValueError: raise ValueError( f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}" - ) + ) from None if explicit: self.coordinates = ",".join( @@ -176,7 +175,7 @@ class ZoneConfig(BaseModel): except ValueError: raise ValueError( f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}" - ) + ) from None if explicit: self.coordinates = ",".join( diff --git a/frigate/config/camera_group.py b/frigate/config/camera_group.py index 65319001a..8e08f560f 100644 --- a/frigate/config/camera_group.py +++ b/frigate/config/camera_group.py @@ -1,5 +1,3 @@ -from typing import Union - from pydantic import Field, field_validator from .base import FrigateBaseModel @@ -8,7 +6,7 @@ __all__ = ["CameraGroupConfig"] class CameraGroupConfig(FrigateBaseModel): - cameras: Union[str, list[str]] = Field( + cameras: str | list[str] = Field( default_factory=list, title="Camera list", description="Array of camera names included in this group.", diff --git a/frigate/config/classification.py b/frigate/config/classification.py index 708f854e3..f4dc5f600 100644 --- a/frigate/config/classification.py +++ b/frigate/config/classification.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Dict, List, Optional, Union from pydantic import ConfigDict, Field, field_validator @@ -68,7 +67,7 @@ class AudioTranscriptionConfig(FrigateBaseModel): title="Model size", description="Model size to use for offline audio event transcription.", ) - live_enabled: Optional[bool] = Field( + live_enabled: bool | None = Field( default=False, title="Live transcription", description="Enable streaming live transcription for audio as it is received.", @@ -98,7 +97,7 @@ class CustomClassificationStateCameraConfig(FrigateBaseModel): class CustomClassificationStateConfig(FrigateBaseModel): - cameras: Dict[str, CustomClassificationStateCameraConfig] = Field( + cameras: dict[str, CustomClassificationStateCameraConfig] = Field( title="Classification cameras", description="Per-camera crop and settings for running state classification.", ) @@ -160,7 +159,7 @@ class ClassificationConfig(FrigateBaseModel): title="Bird classification config", description="Settings specific to bird classification models.", ) - custom: Dict[str, CustomClassificationConfig] = Field( + custom: dict[str, CustomClassificationConfig] = Field( default={}, title="Custom Classification Models", description="Configuration for custom classification models used for objects or state detection.", @@ -173,12 +172,12 @@ class SemanticSearchConfig(FrigateBaseModel): title="Enable semantic search", description="Enable or disable the semantic search feature.", ) - reindex: Optional[bool] = Field( + reindex: bool | None = Field( default=False, title="Reindex on startup", description="Trigger a full reindex of historical tracked objects into the embeddings database.", ) - model: Optional[Union[SemanticSearchModelEnum, str]] = Field( + model: SemanticSearchModelEnum | str | None = Field( default=SemanticSearchModelEnum.jinav1, title="Semantic search model or GenAI provider name", description="The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role.", @@ -199,7 +198,7 @@ class SemanticSearchConfig(FrigateBaseModel): title="Model size", description="Select model size; 'small' runs on CPU and 'large' typically requires GPU.", ) - device: Optional[str] = Field( + device: str | None = Field( default=None, title="Device", description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", @@ -207,7 +206,7 @@ class SemanticSearchConfig(FrigateBaseModel): class TriggerConfig(FrigateBaseModel): - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( None, title="Friendly name", description="Optional friendly name displayed in the UI for this trigger.", @@ -233,7 +232,7 @@ class TriggerConfig(FrigateBaseModel): gt=0.0, le=1.0, ) - actions: List[TriggerAction] = Field( + actions: list[TriggerAction] = Field( default=[], title="Trigger actions", description="List of actions to execute when trigger matches (notification, sub_label, attribute).", @@ -243,7 +242,7 @@ class TriggerConfig(FrigateBaseModel): class CameraSemanticSearchConfig(FrigateBaseModel): - triggers: Dict[str, TriggerConfig] = Field( + triggers: dict[str, TriggerConfig] = Field( default={}, title="Triggers", description="Actions and matching criteria for camera-specific semantic search triggers.", @@ -307,7 +306,7 @@ class FaceRecognitionConfig(FrigateBaseModel): title="Blur confidence filter", description="Adjust confidence scores based on image blur to reduce false positives for poor quality faces.", ) - device: Optional[str] = Field( + device: str | None = Field( default=None, title="Device", description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", @@ -369,7 +368,7 @@ class LicensePlateRecognitionConfig(FrigateBaseModel): title="Min plate length", description="Minimum number of characters a recognized plate must contain to be considered valid.", ) - format: Optional[str] = Field( + format: str | None = Field( default=None, title="Plate format regex", description="Optional regex to validate recognized plate strings against an expected format.", @@ -380,7 +379,7 @@ class LicensePlateRecognitionConfig(FrigateBaseModel): description="Number of character mismatches allowed when comparing detected plates to known plates.", ge=0, ) - known_plates: Optional[Dict[str, List[str]]] = Field( + known_plates: dict[str, list[str]] | None = Field( default={}, title="Known plates", description="List of plates or regexes to specially track or alert on.", @@ -397,12 +396,12 @@ class LicensePlateRecognitionConfig(FrigateBaseModel): title="Save debug plates", description="Save plate crop images for debugging LPR performance.", ) - device: Optional[str] = Field( + device: str | None = Field( default=None, title="Device", description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", ) - replace_rules: List[ReplaceRule] = Field( + replace_rules: list[ReplaceRule] = Field( default_factory=list, title="Replacement rules", description="Regex replacement rules used to normalize detected plate strings before matching.", @@ -443,10 +442,10 @@ class CameraAudioTranscriptionConfig(FrigateBaseModel): title="Enable transcription", description="Enable or disable manually triggered audio event transcription.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original transcription state" ) - live_enabled: Optional[bool] = Field( + live_enabled: bool | None = Field( default=False, title="Live transcription", description="Enable streaming live transcription for audio as it is received.", diff --git a/frigate/config/config.py b/frigate/config/config.py index 1a12c20e5..8e4c7f2ae 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -4,7 +4,7 @@ import io import json import logging import os -from typing import Any, Dict, Optional +from typing import Any, Self import numpy as np from pydantic import ( @@ -17,7 +17,6 @@ from pydantic import ( model_validator, ) from ruamel.yaml import YAML -from typing_extensions import Self from frigate.const import REGEX_JSON from frigate.detectors import DetectorConfig, ModelConfig @@ -174,7 +173,7 @@ class RuntimeMotionConfig(MotionConfig): class RuntimeFilterConfig(FilterConfig): """Runtime version of FilterConfig with rasterized masks.""" - rasterized_mask: Optional[np.ndarray] = Field(default=None, exclude=True) + rasterized_mask: np.ndarray | None = Field(default=None, exclude=True) def __init__(self, **config): frame_shape = config.get("frame_shape", (1, 1)) @@ -293,7 +292,7 @@ def verify_recording_segments_setup_with_reasonable_time( raise ValueError( f"Camera {camera_config.name} has no segment_time in \ recording output args, segment args are required for record." - ) + ) from None if int(record_args[seg_arg_index + 1]) > 60: raise ValueError( @@ -420,7 +419,7 @@ def verify_lpr_and_face( class FrigateConfig(FrigateBaseModel): - version: Optional[str] = Field( + version: str | None = Field( default=None, title="Current config version", description="Numeric or string version of the active configuration to help detect migrations or format changes.", @@ -496,7 +495,7 @@ class FrigateConfig(FrigateBaseModel): ) # Detector config - detectors: Dict[str, BaseDetectorConfig] = Field( + detectors: dict[str, BaseDetectorConfig] = Field( default=DEFAULT_DETECTORS, title="Detector hardware", description="Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.", @@ -508,14 +507,14 @@ class FrigateConfig(FrigateBaseModel): ) # GenAI config (named provider configs: name -> GenAIConfig) - genai: Dict[str, GenAIConfig] = Field( + genai: dict[str, GenAIConfig] = Field( default_factory=dict, title="Generative AI configuration", description="Settings for integrated generative AI providers used to generate object descriptions and review summaries.", ) # Camera config - cameras: Dict[str, CameraConfig] = Field(title="Cameras", description="Cameras") + cameras: dict[str, CameraConfig] = Field(title="Cameras", description="Cameras") audio: AudioConfig = Field( default_factory=AudioConfig, title="Audio detection", @@ -541,7 +540,7 @@ class FrigateConfig(FrigateBaseModel): title="Live playback", description="Settings to control the jsmpeg live stream resolution and quality. This does not affect restreamed cameras that use go2rtc for live view.", ) - motion: Optional[MotionConfig] = Field( + motion: MotionConfig | None = Field( default=None, title="Motion detection", description="Default motion detection settings applied to cameras unless overridden per-camera.", @@ -599,19 +598,19 @@ class FrigateConfig(FrigateBaseModel): description="License plate recognition settings including detection thresholds, formatting, and known plates.", ) - camera_groups: Dict[str, CameraGroupConfig] = Field( + camera_groups: dict[str, CameraGroupConfig] = Field( default_factory=dict, title="Camera groups", description="Configuration for named camera groups used to organize cameras in the UI.", ) - profiles: Dict[str, ProfileDefinitionConfig] = Field( + profiles: dict[str, ProfileDefinitionConfig] = Field( default_factory=dict, title="Profiles", description="Named profile definitions with friendly names. Camera profiles must reference names defined here.", ) - active_profile: Optional[str] = Field( + active_profile: str | None = Field( default=None, title="Active profile", description="Currently active profile name. Runtime-only, not persisted in YAML.", @@ -1054,7 +1053,7 @@ class FrigateConfig(FrigateBaseModel): @field_validator("cameras") @classmethod - def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]): + def ensure_zones_and_cameras_have_different_names(cls, v: dict[str, CameraConfig]): zones = [zone for camera in v.values() for zone in camera.zones.keys()] for zone in zones: if zone in v.keys(): @@ -1136,7 +1135,7 @@ class FrigateConfig(FrigateBaseModel): @classmethod def parse_object( - cls, obj: Any, *, plus_api: Optional[PlusApi] = None, install: bool = False + cls, obj: Any, *, plus_api: PlusApi | None = None, install: bool = False ): return cls.model_validate( obj, context={"plus_api": plus_api, "install": install} diff --git a/frigate/config/logger.py b/frigate/config/logger.py index c8920a198..906f75e4a 100644 --- a/frigate/config/logger.py +++ b/frigate/config/logger.py @@ -1,5 +1,6 @@ +from typing import Self + from pydantic import Field, ValidationInfo, model_validator -from typing_extensions import Self from frigate.log import LogLevel, apply_log_levels diff --git a/frigate/config/mqtt.py b/frigate/config/mqtt.py index becbe7e69..28c002a0d 100644 --- a/frigate/config/mqtt.py +++ b/frigate/config/mqtt.py @@ -1,7 +1,6 @@ -from typing import Optional +from typing import Self from pydantic import Field, ValidationInfo, model_validator -from typing_extensions import Self from frigate.const import FREQUENCY_STATS_POINTS @@ -43,33 +42,33 @@ class MqttConfig(FrigateBaseModel): title="Stats interval", description="Interval in seconds for publishing system and camera stats to MQTT.", ) - user: Optional[EnvString] = Field( + user: EnvString | None = Field( default=None, title="MQTT username", description="Optional MQTT username; can be provided via environment variables or secrets.", ) - password: Optional[EnvString] = Field( + password: EnvString | None = Field( default=None, title="MQTT password", description="Optional MQTT password; can be provided via environment variables or secrets.", validate_default=True, ) - tls_ca_certs: Optional[str] = Field( + tls_ca_certs: str | None = Field( default=None, title="TLS CA certs", description="Path to CA certificate for TLS connections to the broker (for self-signed certs).", ) - tls_client_cert: Optional[str] = Field( + tls_client_cert: str | None = Field( default=None, title="Client cert", description="Client certificate path for TLS mutual authentication; do not set user/password when using client certs.", ) - tls_client_key: Optional[str] = Field( + tls_client_key: str | None = Field( default=None, title="Client key", description="Private key path for the client certificate.", ) - tls_insecure: Optional[bool] = Field( + tls_insecure: bool | None = Field( default=None, title="TLS insecure", description="Allow insecure TLS connections by skipping hostname verification (not recommended).", diff --git a/frigate/config/network.py b/frigate/config/network.py index f537c73b9..7feaab012 100644 --- a/frigate/config/network.py +++ b/frigate/config/network.py @@ -1,5 +1,3 @@ -from typing import Union - from pydantic import Field from .base import FrigateBaseModel @@ -16,12 +14,12 @@ class IPv6Config(FrigateBaseModel): class ListenConfig(FrigateBaseModel): - internal: Union[int, str] = Field( + internal: int | str = Field( default=5000, title="Internal port", description="Internal listening port for Frigate (default 5000).", ) - external: Union[int, str] = Field( + external: int | str = Field( default=8971, title="External port", description="External listening port for Frigate (default 8971).", diff --git a/frigate/config/profile_manager.py b/frigate/config/profile_manager.py index e0a40ee35..cfd0dc9df 100644 --- a/frigate/config/profile_manager.py +++ b/frigate/config/profile_manager.py @@ -3,9 +3,10 @@ import copy import json import logging -from datetime import datetime, timezone +from collections.abc import Callable +from datetime import UTC, datetime from pathlib import Path -from typing import Any, Callable, Optional +from typing import Any from frigate.config.camera.updater import ( CameraConfigUpdateEnum, @@ -169,9 +170,9 @@ class ProfileManager: def activate_profile( self, - profile_name: Optional[str], + profile_name: str | None, clear_runtime_overrides: bool = True, - ) -> Optional[str]: + ) -> str | None: """Activate a profile by name, or deactivate if None. Args: @@ -256,7 +257,7 @@ class ProfileManager: def _apply_profile_overrides( self, profile_name: str, changed: dict[str, set[str]] - ) -> Optional[str]: + ) -> str | None: """Apply profile overrides for all cameras that have the named profile.""" for cam_name, cam_config in self.config.cameras.items(): profile = cam_config.profiles.get(profile_name) @@ -358,14 +359,14 @@ class ProfileManager: retain=True, ) - def _persist_active_profile(self, profile_name: Optional[str]) -> None: + def _persist_active_profile(self, profile_name: str | None) -> None: """Persist the active profile state to disk as JSON.""" try: data = self._load_persisted_data() data["active"] = profile_name if profile_name is not None: data.setdefault("last_activated", {})[profile_name] = datetime.now( - timezone.utc + UTC ).timestamp() PERSISTENCE_FILE.write_text(json.dumps(data)) except OSError: @@ -384,7 +385,7 @@ class ProfileManager: return {"active": None, "last_activated": {}} @staticmethod - def load_persisted_profile() -> Optional[str]: + def load_persisted_profile() -> str | None: """Load the persisted active profile name from disk.""" data = ProfileManager._load_persisted_data() name = data.get("active") diff --git a/frigate/config/proxy.py b/frigate/config/proxy.py index 8c20c6e6d..196110520 100644 --- a/frigate/config/proxy.py +++ b/frigate/config/proxy.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field, field_validator from .base import FrigateBaseModel @@ -19,7 +17,7 @@ class HeaderMappingConfig(FrigateBaseModel): title="Role header", description="Header containing the authenticated user's role or groups from the upstream proxy.", ) - role_map: Optional[dict[str, list[str]]] = Field( + role_map: dict[str, list[str]] | None = Field( default_factory=dict, title=("Role mapping"), description="Map upstream group values to Frigate roles (for example map admin groups to the admin role).", @@ -32,22 +30,22 @@ class ProxyConfig(FrigateBaseModel): title="Header mapping", description="Map incoming proxy headers to Frigate user and role fields for proxy-based auth.", ) - logout_url: Optional[str] = Field( + logout_url: str | None = Field( default=None, title="Logout URL", description="URL to redirect users to when logging out via the proxy.", ) - auth_secret: Optional[EnvString] = Field( + auth_secret: EnvString | None = Field( default=None, title="Proxy secret", description="Optional secret checked against the X-Proxy-Secret header to verify trusted proxies.", ) - default_role: Optional[str] = Field( + default_role: str | None = Field( default="viewer", title="Default role", description="Default role assigned to proxy-authenticated users when no role mapping applies.", ) - separator: Optional[str] = Field( + separator: str | None = Field( default=",", title="Separator character", description="Character used to split multiple values provided in proxy headers.", diff --git a/frigate/config/telemetry.py b/frigate/config/telemetry.py index f85ff343f..3c219d746 100644 --- a/frigate/config/telemetry.py +++ b/frigate/config/telemetry.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field from .base import FrigateBaseModel @@ -23,7 +21,7 @@ class StatsConfig(FrigateBaseModel): title="Network bandwidth", description="Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities).", ) - intel_gpu_device: Optional[str] = Field( + intel_gpu_device: str | None = Field( default=None, title="Intel GPU device", description="PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present.", diff --git a/frigate/config/ui.py b/frigate/config/ui.py index 057a2b333..5958c0e2c 100644 --- a/frigate/config/ui.py +++ b/frigate/config/ui.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional from pydantic import Field @@ -20,7 +19,7 @@ class UnitSystemEnum(str, Enum): class UIConfig(FrigateBaseModel): - timezone: Optional[str] = Field( + timezone: str | None = Field( default=None, title="Timezone", description="Optional timezone to display across the UI (defaults to browser local time if unset).", diff --git a/frigate/data_processing/common/license_plate/mixin.py b/frigate/data_processing/common/license_plate/mixin.py index faa4a79eb..432787e40 100644 --- a/frigate/data_processing/common/license_plate/mixin.py +++ b/frigate/data_processing/common/license_plate/mixin.py @@ -10,7 +10,7 @@ import random import re import string from pathlib import Path -from typing import Any, List, Tuple +from typing import Any import cv2 import numpy as np @@ -86,7 +86,7 @@ class LicensePlateProcessingMixin: self.similarity_threshold = 0.8 self.cluster_threshold = 0.85 - def _detect(self, image: np.ndarray, debug_frame_id: int) -> List[np.ndarray]: + def _detect(self, image: np.ndarray, debug_frame_id: int) -> list[np.ndarray]: """ Detect possible areas of text in the input image by first resizing and normalizing it, running a detection model, and filtering out low-probability regions. @@ -132,8 +132,8 @@ class LicensePlateProcessingMixin: return self._filter_polygon(boxes, (h, w)) # type: ignore[return-value,arg-type] def _classify( - self, images: List[np.ndarray] - ) -> Tuple[List[np.ndarray], List[Tuple[str, float]]] | None: + self, images: list[np.ndarray] + ) -> tuple[list[np.ndarray], list[tuple[str, float]]] | None: """ Classify the orientation or category of each detected license plate. @@ -163,8 +163,8 @@ class LicensePlateProcessingMixin: return self._process_classification_output(images, outputs) def _recognize( - self, camera: str, images: List[np.ndarray] - ) -> Tuple[List[str], List[List[float]]]: + self, camera: str, images: list[np.ndarray] + ) -> tuple[list[str], list[list[float]]]: """ Recognize the characters on the detected license plates using the recognition model. @@ -205,7 +205,7 @@ class LicensePlateProcessingMixin: def _process_license_plate( self, camera: str, id: str, image: np.ndarray, debug_frame_id: int - ) -> Tuple[List[str], List[List[float]], List[int]]: + ) -> tuple[list[str], list[list[float]], list[int]]: """ Complete pipeline for detecting, classifying, and recognizing license plates in the input image. Combines multi-line plates into a single plate string, grouping boxes by vertical alignment and ordering top to bottom, @@ -469,11 +469,11 @@ class LicensePlateProcessingMixin: def _merge_nearby_boxes( self, - boxes: List[np.ndarray], + boxes: list[np.ndarray], plate_width: float, gap_fraction: float = 0.1, min_overlap_fraction: float = -0.2, - ) -> List[np.ndarray]: + ) -> list[np.ndarray]: """ Merge bounding boxes that are likely part of the same license plate based on proximity, with a dynamic max_gap based on the provided width of the entire license plate. @@ -555,7 +555,7 @@ class LicensePlateProcessingMixin: def _boxes_from_bitmap( self, output: np.ndarray, mask: np.ndarray, dest_width: int, dest_height: int - ) -> Tuple[np.ndarray, List[float]]: + ) -> tuple[np.ndarray, list[float]]: """ Process the binary mask to extract bounding boxes and associated confidence scores. @@ -620,7 +620,7 @@ class LicensePlateProcessingMixin: return np.array(boxes, dtype="int32"), scores @staticmethod - def _get_min_boxes(contour: np.ndarray) -> Tuple[List[Tuple[float, float]], float]: + def _get_min_boxes(contour: np.ndarray) -> tuple[list[tuple[float, float]], float]: """ Calculate the minimum bounding box (rotated rectangle) for a given contour. @@ -659,7 +659,7 @@ class LicensePlateProcessingMixin: return cv2.mean(bitmap[y1 : y2 + 1, x1 : x2 + 1], mask)[0] @staticmethod - def _expand_box(points: List[Tuple[float, float]]) -> np.ndarray: + def _expand_box(points: list[tuple[float, float]]) -> np.ndarray: """ Expand a polygonal shape slightly by a factor determined by the area-to-perimeter ratio. @@ -677,7 +677,7 @@ class LicensePlateProcessingMixin: return expanded def _filter_polygon( - self, points: List[np.ndarray], shape: Tuple[int, int] + self, points: list[np.ndarray], shape: tuple[int, int] ) -> np.ndarray: """ Filter a set of polygons to include only valid ones that fit within an image shape @@ -839,8 +839,8 @@ class LicensePlateProcessingMixin: return padded_image def _process_classification_output( - self, images: List[np.ndarray], outputs: List[np.ndarray] - ) -> Tuple[List[np.ndarray], List[Tuple[str, float]]]: + self, images: list[np.ndarray], outputs: list[np.ndarray] + ) -> tuple[list[np.ndarray], list[tuple[str, float]]]: """ Process the classification model output by matching labels with confidence scores. @@ -1095,8 +1095,8 @@ class LicensePlateProcessingMixin: return None # No detection above the threshold def _get_cluster_rep( - self, plates: List[dict] - ) -> Tuple[str, float, List[float], int]: + self, plates: list[dict] + ) -> tuple[str, float, list[float], int]: """ Cluster plate variants and select the representative from the best cluster. """ @@ -1704,7 +1704,7 @@ class CTCDecoder: """ self.characters = [] if character_dict_path and os.path.exists(character_dict_path): - with open(character_dict_path, "r", encoding="utf-8") as f: + with open(character_dict_path, encoding="utf-8") as f: self.characters = ( ["blank"] + [line.strip() for line in f if line.strip()] + [" "] ) @@ -1812,8 +1812,8 @@ class CTCDecoder: self.char_map = {i: char for i, char in enumerate(self.characters)} def __call__( - self, outputs: List[np.ndarray] - ) -> Tuple[List[str], List[List[float]]]: + self, outputs: list[np.ndarray] + ) -> tuple[list[str], list[list[float]]]: """ Decode a batch of model outputs into character sequences and their confidence scores. diff --git a/frigate/data_processing/post/audio_transcription.py b/frigate/data_processing/post/audio_transcription.py index dbeb21028..16895dc87 100644 --- a/frigate/data_processing/post/audio_transcription.py +++ b/frigate/data_processing/post/audio_transcription.py @@ -4,7 +4,7 @@ import logging import os import threading import time -from typing import Any, Optional +from typing import Any from peewee import DoesNotExist @@ -142,7 +142,7 @@ class AudioTranscriptionPostProcessor(PostProcessorApi): except Exception as e: logger.error(f"Error in audio transcription post-processing: {e}") - def __transcribe_audio(self, audio_data: bytes) -> Optional[str]: + def __transcribe_audio(self, audio_data: bytes) -> str | None: """Transcribe WAV audio data using faster-whisper.""" if not self.recognizer: logger.debug("Recognizer not initialized") @@ -168,8 +168,9 @@ class AudioTranscriptionPostProcessor(PostProcessorApi): return None logger.debug( - "Detected language '%s' with probability %f" - % (info.language, info.language_probability) + "Detected language '%s' with probability %f", + info.language, + info.language_probability, ) return text diff --git a/frigate/data_processing/post/license_plate.py b/frigate/data_processing/post/license_plate.py index aa89aeb12..55f863cf0 100644 --- a/frigate/data_processing/post/license_plate.py +++ b/frigate/data_processing/post/license_plate.py @@ -102,10 +102,8 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi): Recordings.start_time, ) .where( - ( - (frame_time >= Recordings.start_time) - & (frame_time <= Recordings.end_time) - ) + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) ) .where(Recordings.camera == camera_name) .order_by(Recordings.start_time.desc()) diff --git a/frigate/data_processing/post/semantic_trigger.py b/frigate/data_processing/post/semantic_trigger.py index 08f8a2e76..e2b305ea2 100644 --- a/frigate/data_processing/post/semantic_trigger.py +++ b/frigate/data_processing/post/semantic_trigger.py @@ -55,7 +55,7 @@ class SemanticTriggerProcessor(PostProcessorApi): # load stats from disk try: - with open(os.path.join(CONFIG_DIR, ".search_stats.json"), "r") as f: + with open(os.path.join(CONFIG_DIR, ".search_stats.json")) as f: data = json.loads(f.read()) self.thumb_stats.from_dict(data["thumb_stats"]) self.desc_stats.from_dict(data["desc_stats"]) diff --git a/frigate/data_processing/real_time/api.py b/frigate/data_processing/real_time/api.py index b9b7ba26e..98efb532b 100644 --- a/frigate/data_processing/real_time/api.py +++ b/frigate/data_processing/real_time/api.py @@ -4,9 +4,10 @@ import logging import threading from abc import ABC, abstractmethod from collections import deque +from collections.abc import Callable from concurrent.futures import Future from queue import Empty, Full, Queue -from typing import Any, Callable +from typing import Any import numpy as np diff --git a/frigate/data_processing/real_time/audio_transcription.py b/frigate/data_processing/real_time/audio_transcription.py index 3d1536f73..0e1842b77 100644 --- a/frigate/data_processing/real_time/audio_transcription.py +++ b/frigate/data_processing/real_time/audio_transcription.py @@ -4,7 +4,7 @@ import logging import os import queue import threading -from typing import Any, Optional +from typing import Any import numpy as np @@ -75,9 +75,7 @@ class AudioTranscriptionRealTimeProcessor(RealTimeProcessorApi): f"Failed to initialize live streaming audio transcription: {e}" ) - def __process_audio_stream( - self, audio_data: np.ndarray - ) -> Optional[tuple[str, bool]]: + def __process_audio_stream(self, audio_data: np.ndarray) -> tuple[str, bool] | None: if ( self.model_runner.model is None and self.config.audio_transcription.model_size == "small" diff --git a/frigate/data_processing/real_time/face.py b/frigate/data_processing/real_time/face.py index c5c4ec56f..83c8a2e55 100644 --- a/frigate/data_processing/real_time/face.py +++ b/frigate/data_processing/real_time/face.py @@ -7,7 +7,7 @@ import logging import os import shutil from pathlib import Path -from typing import Any, Optional +from typing import Any import cv2 import numpy as np @@ -219,7 +219,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): logger.debug("Not processing due to hitting max rec attempts.") return - face: Optional[dict[str, Any]] = None + face: dict[str, Any] | None = None if self.requires_face_detection: logger.debug("Running manual face detection.") diff --git a/frigate/data_processing/real_time/whisper_online.py b/frigate/data_processing/real_time/whisper_online.py index 024b19fba..066303c93 100644 --- a/frigate/data_processing/real_time/whisper_online.py +++ b/frigate/data_processing/real_time/whisper_online.py @@ -1053,7 +1053,7 @@ if __name__ == "__main__": SAMPLING_RATE = 16000 duration = len(load_audio(audio_path)) / SAMPLING_RATE - logger.info("Audio duration is: %2.2f seconds" % duration) + logger.info("Audio duration is: %2.2f seconds", duration) asr, online = asr_factory(args, logfile=logfile) if args.vac: diff --git a/frigate/debug_replay.py b/frigate/debug_replay.py index 956bc2001..17a727c87 100644 --- a/frigate/debug_replay.py +++ b/frigate/debug_replay.py @@ -131,7 +131,7 @@ class DebugReplayManager: config_file = find_config_file() yaml_parser = YAML() - with open(config_file, "r") as f: + with open(config_file) as f: config_data = yaml_parser.load(f) if "cameras" not in config_data or config_data["cameras"] is None: diff --git a/frigate/detectors/detection_api.py b/frigate/detectors/detection_api.py index 4f03f28aa..b03d607d5 100644 --- a/frigate/detectors/detection_api.py +++ b/frigate/detectors/detection_api.py @@ -1,6 +1,5 @@ import logging from abc import ABC, abstractmethod -from typing import List import numpy as np @@ -11,7 +10,7 @@ logger = logging.getLogger(__name__) class DetectionApi(ABC): type_key: str - supported_models: List[ModelTypeEnum] + supported_models: list[ModelTypeEnum] @abstractmethod def __init__(self, detector_config: BaseDetectorConfig): diff --git a/frigate/detectors/detection_runners.py b/frigate/detectors/detection_runners.py index ee465b3d5..ee30f4fea 100644 --- a/frigate/detectors/detection_runners.py +++ b/frigate/detectors/detection_runners.py @@ -491,7 +491,7 @@ class RKNNModelRunner(BaseModelRunner): except ImportError: logger.error("RKNN Lite not available") - raise ImportError("RKNN Lite not available") + raise ImportError("RKNN Lite not available") from None except Exception as e: logger.error(f"Error loading RKNN model: {e}") raise diff --git a/frigate/detectors/detector_config.py b/frigate/detectors/detector_config.py index 5071e3a74..788fb58a7 100644 --- a/frigate/detectors/detector_config.py +++ b/frigate/detectors/detector_config.py @@ -3,7 +3,7 @@ import json import logging import os from enum import Enum -from typing import Any, Dict, Optional, Tuple +from typing import Any import requests from pydantic import BaseModel, ConfigDict, Field @@ -45,12 +45,12 @@ class ModelTypeEnum(str, Enum): class ModelConfig(BaseModel): - path: Optional[str] = Field( + path: str | None = Field( None, title="Custom object detector model path", description="Path to a custom detection model file (or plus:// for Frigate+ models).", ) - labelmap_path: Optional[str] = Field( + labelmap_path: str | None = Field( None, title="Label map for custom object detector", description="Path to a labelmap file that maps numeric classes to string labels for the detector.", @@ -65,12 +65,12 @@ class ModelConfig(BaseModel): title="Object detection model input height", description="Height of the model input tensor in pixels.", ) - labelmap: Dict[int, str] = Field( + labelmap: dict[int, str] = Field( default_factory=dict, title="Labelmap customization", description="Overrides or remapping entries to merge into the standard labelmap.", ) - attributes_map: Dict[str, list[str]] = Field( + attributes_map: dict[str, list[str]] = Field( default=DEFAULT_ATTRIBUTE_LABEL_MAP, title="Map of object labels to their attribute labels", description="Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate']).", @@ -95,18 +95,18 @@ class ModelConfig(BaseModel): title="Object Detection Model Type", description="Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization.", ) - _merged_labelmap: Optional[Dict[int, str]] = PrivateAttr() - _colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr() + _merged_labelmap: dict[int, str] | None = PrivateAttr() + _colormap: dict[int, tuple[int, int, int]] = PrivateAttr() _all_attributes: list[str] = PrivateAttr() _all_attribute_logos: list[str] = PrivateAttr() _model_hash: str = PrivateAttr() @property - def merged_labelmap(self) -> Dict[int, str]: + def merged_labelmap(self) -> dict[int, str]: return self._merged_labelmap @property - def colormap(self) -> Dict[int, Tuple[int, int, int]]: + def colormap(self) -> dict[int, tuple[int, int, int]]: return self._colormap @property @@ -171,7 +171,7 @@ class ModelConfig(BaseModel): with open(model_info_path, "w") as f: json.dump(model_info, f) else: - with open(model_info_path, "r") as f: + with open(model_info_path) as f: model_info: dict[str, Any] = json.load(f) if detector and detector not in model_info["supportedDetectors"]: @@ -240,12 +240,12 @@ class BaseDetectorConfig(BaseModel): title="Detector Type", description="Type of detector to use for object detection (for example 'cpu', 'edgetpu', 'openvino').", ) - model: Optional[ModelConfig] = Field( + model: ModelConfig | None = Field( default=None, title="Detector specific model configuration", description="Detector-specific model configuration options (path, input size, etc.).", ) - model_path: Optional[str] = Field( + model_path: str | None = Field( default=None, title="Detector specific model path", description="File path to the detector model binary if required by the chosen detector.", diff --git a/frigate/detectors/detector_types.py b/frigate/detectors/detector_types.py index 418fcd625..42129c594 100644 --- a/frigate/detectors/detector_types.py +++ b/frigate/detectors/detector_types.py @@ -2,10 +2,9 @@ import importlib import logging import pkgutil from enum import Enum -from typing import Union +from typing import Annotated, Union from pydantic import Field -from typing_extensions import Annotated from . import plugins from .detection_api import DetectionApi @@ -37,6 +36,6 @@ class StrEnum(str, Enum): DetectorTypeEnum = StrEnum("DetectorTypeEnum", {k: k for k in api_types}) DetectorConfig = Annotated[ - Union[tuple(BaseDetectorConfig.__subclasses__())], + Union[tuple(BaseDetectorConfig.__subclasses__())], # noqa: UP007 Field(discriminator="type"), ] diff --git a/frigate/detectors/plugins/axengine.py b/frigate/detectors/plugins/axengine.py index 383fcd0bf..43a1fffe4 100644 --- a/frigate/detectors/plugins/axengine.py +++ b/frigate/detectors/plugins/axengine.py @@ -39,8 +39,7 @@ class Axengine(DetectionApi): try: import axengine as axe except ModuleNotFoundError: - raise ImportError("AXEngine is not installed.") - return + raise ImportError("AXEngine is not installed.") from None logger.info("__init__ axengine") super().__init__(config) diff --git a/frigate/detectors/plugins/cpu_tfl.py b/frigate/detectors/plugins/cpu_tfl.py index 2224a2bda..d8cfe33e0 100644 --- a/frigate/detectors/plugins/cpu_tfl.py +++ b/frigate/detectors/plugins/cpu_tfl.py @@ -1,7 +1,7 @@ import logging +from typing import Literal from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig diff --git a/frigate/detectors/plugins/deepstack.py b/frigate/detectors/plugins/deepstack.py index 9b5fcd5af..e87b07c44 100644 --- a/frigate/detectors/plugins/deepstack.py +++ b/frigate/detectors/plugins/deepstack.py @@ -1,11 +1,11 @@ import io import logging +from typing import Literal import numpy as np import requests from PIL import Image from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig diff --git a/frigate/detectors/plugins/degirum.py b/frigate/detectors/plugins/degirum.py index 5afb32a3a..9aaf0e6dc 100644 --- a/frigate/detectors/plugins/degirum.py +++ b/frigate/detectors/plugins/degirum.py @@ -1,9 +1,9 @@ import logging import queue +from typing import Literal import numpy as np from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig @@ -46,7 +46,7 @@ class DGDetector(DetectionApi): try: import degirum as dg except ModuleNotFoundError: - raise ImportError("Unable to import DeGirum detector.") + raise ImportError("Unable to import DeGirum detector.") from None self._queue = queue.Queue() self._zoo = dg.connect( diff --git a/frigate/detectors/plugins/edgetpu_tfl.py b/frigate/detectors/plugins/edgetpu_tfl.py index 02bd9f5ec..c4ea485d5 100644 --- a/frigate/detectors/plugins/edgetpu_tfl.py +++ b/frigate/detectors/plugins/edgetpu_tfl.py @@ -1,11 +1,11 @@ import logging import math import os +from typing import Literal import cv2 import numpy as np from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum diff --git a/frigate/detectors/plugins/hailo8l.py b/frigate/detectors/plugins/hailo8l.py index bbe84d52f..63a3c9a2a 100755 --- a/frigate/detectors/plugins/hailo8l.py +++ b/frigate/detectors/plugins/hailo8l.py @@ -4,12 +4,11 @@ import subprocess import threading import urllib.request from functools import partial -from typing import Dict, List, Optional, Tuple +from typing import Literal import cv2 import numpy as np from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.const import MODEL_CACHE_DIR from frigate.detectors.detection_api import DetectionApi @@ -83,8 +82,8 @@ class HailoAsyncInference: input_store: RequestStore, output_store: ResponseStore, batch_size: int = 1, - input_type: Optional[str] = None, - output_type: Optional[Dict[str, str]] = None, + input_type: str | None = None, + output_type: dict[str, str] | None = None, send_original_frame: bool = False, ) -> None: # when importing hailo it activates the driver @@ -125,9 +124,9 @@ class HailoAsyncInference: def callback( self, completion_info, - bindings_list: List, - input_batch: List, - request_ids: List[int], + bindings_list: list, + input_batch: list, + request_ids: list[int], ): if completion_info.exception: logger.error(f"Inference error: {completion_info.exception}") @@ -163,7 +162,7 @@ class HailoAsyncInference: } return configured_infer_model.create_bindings(output_buffers=output_buffers) - def get_input_shape(self) -> Tuple[int, ...]: + def get_input_shape(self) -> tuple[int, ...]: return self.hef.get_input_vstream_infos()[0].shape def run(self) -> None: @@ -304,7 +303,7 @@ class HailoDetector(DetectionApi): urllib.request.urlretrieve(url, destination) logger.debug(f"Downloaded model to {destination}") except Exception as e: - raise RuntimeError(f"Failed to download model from {url}: {str(e)}") + raise RuntimeError(f"Failed to download model from {url}: {str(e)}") from e def check_and_prepare(self) -> str: if not os.path.exists(self.cache_dir): @@ -350,7 +349,7 @@ class HailoDetector(DetectionApi): if not self.inference_thread.is_alive(): raise RuntimeError( "HailoRT inference thread has stopped, restart required." - ) + ) from None return np.zeros((20, 6), dtype=np.float32) diff --git a/frigate/detectors/plugins/memryx.py b/frigate/detectors/plugins/memryx.py index 2c03d14a4..88537273c 100644 --- a/frigate/detectors/plugins/memryx.py +++ b/frigate/detectors/plugins/memryx.py @@ -5,11 +5,11 @@ import shutil import urllib.request import zipfile from queue import Queue +from typing import Literal import cv2 import numpy as np from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import ( @@ -61,7 +61,7 @@ class MemryXDetector(DetectionApi): except ModuleNotFoundError: raise ImportError( "MemryX SDK is not installed. Install it and set up MIX environment." - ) + ) from None return # Initialize stop_event as None, will be set later by set_stop_event() diff --git a/frigate/detectors/plugins/onnx.py b/frigate/detectors/plugins/onnx.py index b9aa00fbd..cbf189916 100644 --- a/frigate/detectors/plugins/onnx.py +++ b/frigate/detectors/plugins/onnx.py @@ -1,8 +1,8 @@ import logging +from typing import Literal import numpy as np from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detection_runners import get_optimized_runner diff --git a/frigate/detectors/plugins/openvino.py b/frigate/detectors/plugins/openvino.py index 1e9fb1ab1..5bd55d4a6 100644 --- a/frigate/detectors/plugins/openvino.py +++ b/frigate/detectors/plugins/openvino.py @@ -1,9 +1,9 @@ import logging +from typing import Literal import numpy as np import openvino as ov from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detection_runners import OpenVINOModelRunner diff --git a/frigate/detectors/plugins/rknn.py b/frigate/detectors/plugins/rknn.py index 15ab93dcb..7bf5e03c1 100644 --- a/frigate/detectors/plugins/rknn.py +++ b/frigate/detectors/plugins/rknn.py @@ -90,7 +90,7 @@ class Rknn(DetectionApi): with open("/proc/device-tree/compatible") as file: soc = file.read().split(",")[-1].strip("\x00") except FileNotFoundError: - raise Exception("Make sure to run docker in privileged mode.") + raise Exception("Make sure to run docker in privileged mode.") from None if soc not in SUPPORTED_RK_SOCS: raise Exception( diff --git a/frigate/detectors/plugins/synaptics.py b/frigate/detectors/plugins/synaptics.py index e6983a29c..6d0ac7f52 100644 --- a/frigate/detectors/plugins/synaptics.py +++ b/frigate/detectors/plugins/synaptics.py @@ -1,9 +1,9 @@ import logging import os +from typing import Literal import numpy as np from pydantic import ConfigDict -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import ( diff --git a/frigate/detectors/plugins/teflon_tfl.py b/frigate/detectors/plugins/teflon_tfl.py index 370d08817..7dd44e6e8 100644 --- a/frigate/detectors/plugins/teflon_tfl.py +++ b/frigate/detectors/plugins/teflon_tfl.py @@ -1,7 +1,7 @@ import logging +from typing import Literal from pydantic import ConfigDict -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig diff --git a/frigate/detectors/plugins/tensorrt.py b/frigate/detectors/plugins/tensorrt.py index 087331a2d..6b3921280 100644 --- a/frigate/detectors/plugins/tensorrt.py +++ b/frigate/detectors/plugins/tensorrt.py @@ -14,8 +14,9 @@ try: except ModuleNotFoundError: TRT_SUPPORT = False +from typing import Literal + from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig @@ -58,7 +59,7 @@ class TensorRTDetectorConfig(BaseDetectorConfig): ) -class HostDeviceMem(object): +class HostDeviceMem: """Simple helper data class that's a little nicer to use than a 2-tuple.""" def __init__(self, host_mem, device_mem, nbytes, size): diff --git a/frigate/detectors/plugins/zmq_ipc.py b/frigate/detectors/plugins/zmq_ipc.py index b0e568eff..cc9a538c8 100644 --- a/frigate/detectors/plugins/zmq_ipc.py +++ b/frigate/detectors/plugins/zmq_ipc.py @@ -1,12 +1,11 @@ import json import logging import os -from typing import Any, List +from typing import Any, Literal import numpy as np import zmq from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig @@ -274,7 +273,7 @@ class ZmqIpcDetector(DetectionApi): } return json.dumps(header).encode("utf-8") - def _decode_response(self, frames: List[bytes]) -> np.ndarray: + def _decode_response(self, frames: list[bytes]) -> np.ndarray: try: if len(frames) == 1: # Single-frame raw float32 (20x6) diff --git a/frigate/embeddings/__init__.py b/frigate/embeddings/__init__.py index 7e54d9703..bf74df681 100644 --- a/frigate/embeddings/__init__.py +++ b/frigate/embeddings/__init__.py @@ -8,7 +8,7 @@ import sys import threading from json.decoder import JSONDecodeError from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Union +from typing import Any import regex from pathvalidate import ValidationError, sanitize_filename @@ -73,7 +73,7 @@ class EmbeddingsContext: # load stats from disk stats_file = os.path.join(CONFIG_DIR, ".search_stats.json") try: - with open(stats_file, "r") as f: + with open(stats_file) as f: data = json.loads(f.read()) self.thumb_stats.from_dict(data["thumb_stats"]) self.desc_stats.from_dict(data["desc_stats"]) @@ -98,7 +98,7 @@ class EmbeddingsContext: self.requestor.stop() def search_thumbnail( - self, query: Union[Event, str], event_ids: list[str] = None + self, query: Event | str, event_ids: list[str] = None ) -> list[tuple[str, float]]: if query.__class__ == Event: cursor = self.db.execute_sql( @@ -255,7 +255,7 @@ class EmbeddingsContext: sanitized_old_name = sanitize_filename(old_name, replacement_text="_") sanitized_new_name = sanitize_filename(new_name, replacement_text="_") except ValidationError as e: - raise ValueError(f"Invalid face name: {str(e)}") + raise ValueError(f"Invalid face name: {str(e)}") from e if not regex.match(valid_name_pattern, old_name): raise ValueError(f"Invalid old face name: {old_name}") diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 52bdf5d91..c4f79d573 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -435,7 +435,7 @@ class EmbeddingMaintainer(threading.Thread): logger.error(f"No processor handled the topic {topic}") return None except Exception as e: - logger.error(f"Unable to handle embeddings request {e}", exc_info=True) + logger.exception(f"Unable to handle embeddings request {e}") self.embeddings_responder.check_for_request(_handle_request) diff --git a/frigate/events/audio.py b/frigate/events/audio.py index d44521787..9af1bd26b 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -7,7 +7,7 @@ import threading import time from multiprocessing.managers import DictProxy from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Tuple +from typing import Any import numpy as np @@ -272,7 +272,7 @@ class AudioEventMaintainer(threading.Thread): self.camera_metrics[self.camera_config.name].audio_rms.value = rms self.camera_metrics[self.camera_config.name].audio_dBFS.value = dBFS - audio_detections: list[Tuple[str, float]] = [] + audio_detections: list[tuple[str, float]] = [] # only run audio detection when volume is above min_volume if rms >= self.camera_config.audio.min_volume: @@ -323,7 +323,7 @@ class AudioEventMaintainer(threading.Thread): else: self.transcription_processor.check_unload_model() - def calculate_audio_levels(self, audio_as_float: np.ndarray) -> Tuple[float, float]: + def calculate_audio_levels(self, audio_as_float: np.ndarray) -> tuple[float, float]: # Calculate RMS (Root-Mean-Square) which represents the average signal amplitude # Note: np.float32 isn't serializable, we must use np.float64 to publish the message rms = np.sqrt(np.mean(np.absolute(np.square(audio_as_float)))) diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index 80bdaccd3..169b2139e 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -2,7 +2,7 @@ import logging import threading from multiprocessing import Queue from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Dict +from typing import Any from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber from frigate.config import FrigateConfig @@ -76,7 +76,7 @@ class EventProcessor(threading.Thread): super().__init__(name="event_processor") self.config = config self.timeline_queue = timeline_queue - self.events_in_process: Dict[str, dict[str, Any]] = {} + self.events_in_process: dict[str, dict[str, Any]] = {} self.stop_event = stop_event self.event_receiver = EventUpdateSubscriber() diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 4ebbd6c80..d005c8098 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -3,7 +3,7 @@ import logging import os from enum import Enum -from typing import Any, Optional +from typing import Any from frigate.const import ( FFMPEG_HVC1_ARGS, @@ -215,7 +215,7 @@ def parse_preset_hardware_acceleration_decode( width: int, height: int, gpu: int, -) -> Optional[list[str]]: +) -> list[str] | None: """Return the correct preset if in preset format otherwise return None.""" if not isinstance(arg, str): return None @@ -420,7 +420,7 @@ PRESETS_INPUT = { } -def parse_preset_input(arg: Any, detect_fps: int) -> Optional[list[str]]: +def parse_preset_input(arg: Any, detect_fps: int) -> list[str] | None: """Return the correct preset if in preset format otherwise return None.""" if not isinstance(arg, str): return None @@ -530,9 +530,7 @@ PRESETS_RECORD_OUTPUT = { } -def parse_preset_output_record( - arg: Any, force_record_hvc1: bool -) -> Optional[list[str]]: +def parse_preset_output_record(arg: Any, force_record_hvc1: bool) -> list[str] | None: """Return the correct preset if in preset format otherwise return None.""" if not isinstance(arg, str): return None diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index e1f91e31d..d02fb899a 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -6,7 +6,8 @@ import logging import os import re import time -from typing import Any, AsyncGenerator, Callable, Optional +from collections.abc import AsyncGenerator, Callable +from typing import Any import numpy as np from pydantic import ValidationError @@ -235,7 +236,7 @@ class GenAIClient: camera_config: CameraConfig, thumbnails: list[bytes], event: Event, - ) -> Optional[str]: + ) -> str | None: """Generate a description for the frame.""" try: prompt = build_object_description_prompt(camera_config, event) @@ -254,9 +255,9 @@ class GenAIClient: self, prompt: str, images: list[bytes], - response_format: Optional[dict] = None, + response_format: dict | None = None, enable_thinking: bool = False, - ) -> Optional[str]: + ) -> str | None: """Submit a request to the provider. ``enable_thinking`` is honored only by providers that report @@ -321,9 +322,9 @@ class GenAIClient: def chat_with_tools( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> dict[str, Any]: """ Send chat messages to LLM with optional tool definitions. @@ -395,9 +396,9 @@ class GenAIClient: async def chat_with_tools_stream( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> AsyncGenerator[tuple[str, Any], None]: """Streaming counterpart to `chat_with_tools`. diff --git a/frigate/genai/manager.py b/frigate/genai/manager.py index a545f81f4..594e3e403 100644 --- a/frigate/genai/manager.py +++ b/frigate/genai/manager.py @@ -6,7 +6,7 @@ no chat feature is active) are never initialized. """ import logging -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from frigate.config import FrigateConfig from frigate.config.camera.genai import GenAIConfig, GenAIRoleEnum @@ -23,7 +23,7 @@ class GenAIClientManager: def __init__(self, config: FrigateConfig) -> None: self._configs: dict[str, GenAIConfig] = {} self._role_map: dict[GenAIRoleEnum, str] = {} - self._clients: dict[str, "GenAIClient"] = {} + self._clients: dict[str, GenAIClient] = {} self.update_config(config) def update_config(self, config: FrigateConfig) -> None: @@ -59,7 +59,7 @@ class GenAIClientManager: for role in genai_cfg.roles: self._role_map[role] = name - def _get_client(self, name: str) -> "Optional[GenAIClient]": + def _get_client(self, name: str) -> "GenAIClient | None": """Return the client for *name*, creating it on first access.""" if name in self._clients: client = self._clients[name] @@ -93,19 +93,19 @@ class GenAIClientManager: return client @property - def chat_client(self) -> "Optional[GenAIClient]": + def chat_client(self) -> "GenAIClient | None": """Client configured for the chat role (e.g. chat with function calling).""" name = self._role_map.get(GenAIRoleEnum.chat) return self._get_client(name) if name else None @property - def description_client(self) -> "Optional[GenAIClient]": + def description_client(self) -> "GenAIClient | None": """Client configured for the descriptions role (e.g. review descriptions, object descriptions).""" name = self._role_map.get(GenAIRoleEnum.descriptions) return self._get_client(name) if name else None @property - def embeddings_client(self) -> "Optional[GenAIClient]": + def embeddings_client(self) -> "GenAIClient | None": """Client configured for the embeddings role.""" name = self._role_map.get(GenAIRoleEnum.embeddings) return self._get_client(name) if name else None diff --git a/frigate/genai/plugins/gemini.py b/frigate/genai/plugins/gemini.py index 9efd24189..915a12498 100644 --- a/frigate/genai/plugins/gemini.py +++ b/frigate/genai/plugins/gemini.py @@ -4,7 +4,8 @@ import base64 import binascii import json import logging -from typing import Any, AsyncGenerator, Optional +from collections.abc import AsyncGenerator +from typing import Any from google import genai from google.genai import errors, types @@ -16,7 +17,7 @@ from frigate.genai import GenAIClient, register_genai_provider logger = logging.getLogger(__name__) -def _decode_thought_signature(value: Any) -> Optional[bytes]: +def _decode_thought_signature(value: Any) -> bytes | None: """Decode a base64-encoded thought_signature carried across conversation turns.""" if not value: return None @@ -30,14 +31,14 @@ def _decode_thought_signature(value: Any) -> Optional[bytes]: return None -def _encode_thought_signature(signature: Optional[bytes]) -> Optional[str]: +def _encode_thought_signature(signature: bytes | None) -> str | None: """Encode bytes thought_signature as base64 so it survives JSON-friendly transport.""" if not signature: return None return base64.b64encode(signature).decode("ascii") -def _stats_from_gemini_usage(usage: Any) -> Optional[dict[str, Any]]: +def _stats_from_gemini_usage(usage: Any) -> dict[str, Any] | None: """Build a stats dict from a Gemini usage_metadata object.""" prompt_tokens = getattr(usage, "prompt_token_count", None) completion_tokens = getattr(usage, "candidates_token_count", None) @@ -84,9 +85,9 @@ class GeminiClient(GenAIClient): self, prompt: str, images: list[bytes], - response_format: Optional[dict] = None, + response_format: dict | None = None, enable_thinking: bool = False, - ) -> Optional[str]: + ) -> str | None: """Submit a request to Gemini.""" contents = [prompt] + [ types.Part.from_bytes(data=img, mime_type="image/jpeg") for img in images @@ -141,9 +142,9 @@ class GeminiClient(GenAIClient): def chat_with_tools( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> dict[str, Any]: """ Send chat messages to Gemini with optional tool definitions. @@ -399,9 +400,9 @@ class GeminiClient(GenAIClient): async def chat_with_tools_stream( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> AsyncGenerator[tuple[str, Any], None]: """ Stream chat with tools; yields content deltas then final message. @@ -554,7 +555,7 @@ class GeminiClient(GenAIClient): reasoning_parts: list[str] = [] tool_calls_by_index: dict[int, dict[str, Any]] = {} finish_reason = "stop" - usage_stats: Optional[dict[str, Any]] = None + usage_stats: dict[str, Any] | None = None stream = await self.provider.aio.models.generate_content_stream( model=self.genai_config.model, diff --git a/frigate/genai/plugins/llama_cpp.py b/frigate/genai/plugins/llama_cpp.py index d5458cf8f..f4bf4373f 100644 --- a/frigate/genai/plugins/llama_cpp.py +++ b/frigate/genai/plugins/llama_cpp.py @@ -4,7 +4,8 @@ import base64 import io import json import logging -from typing import Any, AsyncGenerator, Optional, cast +from collections.abc import AsyncGenerator +from typing import Any, cast import httpx import numpy as np @@ -18,7 +19,7 @@ from frigate.genai.utils import parse_tool_calls_from_message logger = logging.getLogger(__name__) -def _stats_from_llama_cpp_chunk(data: dict[str, Any]) -> Optional[dict[str, Any]]: +def _stats_from_llama_cpp_chunk(data: dict[str, Any]) -> dict[str, Any] | None: """Build a stats dict from a llama.cpp streaming chunk. Final-chunk `usage` carries authoritative token counts. Per-chunk @@ -311,9 +312,9 @@ class LlamaCppClient(GenAIClient): self, prompt: str, images: list[bytes], - response_format: Optional[dict] = None, + response_format: dict | None = None, enable_thinking: bool = False, - ) -> Optional[str]: + ) -> str | None: """Submit a request to llama.cpp server.""" if self.provider is None: logger.warning( @@ -521,10 +522,10 @@ class LlamaCppClient(GenAIClient): def _build_payload( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]], - tool_choice: Optional[str], + tools: list[dict[str, Any]] | None, + tool_choice: str | None, stream: bool = False, - enable_thinking: Optional[bool] = None, + enable_thinking: bool | None = None, ) -> dict[str, Any]: """Build request payload for chat completions (sync or stream).""" openai_tool_choice = None @@ -588,7 +589,7 @@ class LlamaCppClient(GenAIClient): @staticmethod def _streamed_tool_calls_to_list( tool_calls_by_index: dict[int, dict[str, Any]], - ) -> Optional[list[dict[str, Any]]]: + ) -> list[dict[str, Any]] | None: """Convert streamed tool_calls index map to list of {id, name, arguments}.""" if not tool_calls_by_index: return None @@ -758,9 +759,9 @@ class LlamaCppClient(GenAIClient): def chat_with_tools( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> dict[str, Any]: """ Send chat messages to llama.cpp server with optional tool definitions. @@ -830,9 +831,9 @@ class LlamaCppClient(GenAIClient): async def chat_with_tools_stream( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> AsyncGenerator[tuple[str, Any], None]: """Stream chat with tools via OpenAI-compatible streaming API.""" if self.provider is None: diff --git a/frigate/genai/plugins/ollama.py b/frigate/genai/plugins/ollama.py index 08176f524..f323b0faa 100644 --- a/frigate/genai/plugins/ollama.py +++ b/frigate/genai/plugins/ollama.py @@ -4,7 +4,8 @@ import base64 import binascii import json import logging -from typing import Any, AsyncGenerator, Optional +from collections.abc import AsyncGenerator +from typing import Any from httpx import RemoteProtocolError, TimeoutException from ollama import AsyncClient as OllamaAsyncClient @@ -18,7 +19,7 @@ from frigate.genai.utils import parse_tool_calls_from_message logger = logging.getLogger(__name__) -def _extract_ollama_stats(response: Any) -> Optional[dict[str, Any]]: +def _extract_ollama_stats(response: Any) -> dict[str, Any] | None: """Build a stats dict from Ollama's response metadata. Ollama reports eval_count/eval_duration (generation) and @@ -51,7 +52,7 @@ def _extract_ollama_stats(response: Any) -> Optional[dict[str, Any]]: def _normalize_multimodal_content( content: Any, -) -> tuple[Optional[str], Optional[list[bytes]]]: +) -> tuple[str | None, list[bytes] | None]: """Convert OpenAI-style multimodal content to Ollama's (text, images) shape. The chat API constructs user messages with content as a list of @@ -98,7 +99,7 @@ class OllamaClient(GenAIClient): provider: ApiClient | None provider_options: dict[str, Any] - _supports_thinking_cache: Optional[bool] = None + _supports_thinking_cache: bool | None = None @property def supports_toggleable_thinking(self) -> bool: @@ -193,9 +194,9 @@ class OllamaClient(GenAIClient): self, prompt: str, images: list[bytes], - response_format: Optional[dict] = None, + response_format: dict | None = None, enable_thinking: bool = False, - ) -> Optional[str]: + ) -> str | None: """Submit a request to Ollama""" if self.provider is None: logger.warning( @@ -290,10 +291,10 @@ class OllamaClient(GenAIClient): def _build_request_params( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]], - tool_choice: Optional[str], + tools: list[dict[str, Any]] | None, + tool_choice: str | None, stream: bool = False, - enable_thinking: Optional[bool] = None, + enable_thinking: bool | None = None, ) -> dict[str, Any]: """Build request_messages and params for chat (sync or stream).""" request_messages = [] @@ -385,9 +386,9 @@ class OllamaClient(GenAIClient): def chat_with_tools( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> dict[str, Any]: if self.provider is None: logger.warning( @@ -426,9 +427,9 @@ class OllamaClient(GenAIClient): async def chat_with_tools_stream( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> AsyncGenerator[tuple[str, Any], None]: """Stream chat with tools; yields content deltas then final message. diff --git a/frigate/genai/plugins/openai.py b/frigate/genai/plugins/openai.py index 3e862f8fd..40b1ee09e 100644 --- a/frigate/genai/plugins/openai.py +++ b/frigate/genai/plugins/openai.py @@ -3,7 +3,8 @@ import base64 import json import logging -from typing import Any, AsyncGenerator, Optional +from collections.abc import AsyncGenerator +from typing import Any from httpx import TimeoutException from openai import OpenAI @@ -14,7 +15,7 @@ from frigate.genai import GenAIClient, register_genai_provider logger = logging.getLogger(__name__) -def _stats_from_openai_usage(usage: Any) -> Optional[dict[str, Any]]: +def _stats_from_openai_usage(usage: Any) -> dict[str, Any] | None: """Build a stats dict from an OpenAI-compatible usage object.""" if usage is None: return None @@ -35,7 +36,7 @@ class OpenAIClient(GenAIClient): """Generative AI client for Frigate using OpenAI.""" provider: OpenAI - context_size: Optional[int] = None + context_size: int | None = None def _init_provider(self) -> OpenAI: """Initialize the client. @@ -60,9 +61,9 @@ class OpenAIClient(GenAIClient): self, prompt: str, images: list[bytes], - response_format: Optional[dict] = None, + response_format: dict | None = None, enable_thinking: bool = False, - ) -> Optional[str]: + ) -> str | None: """Submit a request to OpenAI.""" encoded_images = [base64.b64encode(image).decode("utf-8") for image in images] messages_content: list[dict] = [ @@ -186,9 +187,9 @@ class OpenAIClient(GenAIClient): def chat_with_tools( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> dict[str, Any]: """ Send chat messages to OpenAI with optional tool definitions. @@ -307,9 +308,9 @@ class OpenAIClient(GenAIClient): async def chat_with_tools_stream( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> AsyncGenerator[tuple[str, Any], None]: """ Stream chat with tools; yields content deltas then final message. @@ -357,7 +358,7 @@ class OpenAIClient(GenAIClient): reasoning_parts: list[str] = [] tool_calls_by_index: dict[int, dict[str, Any]] = {} finish_reason = "stop" - usage_stats: Optional[dict[str, Any]] = None + usage_stats: dict[str, Any] | None = None stream = self.provider.chat.completions.create(**request_params) diff --git a/frigate/genai/prompts.py b/frigate/genai/prompts.py index af6ddab88..5f1d328d7 100644 --- a/frigate/genai/prompts.py +++ b/frigate/genai/prompts.py @@ -6,7 +6,7 @@ transport. """ import datetime -from typing import Any, Dict, List, Optional +from typing import Any from playhouse.shortcuts import model_to_dict @@ -216,7 +216,7 @@ def build_object_description_prompt( return template.format(**model_to_dict(event)) -def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]]: +def get_attribute_classifications(config: FrigateConfig) -> list[dict[str, Any]]: """Return enabled custom classification models of `attribute` type. Each entry: {"name": , "objects": [, ...]}. @@ -224,7 +224,7 @@ def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]] types, which can later be filtered via the search_objects `attribute` field. """ - result: List[Dict[str, Any]] = [] + result: list[dict[str, Any]] = [] for model_key, model_config in config.classification.custom.items(): if not model_config.enabled or model_config.object_config is None: @@ -248,8 +248,8 @@ def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]] def get_tool_definitions( semantic_search_enabled: bool = False, - attribute_classifications: Optional[List[Dict[str, Any]]] = None, -) -> List[Dict[str, Any]]: + attribute_classifications: list[dict[str, Any]] | None = None, +) -> list[dict[str, Any]]: """ Get OpenAI-compatible tool definitions for Frigate. @@ -260,7 +260,7 @@ def get_tool_definitions( included. When attribute classification models are configured, an `attribute` parameter is exposed for filtering by their labels. """ - search_objects_properties: Dict[str, Any] = { + search_objects_properties: dict[str, Any] = { "camera": { "type": "string", "description": "Camera name to filter by (optional).", @@ -657,9 +657,9 @@ def get_tool_definitions( def build_chat_system_prompt( config: FrigateConfig, - allowed_cameras: List[str], + allowed_cameras: list[str], semantic_search_enabled: bool, - attribute_classifications: List[Dict[str, Any]], + attribute_classifications: list[dict[str, Any]], ) -> str: """Build the system prompt for the chat completion endpoint. @@ -671,7 +671,7 @@ def build_chat_system_prompt( current_date_str = current_datetime.strftime("%Y-%m-%d") current_time_str = current_datetime.strftime("%I:%M:%S %p") - cameras_info: List[str] = [] + cameras_info: list[str] = [] has_speed_zone = False for camera_id in allowed_cameras: if camera_id not in config.cameras: diff --git a/frigate/genai/utils.py b/frigate/genai/utils.py index a382647cb..fbd6f2110 100644 --- a/frigate/genai/utils.py +++ b/frigate/genai/utils.py @@ -2,14 +2,14 @@ import json import logging -from typing import Any, List, Optional +from typing import Any logger = logging.getLogger(__name__) def parse_tool_calls_from_message( message: dict[str, Any], -) -> Optional[list[dict[str, Any]]]: +) -> list[dict[str, Any]] | None: """ Parse tool_calls from an OpenAI-style message dict. @@ -52,7 +52,7 @@ def parse_tool_calls_from_message( def build_assistant_message_for_conversation( content: Any, - tool_calls_raw: Optional[List[dict[str, Any]]], + tool_calls_raw: list[dict[str, Any]] | None, ) -> dict[str, Any]: """ Build the assistant message dict in OpenAI format for appending to a conversation. diff --git a/frigate/jobs/debug_replay.py b/frigate/jobs/debug_replay.py index 3d8b2d6b6..c5e4ed876 100644 --- a/frigate/jobs/debug_replay.py +++ b/frigate/jobs/debug_replay.py @@ -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 diff --git a/frigate/jobs/export.py b/frigate/jobs/export.py index 63bd4e759..1c88ec584 100644 --- a/frigate/jobs/export.py +++ b/frigate/jobs/export.py @@ -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) diff --git a/frigate/jobs/job.py b/frigate/jobs/job.py index a445eebf5..c40087d0c 100644 --- a/frigate/jobs/job.py +++ b/frigate/jobs/job.py @@ -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.""" diff --git a/frigate/jobs/manager.py b/frigate/jobs/manager.py index 8aa77b3c7..c2fb44af3 100644 --- a/frigate/jobs/manager.py +++ b/frigate/jobs/manager.py @@ -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: diff --git a/frigate/jobs/media_sync.py b/frigate/jobs/media_sync.py index 4a3fdc355..1cd820986 100644 --- a/frigate/jobs/media_sync.py +++ b/frigate/jobs/media_sync.py @@ -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)) diff --git a/frigate/jobs/motion_search.py b/frigate/jobs/motion_search.py index 13fc841e9..15cd104f7 100644 --- a/frigate/jobs/motion_search.py +++ b/frigate/jobs/motion_search.py @@ -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) diff --git a/frigate/jobs/vlm_watch.py b/frigate/jobs/vlm_watch.py index 41ed830f1..1e6a542c2 100644 --- a/frigate/jobs/vlm_watch.py +++ b/frigate/jobs/vlm_watch.py @@ -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 diff --git a/frigate/log.py b/frigate/log.py index cd475a4bd..3c5f8ec8b 100644 --- a/frigate/log.py +++ b/frigate/log.py @@ -6,13 +6,14 @@ import os import sys import threading from collections import deque +from collections.abc import Callable, Generator from contextlib import contextmanager from enum import Enum from functools import wraps from logging.handlers import QueueHandler, QueueListener from multiprocessing.managers import SyncManager from queue import Empty, Queue -from typing import Any, Callable, Deque, Generator, Optional +from typing import Any from frigate.util.builtin import clean_camera_user_pass @@ -47,8 +48,8 @@ class LogLevel(str, Enum): critical = "critical" -log_listener: Optional[QueueListener] = None -log_queue: Optional[Queue] = None +log_listener: QueueListener | None = None +log_queue: Queue | None = None def setup_logging(manager: SyncManager) -> None: @@ -118,7 +119,7 @@ class LogPipe(threading.Thread): super().__init__(daemon=False) self.logger = logging.getLogger(log_name) self.level = level - self.deque: Deque[str] = deque(maxlen=100) + self.deque: deque[str] = deque(maxlen=100) self.fdRead, self.fdWrite = os.pipe() self.pipeReader = os.fdopen(self.fdRead) self.start() diff --git a/frigate/motion/__init__.py b/frigate/motion/__init__.py index 58f781f46..e6ead35bb 100644 --- a/frigate/motion/__init__.py +++ b/frigate/motion/__init__.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from typing import Tuple from numpy import ndarray @@ -10,7 +9,7 @@ class MotionDetector(ABC): @abstractmethod def __init__( self, - frame_shape: Tuple[int, int, int], + frame_shape: tuple[int, int, int], config: MotionConfig, fps: int, improve_contrast: bool, diff --git a/frigate/motion/improved_motion.py b/frigate/motion/improved_motion.py index 6694dafff..598eeacd1 100644 --- a/frigate/motion/improved_motion.py +++ b/frigate/motion/improved_motion.py @@ -1,5 +1,4 @@ import logging -from typing import Optional import cv2 import numpy as np @@ -19,7 +18,7 @@ class ImprovedMotionDetector(MotionDetector): frame_shape: tuple[int, ...], config: RuntimeMotionConfig, fps: int, - ptz_metrics: Optional[PTZMetrics] = None, + ptz_metrics: PTZMetrics | None = None, name: str = "improved", blur_radius: int = 1, interpolation: int = cv2.INTER_NEAREST, diff --git a/frigate/object_detection/base.py b/frigate/object_detection/base.py index f2336f3da..bc7910e4d 100644 --- a/frigate/object_detection/base.py +++ b/frigate/object_detection/base.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from collections import deque from multiprocessing import Queue, Value from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Optional +from typing import Any import numpy as np import zmq @@ -42,9 +42,9 @@ class ObjectDetector(ABC): class BaseLocalDetector(ObjectDetector): def __init__( self, - detector_config: Optional[BaseDetectorConfig] = None, - labels: Optional[str] = None, - stop_event: Optional[MpEvent] = None, + detector_config: BaseDetectorConfig | None = None, + labels: str | None = None, + stop_event: MpEvent | None = None, ) -> None: self.fps = EventsPerSecond() if labels is None: diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index 477e9a0db..cd29dfe94 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -10,7 +10,7 @@ import subprocess as sp import threading import traceback from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Optional +from typing import Any import cv2 import numpy as np @@ -379,8 +379,8 @@ class BirdsEyeFrameManager: def copy_to_position( self, position: Any, - camera: Optional[str] = None, - frame: Optional[np.ndarray] = None, + camera: str | None = None, + frame: np.ndarray | None = None, ) -> None: if camera is None: frame = None @@ -428,7 +428,7 @@ class BirdsEyeFrameManager: } return coordinates - def update_frame(self, frame: Optional[np.ndarray] = None) -> tuple[bool, bool]: + def update_frame(self, frame: np.ndarray | None = None) -> tuple[bool, bool]: """ Update birdseye, optionally with a new frame. Returns (frame_changed, layout_changed) to indicate if the frame or layout changed. @@ -592,12 +592,12 @@ class BirdsEyeFrameManager: self, cameras_to_add: list[str], coefficient: float, - ) -> Optional[list[list[Any]]]: + ) -> list[list[Any]] | None: """Calculate the optimal layout for 2+ cameras.""" def map_layout( camera_layout: list[list[Any]], row_height: int - ) -> tuple[int, int, Optional[list[list[Any]]]]: + ) -> tuple[int, int, list[list[Any]] | None]: """Map the calculated layout.""" candidate_layout = [] starting_x = 0 @@ -825,7 +825,7 @@ class Birdseye: self.stop_event = stop_event self.requestor = InterProcessRequestor() self.idle_fps: float = self.config.birdseye.idle_heartbeat_fps - self._idle_interval: Optional[float] = ( + self._idle_interval: float | None = ( (1.0 / self.idle_fps) if self.idle_fps > 0 else None ) diff --git a/frigate/output/preview.py b/frigate/output/preview.py index bf3c4bc7e..cce05565a 100644 --- a/frigate/output/preview.py +++ b/frigate/output/preview.py @@ -251,7 +251,7 @@ class PreviewRecorder: # end segment at end of hour (use UTC to avoid DST issues) self.segment_end = ( - (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)) + (datetime.datetime.now(datetime.UTC) + datetime.timedelta(hours=1)) .replace(minute=0, second=0, microsecond=0) .timestamp() ) @@ -263,7 +263,7 @@ class PreviewRecorder: # check for existing items in cache start_ts = ( - datetime.datetime.now(datetime.timezone.utc) + datetime.datetime.now(datetime.UTC) .replace(minute=0, second=0, microsecond=0) .timestamp() ) @@ -298,7 +298,7 @@ class PreviewRecorder: def reset_frame_cache(self, frame_time: float) -> None: self.segment_end = ( ( - datetime.datetime.fromtimestamp(frame_time, tz=datetime.timezone.utc) + datetime.datetime.fromtimestamp(frame_time, tz=datetime.UTC) + datetime.timedelta(hours=1) ) .replace(minute=0, second=0, microsecond=0) diff --git a/frigate/plus.py b/frigate/plus.py index 2870d2ae5..d528aa175 100644 --- a/frigate/plus.py +++ b/frigate/plus.py @@ -4,7 +4,7 @@ import logging import os import re from pathlib import Path -from typing import Any, List +from typing import Any import cv2 import requests @@ -140,8 +140,8 @@ class PlusApi: def add_false_positive( self, plus_id: str, - region: List[float], - bbox: List[float], + region: list[float], + bbox: list[float], score: float, label: str, model_hash: str, @@ -184,7 +184,7 @@ class PlusApi: def add_annotation( self, plus_id: str, - bbox: List[float], + bbox: list[float], label: str, difficult: bool = False, ) -> None: diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index 55c8e6ecf..9a114c952 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -756,7 +756,7 @@ class PtzAutoTracker: try: # Asynchronously wait for move data with a timeout move_data = await asyncio.wait_for(move_queue.get(), timeout=0.1) - except asyncio.TimeoutError: + except TimeoutError: continue async with self.move_queue_locks[camera]: @@ -948,8 +948,8 @@ class PtzAutoTracker: if invalid: logger.debug( - f"{camera}: Invalid velocity: {tuple(np.round(velocities, 2).flatten().astype(int))}: Invalid because: " - + ", ".join( + f"{camera}: Invalid velocity: {tuple(np.round(velocities, 2).flatten().astype(int))}: Invalid because: %s", + ", ".join( [ var_name for var_name, is_invalid in [ @@ -961,7 +961,7 @@ class PtzAutoTracker: ] if is_invalid ] - ) + ), ) # invalid velocity return False, np.zeros((4,)) diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index e48b3e787..d29923e23 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -856,7 +856,7 @@ class OnvifController: try: # Wait with a timeout to prevent blocking indefinitely future.result(timeout=10) - except asyncio.TimeoutError: + except TimeoutError: logger.error(f"Command {command} timed out for camera {camera_name}") except Exception as e: logger.error( diff --git a/frigate/record/export.py b/frigate/record/export.py index 16f322e42..5d307077c 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -9,9 +9,9 @@ import shutil import string import subprocess as sp import threading +from collections.abc import Callable from enum import Enum from pathlib import Path -from typing import Callable, Optional import pytz # type: ignore[import-untyped] from peewee import DoesNotExist @@ -209,17 +209,17 @@ class RecordingExporter(threading.Thread): config: FrigateConfig, id: str, camera: str, - name: Optional[str], - image: Optional[str], + name: str | None, + image: str | None, start_time: int, end_time: int, playback_source: PlaybackSourceEnum, - export_case_id: Optional[str] = None, - ffmpeg_input_args: Optional[str] = None, - ffmpeg_output_args: Optional[str] = None, + export_case_id: str | None = None, + ffmpeg_input_args: str | None = None, + ffmpeg_output_args: str | None = None, cpu_fallback: bool = False, - chapters: Optional[ChaptersEnum] = None, - on_progress: Optional[Callable[[str, float], None]] = None, + chapters: ChaptersEnum | None = None, + on_progress: Callable[[str, float], None] | None = None, ) -> None: super().__init__() self.config = config @@ -283,7 +283,7 @@ class RecordingExporter(threading.Thread): return input_duration * factor - def _sum_source_duration_seconds(self) -> Optional[float]: + def _sum_source_duration_seconds(self) -> float | None: """Sum saved-video seconds inside [start_time, end_time]. Queries Recordings or Previews depending on the playback source, @@ -383,7 +383,7 @@ class RecordingExporter(threading.Thread): def _chapter_metadata_path(self) -> str: return os.path.join(CACHE_DIR, f"export_chapters_{self.export_id}.txt") - def _build_chapter_metadata_file(self, recordings: list) -> Optional[str]: + def _build_chapter_metadata_file(self, recordings: list) -> str | None: """Write an FFmpeg metadata file with chapters for review items in range. Chapter offsets are computed in *output time*: the VOD endpoint @@ -514,7 +514,7 @@ class RecordingExporter(threading.Thread): def _build_recording_segment_chapter_metadata_file( self, recordings: list - ) -> Optional[str]: + ) -> str | None: """Write an FFmpeg metadata file with one chapter per recording segment. Each chapter's title is the segment's wallclock start time in @@ -530,14 +530,14 @@ class RecordingExporter(threading.Thread): return None tz_name = self.config.ui.timezone - tz: Optional[datetime.tzinfo] = None + tz: datetime.tzinfo | None = None if tz_name: try: tz = pytz.timezone(tz_name) except pytz.UnknownTimeZoneError: tz = None if tz is None: - tz = datetime.timezone.utc + tz = datetime.UTC chapter_blocks: list[str] = [] output_offset_ms = 0 @@ -591,7 +591,7 @@ class RecordingExporter(threading.Thread): if ( self.start_time - < datetime.datetime.now(datetime.timezone.utc) + < datetime.datetime.now(datetime.UTC) .replace(minute=0, second=0, microsecond=0) .timestamp() ): @@ -767,7 +767,7 @@ class RecordingExporter(threading.Thread): # add metadata title = f"Frigate Recording for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}" creation_time = datetime.datetime.fromtimestamp( - self.start_time, tz=datetime.timezone.utc + self.start_time, tz=datetime.UTC ).strftime("%Y-%m-%dT%H:%M:%S.%fZ") ffmpeg_cmd.extend( [ @@ -876,7 +876,7 @@ class RecordingExporter(threading.Thread): # add metadata title = f"Frigate Preview for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}" creation_time = datetime.datetime.fromtimestamp( - self.start_time, tz=datetime.timezone.utc + self.start_time, tz=datetime.UTC ).strftime("%Y-%m-%dT%H:%M:%S.%fZ") ffmpeg_cmd.extend( [ diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index 7141ddb63..7f9dbc19d 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -11,7 +11,7 @@ import time from collections import defaultdict from multiprocessing.synchronize import Event as MpEvent from pathlib import Path -from typing import Any, Optional, Tuple +from typing import Any import numpy as np import psutil @@ -100,7 +100,7 @@ class RecordingMaintainer(threading.Thread): self.stop_event = stop_event self.object_recordings_info: dict[str, list] = defaultdict(list) self.audio_recordings_info: dict[str, list] = defaultdict(list) - self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {} + self.end_time_cache: dict[str, tuple[datetime.datetime, float]] = {} self.unexpected_cache_files_logged: bool = False async def move_files(self) -> None: @@ -127,7 +127,7 @@ class RecordingMaintainer(threading.Thread): start_time = datetime.datetime.strptime( date, CACHE_SEGMENT_FORMAT - ).astimezone(datetime.timezone.utc) + ).astimezone(datetime.UTC) if ( camera not in newest_cache_segments or start_time > newest_cache_segments[camera]["start_time"] @@ -187,7 +187,7 @@ class RecordingMaintainer(threading.Thread): # important that start_time is utc because recordings are stored and compared in utc start_time = datetime.datetime.strptime( date, CACHE_SEGMENT_FORMAT - ).astimezone(datetime.timezone.utc) + ).astimezone(datetime.UTC) grouped_recordings[camera].append( { @@ -305,9 +305,7 @@ class RecordingMaintainer(threading.Thread): self._expire_stale_recordings_info(grouped_recordings) - recordings_to_insert: list[Optional[dict[str, Any]]] = await asyncio.gather( - *tasks - ) + recordings_to_insert: list[dict[str, Any] | None] = await asyncio.gather(*tasks) # fire and forget recordings entries self.requestor.send_data( @@ -336,7 +334,7 @@ class RecordingMaintainer(threading.Thread): async def validate_and_move_segment( self, camera: str, reviews: Any, recording: dict[str, Any] - ) -> Optional[dict[str, Any]]: + ) -> dict[str, Any] | None: cache_path: str = recording["cache_path"] start_time: datetime.datetime = recording["start_time"] @@ -413,7 +411,7 @@ class RecordingMaintainer(threading.Thread): if ( datetime.datetime.fromtimestamp( most_recently_processed_frame_time - ).astimezone(datetime.timezone.utc) + ).astimezone(datetime.UTC) >= end_time ): record_mode = ( @@ -495,7 +493,7 @@ class RecordingMaintainer(threading.Thread): ) retain_cutoff = datetime.datetime.fromtimestamp( most_recently_processed_frame_time - record_config.event_pre_capture - ).astimezone(datetime.timezone.utc) + ).astimezone(datetime.UTC) if end_time < retain_cutoff: self.drop_segment(cache_path) @@ -621,7 +619,7 @@ class RecordingMaintainer(threading.Thread): duration: float, cache_path: str, segment_info: SegmentInfo, - ) -> Optional[dict[str, Any]]: + ) -> dict[str, Any] | None: # directory will be in utc due to start_time being in utc directory = os.path.join( RECORD_DIR, diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index cfc59744c..f3abc1bba 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -11,7 +11,7 @@ import sys import threading from multiprocessing.synchronize import Event as MpEvent from pathlib import Path -from typing import Any, Optional +from typing import Any import cv2 import numpy as np @@ -267,7 +267,7 @@ class ReviewSegmentMaintainer(threading.Thread): def __init__(self, config: FrigateConfig, stop_event: MpEvent): super().__init__(name="review_segment_maintainer") self.config = config - self.active_review_segments: dict[str, Optional[PendingReviewSegment]] = {} + self.active_review_segments: dict[str, PendingReviewSegment | None] = {} self.frame_manager = SharedMemoryFrameManager() # create communication for review segments @@ -323,7 +323,7 @@ class ReviewSegmentMaintainer(threading.Thread): self, segment: PendingReviewSegment, camera_config: CameraConfig, - frame: Optional[np.ndarray], + frame: np.ndarray | None, objects: list[dict[str, Any]], prev_data: dict[str, Any], ) -> None: diff --git a/frigate/service_manager/multiprocessing.py b/frigate/service_manager/multiprocessing.py index 87bb4ffee..88d497dd6 100644 --- a/frigate/service_manager/multiprocessing.py +++ b/frigate/service_manager/multiprocessing.py @@ -9,7 +9,6 @@ from abc import ABC, abstractmethod from asyncio.exceptions import TimeoutError from logging.handlers import QueueHandler from types import FrameType -from typing import Optional import frigate.log @@ -22,13 +21,13 @@ DEFAULT_STOP_TIMEOUT = 10 # seconds class BaseServiceProcess(Service, ABC): """A Service the manages a multiprocessing.Process.""" - _process: Optional[mp.Process] + _process: mp.Process | None def __init__( self, *, - name: Optional[str] = None, - manager: Optional[ServiceManager] = None, + name: str | None = None, + manager: ServiceManager | None = None, ) -> None: super().__init__(name=name, manager=manager) @@ -55,7 +54,7 @@ class BaseServiceProcess(Service, ABC): self, *, force: bool = False, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> None: if timeout is None: timeout = DEFAULT_STOP_TIMEOUT @@ -85,7 +84,7 @@ class BaseServiceProcess(Service, ABC): self.manager.logger.info(f"{self.name} stopped") @property - def pid(self) -> Optional[int]: + def pid(self) -> int | None: return self._process.pid if self._process else None def _run(self) -> None: @@ -143,7 +142,7 @@ class ServiceProcess(BaseServiceProcess): faulthandler.enable() - def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: + def receiveSignal(signalNumber: int, frame: FrameType | None) -> None: # Get the stop_event through the dict to bypass lazy initialization. stop_event = self.__dict__.get("stop_event") if stop_event is not None: diff --git a/frigate/service_manager/multiprocessing_waiter.py b/frigate/service_manager/multiprocessing_waiter.py index 8acdf583c..e356fac1d 100644 --- a/frigate/service_manager/multiprocessing_waiter.py +++ b/frigate/service_manager/multiprocessing_waiter.py @@ -7,7 +7,7 @@ import threading from multiprocessing.connection import Connection from multiprocessing.connection import wait as mp_wait from socket import socket -from typing import Any, Optional, Union +from typing import Any logger = logging.getLogger(__name__) @@ -118,10 +118,10 @@ class MultiprocessingWaiter(threading.Thread): waiter_lock = threading.Lock() -waiter_thread: Optional[MultiprocessingWaiter] = None +waiter_thread: MultiprocessingWaiter | None = None -async def wait(object: Union[mp.Process, Connection, socket]) -> None: +async def wait(object: mp.Process | Connection | socket) -> None: """Wait for the supplied object to be ready. Under the hood, this uses multiprocessing.connection.wait() and a background thread manage the @@ -129,7 +129,7 @@ async def wait(object: Union[mp.Process, Connection, socket]) -> None: """ global waiter_thread, waiter_lock - sentinel: Union[Connection, socket, int] + sentinel: Connection | socket | int if isinstance(object, mp.Process): sentinel = object.sentinel elif isinstance(object, Connection) or isinstance(object, socket): diff --git a/frigate/service_manager/service.py b/frigate/service_manager/service.py index 89d766e9d..34631b621 100644 --- a/frigate/service_manager/service.py +++ b/frigate/service_manager/service.py @@ -5,12 +5,11 @@ import atexit import logging import threading from abc import ABC, abstractmethod +from collections.abc import Coroutine from contextvars import ContextVar from dataclasses import dataclass from functools import partial -from typing import Coroutine, Optional, Union, cast - -from typing_extensions import Self +from typing import Self, cast class Service(ABC): @@ -19,8 +18,8 @@ class Service(ABC): def __init__( self, *, - name: Optional[str] = None, - manager: Optional[ServiceManager] = None, + name: str | None = None, + manager: ServiceManager | None = None, ): if name: self.__dict__["name"] = name @@ -42,13 +41,13 @@ class Service(ABC): try: return self.__manager except AttributeError: - raise RuntimeError("Cannot access associated service manager") + raise RuntimeError("Cannot access associated service manager") from None def start( self, *, wait: bool = False, - wait_timeout: Optional[float] = None, + wait_timeout: float | None = None, ) -> Self: """Start this service. @@ -70,9 +69,9 @@ class Service(ABC): self, *, force: bool = False, - timeout: Optional[float] = None, + timeout: float | None = None, wait: bool = False, - wait_timeout: Optional[float] = None, + wait_timeout: float | None = None, ) -> Self: """Stop this service. @@ -97,9 +96,9 @@ class Service(ABC): self, *, force: bool = False, - stop_timeout: Optional[float] = None, + stop_timeout: float | None = None, wait: bool = False, - wait_timeout: Optional[float] = None, + wait_timeout: float | None = None, ) -> Self: """Restart this service. @@ -129,7 +128,7 @@ class Service(ABC): self, *, force: bool = False, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> None: pass @@ -137,14 +136,14 @@ class Service(ABC): self, *, force: bool = False, - stop_timeout: Optional[float] = None, + stop_timeout: float | None = None, ) -> None: await self.on_stop(force=force, timeout=stop_timeout) await self.on_start() default_service_manager_lock = threading.Lock() -default_service_manager: Optional[ServiceManager] = None +default_service_manager: ServiceManager | None = None current_service_manager: ContextVar[ServiceManager] = ContextVar( "current_service_manager" @@ -162,8 +161,8 @@ class Command: """ coro: Coroutine - lock: Optional[asyncio.Lock] = None - done: Optional[threading.Event] = None + lock: asyncio.Lock | None = None + done: threading.Event | None = None class ServiceManager: @@ -189,7 +188,7 @@ class ServiceManager: _services_lock: threading.Lock # Commands will be queued with associated event loop. Queueing `None` signals shutdown. - _command_queue: asyncio.Queue[Union[Command, None]] + _command_queue: asyncio.Queue[Command | None] _event_loop: asyncio.AbstractEventLoop # The pending command counter is used to ensure all commands have been queued before shutdown. @@ -204,7 +203,7 @@ class ServiceManager: # Will be acquired to ensure the shutdown sentinel is sent only once. Never released. _shutdown_lock: threading.Lock - def __init__(self, *, name: Optional[str] = None): + def __init__(self, *, name: str | None = None): self._name = name if name is not None else (__package__ or __name__) self._logger = logging.getLogger(self.name) @@ -276,8 +275,8 @@ class ServiceManager: coro: Coroutine, *, wait: bool = False, - wait_timeout: Optional[float] = None, - lock: Optional[asyncio.Lock] = None, + wait_timeout: float | None = None, + lock: asyncio.Lock | None = None, ) -> None: """Run an async task in the service manager thread. @@ -299,7 +298,7 @@ class ServiceManager: cmd.done.wait(timeout=wait_timeout) def shutdown( - self, *, wait: bool = False, wait_timeout: Optional[float] = None + self, *, wait: bool = False, wait_timeout: float | None = None ) -> None: """Shutdown the service manager thread. @@ -321,7 +320,7 @@ class ServiceManager: if not self._manager_thread.is_alive(): raise RuntimeError(f"ServiceManager {self.name} is not running") - def _send_command(self, command: Union[Command, None]) -> None: + def _send_command(self, command: Command | None) -> None: self._ensure_running() async def queue_command() -> None: @@ -336,7 +335,7 @@ class ServiceManager: self._ensure_running() with self._services_lock: - name_conflict: Optional[Service] = next( + name_conflict: Service | None = next( ( existing for name, existing in self._services.items() diff --git a/frigate/stats/emitter.py b/frigate/stats/emitter.py index 13f50c586..7c2e328c7 100644 --- a/frigate/stats/emitter.py +++ b/frigate/stats/emitter.py @@ -6,7 +6,7 @@ import logging import threading import time from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Optional +from typing import Any from frigate.comms.inter_process import InterProcessRequestor from frigate.config import FrigateConfig @@ -49,9 +49,7 @@ class StatsEmitter(threading.Thread): self.stats_history.append(stats) return stats - def get_stats_history( - self, keys: Optional[list[str]] = None - ) -> list[dict[str, Any]]: + def get_stats_history(self, keys: list[str] | None = None) -> list[dict[str, Any]]: """Get stats history. Supports dot-notation for nested keys to avoid returning large objects diff --git a/frigate/stats/intel_gpu_info.py b/frigate/stats/intel_gpu_info.py index 5ca3066fb..5bbb5c335 100644 --- a/frigate/stats/intel_gpu_info.py +++ b/frigate/stats/intel_gpu_info.py @@ -2,7 +2,6 @@ import logging import re -from typing import Optional logger = logging.getLogger(__name__) @@ -16,7 +15,7 @@ class IntelGpuNameResolver: name with the pdev string used by DRM fdinfo. """ - _names: Optional[dict[str, str]] = None + _names: dict[str, str] | None = None def get_names(self) -> dict[str, str]: if self._names is not None: @@ -39,7 +38,7 @@ class IntelGpuNameResolver: self._names = names return names - cpu_name: Optional[str] = None + cpu_name: str | None = None if "CPU" in devices: try: cpu_name = self._strip_trademarks( @@ -70,14 +69,14 @@ class IntelGpuNameResolver: return names @staticmethod - def _format_pdev(pci) -> Optional[str]: + def _format_pdev(pci) -> str | None: try: return f"{pci.domain:04x}:{pci.bus:02x}:{pci.device:02x}.{pci.function:x}" except AttributeError: return None @classmethod - def _resolve_name(cls, raw_name: str, device_type, cpu_name: Optional[str]) -> str: + def _resolve_name(cls, raw_name: str, device_type, cpu_name: str | None) -> str: """Build a display name for a GPU. Modern integrated Intel GPUs are reported by OpenVINO with a generic diff --git a/frigate/stats/prometheus.py b/frigate/stats/prometheus.py index d2e229568..9797d569e 100644 --- a/frigate/stats/prometheus.py +++ b/frigate/stats/prometheus.py @@ -1,6 +1,6 @@ import logging import re -from typing import Any, Dict, List +from typing import Any from prometheus_client import CONTENT_TYPE_LATEST, generate_latest from prometheus_client.core import ( @@ -11,7 +11,7 @@ from prometheus_client.core import ( ) -class CustomCollector(object): +class CustomCollector: def __init__(self, _url): self.complete_stats = {} # Store complete stats data self.process_stats = {} # Keep for CPU processing @@ -491,7 +491,7 @@ collector = CustomCollector(None) REGISTRY.register(collector) -def update_metrics(stats: Dict[str, Any], event_counts: List[Dict[str, Any]]): +def update_metrics(stats: dict[str, Any], event_counts: list[dict[str, Any]]): """Updates the Prometheus metrics with the given stats data.""" try: # Store the complete stats for later use by collect() diff --git a/frigate/stats/util.py b/frigate/stats/util.py index 56efce5d0..d3cb81986 100644 --- a/frigate/stats/util.py +++ b/frigate/stats/util.py @@ -7,7 +7,7 @@ import shutil import time from json import JSONDecodeError from multiprocessing.managers import DictProxy -from typing import Any, Optional +from typing import Any import requests from requests.exceptions import RequestException @@ -78,7 +78,7 @@ def stats_init( return stats_tracking -def read_temperature(path: str) -> Optional[float]: +def read_temperature(path: str) -> float | None: if os.path.isfile(path): with open(path) as f: line = f.readline().strip() @@ -106,7 +106,7 @@ def get_temperatures() -> dict[str, float]: def get_detector_temperature( detector_type: str, detector_index_by_type: dict[str, int], -) -> Optional[float]: +) -> float | None: """Get temperature for a specific detector based on its type.""" if detector_type == "edgetpu": # Get temperatures for all attached Corals diff --git a/frigate/test/http_api/test_http_media.py b/frigate/test/http_api/test_http_media.py index fa4e69473..b2d83cbc8 100644 --- a/frigate/test/http_api/test_http_media.py +++ b/frigate/test/http_api/test_http_media.py @@ -1,6 +1,6 @@ """Unit tests for recordings/media API endpoints.""" -from datetime import datetime, timezone +from datetime import UTC, datetime import pytz from fastapi import Request @@ -306,8 +306,8 @@ class TestHttpMedia(BaseTestHttp): Test recordings summary with UTC timezone (no DST). """ # Use UTC timestamps directly - march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=timezone.utc).timestamp() - march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=timezone.utc).timestamp() + march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=UTC).timestamp() + march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=UTC).timestamp() with AuthTestClient(self.app) as client: Recordings.insert( diff --git a/frigate/test/test_export_progress.py b/frigate/test/test_export_progress.py index 835cf91b9..81dcdb96e 100644 --- a/frigate/test/test_export_progress.py +++ b/frigate/test/test_export_progress.py @@ -416,7 +416,7 @@ class TestSaveThumbnailFromPreviewFrames(unittest.TestCase): # comparison takes the current-hour branch (preview frames). import datetime - now = datetime.datetime.now(datetime.timezone.utc).timestamp() + now = datetime.datetime.now(datetime.UTC).timestamp() exporter = _make_exporter() exporter.export_id = "thumb_short" exporter.start_time = now diff --git a/frigate/test/test_keyframe_analysis.py b/frigate/test/test_keyframe_analysis.py index 85302b565..14dad5849 100644 --- a/frigate/test/test_keyframe_analysis.py +++ b/frigate/test/test_keyframe_analysis.py @@ -1,6 +1,5 @@ """Tests for keyframe-spacing analysis used to detect smart/+ codecs.""" -import asyncio import unittest from unittest.mock import AsyncMock, MagicMock, patch @@ -90,7 +89,7 @@ class TestAnalyzeRecordKeyframes(unittest.IsolatedAsyncioTestCase): async def test_timeout_returns_unknown(self): proc = MagicMock() - proc.communicate = AsyncMock(side_effect=asyncio.TimeoutError()) + proc.communicate = AsyncMock(side_effect=TimeoutError()) proc.kill = MagicMock() ffmpeg = MagicMock() ffmpeg.ffprobe_path = "/usr/bin/ffprobe" diff --git a/frigate/test/test_maintainer.py b/frigate/test/test_maintainer.py index 1d849429f..715cd5a1a 100644 --- a/frigate/test/test_maintainer.py +++ b/frigate/test/test_maintainer.py @@ -93,7 +93,7 @@ class TestMaintainer(unittest.IsolatedAsyncioTestCase): stop_event = MagicMock() maintainer = RecordingMaintainer(config, stop_event) - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(datetime.UTC) start_time = now - datetime.timedelta(seconds=20) end_time = now - datetime.timedelta(seconds=10) cache_path = "/tmp/cache/test_cam@20260417150000+0000.mp4" diff --git a/frigate/track/norfair_tracker.py b/frigate/track/norfair_tracker.py index 7c695a7a7..6b39885c0 100644 --- a/frigate/track/norfair_tracker.py +++ b/frigate/track/norfair_tracker.py @@ -1,7 +1,8 @@ import logging import random import string -from typing import Any, Sequence, cast +from collections.abc import Sequence +from typing import Any, cast import cv2 import numpy as np diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py index 03117df69..0a9c6f74f 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -5,7 +5,7 @@ import math import os from collections import defaultdict from statistics import median -from typing import Any, Optional, cast +from typing import Any, cast import cv2 import numpy as np @@ -81,7 +81,7 @@ class TrackedObject: self.previous = self.to_dict() @property - def max_severity(self) -> Optional[str]: + def max_severity(self) -> str | None: review_config = self.camera_config.review if ( @@ -590,7 +590,7 @@ class TrackedObjectAttribute: "box": self.box, } - def find_best_object(self, objects: list[dict[str, Any]]) -> Optional[str]: + def find_best_object(self, objects: list[dict[str, Any]]) -> str | None: """Find the best attribute for each object and return its ID.""" best_object_area: float | None = None best_object_id: str | None = None diff --git a/frigate/util/audio.py b/frigate/util/audio.py index eede9c0ea..28fb0a6ba 100644 --- a/frigate/util/audio.py +++ b/frigate/util/audio.py @@ -3,7 +3,6 @@ import logging import os import subprocess as sp -from typing import Optional from pathvalidate import sanitize_filename @@ -19,7 +18,7 @@ def get_audio_from_recording( start_ts: float, end_ts: float, sample_rate: int = 16000, -) -> Optional[bytes]: +) -> bytes | None: """Extract audio from recording files between start_ts and end_ts in WAV format suitable for sherpa-onnx. Args: diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py index e10b76278..35dba2f3a 100644 --- a/frigate/util/builtin.py +++ b/frigate/util/builtin.py @@ -15,7 +15,7 @@ from collections import deque from collections.abc import Mapping from multiprocessing.managers import ValueProxy from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any import numpy as np from ruamel.yaml import YAML @@ -152,7 +152,7 @@ def get_record_segment_time(config: "CameraConfig") -> int: def load_labels( - path: Optional[str], encoding="utf-8", prefill=91, indexed: bool | None = None + path: str | None, encoding="utf-8", prefill=91, indexed: bool | None = None ): """Loads labels from file (with or without index numbers). Args: @@ -164,7 +164,7 @@ def load_labels( if path is None: return {} - with open(path, "r", encoding=encoding) as f: + with open(path, encoding=encoding) as f: labels = {index: "unknown" for index in range(prefill)} lines = f.readlines() if not lines: @@ -180,8 +180,8 @@ def load_labels( def to_relative_box( - width: int, height: int, box: Tuple[int, int, int, int] -) -> Tuple[int | float, int | float, int | float, int | float]: + width: int, height: int, box: tuple[int, int, int, int] +) -> tuple[int | float, int | float, int | float, int | float]: return ( box[0] / width, # x box[1] / height, # y @@ -195,7 +195,7 @@ def create_mask(frame_shape, mask): mask_img[:] = 255 -def process_config_query_string(query_string: Dict[str, list]) -> Dict[str, Any]: +def process_config_query_string(query_string: dict[str, list]) -> dict[str, Any]: updates = {} for key_path_str, new_value_list in query_string.items(): # use the string key as-is for updates dictionary @@ -213,8 +213,8 @@ def process_config_query_string(query_string: Dict[str, list]) -> Dict[str, Any] def flatten_config_data( - config_data: Dict[str, Any], parent_key: str = "" -) -> Dict[str, Any]: + config_data: dict[str, Any], parent_key: str = "" +) -> dict[str, Any]: items = [] for key, value in config_data.items(): escaped_key = escape_config_key_segment(str(key)) @@ -261,12 +261,12 @@ def split_config_key_path(key_path_str: str) -> list[str]: return parts -def update_yaml_file_bulk(file_path: str, updates: Dict[str, Any]): +def update_yaml_file_bulk(file_path: str, updates: dict[str, Any]): yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) try: - with open(file_path, "r") as f: + with open(file_path) as f: data = yaml.load(f) except FileNotFoundError: logger.error( @@ -438,9 +438,7 @@ def generate_color_palette(n): return colors -def serialize( - vector: Union[list[float], np.ndarray, float], pack: bool = True -) -> bytes: +def serialize(vector: list[float] | np.ndarray | float, pack: bool = True) -> bytes: """Serializes a list of floats, numpy array, or single float into a compact "raw bytes" format""" if isinstance(vector, np.ndarray): # Convert numpy array to list of floats @@ -459,7 +457,7 @@ def serialize( else: return vector except struct.error as e: - raise ValueError(f"Failed to pack vector: {e}. Vector: {vector}") + raise ValueError(f"Failed to pack vector: {e}. Vector: {vector}") from e def deserialize(bytes_data: bytes) -> list[float]: diff --git a/frigate/util/classification.py b/frigate/util/classification.py index 30fc4c668..a9345bbc5 100644 --- a/frigate/util/classification.py +++ b/frigate/util/classification.py @@ -84,7 +84,7 @@ def read_training_metadata(model_name: str) -> dict[str, any] | None: return None try: - with open(metadata_path, "r") as f: + with open(metadata_path) as f: metadata = json.load(f) return metadata except Exception as e: @@ -294,7 +294,7 @@ class ClassificationTrainingProcess(FrigateProcess): return True except Exception as e: - logger.error(f"Training failed for {self.model_name}: {e}", exc_info=True) + logger.exception(f"Training failed for {self.model_name}: {e}") return False @@ -732,7 +732,7 @@ def collect_object_classification_examples( # Step 1: Query events for the specified label and cameras events = list( - Event.select().where((Event.label == label)).order_by(Event.start_time.asc()) + Event.select().where(Event.label == label).order_by(Event.start_time.asc()) ) if not events: diff --git a/frigate/util/config.py b/frigate/util/config.py index 5a4f3816c..3e093a978 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -4,7 +4,7 @@ import asyncio import logging import os import shutil -from typing import Any, Optional, Union +from typing import Any from ruamel.yaml import YAML @@ -78,7 +78,7 @@ def migrate_frigate_config(config_file: str): yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) - with open(config_file, "r") as f: + with open(config_file) as f: config: dict[str, dict[str, Any]] = yaml.load(f) if config is None: @@ -477,7 +477,7 @@ def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any] def _convert_legacy_mask_to_dict( - mask: Optional[Union[str, list]], mask_type: str = "motion_mask", label: str = "" + mask: str | list | None, mask_type: str = "motion_mask", label: str = "" ) -> dict[str, dict[str, Any]]: """Convert legacy mask format (str or list[str]) to new dict format. @@ -659,10 +659,10 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any] def get_relative_coordinates( - mask: Optional[Union[str, list]], + mask: str | list | None, frame_shape: tuple[int, int], camera_name: str = "", -) -> Union[str, list]: +) -> str | list: # masks and zones are saved as relative coordinates # we know if any points are > 1 then it is using the # old native resolution coordinates @@ -720,7 +720,7 @@ def get_relative_coordinates( def convert_area_to_pixels( - area_value: Union[int, float], frame_shape: tuple[int, int] + area_value: int | float, frame_shape: tuple[int, int] ) -> int: """ Convert area specification to pixels. @@ -762,7 +762,7 @@ class StreamInfoRetriever: return info -def apply_section_update(camera_config, section: str, update: dict) -> Optional[str]: +def apply_section_update(camera_config, section: str, update: dict) -> str | None: """Merge an update dict into a camera config section and rebuild runtime variants. For motion and object filter sections, the plain Pydantic models are rebuilt diff --git a/frigate/util/downloader.py b/frigate/util/downloader.py index ee80b3816..a8b593f15 100644 --- a/frigate/util/downloader.py +++ b/frigate/util/downloader.py @@ -1,8 +1,8 @@ import logging import os import threading +from collections.abc import Callable from pathlib import Path -from typing import Callable, List import requests @@ -19,7 +19,7 @@ class ModelDownloader: self, model_name: str, download_path: str, - file_names: List[str], + file_names: list[str], download_func: Callable[[str], None], complete_func: Callable[[], None] | None = None, silent: bool = False, diff --git a/frigate/util/ffmpeg.py b/frigate/util/ffmpeg.py index 9f4c5569a..87601b91d 100644 --- a/frigate/util/ffmpeg.py +++ b/frigate/util/ffmpeg.py @@ -2,7 +2,8 @@ import logging import subprocess as sp -from typing import Any, Callable, Optional +from collections.abc import Callable +from typing import Any from frigate.const import PROCESS_PRIORITY_LOW from frigate.log import LogPipe @@ -68,9 +69,9 @@ def run_ffmpeg_with_progress( cmd: list[str], *, expected_duration_seconds: float, - on_progress: Optional[Callable[[float], None]] = None, - stdin_payload: Optional[str] = None, - process_started: Optional[Callable[[sp.Popen], None]] = None, + on_progress: Callable[[float], None] | None = None, + stdin_payload: str | None = None, + process_started: Callable[[sp.Popen], None] | None = None, use_low_priority: bool = True, ) -> tuple[int, str]: """Run an ffmpeg command, streaming progress via `-progress pipe:2`. diff --git a/frigate/util/file.py b/frigate/util/file.py index f858dca68..e259d1345 100644 --- a/frigate/util/file.py +++ b/frigate/util/file.py @@ -7,7 +7,7 @@ import os import time from datetime import datetime from pathlib import Path -from typing import Any, Optional +from typing import Any import cv2 from numpy import ndarray @@ -323,7 +323,7 @@ class FileLock: self.timeout = timeout self.poll_interval = poll_interval self.stale_timeout = stale_timeout - self._fd: Optional[int] = None + self._fd: int | None = None self._acquired = False if cleanup_stale_on_init: @@ -367,7 +367,7 @@ class FileLock: return False - def acquire(self, timeout: Optional[int] = None) -> bool: + def acquire(self, timeout: int | None = None) -> bool: """ Acquire the file lock using fcntl.flock(). @@ -400,7 +400,7 @@ class FileLock: self._acquired = True logger.debug(f"Acquired lock: {self.lock_path}") return True - except (OSError, IOError): + except OSError: # Lock is held by another process if time.time() - start_time >= timeout: logger.warning(f"Timeout waiting for lock: {self.lock_path}") diff --git a/frigate/util/image.py b/frigate/util/image.py index d2832d97a..dde25e242 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from multiprocessing import resource_tracker as _mprt from multiprocessing import shared_memory as _mpshm from string import printable -from typing import Any, AnyStr, Optional +from typing import Any, AnyStr import cv2 import numpy as np @@ -270,7 +270,7 @@ def draw_box_with_label( ) -def get_image_quality_params(ext: str, quality: Optional[int]) -> list[int]: +def get_image_quality_params(ext: str, quality: int | None) -> list[int]: if ext in ("jpg", "jpeg"): return [int(cv2.IMWRITE_JPEG_QUALITY), quality if quality is not None else 70] @@ -921,7 +921,7 @@ def yuv_region_2_bgr(frame, region): raise -def intersection(box_a, box_b) -> Optional[list[int]]: +def intersection(box_a, box_b) -> list[int] | None: """Return intersection box or None if boxes do not intersect.""" if ( box_a[2] < box_b[0] @@ -994,7 +994,7 @@ class FrameManager(ABC): pass @abstractmethod - def write(self, name: str) -> Optional[memoryview]: + def write(self, name: str) -> memoryview | None: pass @abstractmethod @@ -1021,7 +1021,7 @@ class UntrackedSharedMemory(_mpshm.SharedMemory): def __init__( self, - name: Optional[str] = None, + name: str | None = None, create: bool = False, size: int = 0, *, @@ -1075,7 +1075,7 @@ class SharedMemoryFrameManager(FrameManager): self.shm_store[name] = shm return shm.buf - def write(self, name: str) -> Optional[memoryview]: + def write(self, name: str) -> memoryview | None: try: if name in self.shm_store: shm = self.shm_store[name] @@ -1087,7 +1087,7 @@ class SharedMemoryFrameManager(FrameManager): logger.info(f"the file {name} not found") return None - def get(self, name: str, shape) -> Optional[np.ndarray]: + def get(self, name: str, shape) -> np.ndarray | None: try: required = int(np.prod(shape)) shm = self.shm_store.get(name) @@ -1185,10 +1185,10 @@ def run_ffmpeg_snapshot( ffmpeg, input_path: str, codec: str, - seek_time: Optional[float] = None, - height: Optional[int] = None, - timeout: Optional[int] = None, -) -> tuple[Optional[bytes], str]: + seek_time: float | None = None, + height: int | None = None, + timeout: int | None = None, +) -> tuple[bytes | None, str]: """Run ffmpeg to extract a snapshot/image from a video source.""" ffmpeg_cmd = [ ffmpeg.ffmpeg_path, @@ -1238,8 +1238,8 @@ def get_image_from_recording( file_path: str, relative_frame_time: float, codec: str, - height: Optional[int] = None, -) -> Optional[Any]: + height: int | None = None, +) -> Any | None: """retrieve a frame from given time in recording file.""" image_data, _ = run_ffmpeg_snapshot( @@ -1261,7 +1261,7 @@ def get_histogram(image, x_min, y_min, x_max, y_max): def create_thumbnail( yuv_frame: np.ndarray, box: tuple[int, int, int, int], height=500 -) -> Optional[bytes]: +) -> bytes | None: """Return jpg thumbnail of a region of the frame.""" frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2BGR_I420) region = calculate_region( diff --git a/frigate/util/media.py b/frigate/util/media.py index 31374c596..e4e84e914 100644 --- a/frigate/util/media.py +++ b/frigate/util/media.py @@ -5,9 +5,9 @@ import errno import logging import os import subprocess as sp +from collections.abc import Iterable from dataclasses import dataclass, field from pathlib import Path -from typing import Iterable from peewee import DatabaseError, chunked @@ -110,7 +110,7 @@ def sync_recordings( # start checking on the hour 36 hours ago check_point = datetime.datetime.now().replace( minute=0, second=0, microsecond=0 - ).astimezone(datetime.timezone.utc) - datetime.timedelta(hours=36) + ).astimezone(datetime.UTC) - datetime.timedelta(hours=36) # Gather DB recordings to inspect if limited: @@ -793,7 +793,7 @@ def write_orphan_report( f.write("# Media Sync Orphan Report\n") f.write(f"# Job: {job_id}\n") f.write( - f"# Date: {datetime.datetime.now().astimezone(datetime.timezone.utc).isoformat()}\n" + f"# Date: {datetime.datetime.now().astimezone(datetime.UTC).isoformat()}\n" ) f.write(f"# Mode: dry_run={dry_run}\n\n") diff --git a/frigate/util/process.py b/frigate/util/process.py index 1613c1e43..060c39c7b 100644 --- a/frigate/util/process.py +++ b/frigate/util/process.py @@ -6,9 +6,9 @@ import os import pathlib import subprocess import threading +from collections.abc import Callable from logging.handlers import QueueHandler from multiprocessing.synchronize import Event as MpEvent -from typing import Callable, Optional from setproctitle import setproctitle @@ -23,11 +23,11 @@ class BaseProcess(mp.Process): stop_event: MpEvent, priority: int, *, - name: Optional[str] = None, - target: Optional[Callable] = None, + name: str | None = None, + target: Callable | None = None, args: tuple = (), kwargs: dict = {}, - daemon: Optional[bool] = None, + daemon: bool | None = None, ): self.priority = priority self.stop_event = stop_event @@ -121,7 +121,7 @@ class FrigateProcess(BaseProcess): f"If process crashes, manually generate with: memray flamegraph {binary_file}" ) except Exception as e: - self.logger.error(f"Failed to setup memray profiling: {e}", exc_info=True) + self.logger.exception(f"Failed to setup memray profiling: {e}") def _cleanup_memray(self, safe_name: str, binary_file: pathlib.Path) -> None: """Stop memray tracking and generate HTML report.""" @@ -156,4 +156,4 @@ class FrigateProcess(BaseProcess): except subprocess.TimeoutExpired: self.logger.error("Memray report generation timed out") except Exception as e: - self.logger.error(f"Failed to cleanup memray profiling: {e}", exc_info=True) + self.logger.exception(f"Failed to cleanup memray profiling: {e}") diff --git a/frigate/util/rknn_converter.py b/frigate/util/rknn_converter.py index 5660c7601..ad387413d 100644 --- a/frigate/util/rknn_converter.py +++ b/frigate/util/rknn_converter.py @@ -6,7 +6,6 @@ import subprocess import sys import time from pathlib import Path -from typing import Optional from frigate.const import SUPPORTED_RK_SOCS from frigate.util.file import FileLock @@ -139,7 +138,7 @@ def ensure_rknn_toolkit() -> bool: return False -def get_soc_type() -> Optional[str]: +def get_soc_type() -> str | None: """Get the SoC type from device tree.""" try: with open("/proc/device-tree/compatible") as file: @@ -160,7 +159,7 @@ def convert_onnx_to_rknn( output_path: str, model_type: str, quantization: bool = False, - soc: Optional[str] = None, + soc: str | None = None, ) -> bool: """ Convert ONNX model to RKNN format. @@ -345,7 +344,7 @@ def wait_for_conversion_completion( def auto_convert_model( model_path: str, model_type: str | None = None, quantization: bool = False -) -> Optional[str]: +) -> str | None: """ Automatically convert a model to RKNN format if needed. diff --git a/frigate/util/schema.py b/frigate/util/schema.py index 5ba1bc061..9af651ea0 100644 --- a/frigate/util/schema.py +++ b/frigate/util/schema.py @@ -1,11 +1,11 @@ """JSON schema utilities for Frigate.""" -from typing import Any, Dict, Type +from typing import Any from pydantic import BaseModel, TypeAdapter -def get_config_schema(config_class: Type[BaseModel]) -> Dict[str, Any]: +def get_config_schema(config_class: type[BaseModel]) -> dict[str, Any]: """ Returns the JSON schema for FrigateConfig with polymorphic detectors. diff --git a/frigate/util/services.py b/frigate/util/services.py index 6bc0bcbe0..956e7893c 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -12,7 +12,7 @@ import subprocess as sp import time import traceback from datetime import datetime -from typing import Any, List, Optional, Tuple +from typing import Any import cv2 import psutil @@ -59,7 +59,7 @@ def get_cgroups_version() -> str: return "unknown" try: - with open("/proc/mounts", "r") as f: + with open("/proc/mounts") as f: mounts = f.readlines() for mount in mounts: @@ -89,7 +89,7 @@ def get_docker_memlimit_bytes() -> int: memlimit_path = "/sys/fs/cgroup/memory.max" try: - with open(memlimit_path, "r") as f: + with open(memlimit_path) as f: value = f.read().strip() if value.isnumeric(): @@ -127,7 +127,7 @@ def get_cpu_stats() -> dict[str, dict]: if not any(keyword in cmdline for keyword in keywords): continue - with open(f"/proc/{pid}/stat", "r") as f: + with open(f"/proc/{pid}/stat") as f: stats = f.readline().split() utime = int(stats[13]) stime = int(stats[14]) @@ -146,7 +146,7 @@ def get_cpu_stats() -> dict[str, dict]: process_usage_sec = process_utime_sec + process_stime_sec cpu_average_usage = process_usage_sec * 100 // process_elapsed_sec - with open(f"/proc/{pid}/statm", "r") as f: + with open(f"/proc/{pid}/statm") as f: mem_stats = f.readline().split() mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024 @@ -171,7 +171,7 @@ def get_physical_interfaces(interfaces) -> list: if not interfaces: return [] - with open("/proc/net/dev", "r") as file: + with open("/proc/net/dev") as file: lines = file.readlines() physical_interfaces = [] @@ -238,7 +238,7 @@ def is_vaapi_amd_driver() -> bool: return any("AMD Radeon Graphics" in line for line in output) -def get_amd_gpu_stats() -> Optional[dict[str, str]]: +def get_amd_gpu_stats() -> dict[str, str] | None: """Get stats using radeontop.""" radeontop_command = ["radeontop", "-d", "-", "-l", "1"] @@ -287,7 +287,7 @@ _XE_ENGINE_KEYS = { } -def _resolve_intel_gpu_pdev(device: Optional[str]) -> Optional[str]: +def _resolve_intel_gpu_pdev(device: str | None) -> str | None: """Map a configured GPU hint (/dev/dri/card1, renderD128, or a PCI bus address) to its drm-pdev string so we can filter fdinfo entries to that device. Returns None when no hint is supplied or it cannot be resolved.""" @@ -304,7 +304,7 @@ def _resolve_intel_gpu_pdev(device: Optional[str]) -> Optional[str]: return None -def _read_intel_drm_fdinfo(target_pdev: Optional[str]) -> dict: +def _read_intel_drm_fdinfo(target_pdev: str | None) -> dict: """Snapshot DRM fdinfo for every Intel client visible in /proc. Returns a dict keyed by (pdev, drm-client-id, pid) so the same context @@ -394,8 +394,8 @@ def _read_intel_drm_fdinfo(target_pdev: Optional[str]) -> dict: def get_intel_gpu_stats( - intel_gpu_device: Optional[str], -) -> Optional[dict[str, dict[str, Any]]]: + intel_gpu_device: str | None, +) -> dict[str, dict[str, Any]] | None: """Get stats by reading DRM fdinfo files, bucketed per-pdev. Each DRM client FD exposes monotonic per-engine busy counters via @@ -509,12 +509,12 @@ def get_intel_gpu_stats( return results -def get_openvino_npu_stats() -> Optional[dict[str, str]]: +def get_openvino_npu_stats() -> dict[str, str] | None: """Get NPU stats using openvino.""" NPU_RUNTIME_PATH = "/sys/devices/pci0000:00/0000:00:0b.0/power/runtime_active_time" try: - with open(NPU_RUNTIME_PATH, "r") as f: + with open(NPU_RUNTIME_PATH) as f: initial_runtime = float(f.read().strip()) initial_time = time.time() @@ -523,7 +523,7 @@ def get_openvino_npu_stats() -> Optional[dict[str, str]]: time.sleep(1.0) # Read runtime value again - with open(NPU_RUNTIME_PATH, "r") as f: + with open(NPU_RUNTIME_PATH) as f: current_runtime = float(f.read().strip()) current_time = time.time() @@ -542,10 +542,10 @@ def get_openvino_npu_stats() -> Optional[dict[str, str]]: return None -def get_rockchip_gpu_stats() -> Optional[dict[str, str | float]]: +def get_rockchip_gpu_stats() -> dict[str, str | float] | None: """Get GPU stats using rk.""" try: - with open("/sys/kernel/debug/rkrga/load", "r") as f: + with open("/sys/kernel/debug/rkrga/load") as f: content = f.read() except FileNotFoundError: return None @@ -563,7 +563,7 @@ def get_rockchip_gpu_stats() -> Optional[dict[str, str | float]]: stats: dict[str, str | float] = {"gpu": average_load, "mem": "-%"} try: - with open("/sys/class/thermal/thermal_zone5/temp", "r") as f: + with open("/sys/class/thermal/thermal_zone5/temp") as f: line = f.readline().strip() stats["temp"] = round(int(line) / 1000, 1) except (FileNotFoundError, OSError, ValueError): @@ -572,10 +572,10 @@ def get_rockchip_gpu_stats() -> Optional[dict[str, str | float]]: return stats -def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]: +def get_rockchip_npu_stats() -> dict[str, float | str] | None: """Get NPU stats using rk.""" try: - with open("/sys/kernel/debug/rknpu/load", "r") as f: + with open("/sys/kernel/debug/rknpu/load") as f: npu_output = f.read() if "Core0:" in npu_output: @@ -595,7 +595,7 @@ def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]: stats: dict[str, float | str] = {"npu": mean, "mem": "-%"} try: - with open("/sys/class/thermal/thermal_zone6/temp", "r") as f: + with open("/sys/class/thermal/thermal_zone6/temp") as f: line = f.readline().strip() stats["temp"] = round(int(line) / 1000, 1) except (FileNotFoundError, OSError, ValueError): @@ -604,7 +604,7 @@ def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]: return stats -def get_axcl_npu_stats() -> Optional[dict[str, str | float]]: +def get_axcl_npu_stats() -> dict[str, str | float] | None: """Get NPU stats using axcl.""" # Check if axcl-smi exists axcl_smi_path = "/usr/bin/axcl/axcl-smi" @@ -721,18 +721,18 @@ def get_nvidia_gpu_stats() -> dict[int, dict]: return results -def get_jetson_stats() -> Optional[dict[int, dict]]: +def get_jetson_stats() -> dict[int, dict] | None: results = {} try: results["mem"] = "-" # no discrete gpu memory if os.path.exists("/sys/devices/gpu.0/load"): - with open("/sys/devices/gpu.0/load", "r") as f: + with open("/sys/devices/gpu.0/load") as f: gpuload = float(f.readline()) / 10 results["gpu"] = f"{gpuload}%" elif os.path.exists("/sys/devices/platform/gpu.0/load"): - with open("/sys/devices/platform/gpu.0/load", "r") as f: + with open("/sys/devices/platform/gpu.0/load") as f: gpuload = float(f.readline()) / 10 results["gpu"] = f"{gpuload}%" else: @@ -793,7 +793,7 @@ def get_hailo_temps() -> dict[str, float]: def is_go2rtc_arbitrary_exec_allowed() -> bool: """Read the GO2RTC_ALLOW_ARBITRARY_EXEC override from env, docker secrets, or the Home Assistant add-on options file.""" - raw: Optional[str] = None + raw: str | None = None if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ: raw = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC") elif ( @@ -839,7 +839,7 @@ def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedPro else: format_entries = None - def run(rtsp_transport: Optional[str] = None) -> sp.CompletedProcess: + def run(rtsp_transport: str | None = None) -> sp.CompletedProcess: cmd = [ffmpeg.ffprobe_path] if rtsp_transport: cmd += ["-rtsp_transport", rtsp_transport] @@ -883,14 +883,14 @@ KEYFRAME_PROBE_WINDOW_SECONDS = 20 KEYFRAME_GAP_WARNING_SECONDS = 4.0 -def parse_keyframe_packets(output: str) -> Tuple[List[float], Optional[float]]: +def parse_keyframe_packets(output: str) -> tuple[list[float], float | None]: """Parse ffprobe CSV `pts_time,flags` output. Returns the presentation timestamps of keyframes (flags containing "K") and the maximum timestamp observed across all packets. """ - keyframe_pts: List[float] = [] - max_pts: Optional[float] = None + keyframe_pts: list[float] = [] + max_pts: float | None = None for line in output.splitlines(): parts = line.split(",") @@ -909,7 +909,7 @@ def parse_keyframe_packets(output: str) -> Tuple[List[float], Optional[float]]: def classify_keyframe_gaps( - keyframe_pts: List[float], segment_time: int + keyframe_pts: list[float], segment_time: int ) -> dict[str, Any]: """Classify keyframe spacing for recording suitability. @@ -990,7 +990,7 @@ async def analyze_record_keyframes( stderr=asyncio.subprocess.PIPE, ) stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=window + 15) - except asyncio.TimeoutError: + except TimeoutError: logger.warning("Keyframe probe timed out for record stream") proc.kill() return classify_keyframe_gaps([], segment_time) @@ -1004,7 +1004,7 @@ async def analyze_record_keyframes( return result -def vainfo_hwaccel(device_name: Optional[str] = None) -> sp.CompletedProcess: +def vainfo_hwaccel(device_name: str | None = None) -> sp.CompletedProcess: """Run vainfo.""" if not device_name: cmd = ["vainfo"] @@ -1081,8 +1081,8 @@ async def get_video_properties( ) -> dict[str, Any]: async def probe_with_ffprobe( url: str, - rtsp_transport: Optional[str] = None, - ) -> tuple[bool, int, int, Optional[str], float]: + rtsp_transport: str | None = None, + ) -> tuple[bool, int, int, str | None, float]: """Fallback using ffprobe: returns (valid, width, height, codec, duration).""" cmd = [ffmpeg.ffprobe_path] if rtsp_transport: @@ -1105,7 +1105,7 @@ async def get_video_properties( ) try: stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=6) - except asyncio.TimeoutError: + except TimeoutError: logger.info( "ffprobe timed out while probing %s (transport=%s)", clean_camera_user_pass(url), @@ -1137,7 +1137,7 @@ async def get_video_properties( except (json.JSONDecodeError, ValueError, KeyError, sp.SubprocessError): return False, 0, 0, None, -1 - def probe_with_cv2(url: str) -> tuple[bool, int, int, Optional[str], float]: + def probe_with_cv2(url: str) -> tuple[bool, int, int, str | None, float]: """Primary attempt using cv2: returns (valid, width, height, fourcc, duration).""" cap = cv2.VideoCapture(url) if not cap.isOpened(): @@ -1197,10 +1197,10 @@ async def get_video_properties( def process_logs( contents: str, - service: Optional[str] = None, - start: Optional[int] = None, - end: Optional[int] = None, -) -> Tuple[int, List[str]]: + service: str | None = None, + start: int | None = None, + end: int | None = None, +) -> tuple[int, list[str]]: log_lines = [] last_message = None last_timestamp = None diff --git a/frigate/util/time.py b/frigate/util/time.py index 1e7b49c24..861bec17f 100644 --- a/frigate/util/time.py +++ b/frigate/util/time.py @@ -2,7 +2,6 @@ import datetime import logging -from typing import Tuple from zoneinfo import ZoneInfoNotFoundError import pytz @@ -11,7 +10,7 @@ from tzlocal import get_localzone logger = logging.getLogger(__name__) -def get_tz_modifiers(tz_name: str) -> Tuple[str, str, float]: +def get_tz_modifiers(tz_name: str) -> tuple[str, str, float]: seconds_offset = ( datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds() ) @@ -27,24 +26,18 @@ def get_tomorrow_at_time(hour: int) -> datetime.datetime: try: tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1) except ZoneInfoNotFoundError: - tomorrow = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta( - days=1 - ) + tomorrow = datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=1) logger.warning( "Using utc for maintenance due to missing or incorrect timezone set" ) - return tomorrow.replace(hour=hour, minute=0, second=0).astimezone( - datetime.timezone.utc - ) + return tomorrow.replace(hour=hour, minute=0, second=0).astimezone(datetime.UTC) def is_current_hour(timestamp: int) -> bool: """Returns if timestamp is in the current UTC hour.""" start_of_next_hour = ( - datetime.datetime.now(datetime.timezone.utc).replace( - minute=0, second=0, microsecond=0 - ) + datetime.datetime.now(datetime.UTC).replace(minute=0, second=0, microsecond=0) + datetime.timedelta(hours=1) ).timestamp() return timestamp < start_of_next_hour diff --git a/frigate/video/detect.py b/frigate/video/detect.py index 89124a75d..8c5a31e8d 100644 --- a/frigate/video/detect.py +++ b/frigate/video/detect.py @@ -3,7 +3,7 @@ import logging import queue import time -from datetime import datetime, timezone +from datetime import UTC, datetime from multiprocessing import Queue from multiprocessing.synchronize import Event as MpEvent from typing import Any @@ -273,7 +273,7 @@ def process_frames( time.sleep(0.1) continue - if datetime.now().astimezone(timezone.utc) > next_region_update: + if datetime.now().astimezone(UTC) > next_region_update: region_grid = requestor.send_data(REQUEST_REGION_GRID, camera_config.name) next_region_update = get_tomorrow_at_time(2) diff --git a/frigate/video/ffmpeg.py b/frigate/video/ffmpeg.py index 40b18b7da..24b780533 100644 --- a/frigate/video/ffmpeg.py +++ b/frigate/video/ffmpeg.py @@ -6,7 +6,7 @@ import subprocess as sp import threading import time from collections import deque -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from multiprocessing import Queue, Value from multiprocessing.synchronize import Event as MpEvent from typing import Any @@ -260,7 +260,7 @@ class CameraWatchdog(threading.Thread): self.start_all_ffmpeg() # If recording is enabled at startup, set the grace period timer if self.config.record.enabled: - self.record_enable_time = datetime.now().astimezone(timezone.utc) + self.record_enable_time = datetime.now().astimezone(UTC) time.sleep(self.sleeptime) last_restart_time = datetime.now().timestamp() @@ -280,7 +280,7 @@ class CameraWatchdog(threading.Thread): self.latest_valid_segment_time = 0 self.latest_invalid_segment_time = 0 self.latest_cache_segment_time = 0 - self.record_enable_time = datetime.now().astimezone(timezone.utc) + self.record_enable_time = datetime.now().astimezone(UTC) last_restart_time = datetime.now().timestamp() continue @@ -294,7 +294,7 @@ class CameraWatchdog(threading.Thread): self.latest_valid_segment_time = 0 self.latest_invalid_segment_time = 0 self.latest_cache_segment_time = 0 - self.record_enable_time = datetime.now().astimezone(timezone.utc) + self.record_enable_time = datetime.now().astimezone(UTC) else: self.logger.debug(f"Disabling camera {self.config.name}") self.stop_all_ffmpeg() @@ -318,7 +318,7 @@ class CameraWatchdog(threading.Thread): self.latest_valid_segment_time = 0 self.latest_invalid_segment_time = 0 self.latest_cache_segment_time = 0 - self.record_enable_time = datetime.now().astimezone(timezone.utc) + self.record_enable_time = datetime.now().astimezone(UTC) last_restart_time = datetime.now().timestamp() self.was_record_enabled_in_config = record_enabled_in_config continue @@ -402,7 +402,7 @@ class CameraWatchdog(threading.Thread): poll = p["process"].poll() if self.config.record.enabled and "record" in p["roles"]: - now_utc = datetime.now().astimezone(timezone.utc) + now_utc = datetime.now().astimezone(UTC) # Check if we're within the grace period after enabling recording # Grace period: 90 seconds allows time for ffmpeg to start and create first segment @@ -411,25 +411,19 @@ class CameraWatchdog(threading.Thread): ) < timedelta(seconds=90) latest_cache_dt = ( - datetime.fromtimestamp( - self.latest_cache_segment_time, tz=timezone.utc - ) + datetime.fromtimestamp(self.latest_cache_segment_time, tz=UTC) if self.latest_cache_segment_time > 0 else now_utc - timedelta(seconds=1) ) latest_valid_dt = ( - datetime.fromtimestamp( - self.latest_valid_segment_time, tz=timezone.utc - ) + datetime.fromtimestamp(self.latest_valid_segment_time, tz=UTC) if self.latest_valid_segment_time > 0 else now_utc - timedelta(seconds=1) ) latest_invalid_dt = ( - datetime.fromtimestamp( - self.latest_invalid_segment_time, tz=timezone.utc - ) + datetime.fromtimestamp(self.latest_invalid_segment_time, tz=UTC) if self.latest_invalid_segment_time > 0 else now_utc - timedelta(seconds=1) ) diff --git a/frigate/watchdog.py b/frigate/watchdog.py index 7ae42d988..88023d512 100644 --- a/frigate/watchdog.py +++ b/frigate/watchdog.py @@ -3,9 +3,9 @@ import logging import threading import time from collections import deque +from collections.abc import Callable from dataclasses import dataclass, field from multiprocessing.synchronize import Event as MpEvent -from typing import Callable from frigate.object_detection.base import ObjectDetectProcess from frigate.util.process import FrigateProcess diff --git a/generate_config_translations.py b/generate_config_translations.py index 7f9c9bc50..aa115e44c 100644 --- a/generate_config_translations.py +++ b/generate_config_translations.py @@ -10,7 +10,7 @@ import json import logging import sys from pathlib import Path -from typing import Any, Dict, get_args, get_origin +from typing import Any, get_args, get_origin from frigate.config.config import FrigateConfig from frigate.util.schema import get_config_schema @@ -19,7 +19,7 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -def get_field_translations(field_info) -> Dict[str, str]: +def get_field_translations(field_info) -> dict[str, str]: """Extract title and description from a Pydantic field.""" translations = {} @@ -33,8 +33,8 @@ def get_field_translations(field_info) -> Dict[str, str]: def extract_translations_from_schema( - schema: Dict[str, Any], defs: Dict[str, Any] = None -) -> Dict[str, Any]: + schema: dict[str, Any], defs: dict[str, Any] = None +) -> dict[str, Any]: """ Recursively extract translations (titles and descriptions) from a JSON schema. @@ -202,7 +202,7 @@ def extract_translations_from_schema( return translations -def generate_section_translation(config_class: type) -> Dict[str, Any]: +def generate_section_translation(config_class: type) -> dict[str, Any]: """ Generate translation structure for a config section using its JSON schema. """ @@ -211,8 +211,8 @@ def generate_section_translation(config_class: type) -> Dict[str, Any]: def get_detector_translations( - config_schema: Dict[str, Any], -) -> tuple[Dict[str, Any], Dict[str, Any], set[str]]: + config_schema: dict[str, Any], +) -> tuple[dict[str, Any], dict[str, Any], set[str]]: """Build detector type translations with nested fields based on schema definitions. Returns a tuple of (type_translations, shared_fields, nested_field_keys). @@ -225,8 +225,8 @@ def get_detector_translations( mapping = discriminator.get("mapping", {}) # First pass: collect all nested fields per detector type - all_nested: Dict[str, Dict[str, Any]] = {} - type_meta: Dict[str, Dict[str, str]] = {} + all_nested: dict[str, dict[str, Any]] = {} + type_meta: dict[str, dict[str, str]] = {} for detector_type, ref in mapping.items(): if not isinstance(ref, str) or not ref.startswith("#/$defs/"): @@ -237,7 +237,7 @@ def get_detector_translations( if not ref_schema: continue - meta: Dict[str, str] = {} + meta: dict[str, str] = {} title = ref_schema.get("title") description = ref_schema.get("description") if title: @@ -252,7 +252,7 @@ def get_detector_translations( } # Find fields that are identical across all types that have them - shared_fields: Dict[str, Any] = {} + shared_fields: dict[str, Any] = {} if all_nested: # Collect all field keys across all types all_keys: set[str] = set() @@ -265,10 +265,10 @@ def get_detector_translations( shared_fields[key] = values[0] # Build per-type translations with only unique (non-shared) fields - type_translations: Dict[str, Any] = {} + type_translations: dict[str, Any] = {} nested_field_keys: set[str] = set() for detector_type, nested in all_nested.items(): - type_entry: Dict[str, Any] = {} + type_entry: dict[str, Any] = {} type_entry.update(type_meta.get(detector_type, {})) unique_fields = {k: v for k, v in nested.items() if k not in shared_fields} @@ -387,7 +387,7 @@ def main(): # doesn't capture the attribute-specific semantics. Keys # match the FilterConfig field name; values are partial # overrides applied AFTER the generic rewrites. - attribute_field_overrides: Dict[str, Dict[str, str]] = { + attribute_field_overrides: dict[str, dict[str, str]] = { "min_score": { "description": ( "Minimum single-frame detection confidence required " @@ -401,7 +401,7 @@ def main(): text = text.replace(source, replacement) return text - attribute_variant: Dict[str, Any] = {} + attribute_variant: dict[str, Any] = {} for key, value in filters_block.items(): if key in ("label", "description"): if isinstance(value, str): @@ -409,7 +409,7 @@ def main(): continue if not isinstance(value, dict): continue - field_trans: Dict[str, str] = {} + field_trans: dict[str, str] = {} if isinstance(value.get("label"), str): field_trans["label"] = rewrite(value["label"]) if isinstance(value.get("description"), str): diff --git a/pyproject.toml b/pyproject.toml index d17a60e72..775db6026 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +[tool.ruff] +target-version = "py311" + [tool.ruff.lint] -ignore = ["E501","E711","E712"] -extend-select = ["I"] +ignore = ["E501","E711","E712","UP031","UP032","UP042","G004"] +extend-select = ["I", "UP", "G", "ASYNC210", "B904"]