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 peewee import operator
from frigate.api.defs.app_body import AppConfigSetBody
from frigate.api.defs.app_query_parameters import AppTimelineHourlyQueryParameters
from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters
from frigate.api.defs.request.app_body import AppConfigSetBody
from frigate.api.defs.tags import Tags
from frigate.config import FrigateConfig
from frigate.const import CONFIG_DIR

View File

@ -18,7 +18,7 @@ from joserfc import jwt
from peewee import DoesNotExist
from slowapi import Limiter
from frigate.api.defs.app_body import (
from frigate.api.defs.request.app_body import (
AppPostLoginBody,
AppPostUsersBody,
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 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,
EventsDeleteBody,
EventsDescriptionBody,
@ -22,19 +31,15 @@ from frigate.api.defs.events_body import (
EventsSubLabelBody,
SubmitPlusBody,
)
from frigate.api.defs.events_query_parameters import (
DEFAULT_TIME_RANGE,
EventsQueryParams,
EventsSearchQueryParams,
EventsSummaryQueryParams,
)
from frigate.api.defs.regenerate_query_parameters import (
RegenerateQueryParameters,
from frigate.api.defs.response.event_response import (
EventCreateResponse,
EventMultiDeleteResponse,
EventResponse,
EventUploadPlusResponse,
)
from frigate.api.defs.response.generic_response import GenericResponse
from frigate.api.defs.tags import Tags
from frigate.const import (
CLIPS_DIR,
)
from frigate.const import CLIPS_DIR
from frigate.embeddings import EmbeddingsContext
from frigate.events.external import ExternalEventProcessor
from frigate.models import Event, ReviewSegment, Timeline
@ -46,7 +51,7 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.events])
@router.get("/events")
@router.get("/events", response_model=list[EventResponse])
def events(params: EventsQueryParams = Depends()):
camera = params.camera
cameras = params.cameras
@ -265,7 +270,7 @@ def events(params: EventsQueryParams = Depends()):
return JSONResponse(content=list(events))
@router.get("/events/explore")
@router.get("/events/explore", response_model=list[EventResponse])
def events_explore(limit: int = 10):
# get distinct labels for all events
distinct_labels = Event.select(Event.label).distinct().order_by(Event.label)
@ -310,7 +315,8 @@ def events_explore(limit: int = 10):
"data": {
k: v
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],
}
@ -326,7 +332,7 @@ def events_explore(limit: int = 10):
return JSONResponse(content=processed_events)
@router.get("/event_ids")
@router.get("/event_ids", response_model=list[EventResponse])
def event_ids(ids: str):
ids = ids.split(",")
@ -647,7 +653,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
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):
try:
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)
@router.post("/events/{event_id}/retain")
@router.post("/events/{event_id}/retain", response_model=GenericResponse)
def set_retain(event_id: str):
try:
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):
if not request.app.frigate_config.plus_api.is_active():
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):
if not request.app.frigate_config.plus_api.is_active():
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):
try:
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(
request: Request,
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(
request: Request,
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(
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"}
@router.delete("/events/{event_id}")
@router.delete("/events/{event_id}", response_model=GenericResponse)
def delete_event(request: Request, event_id: str):
result = delete_single_event(event_id, request)
status_code = 200 if result["success"] else 404
return JSONResponse(content=result, status_code=status_code)
@router.delete("/events/")
@router.delete("/events/", response_model=EventMultiDeleteResponse)
def delete_events(request: Request, body: EventsDeleteBody):
if not body.event_ids:
return JSONResponse(
@ -1097,7 +1103,7 @@ def delete_events(request: Request, body: EventsDeleteBody):
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(
request: Request,
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):
try:
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.responses import JSONResponse
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.tags import Tags
@ -207,3 +208,14 @@ def export_delete(event_id: str):
),
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 tzlocal import get_localzone_name
from frigate.api.defs.media_query_parameters import (
from frigate.api.defs.query.media_query_parameters import (
Extension,
MediaEventsSnapshotQueryParams,
MediaLatestFrameQueryParams,

View File

@ -12,14 +12,14 @@ from fastapi.responses import JSONResponse
from peewee import Case, DoesNotExist, fn, operator
from playhouse.shortcuts import model_to_dict
from frigate.api.defs.generic_response import GenericResponse
from frigate.api.defs.review_body import ReviewModifyMultipleBody
from frigate.api.defs.review_query_parameters import (
from frigate.api.defs.query.review_query_parameters import (
ReviewActivityMotionQueryParams,
ReviewQueryParams,
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,
ReviewSegmentResponse,
ReviewSummaryResponse,
@ -364,7 +364,7 @@ def delete_reviews(body: ReviewModifyMultipleBody):
ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute()
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
response_json = response.json()
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
review_ids_in_db_after = self._get_reviews([id])
assert len(review_ids_in_db_after) == 1
@ -540,7 +540,7 @@ class TestHttpReview(BaseTestHttp):
assert response.status_code == 200
response_json = response.json()
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
review_ids_in_db_after = self._get_reviews([id])
assert len(review_ids_in_db_after) == 0
@ -562,7 +562,7 @@ class TestHttpReview(BaseTestHttp):
assert response.status_code == 200
response_json = response.json()
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
review_ids_in_db_after = self._get_reviews(ids)

View File

@ -168,7 +168,7 @@ class TestHttp(unittest.TestCase):
assert event
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):
app = create_fastapi_app(