Api improvements (#15327)

* Organize api files

* Add more API definitions for events

* Add export select by ID

* Typing fixes

* Update openapi spec

* Change type

* Fix test

* Fix message

* Fix tests
This commit is contained in:
Nicolas Mowen 2024-12-06 08:04:02 -06:00 committed by GitHub
parent 47d495fc01
commit d3b631a952
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 813 additions and 526 deletions

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,8 @@ from fastapi.responses import JSONResponse, PlainTextResponse
from markupsafe import escape from markupsafe import escape
from peewee import operator from peewee import operator
from frigate.api.defs.app_body import AppConfigSetBody from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters
from frigate.api.defs.app_query_parameters import AppTimelineHourlyQueryParameters from frigate.api.defs.request.app_body import AppConfigSetBody
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

View File

@ -18,7 +18,7 @@ from joserfc import jwt
from peewee import DoesNotExist from peewee import DoesNotExist
from slowapi import Limiter from slowapi import Limiter
from frigate.api.defs.app_body import ( from frigate.api.defs.request.app_body import (
AppPostLoginBody, AppPostLoginBody,
AppPostUsersBody, AppPostUsersBody,
AppPutPasswordBody, AppPutPasswordBody,

View File

@ -0,0 +1,40 @@
from typing import Any, Optional
from pydantic import BaseModel
class EventResponse(BaseModel):
id: str
label: str
sub_label: Optional[str]
camera: str
start_time: float
end_time: Optional[float]
false_positive: bool
zones: list[str]
thumbnail: str
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]
data: dict[str, Any]
class EventCreateResponse(BaseModel):
success: bool
message: str
event_id: str
class EventMultiDeleteResponse(BaseModel):
success: bool
deleted_events: list[str]
not_found_events: list[str]
class EventUploadPlusResponse(BaseModel):
success: bool
plus_id: str

View File

@ -14,7 +14,16 @@ from fastapi.responses import JSONResponse
from peewee import JOIN, DoesNotExist, fn, operator from peewee import JOIN, DoesNotExist, fn, operator
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from frigate.api.defs.events_body import ( from frigate.api.defs.query.events_query_parameters import (
DEFAULT_TIME_RANGE,
EventsQueryParams,
EventsSearchQueryParams,
EventsSummaryQueryParams,
)
from frigate.api.defs.query.regenerate_query_parameters import (
RegenerateQueryParameters,
)
from frigate.api.defs.request.events_body import (
EventsCreateBody, EventsCreateBody,
EventsDeleteBody, EventsDeleteBody,
EventsDescriptionBody, EventsDescriptionBody,
@ -22,19 +31,15 @@ from frigate.api.defs.events_body import (
EventsSubLabelBody, EventsSubLabelBody,
SubmitPlusBody, SubmitPlusBody,
) )
from frigate.api.defs.events_query_parameters import ( from frigate.api.defs.response.event_response import (
DEFAULT_TIME_RANGE, EventCreateResponse,
EventsQueryParams, EventMultiDeleteResponse,
EventsSearchQueryParams, EventResponse,
EventsSummaryQueryParams, EventUploadPlusResponse,
)
from frigate.api.defs.regenerate_query_parameters import (
RegenerateQueryParameters,
) )
from frigate.api.defs.response.generic_response import GenericResponse
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.const import ( from frigate.const import CLIPS_DIR
CLIPS_DIR,
)
from frigate.embeddings import EmbeddingsContext from frigate.embeddings import EmbeddingsContext
from frigate.events.external import ExternalEventProcessor from frigate.events.external import ExternalEventProcessor
from frigate.models import Event, ReviewSegment, Timeline from frigate.models import Event, ReviewSegment, Timeline
@ -46,7 +51,7 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.events]) router = APIRouter(tags=[Tags.events])
@router.get("/events") @router.get("/events", response_model=list[EventResponse])
def events(params: EventsQueryParams = Depends()): def events(params: EventsQueryParams = Depends()):
camera = params.camera camera = params.camera
cameras = params.cameras cameras = params.cameras
@ -265,7 +270,7 @@ def events(params: EventsQueryParams = Depends()):
return JSONResponse(content=list(events)) return JSONResponse(content=list(events))
@router.get("/events/explore") @router.get("/events/explore", response_model=list[EventResponse])
def events_explore(limit: int = 10): def events_explore(limit: int = 10):
# get distinct labels for all events # get distinct labels for all events
distinct_labels = Event.select(Event.label).distinct().order_by(Event.label) distinct_labels = Event.select(Event.label).distinct().order_by(Event.label)
@ -310,7 +315,8 @@ def events_explore(limit: int = 10):
"data": { "data": {
k: v k: v
for k, v in event.data.items() for k, v in event.data.items()
if k in ["type", "score", "top_score", "description"] if k
in ["type", "score", "top_score", "description", "sub_label_score"]
}, },
"event_count": label_counts[event.label], "event_count": label_counts[event.label],
} }
@ -326,7 +332,7 @@ def events_explore(limit: int = 10):
return JSONResponse(content=processed_events) return JSONResponse(content=processed_events)
@router.get("/event_ids") @router.get("/event_ids", response_model=list[EventResponse])
def event_ids(ids: str): def event_ids(ids: str):
ids = ids.split(",") ids = ids.split(",")
@ -647,7 +653,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
return JSONResponse(content=[e for e in groups.dicts()]) return JSONResponse(content=[e for e in groups.dicts()])
@router.get("/events/{event_id}") @router.get("/events/{event_id}", response_model=EventResponse)
def event(event_id: str): def event(event_id: str):
try: try:
return model_to_dict(Event.get(Event.id == event_id)) return model_to_dict(Event.get(Event.id == event_id))
@ -655,7 +661,7 @@ def event(event_id: str):
return JSONResponse(content="Event not found", status_code=404) return JSONResponse(content="Event not found", status_code=404)
@router.post("/events/{event_id}/retain") @router.post("/events/{event_id}/retain", response_model=GenericResponse)
def set_retain(event_id: str): def set_retain(event_id: str):
try: try:
event = Event.get(Event.id == event_id) event = Event.get(Event.id == event_id)
@ -674,7 +680,7 @@ def set_retain(event_id: str):
) )
@router.post("/events/{event_id}/plus") @router.post("/events/{event_id}/plus", response_model=EventUploadPlusResponse)
def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None): def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None):
if not request.app.frigate_config.plus_api.is_active(): if not request.app.frigate_config.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set" message = "PLUS_API_KEY environment variable is not set"
@ -786,7 +792,7 @@ def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None):
) )
@router.put("/events/{event_id}/false_positive") @router.put("/events/{event_id}/false_positive", response_model=EventUploadPlusResponse)
def false_positive(request: Request, event_id: str): def false_positive(request: Request, event_id: str):
if not request.app.frigate_config.plus_api.is_active(): if not request.app.frigate_config.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set" message = "PLUS_API_KEY environment variable is not set"
@ -875,7 +881,7 @@ def false_positive(request: Request, event_id: str):
) )
@router.delete("/events/{event_id}/retain") @router.delete("/events/{event_id}/retain", response_model=GenericResponse)
def delete_retain(event_id: str): def delete_retain(event_id: str):
try: try:
event = Event.get(Event.id == event_id) event = Event.get(Event.id == event_id)
@ -894,7 +900,7 @@ def delete_retain(event_id: str):
) )
@router.post("/events/{event_id}/sub_label") @router.post("/events/{event_id}/sub_label", response_model=GenericResponse)
def set_sub_label( def set_sub_label(
request: Request, request: Request,
event_id: str, event_id: str,
@ -946,7 +952,7 @@ def set_sub_label(
) )
@router.post("/events/{event_id}/description") @router.post("/events/{event_id}/description", response_model=GenericResponse)
def set_description( def set_description(
request: Request, request: Request,
event_id: str, event_id: str,
@ -993,7 +999,7 @@ def set_description(
) )
@router.put("/events/{event_id}/description/regenerate") @router.put("/events/{event_id}/description/regenerate", response_model=GenericResponse)
def regenerate_description( def regenerate_description(
request: Request, event_id: str, params: RegenerateQueryParameters = Depends() request: Request, event_id: str, params: RegenerateQueryParameters = Depends()
): ):
@ -1064,14 +1070,14 @@ def delete_single_event(event_id: str, request: Request) -> dict:
return {"success": True, "message": f"Event {event_id} deleted"} return {"success": True, "message": f"Event {event_id} deleted"}
@router.delete("/events/{event_id}") @router.delete("/events/{event_id}", response_model=GenericResponse)
def delete_event(request: Request, event_id: str): def delete_event(request: Request, event_id: str):
result = delete_single_event(event_id, request) result = delete_single_event(event_id, request)
status_code = 200 if result["success"] else 404 status_code = 200 if result["success"] else 404
return JSONResponse(content=result, status_code=status_code) return JSONResponse(content=result, status_code=status_code)
@router.delete("/events/") @router.delete("/events/", response_model=EventMultiDeleteResponse)
def delete_events(request: Request, body: EventsDeleteBody): def delete_events(request: Request, body: EventsDeleteBody):
if not body.event_ids: if not body.event_ids:
return JSONResponse( return JSONResponse(
@ -1097,7 +1103,7 @@ def delete_events(request: Request, body: EventsDeleteBody):
return JSONResponse(content=response, status_code=200) return JSONResponse(content=response, status_code=200)
@router.post("/events/{camera_name}/{label}/create") @router.post("/events/{camera_name}/{label}/create", response_model=EventCreateResponse)
def create_event( def create_event(
request: Request, request: Request,
camera_name: str, camera_name: str,
@ -1153,7 +1159,7 @@ def create_event(
) )
@router.put("/events/{event_id}/end") @router.put("/events/{event_id}/end", response_model=GenericResponse)
def end_event(request: Request, event_id: str, body: EventsEndBody): def end_event(request: Request, event_id: str, body: EventsEndBody):
try: try:
end_time = body.end_time or datetime.datetime.now().timestamp() end_time = body.end_time or datetime.datetime.now().timestamp()

View File

@ -9,6 +9,7 @@ import psutil
from fastapi import APIRouter, Request from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from peewee import DoesNotExist from peewee import DoesNotExist
from playhouse.shortcuts import model_to_dict
from frigate.api.defs.request.export_recordings_body import ExportRecordingsBody from frigate.api.defs.request.export_recordings_body import ExportRecordingsBody
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
@ -207,3 +208,14 @@ def export_delete(event_id: str):
), ),
status_code=200, status_code=200,
) )
@router.get("/exports/{export_id}")
def get_export(export_id: str):
try:
return JSONResponse(content=model_to_dict(Export.get(Export.id == export_id)))
except DoesNotExist:
return JSONResponse(
content={"success": False, "message": "Export not found"},
status_code=404,
)

View File

@ -20,7 +20,7 @@ from pathvalidate import sanitize_filename
from peewee import DoesNotExist, fn from peewee import DoesNotExist, fn
from tzlocal import get_localzone_name from tzlocal import get_localzone_name
from frigate.api.defs.media_query_parameters import ( from frigate.api.defs.query.media_query_parameters import (
Extension, Extension,
MediaEventsSnapshotQueryParams, MediaEventsSnapshotQueryParams,
MediaLatestFrameQueryParams, MediaLatestFrameQueryParams,

View File

@ -12,14 +12,14 @@ from fastapi.responses import JSONResponse
from peewee import Case, DoesNotExist, fn, operator from peewee import Case, DoesNotExist, fn, operator
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from frigate.api.defs.generic_response import GenericResponse from frigate.api.defs.query.review_query_parameters import (
from frigate.api.defs.review_body import ReviewModifyMultipleBody
from frigate.api.defs.review_query_parameters import (
ReviewActivityMotionQueryParams, ReviewActivityMotionQueryParams,
ReviewQueryParams, ReviewQueryParams,
ReviewSummaryQueryParams, ReviewSummaryQueryParams,
) )
from frigate.api.defs.review_responses import ( from frigate.api.defs.request.review_body import ReviewModifyMultipleBody
from frigate.api.defs.response.generic_response import GenericResponse
from frigate.api.defs.response.review_response import (
ReviewActivityMotionResponse, ReviewActivityMotionResponse,
ReviewSegmentResponse, ReviewSegmentResponse,
ReviewSummaryResponse, ReviewSummaryResponse,
@ -364,7 +364,7 @@ def delete_reviews(body: ReviewModifyMultipleBody):
ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute() ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute()
return JSONResponse( return JSONResponse(
content=({"success": True, "message": "Delete reviews"}), status_code=200 content=({"success": True, "message": "Deleted review items."}), status_code=200
) )

View File

@ -525,7 +525,7 @@ class TestHttpReview(BaseTestHttp):
assert response.status_code == 200 assert response.status_code == 200
response_json = response.json() response_json = response.json()
assert response_json["success"] == True assert response_json["success"] == True
assert response_json["message"] == "Delete reviews" assert response_json["message"] == "Deleted review items."
# Verify that in DB the review segment was not deleted # Verify that in DB the review segment was not deleted
review_ids_in_db_after = self._get_reviews([id]) review_ids_in_db_after = self._get_reviews([id])
assert len(review_ids_in_db_after) == 1 assert len(review_ids_in_db_after) == 1
@ -540,7 +540,7 @@ class TestHttpReview(BaseTestHttp):
assert response.status_code == 200 assert response.status_code == 200
response_json = response.json() response_json = response.json()
assert response_json["success"] == True assert response_json["success"] == True
assert response_json["message"] == "Delete reviews" assert response_json["message"] == "Deleted review items."
# Verify that in DB the review segment was deleted # Verify that in DB the review segment was deleted
review_ids_in_db_after = self._get_reviews([id]) review_ids_in_db_after = self._get_reviews([id])
assert len(review_ids_in_db_after) == 0 assert len(review_ids_in_db_after) == 0
@ -562,7 +562,7 @@ class TestHttpReview(BaseTestHttp):
assert response.status_code == 200 assert response.status_code == 200
response_json = response.json() response_json = response.json()
assert response_json["success"] == True assert response_json["success"] == True
assert response_json["message"] == "Delete reviews" assert response_json["message"] == "Deleted review items."
# Verify that in DB all review segments and recordings that were passed were deleted # Verify that in DB all review segments and recordings that were passed were deleted
review_ids_in_db_after = self._get_reviews(ids) review_ids_in_db_after = self._get_reviews(ids)

View File

@ -168,7 +168,7 @@ class TestHttp(unittest.TestCase):
assert event assert event
assert event["id"] == id assert event["id"] == id
assert event == model_to_dict(Event.get(Event.id == id)) assert event["id"] == model_to_dict(Event.get(Event.id == id))["id"]
def test_get_bad_event(self): def test_get_bad_event(self):
app = create_fastapi_app( app = create_fastapi_app(