mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-14 23:25:25 +03:00
Removed flask app in favour of FastAPI app. Implemented FastAPI middleware to check CSRF, connect and disconnect from DB. Added middleware x-forwared-for headers
This commit is contained in:
parent
655d24a653
commit
8f8d8e1e4c
@ -1,6 +1,7 @@
|
|||||||
click == 8.1.*
|
click == 8.1.*
|
||||||
Flask == 3.0.*
|
# FastAPI
|
||||||
Flask_Limiter == 3.8.*
|
starlette-context == 0.3.6
|
||||||
|
fastapi == 0.115.0
|
||||||
imutils == 0.5.*
|
imutils == 0.5.*
|
||||||
joserfc == 1.0.*
|
joserfc == 1.0.*
|
||||||
markupsafe == 2.1.*
|
markupsafe == 2.1.*
|
||||||
|
|||||||
@ -26,6 +26,8 @@ In the event that you are locked out of your instance, you can tell Frigate to r
|
|||||||
|
|
||||||
## Login failure rate limiting
|
## Login failure rate limiting
|
||||||
|
|
||||||
|
# TODO: Rui Update to use FastAPI
|
||||||
|
|
||||||
In order to limit the risk of brute force attacks, rate limiting is available for login failures. This is implemented with Flask-Limiter, and the string notation for valid values is available in [the documentation](https://flask-limiter.readthedocs.io/en/stable/configuration.html#rate-limit-string-notation).
|
In order to limit the risk of brute force attacks, rate limiting is available for login failures. This is implemented with Flask-Limiter, and the string notation for valid values is available in [the documentation](https://flask-limiter.readthedocs.io/en/stable/configuration.html#rate-limit-string-notation).
|
||||||
|
|
||||||
For example, `1/second;5/minute;20/hour` will rate limit the login endpoint when failures occur more than:
|
For example, `1/second;5/minute;20/hour` will rate limit the login endpoint when failures occur more than:
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
import faulthandler
|
import faulthandler
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from flask import cli
|
|
||||||
|
|
||||||
from frigate.app import FrigateApp
|
from frigate.app import FrigateApp
|
||||||
|
|
||||||
faulthandler.enable()
|
faulthandler.enable()
|
||||||
|
|
||||||
threading.current_thread().name = "frigate"
|
threading.current_thread().name = "frigate"
|
||||||
|
|
||||||
cli.show_server_banner = lambda *x: None
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
frigate_app = FrigateApp()
|
frigate_app = FrigateApp()
|
||||||
|
|
||||||
|
|||||||
@ -14,25 +14,15 @@ from fastapi import APIRouter, Path, Request, Response
|
|||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from flask import Flask, jsonify, request
|
|
||||||
from markupsafe import escape
|
from markupsafe import escape
|
||||||
from peewee import operator
|
from peewee import operator
|
||||||
from playhouse.sqliteq import SqliteQueueDatabase
|
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
||||||
|
|
||||||
from frigate.api.auth import get_jwt_secret, limiter
|
|
||||||
from frigate.api.defs.app_body import AppConfigSetBody
|
from frigate.api.defs.app_body import AppConfigSetBody
|
||||||
from frigate.api.defs.app_query_parameters import AppTimelineHourlyQueryParameters
|
from frigate.api.defs.app_query_parameters import AppTimelineHourlyQueryParameters
|
||||||
from frigate.api.defs.tags import Tags
|
from frigate.api.defs.tags import Tags
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.const import CONFIG_DIR
|
from frigate.const import CONFIG_DIR
|
||||||
from frigate.embeddings import EmbeddingsContext
|
|
||||||
from frigate.events.external import ExternalEventProcessor
|
|
||||||
from frigate.models import Event, Timeline
|
from frigate.models import Event, Timeline
|
||||||
from frigate.plus import PlusApi
|
|
||||||
from frigate.ptz.onvif import OnvifController
|
|
||||||
from frigate.stats.emitter import StatsEmitter
|
|
||||||
from frigate.storage import StorageMaintainer
|
|
||||||
from frigate.util.builtin import (
|
from frigate.util.builtin import (
|
||||||
clean_camera_user_pass,
|
clean_camera_user_pass,
|
||||||
get_tz_modifiers,
|
get_tz_modifiers,
|
||||||
@ -47,56 +37,6 @@ logger = logging.getLogger(__name__)
|
|||||||
router = APIRouter(tags=[Tags.app])
|
router = APIRouter(tags=[Tags.app])
|
||||||
|
|
||||||
|
|
||||||
def create_app(
|
|
||||||
frigate_config,
|
|
||||||
database: SqliteQueueDatabase,
|
|
||||||
embeddings: Optional[EmbeddingsContext],
|
|
||||||
detected_frames_processor,
|
|
||||||
storage_maintainer: StorageMaintainer,
|
|
||||||
onvif: OnvifController,
|
|
||||||
external_processor: ExternalEventProcessor,
|
|
||||||
plus_api: PlusApi,
|
|
||||||
stats_emitter: StatsEmitter,
|
|
||||||
):
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def check_csrf():
|
|
||||||
if request.method in ["GET", "HEAD", "OPTIONS", "TRACE"]:
|
|
||||||
pass
|
|
||||||
if "origin" in request.headers and "x-csrf-token" not in request.headers:
|
|
||||||
return jsonify({"success": False, "message": "Missing CSRF header"}), 401
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def _db_connect():
|
|
||||||
if database.is_closed():
|
|
||||||
database.connect()
|
|
||||||
|
|
||||||
@app.teardown_request
|
|
||||||
def _db_close(exc):
|
|
||||||
if not database.is_closed():
|
|
||||||
database.close()
|
|
||||||
|
|
||||||
app.frigate_config = frigate_config
|
|
||||||
app.embeddings = embeddings
|
|
||||||
app.detected_frames_processor = detected_frames_processor
|
|
||||||
app.storage_maintainer = storage_maintainer
|
|
||||||
app.onvif = onvif
|
|
||||||
app.external_processor = external_processor
|
|
||||||
app.plus_api = plus_api
|
|
||||||
app.camera_error_image = None
|
|
||||||
app.stats_emitter = stats_emitter
|
|
||||||
app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None
|
|
||||||
# update the request_address with the x-forwarded-for header from nginx
|
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
|
|
||||||
# initialize the rate limiter for the login endpoint
|
|
||||||
limiter.init_app(app)
|
|
||||||
if frigate_config.auth.failed_login_rate_limit is None:
|
|
||||||
limiter.enabled = False
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
def is_healthy():
|
def is_healthy():
|
||||||
return "Frigate is running. Alive and healthy!"
|
return "Frigate is running. Alive and healthy!"
|
||||||
@ -306,7 +246,7 @@ def config_save(save_option: str, body: dict):
|
|||||||
|
|
||||||
|
|
||||||
@router.put("/config/set")
|
@router.put("/config/set")
|
||||||
def config_set(body: AppConfigSetBody):
|
def config_set(request: Request, body: AppConfigSetBody):
|
||||||
config_file = os.environ.get("CONFIG_FILE", f"{CONFIG_DIR}/config.yml")
|
config_file = os.environ.get("CONFIG_FILE", f"{CONFIG_DIR}/config.yml")
|
||||||
|
|
||||||
# Check if we can use .yaml instead of .yml
|
# Check if we can use .yaml instead of .yml
|
||||||
|
|||||||
@ -182,20 +182,19 @@ def auth(request: Request):
|
|||||||
auth_config: AuthConfig = request.app.frigate_config.auth
|
auth_config: AuthConfig = request.app.frigate_config.auth
|
||||||
proxy_config: ProxyConfig = request.app.frigate_config.proxy
|
proxy_config: ProxyConfig = request.app.frigate_config.proxy
|
||||||
|
|
||||||
success_response = Response(content={}, status_code=202)
|
success_response = Response("", status_code=202)
|
||||||
|
|
||||||
# dont require auth if the request is on the internal port
|
# dont require auth if the request is on the internal port
|
||||||
# this header is set by Frigate's nginx proxy, so it cant be spoofed
|
# this header is set by Frigate's nginx proxy, so it cant be spoofed
|
||||||
if request.headers.get("x-server-port", 0, type=int) == 5000:
|
if int(request.headers.get("x-server-port", default=0)) == 5000:
|
||||||
return success_response
|
return success_response
|
||||||
|
|
||||||
fail_response = Response(content={}, status_code=401)
|
fail_response = Response("", status_code=401)
|
||||||
|
|
||||||
# ensure the proxy secret matches if configured
|
# ensure the proxy secret matches if configured
|
||||||
if (
|
if (
|
||||||
proxy_config.auth_secret is not None
|
proxy_config.auth_secret is not None
|
||||||
and request.headers.get("x-proxy-secret", "", type=str)
|
and request.headers.get("x-proxy-secret", "") != proxy_config.auth_secret
|
||||||
!= proxy_config.auth_secret
|
|
||||||
):
|
):
|
||||||
logger.debug("X-Proxy-Secret header does not match configured secret value")
|
logger.debug("X-Proxy-Secret header does not match configured secret value")
|
||||||
return fail_response
|
return fail_response
|
||||||
@ -207,7 +206,6 @@ def auth(request: Request):
|
|||||||
if proxy_config.header_map.user is not None:
|
if proxy_config.header_map.user is not None:
|
||||||
upstream_user_header_value = request.headers.get(
|
upstream_user_header_value = request.headers.get(
|
||||||
proxy_config.header_map.user,
|
proxy_config.header_map.user,
|
||||||
type=str,
|
|
||||||
default="anonymous",
|
default="anonymous",
|
||||||
)
|
)
|
||||||
success_response.headers["remote-user"] = upstream_user_header_value
|
success_response.headers["remote-user"] = upstream_user_header_value
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
|
from starlette_context import middleware, plugins
|
||||||
|
|
||||||
from frigate.api import app as main_app
|
from frigate.api import app as main_app
|
||||||
from frigate.api import auth, event, export, media, notification, preview, review
|
from frigate.api import auth, event, export, media, notification, preview, review
|
||||||
@ -16,21 +19,62 @@ from frigate.storage import StorageMaintainer
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def check_csrf(request: Request):
|
||||||
|
if request.method in ["GET", "HEAD", "OPTIONS", "TRACE"]:
|
||||||
|
pass
|
||||||
|
if "origin" in request.headers and "x-csrf-token" not in request.headers:
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "Missing CSRF header"},
|
||||||
|
status_code=401,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_fastapi_app(
|
def create_fastapi_app(
|
||||||
frigate_config,
|
frigate_config,
|
||||||
|
database: SqliteQueueDatabase,
|
||||||
embeddings: Optional[EmbeddingsContext],
|
embeddings: Optional[EmbeddingsContext],
|
||||||
detected_frames_processor,
|
detected_frames_processor,
|
||||||
storage_maintainer: StorageMaintainer,
|
storage_maintainer: StorageMaintainer,
|
||||||
onvif: OnvifController,
|
onvif: OnvifController,
|
||||||
|
external_processor: ExternalEventProcessor,
|
||||||
plus_api: PlusApi,
|
plus_api: PlusApi,
|
||||||
stats_emitter: StatsEmitter,
|
stats_emitter: StatsEmitter,
|
||||||
external_processor: ExternalEventProcessor,
|
|
||||||
):
|
):
|
||||||
logger.info("Starting FastAPI app")
|
logger.info("Starting FastAPI app")
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
debug=False,
|
debug=False,
|
||||||
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# update the request_address with the x-forwarded-for header from nginx
|
||||||
|
app.add_middleware(
|
||||||
|
middleware.ContextMiddleware,
|
||||||
|
plugins=(plugins.ForwardedForPlugin(),),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Middleware to connect to DB before and close connection after request
|
||||||
|
# https://github.com/fastapi/full-stack-fastapi-template/issues/224#issuecomment-737423886
|
||||||
|
# https://fastapi.tiangolo.com/tutorial/middleware/#before-and-after-the-response
|
||||||
|
@app.middleware("http")
|
||||||
|
async def frigate_middleware(request: Request, call_next):
|
||||||
|
# Before request
|
||||||
|
check_csrf(request)
|
||||||
|
if database.is_closed():
|
||||||
|
database.connect()
|
||||||
|
|
||||||
|
response = await call_next(request)
|
||||||
|
|
||||||
|
# After request https://stackoverflow.com/a/75487519
|
||||||
|
if not database.is_closed():
|
||||||
|
database.close()
|
||||||
|
return response
|
||||||
|
|
||||||
|
# TODO: Rui
|
||||||
|
# initialize the rate limiter for the login endpoint
|
||||||
|
# limiter.init_app(app)
|
||||||
|
# if frigate_config.auth.failed_login_rate_limit is None:
|
||||||
|
# limiter.enabled = False
|
||||||
|
|
||||||
# Routes
|
# Routes
|
||||||
app.include_router(main_app.router)
|
app.include_router(main_app.router)
|
||||||
app.include_router(media.router)
|
app.include_router(media.router)
|
||||||
|
|||||||
@ -15,13 +15,11 @@ from typing import Optional
|
|||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi.middleware.wsgi import WSGIMiddleware
|
|
||||||
from peewee_migrate import Router
|
from peewee_migrate import Router
|
||||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||||
from playhouse.sqliteq import SqliteQueueDatabase
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from frigate.api.app import create_app
|
|
||||||
from frigate.api.auth import hash_password
|
from frigate.api.auth import hash_password
|
||||||
from frigate.api.fastapi_app import create_fastapi_app
|
from frigate.api.fastapi_app import create_fastapi_app
|
||||||
from frigate.comms.config_updater import ConfigPublisher
|
from frigate.comms.config_updater import ConfigPublisher
|
||||||
@ -388,7 +386,7 @@ class FrigateApp:
|
|||||||
self.inter_zmq_proxy = ZmqProxy()
|
self.inter_zmq_proxy = ZmqProxy()
|
||||||
|
|
||||||
def init_web_server(self) -> None:
|
def init_web_server(self) -> None:
|
||||||
self.flask_app = create_app(
|
self.fastapi_app = create_fastapi_app(
|
||||||
self.config,
|
self.config,
|
||||||
self.db,
|
self.db,
|
||||||
self.embeddings,
|
self.embeddings,
|
||||||
@ -400,17 +398,6 @@ class FrigateApp:
|
|||||||
self.stats_emitter,
|
self.stats_emitter,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.fastapi_app = create_fastapi_app(
|
|
||||||
self.config,
|
|
||||||
self.embeddings,
|
|
||||||
self.detected_frames_processor,
|
|
||||||
self.storage_maintainer,
|
|
||||||
self.onvif_controller,
|
|
||||||
self.plus_api,
|
|
||||||
self.stats_emitter,
|
|
||||||
self.external_event_processor,
|
|
||||||
)
|
|
||||||
|
|
||||||
def init_onvif(self) -> None:
|
def init_onvif(self) -> None:
|
||||||
self.onvif_controller = OnvifController(self.config, self.ptz_metrics)
|
self.onvif_controller = OnvifController(self.config, self.ptz_metrics)
|
||||||
|
|
||||||
@ -761,6 +748,7 @@ class FrigateApp:
|
|||||||
self.start_watchdog()
|
self.start_watchdog()
|
||||||
self.init_auth()
|
self.init_auth()
|
||||||
|
|
||||||
|
# TODO: Rui. What to do in this case? Maybe https://github.com/encode/uvicorn/issues/1579#issuecomment-1419635974
|
||||||
# Flask only listens for SIGINT, so we need to catch SIGTERM and send SIGINT
|
# Flask only listens for SIGINT, so we need to catch SIGTERM and send SIGINT
|
||||||
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
|
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
|
||||||
os.kill(os.getpid(), signal.SIGINT)
|
os.kill(os.getpid(), signal.SIGINT)
|
||||||
@ -768,8 +756,6 @@ class FrigateApp:
|
|||||||
signal.signal(signal.SIGTERM, receiveSignal)
|
signal.signal(signal.SIGTERM, receiveSignal)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Run the flask app inside fastapi: https://fastapi.tiangolo.com/advanced/sub-applications/
|
|
||||||
self.fastapi_app.mount("", WSGIMiddleware(self.flask_app))
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
self.fastapi_app,
|
self.fastapi_app,
|
||||||
host="127.0.0.1",
|
host="127.0.0.1",
|
||||||
@ -778,7 +764,7 @@ class FrigateApp:
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
logger.info("FastAPI/Flask has exited...")
|
logger.info("FastAPI has exited...")
|
||||||
|
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,6 @@ from playhouse.shortcuts import model_to_dict
|
|||||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||||
from playhouse.sqliteq import SqliteQueueDatabase
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
|
|
||||||
from frigate.api.app import create_app
|
|
||||||
from frigate.api.fastapi_app import create_fastapi_app
|
from frigate.api.fastapi_app import create_fastapi_app
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.models import Event, Recordings
|
from frigate.models import Event, Recordings
|
||||||
@ -115,7 +114,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def test_get_event_list(self):
|
def test_get_event_list(self):
|
||||||
app = create_app(
|
app = create_fastapi_app(
|
||||||
FrigateConfig(**self.minimal_config),
|
FrigateConfig(**self.minimal_config),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
@ -152,7 +151,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
assert not events
|
assert not events
|
||||||
|
|
||||||
def test_get_good_event(self):
|
def test_get_good_event(self):
|
||||||
app = create_app(
|
app = create_fastapi_app(
|
||||||
FrigateConfig(**self.minimal_config),
|
FrigateConfig(**self.minimal_config),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
@ -174,7 +173,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
assert event == model_to_dict(Event.get(Event.id == id))
|
assert event == model_to_dict(Event.get(Event.id == id))
|
||||||
|
|
||||||
def test_get_bad_event(self):
|
def test_get_bad_event(self):
|
||||||
app = create_app(
|
app = create_fastapi_app(
|
||||||
FrigateConfig(**self.minimal_config),
|
FrigateConfig(**self.minimal_config),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
@ -195,7 +194,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
assert not event
|
assert not event
|
||||||
|
|
||||||
def test_delete_event(self):
|
def test_delete_event(self):
|
||||||
app = create_app(
|
app = create_fastapi_app(
|
||||||
FrigateConfig(**self.minimal_config),
|
FrigateConfig(**self.minimal_config),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
@ -218,7 +217,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
assert not event
|
assert not event
|
||||||
|
|
||||||
def test_event_retention(self):
|
def test_event_retention(self):
|
||||||
app = create_app(
|
app = create_fastapi_app(
|
||||||
FrigateConfig(**self.minimal_config),
|
FrigateConfig(**self.minimal_config),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
@ -245,7 +244,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
assert event["retain_indefinitely"] is False
|
assert event["retain_indefinitely"] is False
|
||||||
|
|
||||||
def test_event_time_filtering(self):
|
def test_event_time_filtering(self):
|
||||||
app = create_app(
|
app = create_fastapi_app(
|
||||||
FrigateConfig(**self.minimal_config),
|
FrigateConfig(**self.minimal_config),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
@ -284,7 +283,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
|
|
||||||
def test_set_delete_sub_label(self):
|
def test_set_delete_sub_label(self):
|
||||||
app = create_app(
|
app = create_fastapi_app(
|
||||||
FrigateConfig(**self.minimal_config),
|
FrigateConfig(**self.minimal_config),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
@ -320,7 +319,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
assert event["sub_label"] == ""
|
assert event["sub_label"] == ""
|
||||||
|
|
||||||
def test_sub_label_list(self):
|
def test_sub_label_list(self):
|
||||||
app = create_app(
|
app = create_fastapi_app(
|
||||||
FrigateConfig(**self.minimal_config),
|
FrigateConfig(**self.minimal_config),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
@ -346,7 +345,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
assert sub_labels == [sub_label]
|
assert sub_labels == [sub_label]
|
||||||
|
|
||||||
def test_config(self):
|
def test_config(self):
|
||||||
app = create_app(
|
app = create_fastapi_app(
|
||||||
FrigateConfig(**self.minimal_config).runtime_config(),
|
FrigateConfig(**self.minimal_config).runtime_config(),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
@ -386,7 +385,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
def test_stats(self):
|
def test_stats(self):
|
||||||
stats = Mock(spec=StatsEmitter)
|
stats = Mock(spec=StatsEmitter)
|
||||||
stats.get_latest_stats.return_value = self.test_stats
|
stats.get_latest_stats.return_value = self.test_stats
|
||||||
app = create_app(
|
app = create_fastapi_app(
|
||||||
FrigateConfig(**self.minimal_config).runtime_config(),
|
FrigateConfig(**self.minimal_config).runtime_config(),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user