mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-12 01:32:48 +03:00
* bump backend and frontend dependencies
Bumps every outdated dependency that passed testing, with the code changes each one needs.
- peewee 4.5 and peewee_migrate 2.3: use SqliteDatabase in place of the removed SqliteExtDatabase, and fix types for peewee's bundled stubs
- mypy 2.4.0: remove unused type ignores and add the annotations mypy 2 needs
- pandas 3.0: convert the motion activity index with as_unit("s"), because pandas 3 infers second resolution for whole-second timestamps and the old // 10**9 returned 1
- transformers 5.19: load Jina tokenizers from the saved directory with trust_remote_code off and use CLIPImageProcessor, since v5 removed CLIPFeatureExtractor and remote code pulled in torch
- tensorflow-cpu 2.21: pin protobuf to 7.36 in the TensorRT amd64 image, because the old 3.20.3 pin was installed over TensorFlow and broke its import
- pydantic 2.13 and google-genai 2.29: regenerate the API spec
- ruff 0.16: set an explicit lint select, since 0.16 widened the default rules
- react-router-dom 7: set useTransitions={false} on BrowserRouter to keep v6 update timing, which useSearchEffect relies on
- typescript 6: drop baseUrl and add node types in tsconfig
- radix: update to the latest release and remove the slot patch, which upstream now includes
- apexcharts 7.8: guard the tooltip formatter against null values
- cryptography 50, joserfc 1.7, pywebpush 2.5, openai 3.26, aiohttp, uvicorn, onvif-zeep-async, scipy, framer-motion 14, react-day-picker 10 and other minor and patch bumps
* silence httpx2 request logging
* redownload an incomplete jina tokenizer
An interrupted tokenizer download left the Hugging Face cache directory without the saved tokenizer files, and since the download is skipped whenever that directory exists, every restart failed to load the tokenizer. The Jina constructors now remove a tokenizer directory that has no tokenizer_config.json so the download runs again.
280 lines
8.9 KiB
Python
280 lines
8.9 KiB
Python
import datetime
|
|
import logging
|
|
import os
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from fastapi import Request
|
|
from fastapi.testclient import TestClient
|
|
from peewee import SqliteDatabase
|
|
from peewee_migrate import Router
|
|
from playhouse.sqliteq import SqliteQueueDatabase
|
|
from pydantic import Json
|
|
|
|
from frigate.api.fastapi_app import create_fastapi_app
|
|
from frigate.config import FrigateConfig
|
|
from frigate.const import BASE_DIR, CACHE_DIR
|
|
from frigate.debug_replay import DebugReplayManager
|
|
from frigate.jobs.export import JobStatePublisher
|
|
from frigate.models import Event, Recordings, ReviewSegment
|
|
from frigate.review.types import SeverityEnum
|
|
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
|
|
|
|
|
|
class AuthTestClient(TestClient):
|
|
"""TestClient that automatically adds auth headers to all requests."""
|
|
|
|
def request(self, *args, **kwargs):
|
|
# Add default auth headers if not already present
|
|
headers = kwargs.get("headers") or {}
|
|
if "remote-user" not in headers:
|
|
headers["remote-user"] = "admin"
|
|
if "remote-role" not in headers:
|
|
headers["remote-role"] = "admin"
|
|
kwargs["headers"] = headers
|
|
return super().request(*args, **kwargs)
|
|
|
|
|
|
class BaseTestHttp(unittest.TestCase):
|
|
def setUp(self, models):
|
|
# setup clean database for each test run
|
|
migrate_db = SqliteDatabase("test.db")
|
|
del logging.getLogger("peewee_migrate").handlers[:]
|
|
router = Router(migrate_db)
|
|
router.run()
|
|
migrate_db.close()
|
|
self.db = SqliteQueueDatabase(TEST_DB)
|
|
self.db.bind(models)
|
|
|
|
# The export job manager broadcasts via JobStatePublisher on
|
|
# enqueue/start/finish. There is no dispatcher process bound to
|
|
# the IPC socket in tests, so a real publish() would block on
|
|
# recv_json forever. Replace publish with a no-op for the
|
|
# lifetime of this test; the lookup goes through the class so any
|
|
# already-instantiated publisher (the singleton manager's) picks
|
|
# up the no-op too.
|
|
publisher_patch = patch.object(
|
|
JobStatePublisher, "publish", lambda self, payload: None
|
|
)
|
|
publisher_patch.start()
|
|
self.addCleanup(publisher_patch.stop)
|
|
|
|
self.minimal_config = {
|
|
"mqtt": {"host": "mqtt"},
|
|
"cameras": {
|
|
"front_door": {
|
|
"ffmpeg": {
|
|
"inputs": [
|
|
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
|
]
|
|
},
|
|
"detect": {
|
|
"height": 1080,
|
|
"width": 1920,
|
|
"fps": 5,
|
|
},
|
|
}
|
|
},
|
|
}
|
|
self.test_stats = {
|
|
"camera_fps": 5.0,
|
|
"process_fps": 5.0,
|
|
"skipped_fps": 0.0,
|
|
"detection_fps": 13.7,
|
|
"detectors": {
|
|
"cpu1": {
|
|
"detection_start": 0.0,
|
|
"inference_speed": 91.43,
|
|
"pid": 42,
|
|
},
|
|
"cpu2": {
|
|
"detection_start": 0.0,
|
|
"inference_speed": 84.99,
|
|
"pid": 44,
|
|
},
|
|
},
|
|
"front_door": {
|
|
"camera_fps": 0.0,
|
|
"capture_pid": 53,
|
|
"detection_fps": 0.0,
|
|
"pid": 52,
|
|
"process_fps": 0.0,
|
|
"skipped_fps": 0.0,
|
|
},
|
|
"service": {
|
|
"storage": {
|
|
"/dev/shm": {
|
|
"free": 50.5,
|
|
"mount_type": "tmpfs",
|
|
"total": 67.1,
|
|
"used": 16.6,
|
|
},
|
|
os.path.join(BASE_DIR, "clips"): {
|
|
"free": 42429.9,
|
|
"mount_type": "ext4",
|
|
"total": 244529.7,
|
|
"used": 189607.0,
|
|
},
|
|
os.path.join(BASE_DIR, "recordings"): {
|
|
"free": 0.2,
|
|
"mount_type": "ext4",
|
|
"total": 8.0,
|
|
"used": 7.8,
|
|
},
|
|
CACHE_DIR: {
|
|
"free": 976.8,
|
|
"mount_type": "tmpfs",
|
|
"total": 1000.0,
|
|
"used": 23.2,
|
|
},
|
|
},
|
|
"uptime": 101113,
|
|
"version": "0.10.1",
|
|
"latest_version": "0.11",
|
|
},
|
|
}
|
|
|
|
def tearDown(self):
|
|
if not self.db.is_closed():
|
|
self.db.close()
|
|
|
|
try:
|
|
for file in TEST_DB_CLEANUPS:
|
|
os.remove(file)
|
|
except OSError:
|
|
pass
|
|
|
|
def create_app(
|
|
self,
|
|
stats=None,
|
|
event_metadata_publisher=None,
|
|
notice_registry=None,
|
|
enforce_default_admin=False,
|
|
):
|
|
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
|
|
|
|
app = create_fastapi_app(
|
|
FrigateConfig(**self.minimal_config),
|
|
self.db,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
stats,
|
|
event_metadata_publisher,
|
|
None,
|
|
DebugReplayManager(),
|
|
enforce_default_admin=enforce_default_admin,
|
|
notice_registry=notice_registry,
|
|
)
|
|
|
|
# Default test mocks for authentication
|
|
# Tests can override these in their setUp if needed
|
|
# This mock uses headers set by AuthTestClient
|
|
async def mock_get_current_user(request: Request):
|
|
username = request.headers.get("remote-user")
|
|
role = request.headers.get("remote-role")
|
|
if not username or not role:
|
|
from fastapi.responses import JSONResponse
|
|
|
|
return JSONResponse(
|
|
content={"message": "No authorization headers."}, status_code=401
|
|
)
|
|
return {"username": username, "role": role}
|
|
|
|
async def mock_get_allowed_cameras_for_filter(request: Request):
|
|
return list(self.minimal_config.get("cameras", {}).keys())
|
|
|
|
app.dependency_overrides[get_current_user] = mock_get_current_user
|
|
app.dependency_overrides[get_allowed_cameras_for_filter] = (
|
|
mock_get_allowed_cameras_for_filter
|
|
)
|
|
|
|
return app
|
|
|
|
def insert_mock_event(
|
|
self,
|
|
id: str,
|
|
start_time: float | None = None,
|
|
end_time: float | None = None,
|
|
has_clip: bool = True,
|
|
top_score: int = 100,
|
|
score: int = 0,
|
|
data: Json = {},
|
|
camera: str = "front_door",
|
|
) -> Event:
|
|
"""Inserts a basic event model with a given id."""
|
|
if start_time is None:
|
|
start_time = datetime.datetime.now().timestamp()
|
|
if end_time is None:
|
|
end_time = start_time + 20
|
|
|
|
return Event.insert(
|
|
id=id,
|
|
label="Mock",
|
|
camera=camera,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
top_score=top_score,
|
|
score=score,
|
|
false_positive=False,
|
|
zones=list(),
|
|
thumbnail="",
|
|
region=[],
|
|
box=[],
|
|
area=0,
|
|
has_clip=has_clip,
|
|
has_snapshot=True,
|
|
data=data,
|
|
).execute()
|
|
|
|
def insert_mock_review_segment(
|
|
self,
|
|
id: str,
|
|
start_time: float | None = None,
|
|
end_time: float | None = None,
|
|
severity: SeverityEnum = SeverityEnum.alert,
|
|
data: dict | None = None,
|
|
camera: str = "front_door",
|
|
) -> ReviewSegment:
|
|
"""Inserts a review segment model with a given id."""
|
|
if start_time is None:
|
|
start_time = datetime.datetime.now().timestamp()
|
|
if end_time is None:
|
|
end_time = start_time + 20
|
|
if data is None:
|
|
data = {}
|
|
|
|
return ReviewSegment.insert(
|
|
id=id,
|
|
camera=camera,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
severity=severity,
|
|
thumb_path=False,
|
|
data=data,
|
|
).execute()
|
|
|
|
def insert_mock_recording(
|
|
self,
|
|
id: str,
|
|
start_time: float | None = None,
|
|
end_time: float | None = None,
|
|
motion: int = 0,
|
|
) -> Event:
|
|
"""Inserts a recording model with a given id."""
|
|
if start_time is None:
|
|
start_time = datetime.datetime.now().timestamp()
|
|
if end_time is None:
|
|
end_time = start_time + 20
|
|
|
|
return Recordings.insert(
|
|
id=id,
|
|
path=id,
|
|
camera="front_door",
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
duration=end_time - start_time,
|
|
motion=motion,
|
|
).execute()
|