mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-11 13:21:10 +03:00
Increase ruff coverage (#23644)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / Jetson Jetpack 6 (push) Waiting to run
* Pin ruff * Add python upgrade fixes This enables python upgrade checks in ruff to look for deprecated types and patterns. This namely fixes: - usage of deprecated `Typing` which is now built in - some specific exceptions which are caught and have new aliases Some specific UP checks were also ignored as they are stylistic / unimportant and likely to cause bugs * Remove async blocking calls Use asyncio.to_thread on two remaining blocking calls to fix hanging event thread loop. Enable this specific rule to block it in the future. * Use proper logging mechanism * Correctly format logs * Raise with context When raising an exception include the from context to improve debugging * Cleanup
This commit is contained in:
+34
-34
@@ -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(
|
||||
|
||||
+6
-7
@@ -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."""
|
||||
|
||||
@@ -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,
|
||||
|
||||
+10
-11
@@ -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,
|
||||
|
||||
+11
-10
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'",
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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. "
|
||||
|
||||
@@ -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)"
|
||||
)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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=())
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
+68
-73
@@ -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(
|
||||
|
||||
+24
-24
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
+7
-14
@@ -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)
|
||||
|
||||
+15
-16
@@ -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`
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
+10
-11
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user