diff --git a/build.sh b/build.sh index 5ef7513..91cf2df 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -e -m # this block ensures we can invoke this script from anywhere and have it automatically change to this folder first pushd "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 @@ -42,14 +42,21 @@ done echo -e "\ngenerating the api..." DJANGOLANG_API_ROOT=/api DJANGOLANG_PACKAGE_NAME=api POSTGRES_DB=camry POSTGRES_PASSWORD=NoNVR!11 djangolang template -if test -e ./cmd/api; then - rm -frv ./cmd/api -fi -cp -frv ./pkg/api/cmd ./cmd/api +# TODO: you don't want to do this once you put some custom stuff in your api entrypoint +# if test -e ./cmd/api; then +# rm -frv ./cmd/api +# fi +# cp -frv ./pkg/api/cmd ./cmd/api # dump out the OpenAPI v3 schema for the Djangolang API mkdir -p ./schema -DJANGOLANG_API_ROOT=/api ./pkg/api/bin/api dump-openapi-json >./schema/openapi.json +# DJANGOLANG_API_ROOT=/api ./pkg/api/bin/api dump-openapi-json >./schema/openapi.json +go build -o ./cmd/api/api ./cmd/api +REDIS_URL=redis://localhost:6379 DJANGOLANG_API_ROOT=/api POSTGRES_DB=camry POSTGRES_PASSWORD=NoNVR\!11 ./cmd/api/api serve & +pid=$! +sleep 5 +curl http://localhost:7070/api/openapi.json >./schema/openapi.json +kill -15 ${pid} # generate the client for use by the frontend echo -e "\ngenerating typescript client..." diff --git a/cmd/api/api b/cmd/api/api new file mode 100755 index 0000000..89020d0 Binary files /dev/null and b/cmd/api/api differ diff --git a/cmd/api/main.go b/cmd/api/main.go index 71aa46d..2de1453 100755 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,13 +1,170 @@ package main import ( + "context" + "fmt" "log" + "net/http" "os" "strings" + "time" + "github.com/go-chi/chi/v5" + "github.com/gomodule/redigo/redis" + "github.com/initialed85/camry/internal" "github.com/initialed85/camry/pkg/api" + "github.com/initialed85/djangolang/pkg/helpers" + "github.com/initialed85/djangolang/pkg/server" ) +type ClaimRequest struct { + ClaimDurationSeconds float64 `json:"claim_duration_seconds"` +} + +func RunServeWithEnvironment( + httpMiddlewares []server.HTTPMiddleware, + objectMiddlewares []server.ObjectMiddleware, + addCustomHandlers func(chi.Router) error, +) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + port, err := helpers.GetPort() + if err != nil { + log.Fatalf("%v", err) + } + + db, err := helpers.GetDBFromEnvironment(ctx) + if err != nil { + log.Fatalf("%v", err) + } + defer func() { + db.Close() + }() + + go func() { + helpers.WaitForCtrlC(ctx) + cancel() + }() + + redisURL, err := helpers.GetRedisURL() + if err != nil { + log.Fatalf("%v", err) + } + + redisPool := &redis.Pool{ + DialContext: func(ctx context.Context) (redis.Conn, error) { + return redis.DialURLContext(ctx, redisURL) + }, + MaxIdle: 2, + MaxActive: 100, + IdleTimeout: 300, + Wait: false, + MaxConnLifetime: 86400, + } + + defer func() { + _ = redisPool.Close() + }() + + actualAddCustomHandlers := func(r chi.Router) error { + claimVideoForObjectDetectorHandler, err := api.GetCustomHTTPHandler( + http.MethodPatch, + "/claim-video-for-object-detector", + http.StatusOK, + func( + ctx context.Context, + pathParams server.EmptyPathParams, + queryParams server.EmptyQueryParams, + req ClaimRequest, + rawReq any, + ) (*api.Video, error) { + now := time.Now().UTC() + + claimUntil := now.Add(time.Second * time.Duration(req.ClaimDurationSeconds)) + + if claimUntil.Sub(now) <= 0 { + return nil, fmt.Errorf("claim_duration_seconds too short; must result in a claim that expires in the future") + } + + tx, err := db.Begin(ctx) + if err != nil { + return nil, err + } + + defer func() { + _ = tx.Rollback(ctx) + }() + + video := &api.Video{} + + err = video.LockTable(ctx, tx, false) + if err != nil { + return nil, err + } + + videos, _, _, _, _, err := api.SelectVideos( + ctx, + tx, + fmt.Sprintf( + "%v < now()", + api.VideoTableObjectDetectorClaimedUntilColumn, + ), + internal.Ptr(fmt.Sprintf( + "%v DESC", + api.VideoTableObjectDetectorClaimedUntilColumn, + )), + internal.Ptr(1), + nil, + ) + if err != nil { + return nil, err + } + + if len(videos) != 1 { + return nil, fmt.Errorf("wanted exactly 1 unclaimed video, got %d", len(videos)) + } + + video = videos[0] + + video.ObjectDetectorClaimedUntil = claimUntil + video.ObjectTrackerClaimedUntil = time.Time{} // zero to ensure we don't wipe out an existing value + + err = video.Update(ctx, tx, false) + if err != nil { + return nil, err + } + + err = tx.Commit(ctx) + if err != nil { + return nil, err + } + + return video, nil + }, + ) + if err != nil { + return err + } + + r.Patch( + claimVideoForObjectDetectorHandler.Path, + claimVideoForObjectDetectorHandler.ServeHTTP, + ) + + if addCustomHandlers != nil { + err = addCustomHandlers(r) + if err != nil { + return err + } + } + + return nil + } + + api.RunServeWithArguments(ctx, cancel, port, db, redisPool, nil, nil, actualAddCustomHandlers) +} + func main() { if len(os.Args) < 2 { log.Fatal("first argument must be command (one of 'serve', 'dump-openapi-json', 'dump-openapi-yaml')") @@ -24,6 +181,6 @@ func main() { api.RunDumpOpenAPIYAML() case "serve": - api.RunServeWithEnvironment() + RunServeWithEnvironment(nil, nil, nil) } } diff --git a/cmd/main.go b/cmd/main.go index d40e1b7..2777ddc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,7 +28,7 @@ func main() { api.RunDumpOpenAPIYAML() case "serve": - api.RunServeWithEnvironment() + api.RunServeWithEnvironment(nil, nil, nil) case "segment_producer": err = segment_producer.Run() diff --git a/database/migrations/00001_initial.down.sql b/database/migrations/00001_initial.down.sql index e69de29..d4ede07 100644 --- a/database/migrations/00001_initial.down.sql +++ b/database/migrations/00001_initial.down.sql @@ -0,0 +1,5 @@ +DROP TABLE public.detection; + +DROP TABLE public.video; + +DROP TABLE public.camera; diff --git a/database/migrations/00002_more_detection_stuff.down.sql b/database/migrations/00002_more_detection_stuff.down.sql new file mode 100644 index 0000000..d89532a --- /dev/null +++ b/database/migrations/00002_more_detection_stuff.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE video +DROP COLUMN detection_summary; diff --git a/database/migrations/00002_more_detection_stuff.up.sql b/database/migrations/00002_more_detection_stuff.up.sql new file mode 100644 index 0000000..8c97cf9 --- /dev/null +++ b/database/migrations/00002_more_detection_stuff.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE video +ADD COLUMN detection_summary jsonb NOT NULL default '[]'::jsonb; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b02d895..d52c466 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "prettier": "^3.3.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-intersection-observer": "^9.13.0", "react-scripts": "5.0.1", "typescript": "^5.5.4", "use-local-storage-state": "^19.4.0", @@ -15325,6 +15326,20 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-intersection-observer": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz", + "integrity": "sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw==", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index d650ff8..5cf82b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "prettier": "^3.3.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-intersection-observer": "^9.13.0", "react-scripts": "5.0.1", "typescript": "^5.5.4", "use-local-storage-state": "^19.4.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6c3cbdd..7130e21 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,8 +4,8 @@ import Sheet from "@mui/joy/Sheet"; import Typography from "@mui/joy/Typography"; import { useEffect, useState } from "react"; import useLocalStorageState from "use-local-storage-state"; -import CameraToggleButtonGroup from "./components/CameraToggleButtonGroup"; -import DateDropdownMenu, { formatDate } from "./components/DateDropdownMenu"; +import CameraDropdownMenu from "./components/CameraDropdownMenu"; +import DateDropdownMenu from "./components/DateDropdownMenu"; import ModeToggle from "./components/ModeToggle"; import { VideoTable } from "./components/VideoTable"; @@ -24,18 +24,40 @@ function App() { // }; const [responsive, setResponsive] = useState(window.innerWidth < 992); - const [cameraId, setCameraId] = useLocalStorageState< - string | null | undefined - >("cameraId", { - defaultValue: undefined, - }); - const [date, setDate] = useLocalStorageState( - "date", + + const [cameraId, setCameraId] = useLocalStorageState( + "cameraId", { - defaultValue: formatDate(new Date()), + defaultValue: undefined, }, ); + const [startedAtGt, setStartedAtGt] = useLocalStorageState< + string | undefined + >("startedAtGt", { + defaultValue: undefined, + }); + + const [startedAtLte, setStartedAtLte] = useLocalStorageState< + string | undefined + >("startedAtLte", { + defaultValue: undefined, + }); + + console.log( + "state: ", + JSON.stringify( + { + responsive: responsive, + cameraId: cameraId, + startedAtGt: startedAtGt, + startedAtLte: startedAtLte, + }, + null, + 2, + ), + ); + useEffect(() => { const handleResize = () => { setResponsive(window.innerWidth < 992); @@ -62,42 +84,47 @@ function App() { }} > - - - {responsive ? "C" : "Camry"} - - - + {responsive ? "C" : "Camry"} + + - + - + ); } diff --git a/frontend/src/api/api.d.ts b/frontend/src/api/api.d.ts index 3f7a2a7..3680262 100644 --- a/frontend/src/api/api.d.ts +++ b/frontend/src/api/api.d.ts @@ -36,6 +36,22 @@ export interface paths { patch: operations["PatchCamera"]; trace?: never; }; + "/api/custom/claim-video-for-object-detector": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations["PatchCustom0"]; + trace?: never; + }; "/api/detections": { parameters: { query?: never; @@ -104,6 +120,7 @@ export interface paths { export type webhooks = Record; export interface components { schemas: { + Any: Record; Camera: { /** Format: date-time */ created_at?: string; @@ -181,6 +198,7 @@ export interface components { created_at?: string; /** Format: date-time */ deleted_at?: string | null; + detection_summary?: Record; /** Format: int64 */ duration?: number | null; /** Format: date-time */ @@ -560,11 +578,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Camera"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -575,7 +601,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -609,11 +635,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Camera"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -624,7 +658,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -657,11 +691,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Camera"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -672,7 +714,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -709,11 +751,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Camera"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -724,7 +774,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -764,7 +814,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -801,11 +851,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Camera"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -816,7 +874,77 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; + /** Format: int32 */ + status: number; + success: boolean; + }; + }; + }; + }; + }; + PatchCustom0: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** Format: double */ + claim_duration_seconds: number; + }; + }; + }; + responses: { + /** @description Custom0 success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** Format: uuid */ + camera_id?: string; + camera_id_object?: components["schemas"]["NullableCamera"]; + /** Format: date-time */ + created_at?: string; + /** Format: date-time */ + deleted_at?: string | null; + detection_summary?: Record; + /** Format: int64 */ + duration?: number | null; + /** Format: date-time */ + ended_at?: string | null; + file_name?: string; + /** Format: double */ + file_size?: number | null; + /** Format: uuid */ + id?: string; + /** Format: date-time */ + object_detector_claimed_until?: string; + /** Format: date-time */ + object_tracker_claimed_until?: string; + referenced_by_detection_video_id_objects?: components["schemas"]["NullableArrayOfDetection"]; + /** Format: date-time */ + started_at?: string; + status?: string; + thumbnail_name?: string; + /** Format: date-time */ + updated_at?: string; + }; + }; + }; + /** @description Custom0 failure */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -1210,11 +1338,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Detection"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -1225,7 +1361,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -1259,11 +1395,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Detection"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -1274,7 +1418,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -1307,11 +1451,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Detection"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -1322,7 +1474,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -1359,11 +1511,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Detection"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -1374,7 +1534,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -1414,7 +1574,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -1451,11 +1611,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Detection"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -1466,7 +1634,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -2004,11 +2172,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Video"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -2019,7 +2195,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -2053,11 +2229,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Video"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -2068,7 +2252,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -2101,11 +2285,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Video"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -2116,7 +2308,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -2153,11 +2345,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Video"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -2168,7 +2368,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -2208,7 +2408,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; @@ -2245,11 +2445,19 @@ export interface operations { }; content: { "application/json": { - error?: string; + /** Format: int64 */ + count?: number; + error?: string[]; + /** Format: int64 */ + limit?: number; objects?: components["schemas"]["Video"][]; + /** Format: int64 */ + offset?: number; /** Format: int32 */ status: number; success: boolean; + /** Format: int64 */ + total_count?: number; }; }; }; @@ -2260,7 +2468,7 @@ export interface operations { }; content: { "application/json": { - error?: string; + error?: string[]; /** Format: int32 */ status: number; success: boolean; diff --git a/frontend/src/components/CameraDropdownMenu.tsx b/frontend/src/components/CameraDropdownMenu.tsx new file mode 100644 index 0000000..db954eb --- /dev/null +++ b/frontend/src/components/CameraDropdownMenu.tsx @@ -0,0 +1,53 @@ +import Videocam from "@mui/icons-material/Videocam"; +import Dropdown from "@mui/joy/Dropdown"; +import Menu from "@mui/joy/Menu"; +import MenuButton from "@mui/joy/MenuButton"; +import MenuItem from "@mui/joy/MenuItem"; +import { Dispatch, SetStateAction } from "react"; +import { useQuery } from "../api"; + +export interface CameraDropdownMenuProps { + responsive: boolean; + cameraId: string | undefined; + setCameraId: Dispatch>; +} + +export default function CameraDropdownMenu(props: CameraDropdownMenuProps) { + const { data } = useQuery("get", "/api/cameras", { + params: { + query: { + name__asc: "", + }, + }, + }); + + return ( + + + + + + {data?.objects?.map((camera) => { + return ( + { + props.setCameraId( + props.cameraId !== camera.id ? camera.id : undefined, + ); + }} + > + {camera.name} + + ); + })} + + + ); +} diff --git a/frontend/src/components/CameraToggleButtonGroup.tsx b/frontend/src/components/CameraToggleButtonGroup.tsx deleted file mode 100644 index 23598e0..0000000 --- a/frontend/src/components/CameraToggleButtonGroup.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import LiveTvIcon from "@mui/icons-material/LiveTv"; -import { ButtonGroup, IconButton, Typography } from "@mui/joy"; -import Button from "@mui/joy/Button"; -import ToggleButtonGroup from "@mui/joy/ToggleButtonGroup"; -import { Dispatch, SetStateAction, useState } from "react"; -import { useQuery } from "../api"; - -export interface CameraToggleButtonGroupProps { - responsive: boolean; - cameraId: string | null | undefined; - setCameraId: Dispatch>; -} - -export default function CameraToggleButtonGroup( - props: CameraToggleButtonGroupProps, -) { - const [value, setValue] = useState(props.cameraId); - - const { data, error } = useQuery("get", "/api/cameras", { - params: { - query: { - name__asc: "", - }, - }, - }); - - if (error) { - return ( - - Failed to load cameras: {error?.error || error.toString()} - - ); - } - - return ( - { - setValue(newValue); - props.setCameraId(newValue); - }} - > - {data?.objects?.map((c) => { - if (!c || !c?.name) { - return undefined; - } - - return ( - - - - - - - - - ); - })} - - ); -} diff --git a/frontend/src/components/DateDropdownMenu.tsx b/frontend/src/components/DateDropdownMenu.tsx index be3e9a5..5b0b1a1 100644 --- a/frontend/src/components/DateDropdownMenu.tsx +++ b/frontend/src/components/DateDropdownMenu.tsx @@ -4,36 +4,30 @@ import Menu from "@mui/joy/Menu"; import MenuButton from "@mui/joy/MenuButton"; import MenuItem from "@mui/joy/MenuItem"; import { Dispatch, SetStateAction } from "react"; +import { formatDate, truncateDate } from "../helpers"; export interface DateDropdownMenuProps { responsive: boolean; - date: string | null | undefined; - setDate: Dispatch>; + startedAtGt: string | undefined; + setStartedAtGt: Dispatch>; + startedAtLte: string | undefined; + setStartedAtLte: Dispatch>; } -export const formatDate = (date: Date): string => { - const pad = (s: string): string => { - if (s.length < 2) { - return `0${s}`; - } - - return s; - }; - - const year = date.getFullYear().toString(); - const month = pad((date.getMonth() + 1).toString()); - const day = pad(date.getDate().toString()); - - return `${year}-${month}-${day}`; -}; - export default function DateDropdownMenu(props: DateDropdownMenuProps) { var cursor = new Date(); + const dates = []; for (var i = 0; i < 14; i++) { - dates.push(formatDate(cursor)); + dates.push(truncateDate(cursor)); + cursor = new Date(cursor.getTime() - 24 * 60 * 60 * 1000); + } - cursor.setMilliseconds(cursor.getMilliseconds() - 24 * 60 * 60 * 1000); + if (!props.startedAtGt) { + props.setStartedAtGt(cursor.toISOString()); + props.setStartedAtLte( + new Date(cursor.getTime() + 24 * 60 * 60 * 1000).toISOString(), + ); } return ( @@ -50,13 +44,20 @@ export default function DateDropdownMenu(props: DateDropdownMenuProps) { {dates.map((date) => { return ( { - props.setDate(props.date !== date ? date : undefined); + props.setStartedAtGt(date.toISOString()); + props.setStartedAtLte( + new Date(date.getTime() + 24 * 60 * 60 * 1000).toISOString(), + ); }} > - {date} + {formatDate(date)} ); })} diff --git a/frontend/src/components/VideoTable.tsx b/frontend/src/components/VideoTable.tsx index 70868de..0f1e456 100644 --- a/frontend/src/components/VideoTable.tsx +++ b/frontend/src/components/VideoTable.tsx @@ -5,41 +5,93 @@ import CircularProgress from "@mui/joy/CircularProgress"; import Table from "@mui/joy/Table"; import Tooltip from "@mui/joy/Tooltip"; import Typography from "@mui/joy/Typography"; -import { useQuery } from "../api"; +import Container from "@mui/material/Container"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { useEffect } from "react"; +import { useInView } from "react-intersection-observer"; +import { clientForReactQuery, useQuery } from "../api"; import { components } from "../api/api"; -import { formatDate } from "./DateDropdownMenu"; +import { formatDate } from "../helpers"; + +const defaultLimit = 60; export interface VideoTableProps { responsive: boolean; - cameraId: string | null | undefined; - date: string | null | undefined; + cameraId: string | undefined; + startedAtGt: string | undefined; + startedAtLte: string | undefined; } export function VideoTable(props: VideoTableProps) { - const { data: videosData } = useQuery("get", "/api/videos", { + const [ref, inView] = useInView(); + + const { data: allCamerasData } = useQuery("get", "/api/cameras", { params: { query: { - camera_id__eq: props.cameraId || undefined, - started_at__gte: props.date - ? `${props.date}T00:00:00+08:00` - : undefined, - started_at__lte: props.date - ? `${props.date}T23:59:59+08:00` - : undefined, - started_at__desc: "", + name__asc: "", }, }, }); - // useInfiniteQuery({ - // queryKey: ["videos"], - // queryFn: async ({ pageParam = 0 }) => { - // const res = await clientForReactQuery.GET("/api/cameras", { params: { query: {} } }); - // return res.data; - // }, - // initialPageParam: 0, - // getNextPageParam: (lastPage, pages) => 1, - // }); + const visibleCameraCount = props.cameraId + ? 1 + : allCamerasData?.objects?.length || 1; + + const relevantLimit = defaultLimit * visibleCameraCount; + + const { + data: infiniteVideosData, + hasNextPage, + fetchNextPage, + } = useInfiniteQuery({ + queryKey: ["videos"], + queryHash: JSON.stringify(props), + queryFn: async ({ pageParam = 0 }) => { + const res = await clientForReactQuery.GET("/api/videos", { + params: { + query: { + camera_id__eq: props.cameraId || undefined, + started_at__gt: props.startedAtGt && props.startedAtGt, + started_at__lte: props.startedAtLte && props.startedAtLte, + started_at__desc: "", + limit: relevantLimit, + offset: pageParam, + }, + }, + }); + return res.data; + }, + initialPageParam: 0, + getNextPageParam: (lastPage, pages) => { + /* + TODO: this doesn't cater for the fact that we have new data coming in- really we should + use something like timestamp for the cursor, and even then we should probably split out + finished videos from processing videos + */ + + if (lastPage?.count === 0) { + return lastPage?.offset; + } + + return (lastPage?.offset || 0) + relevantLimit; + }, + }); + + const videosData: { + objects: components["schemas"]["Video"][]; + } = { + objects: [], + }; + + infiniteVideosData?.pages.forEach((page) => { + page?.objects?.forEach((object) => { + if (!object) { + return; + } + + videosData?.objects.push(object); + }); + }); const { data: camerasData } = useQuery("get", "/api/cameras", {}); @@ -86,12 +138,28 @@ export function VideoTable(props: VideoTableProps) { } : {}; + useEffect(() => { + if (inView && hasNextPage) { + void fetchNextPage(); + } + }, [fetchNextPage, hasNextPage, inView]); + return ( {`still ); @@ -216,7 +287,7 @@ export function VideoTable(props: VideoTableProps) { } return ( - + - - )} + + +
{formatDate(startedAt)}{" "} @@ -236,16 +307,19 @@ export function VideoTable(props: VideoTableProps) { {fileSize} MB {classNames} - {thumbnail} + + + {thumbnail} + {available ? ( @@ -265,13 +339,18 @@ export function VideoTable(props: VideoTableProps) { }) ) : (
+ (No videos for the selected camera / date)
+ +
); diff --git a/frontend/src/helpers.ts b/frontend/src/helpers.ts new file mode 100644 index 0000000..114f89e --- /dev/null +++ b/frontend/src/helpers.ts @@ -0,0 +1,29 @@ +export const parseDate = (date: string): Date => { + return new Date(date); +}; + +export const formatDate = (date: Date): string => { + const pad = (s: string): string => { + if (s.length < 2) { + return `0${s}`; + } + + return s; + }; + + if (typeof date !== typeof new Date()) { + throw new Error( + `wanted ${new Date()} (${typeof new Date()}), got ${date} (${typeof date})`, + ); + } + + const year = date.getFullYear().toString(); + const month = pad((date.getMonth() + 1).toString()); + const day = pad(date.getDate().toString()); + + return `${year}-${month}-${day}`; +}; + +export const truncateDate = (date: Date): Date => { + return parseDate(formatDate(date)); +}; diff --git a/frontend/src/setupProxy.js b/frontend/src/setupProxy.js index 713e996..4fa882c 100644 --- a/frontend/src/setupProxy.js +++ b/frontend/src/setupProxy.js @@ -4,8 +4,10 @@ module.exports = function (app) { app.use( "/media", createProxyMiddleware({ - target: "http://localhost:6060", - // target: "https://camry.initialed85.cc/media", + target: + process.env.REMOTE === "1" + ? "https://camry.initialed85.cc/media" + : "http://localhost:6060", changeOrigin: true, }), ); @@ -13,8 +15,10 @@ module.exports = function (app) { app.use( "/api", createProxyMiddleware({ - target: "http://localhost:7070/api", - // target: "https://camry.initialed85.cc/api", + target: + process.env.REMOTE === "1" + ? "https://camry.initialed85.cc/api" + : "http://localhost:7070/api", changeOrigin: true, }), ); diff --git a/go.mod b/go.mod index 90a20b6..493d318 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/go-chi/chi/v5 v5.1.0 github.com/gomodule/redigo v1.9.2 github.com/google/uuid v1.6.0 - github.com/initialed85/djangolang v0.0.72 + github.com/initialed85/djangolang v0.0.77 github.com/jackc/pgx/v5 v5.6.0 github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 diff --git a/go.sum b/go.sum index 2fafde2..ac8cf14 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/initialed85/djangolang v0.0.72 h1:s4CIk+rhvSVYJLjfXkuGA+CN+nn9HCqlzqTTaddkPdE= -github.com/initialed85/djangolang v0.0.72/go.mod h1:EFbZ1Utc3A1woO6fSygSmD2XuBQIbJDTkqv79mCELrc= +github.com/initialed85/djangolang v0.0.77 h1:CUoOvrE+vrfqGI4xoCsHR3GdpmpJ0ivXK/F5/dpJ0ZY= +github.com/initialed85/djangolang v0.0.77/go.mod h1:EFbZ1Utc3A1woO6fSygSmD2XuBQIbJDTkqv79mCELrc= github.com/initialed85/structmeta v0.0.0-20240802152142-39f398ef1ab7 h1:G9Z1k4TyxQ/9Kk4ZSuw82WZCxJayZf12Aos2MorzKRg= github.com/initialed85/structmeta v0.0.0-20240802152142-39f398ef1ab7/go.mod h1:hTGWTsfgy6Um+L8e3Qcj8/pBkHGcIGxEpZAKziWhQfc= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= diff --git a/object_detector/api/.openapi-generator/FILES b/object_detector/api/.openapi-generator/FILES index 9a8f6ac..5688b8b 100644 --- a/object_detector/api/.openapi-generator/FILES +++ b/object_detector/api/.openapi-generator/FILES @@ -6,6 +6,7 @@ README.md docs/Camera.md docs/CameraApi.md +docs/Custom0Api.md docs/Detection.md docs/DetectionApi.md docs/DetectionBoundingBoxInner.md @@ -13,6 +14,8 @@ docs/GetCameras200Response.md docs/GetCamerasDefaultResponse.md docs/GetDetections200Response.md docs/GetVideos200Response.md +docs/PatchCustom0200Response.md +docs/PatchCustom0Request.md docs/Vec2.md docs/Video.md docs/VideoApi.md @@ -20,6 +23,7 @@ git_push.sh openapi_client/__init__.py openapi_client/api/__init__.py openapi_client/api/camera_api.py +openapi_client/api/custom0_api.py openapi_client/api/detection_api.py openapi_client/api/video_api.py openapi_client/api_client.py @@ -34,6 +38,8 @@ openapi_client/models/get_cameras200_response.py openapi_client/models/get_cameras_default_response.py openapi_client/models/get_detections200_response.py openapi_client/models/get_videos200_response.py +openapi_client/models/patch_custom0200_response.py +openapi_client/models/patch_custom0_request.py openapi_client/models/vec2.py openapi_client/models/video.py openapi_client/py.typed @@ -46,6 +52,7 @@ test-requirements.txt test/__init__.py test/test_camera.py test/test_camera_api.py +test/test_custom0_api.py test/test_detection.py test/test_detection_api.py test/test_detection_bounding_box_inner.py @@ -53,6 +60,8 @@ test/test_get_cameras200_response.py test/test_get_cameras_default_response.py test/test_get_detections200_response.py test/test_get_videos200_response.py +test/test_patch_custom0200_response.py +test/test_patch_custom0_request.py test/test_vec2.py test/test_video.py test/test_video_api.py diff --git a/object_detector/api/README.md b/object_detector/api/README.md index 2bb1313..49df92b 100644 --- a/object_detector/api/README.md +++ b/object_detector/api/README.md @@ -89,6 +89,7 @@ Class | Method | HTTP request | Description *CameraApi* | [**patch_camera**](docs/CameraApi.md#patch_camera) | **PATCH** /api/cameras/{primaryKey} | *CameraApi* | [**post_cameras**](docs/CameraApi.md#post_cameras) | **POST** /api/cameras | *CameraApi* | [**put_camera**](docs/CameraApi.md#put_camera) | **PUT** /api/cameras/{primaryKey} | +*Custom0Api* | [**patch_custom0**](docs/Custom0Api.md#patch_custom0) | **PATCH** /api/custom/claim-video-for-object-detector | *DetectionApi* | [**delete_detection**](docs/DetectionApi.md#delete_detection) | **DELETE** /api/detections/{primaryKey} | *DetectionApi* | [**get_detection**](docs/DetectionApi.md#get_detection) | **GET** /api/detections/{primaryKey} | *DetectionApi* | [**get_detections**](docs/DetectionApi.md#get_detections) | **GET** /api/detections | @@ -112,6 +113,8 @@ Class | Method | HTTP request | Description - [GetCamerasDefaultResponse](docs/GetCamerasDefaultResponse.md) - [GetDetections200Response](docs/GetDetections200Response.md) - [GetVideos200Response](docs/GetVideos200Response.md) + - [PatchCustom0200Response](docs/PatchCustom0200Response.md) + - [PatchCustom0Request](docs/PatchCustom0Request.md) - [Vec2](docs/Vec2.md) - [Video](docs/Video.md) diff --git a/object_detector/api/docs/Custom0Api.md b/object_detector/api/docs/Custom0Api.md new file mode 100644 index 0000000..dc12782 --- /dev/null +++ b/object_detector/api/docs/Custom0Api.md @@ -0,0 +1,76 @@ +# openapi_client.Custom0Api + +All URIs are relative to *http://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**patch_custom0**](Custom0Api.md#patch_custom0) | **PATCH** /api/custom/claim-video-for-object-detector | + + +# **patch_custom0** +> PatchCustom0200Response patch_custom0(patch_custom0_request) + + + +### Example + + +```python +import openapi_client +from openapi_client.models.patch_custom0200_response import PatchCustom0200Response +from openapi_client.models.patch_custom0_request import PatchCustom0Request +from openapi_client.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to http://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = openapi_client.Configuration( + host = "http://localhost" +) + + +# Enter a context with an instance of the API client +with openapi_client.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = openapi_client.Custom0Api(api_client) + patch_custom0_request = openapi_client.PatchCustom0Request() # PatchCustom0Request | + + try: + api_response = api_instance.patch_custom0(patch_custom0_request) + print("The response of Custom0Api->patch_custom0:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling Custom0Api->patch_custom0: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **patch_custom0_request** | [**PatchCustom0Request**](PatchCustom0Request.md)| | + +### Return type + +[**PatchCustom0200Response**](PatchCustom0200Response.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Custom0 success | - | +**0** | Custom0 failure | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/object_detector/api/docs/GetCameras200Response.md b/object_detector/api/docs/GetCameras200Response.md index 497cab7..251521b 100644 --- a/object_detector/api/docs/GetCameras200Response.md +++ b/object_detector/api/docs/GetCameras200Response.md @@ -5,10 +5,14 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**error** | **str** | | [optional] +**count** | **int** | | [optional] +**error** | **List[str]** | | [optional] +**limit** | **int** | | [optional] **objects** | [**List[Camera]**](Camera.md) | | [optional] +**offset** | **int** | | [optional] **status** | **int** | | **success** | **bool** | | +**total_count** | **int** | | [optional] ## Example diff --git a/object_detector/api/docs/GetCamerasDefaultResponse.md b/object_detector/api/docs/GetCamerasDefaultResponse.md index 87a0268..97e0acc 100644 --- a/object_detector/api/docs/GetCamerasDefaultResponse.md +++ b/object_detector/api/docs/GetCamerasDefaultResponse.md @@ -5,7 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**error** | **str** | | [optional] +**error** | **List[str]** | | [optional] **status** | **int** | | **success** | **bool** | | diff --git a/object_detector/api/docs/GetDetections200Response.md b/object_detector/api/docs/GetDetections200Response.md index 58182be..97fe27b 100644 --- a/object_detector/api/docs/GetDetections200Response.md +++ b/object_detector/api/docs/GetDetections200Response.md @@ -5,10 +5,14 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**error** | **str** | | [optional] +**count** | **int** | | [optional] +**error** | **List[str]** | | [optional] +**limit** | **int** | | [optional] **objects** | [**List[Detection]**](Detection.md) | | [optional] +**offset** | **int** | | [optional] **status** | **int** | | **success** | **bool** | | +**total_count** | **int** | | [optional] ## Example diff --git a/object_detector/api/docs/GetVideos200Response.md b/object_detector/api/docs/GetVideos200Response.md index 7a8c1ef..9a92293 100644 --- a/object_detector/api/docs/GetVideos200Response.md +++ b/object_detector/api/docs/GetVideos200Response.md @@ -5,10 +5,14 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**error** | **str** | | [optional] +**count** | **int** | | [optional] +**error** | **List[str]** | | [optional] +**limit** | **int** | | [optional] **objects** | [**List[Video]**](Video.md) | | [optional] +**offset** | **int** | | [optional] **status** | **int** | | **success** | **bool** | | +**total_count** | **int** | | [optional] ## Example diff --git a/object_detector/api/docs/PatchCustom0200Response.md b/object_detector/api/docs/PatchCustom0200Response.md new file mode 100644 index 0000000..40af4c9 --- /dev/null +++ b/object_detector/api/docs/PatchCustom0200Response.md @@ -0,0 +1,45 @@ +# PatchCustom0200Response + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**camera_id** | **str** | | [optional] +**camera_id_object** | [**Camera**](Camera.md) | | [optional] +**created_at** | **datetime** | | [optional] +**deleted_at** | **datetime** | | [optional] +**detection_summary** | **object** | | [optional] +**duration** | **int** | | [optional] +**ended_at** | **datetime** | | [optional] +**file_name** | **str** | | [optional] +**file_size** | **float** | | [optional] +**id** | **str** | | [optional] +**object_detector_claimed_until** | **datetime** | | [optional] +**object_tracker_claimed_until** | **datetime** | | [optional] +**referenced_by_detection_video_id_objects** | [**List[Detection]**](Detection.md) | | [optional] +**started_at** | **datetime** | | [optional] +**status** | **str** | | [optional] +**thumbnail_name** | **str** | | [optional] +**updated_at** | **datetime** | | [optional] + +## Example + +```python +from openapi_client.models.patch_custom0200_response import PatchCustom0200Response + +# TODO update the JSON string below +json = "{}" +# create an instance of PatchCustom0200Response from a JSON string +patch_custom0200_response_instance = PatchCustom0200Response.from_json(json) +# print the JSON string representation of the object +print(PatchCustom0200Response.to_json()) + +# convert the object into a dict +patch_custom0200_response_dict = patch_custom0200_response_instance.to_dict() +# create an instance of PatchCustom0200Response from a dict +patch_custom0200_response_from_dict = PatchCustom0200Response.from_dict(patch_custom0200_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/object_detector/api/docs/PatchCustom0Request.md b/object_detector/api/docs/PatchCustom0Request.md new file mode 100644 index 0000000..171386b --- /dev/null +++ b/object_detector/api/docs/PatchCustom0Request.md @@ -0,0 +1,29 @@ +# PatchCustom0Request + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**claim_duration_seconds** | **float** | | + +## Example + +```python +from openapi_client.models.patch_custom0_request import PatchCustom0Request + +# TODO update the JSON string below +json = "{}" +# create an instance of PatchCustom0Request from a JSON string +patch_custom0_request_instance = PatchCustom0Request.from_json(json) +# print the JSON string representation of the object +print(PatchCustom0Request.to_json()) + +# convert the object into a dict +patch_custom0_request_dict = patch_custom0_request_instance.to_dict() +# create an instance of PatchCustom0Request from a dict +patch_custom0_request_from_dict = PatchCustom0Request.from_dict(patch_custom0_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/object_detector/api/docs/Video.md b/object_detector/api/docs/Video.md index 99c1266..ade9436 100644 --- a/object_detector/api/docs/Video.md +++ b/object_detector/api/docs/Video.md @@ -9,6 +9,7 @@ Name | Type | Description | Notes **camera_id_object** | [**Camera**](Camera.md) | | [optional] **created_at** | **datetime** | | [optional] **deleted_at** | **datetime** | | [optional] +**detection_summary** | **object** | | [optional] **duration** | **int** | | [optional] **ended_at** | **datetime** | | [optional] **file_name** | **str** | | [optional] diff --git a/object_detector/api/openapi_client/__init__.py b/object_detector/api/openapi_client/__init__.py index f9b4778..227d788 100644 --- a/object_detector/api/openapi_client/__init__.py +++ b/object_detector/api/openapi_client/__init__.py @@ -18,6 +18,7 @@ # import apis into sdk package from openapi_client.api.camera_api import CameraApi +from openapi_client.api.custom0_api import Custom0Api from openapi_client.api.detection_api import DetectionApi from openapi_client.api.video_api import VideoApi @@ -40,5 +41,7 @@ from openapi_client.models.get_cameras_default_response import GetCamerasDefaultResponse from openapi_client.models.get_detections200_response import GetDetections200Response from openapi_client.models.get_videos200_response import GetVideos200Response +from openapi_client.models.patch_custom0200_response import PatchCustom0200Response +from openapi_client.models.patch_custom0_request import PatchCustom0Request from openapi_client.models.vec2 import Vec2 from openapi_client.models.video import Video diff --git a/object_detector/api/openapi_client/api/__init__.py b/object_detector/api/openapi_client/api/__init__.py index 50d61c8..506e647 100644 --- a/object_detector/api/openapi_client/api/__init__.py +++ b/object_detector/api/openapi_client/api/__init__.py @@ -2,6 +2,7 @@ # import apis into api package from openapi_client.api.camera_api import CameraApi +from openapi_client.api.custom0_api import Custom0Api from openapi_client.api.detection_api import DetectionApi from openapi_client.api.video_api import VideoApi diff --git a/object_detector/api/openapi_client/api/custom0_api.py b/object_detector/api/openapi_client/api/custom0_api.py new file mode 100644 index 0000000..c1dac44 --- /dev/null +++ b/object_detector/api/openapi_client/api/custom0_api.py @@ -0,0 +1,305 @@ +# coding: utf-8 + +""" + Djangolang + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from openapi_client.models.patch_custom0200_response import PatchCustom0200Response +from openapi_client.models.patch_custom0_request import PatchCustom0Request + +from openapi_client.api_client import ApiClient, RequestSerialized +from openapi_client.api_response import ApiResponse +from openapi_client.rest import RESTResponseType + + +class Custom0Api: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + def patch_custom0( + self, + patch_custom0_request: PatchCustom0Request, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PatchCustom0200Response: + """patch_custom0 + + + :param patch_custom0_request: (required) + :type patch_custom0_request: PatchCustom0Request + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._patch_custom0_serialize( + patch_custom0_request=patch_custom0_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PatchCustom0200Response", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def patch_custom0_with_http_info( + self, + patch_custom0_request: PatchCustom0Request, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PatchCustom0200Response]: + """patch_custom0 + + + :param patch_custom0_request: (required) + :type patch_custom0_request: PatchCustom0Request + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._patch_custom0_serialize( + patch_custom0_request=patch_custom0_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PatchCustom0200Response", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def patch_custom0_without_preload_content( + self, + patch_custom0_request: PatchCustom0Request, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """patch_custom0 + + + :param patch_custom0_request: (required) + :type patch_custom0_request: PatchCustom0Request + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._patch_custom0_serialize( + patch_custom0_request=patch_custom0_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PatchCustom0200Response", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _patch_custom0_serialize( + self, + patch_custom0_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if patch_custom0_request is not None: + _body_params = patch_custom0_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + ] + + return self.api_client.param_serialize( + method='PATCH', + resource_path='/api/custom/claim-video-for-object-detector', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/object_detector/api/openapi_client/models/__init__.py b/object_detector/api/openapi_client/models/__init__.py index 45b1542..ecc6d45 100644 --- a/object_detector/api/openapi_client/models/__init__.py +++ b/object_detector/api/openapi_client/models/__init__.py @@ -21,5 +21,7 @@ from openapi_client.models.get_cameras_default_response import GetCamerasDefaultResponse from openapi_client.models.get_detections200_response import GetDetections200Response from openapi_client.models.get_videos200_response import GetVideos200Response +from openapi_client.models.patch_custom0200_response import PatchCustom0200Response +from openapi_client.models.patch_custom0_request import PatchCustom0Request from openapi_client.models.vec2 import Vec2 from openapi_client.models.video import Video diff --git a/object_detector/api/openapi_client/models/get_cameras200_response.py b/object_detector/api/openapi_client/models/get_cameras200_response.py index 0c4ffd0..264be89 100644 --- a/object_detector/api/openapi_client/models/get_cameras200_response.py +++ b/object_detector/api/openapi_client/models/get_cameras200_response.py @@ -27,11 +27,15 @@ class GetCameras200Response(BaseModel): """ GetCameras200Response """ # noqa: E501 - error: Optional[StrictStr] = None + count: Optional[StrictInt] = None + error: Optional[List[StrictStr]] = None + limit: Optional[StrictInt] = None objects: Optional[List[Camera]] = None + offset: Optional[StrictInt] = None status: StrictInt success: StrictBool - __properties: ClassVar[List[str]] = ["error", "objects", "status", "success"] + total_count: Optional[StrictInt] = None + __properties: ClassVar[List[str]] = ["count", "error", "limit", "objects", "offset", "status", "success", "total_count"] model_config = ConfigDict( populate_by_name=True, @@ -91,10 +95,14 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ + "count": obj.get("count"), "error": obj.get("error"), + "limit": obj.get("limit"), "objects": [Camera.from_dict(_item) for _item in obj["objects"]] if obj.get("objects") is not None else None, + "offset": obj.get("offset"), "status": obj.get("status"), - "success": obj.get("success") + "success": obj.get("success"), + "total_count": obj.get("total_count") }) return _obj diff --git a/object_detector/api/openapi_client/models/get_cameras_default_response.py b/object_detector/api/openapi_client/models/get_cameras_default_response.py index c4bfceb..4d99a32 100644 --- a/object_detector/api/openapi_client/models/get_cameras_default_response.py +++ b/object_detector/api/openapi_client/models/get_cameras_default_response.py @@ -26,7 +26,7 @@ class GetCamerasDefaultResponse(BaseModel): """ GetCamerasDefaultResponse """ # noqa: E501 - error: Optional[StrictStr] = None + error: Optional[List[StrictStr]] = None status: StrictInt success: StrictBool __properties: ClassVar[List[str]] = ["error", "status", "success"] diff --git a/object_detector/api/openapi_client/models/get_detections200_response.py b/object_detector/api/openapi_client/models/get_detections200_response.py index 30c9928..5361769 100644 --- a/object_detector/api/openapi_client/models/get_detections200_response.py +++ b/object_detector/api/openapi_client/models/get_detections200_response.py @@ -27,11 +27,15 @@ class GetDetections200Response(BaseModel): """ GetDetections200Response """ # noqa: E501 - error: Optional[StrictStr] = None + count: Optional[StrictInt] = None + error: Optional[List[StrictStr]] = None + limit: Optional[StrictInt] = None objects: Optional[List[Detection]] = None + offset: Optional[StrictInt] = None status: StrictInt success: StrictBool - __properties: ClassVar[List[str]] = ["error", "objects", "status", "success"] + total_count: Optional[StrictInt] = None + __properties: ClassVar[List[str]] = ["count", "error", "limit", "objects", "offset", "status", "success", "total_count"] model_config = ConfigDict( populate_by_name=True, @@ -91,10 +95,14 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ + "count": obj.get("count"), "error": obj.get("error"), + "limit": obj.get("limit"), "objects": [Detection.from_dict(_item) for _item in obj["objects"]] if obj.get("objects") is not None else None, + "offset": obj.get("offset"), "status": obj.get("status"), - "success": obj.get("success") + "success": obj.get("success"), + "total_count": obj.get("total_count") }) return _obj diff --git a/object_detector/api/openapi_client/models/get_videos200_response.py b/object_detector/api/openapi_client/models/get_videos200_response.py index 7f9ca1b..1418ab2 100644 --- a/object_detector/api/openapi_client/models/get_videos200_response.py +++ b/object_detector/api/openapi_client/models/get_videos200_response.py @@ -27,11 +27,15 @@ class GetVideos200Response(BaseModel): """ GetVideos200Response """ # noqa: E501 - error: Optional[StrictStr] = None + count: Optional[StrictInt] = None + error: Optional[List[StrictStr]] = None + limit: Optional[StrictInt] = None objects: Optional[List[Video]] = None + offset: Optional[StrictInt] = None status: StrictInt success: StrictBool - __properties: ClassVar[List[str]] = ["error", "objects", "status", "success"] + total_count: Optional[StrictInt] = None + __properties: ClassVar[List[str]] = ["count", "error", "limit", "objects", "offset", "status", "success", "total_count"] model_config = ConfigDict( populate_by_name=True, @@ -91,10 +95,14 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ + "count": obj.get("count"), "error": obj.get("error"), + "limit": obj.get("limit"), "objects": [Video.from_dict(_item) for _item in obj["objects"]] if obj.get("objects") is not None else None, + "offset": obj.get("offset"), "status": obj.get("status"), - "success": obj.get("success") + "success": obj.get("success"), + "total_count": obj.get("total_count") }) return _obj diff --git a/object_detector/api/openapi_client/models/patch_custom0200_response.py b/object_detector/api/openapi_client/models/patch_custom0200_response.py new file mode 100644 index 0000000..a10a389 --- /dev/null +++ b/object_detector/api/openapi_client/models/patch_custom0200_response.py @@ -0,0 +1,162 @@ +# coding: utf-8 + +""" + Djangolang + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict, StrictFloat, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional, Union +from openapi_client.models.camera import Camera +from openapi_client.models.detection import Detection +from typing import Optional, Set +from typing_extensions import Self + +class PatchCustom0200Response(BaseModel): + """ + PatchCustom0200Response + """ # noqa: E501 + camera_id: Optional[StrictStr] = None + camera_id_object: Optional[Camera] = None + created_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + detection_summary: Optional[Dict[str, Any]] = None + duration: Optional[StrictInt] = None + ended_at: Optional[datetime] = None + file_name: Optional[StrictStr] = None + file_size: Optional[Union[StrictFloat, StrictInt]] = None + id: Optional[StrictStr] = None + object_detector_claimed_until: Optional[datetime] = None + object_tracker_claimed_until: Optional[datetime] = None + referenced_by_detection_video_id_objects: Optional[List[Detection]] = None + started_at: Optional[datetime] = None + status: Optional[StrictStr] = None + thumbnail_name: Optional[StrictStr] = None + updated_at: Optional[datetime] = None + __properties: ClassVar[List[str]] = ["camera_id", "camera_id_object", "created_at", "deleted_at", "detection_summary", "duration", "ended_at", "file_name", "file_size", "id", "object_detector_claimed_until", "object_tracker_claimed_until", "referenced_by_detection_video_id_objects", "started_at", "status", "thumbnail_name", "updated_at"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PatchCustom0200Response from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of camera_id_object + if self.camera_id_object: + _dict['camera_id_object'] = self.camera_id_object.to_dict() + # override the default output from pydantic by calling `to_dict()` of each item in referenced_by_detection_video_id_objects (list) + _items = [] + if self.referenced_by_detection_video_id_objects: + for _item in self.referenced_by_detection_video_id_objects: + if _item: + _items.append(_item.to_dict()) + _dict['referenced_by_detection_video_id_objects'] = _items + # set to None if deleted_at (nullable) is None + # and model_fields_set contains the field + if self.deleted_at is None and "deleted_at" in self.model_fields_set: + _dict['deleted_at'] = None + + # set to None if detection_summary (nullable) is None + # and model_fields_set contains the field + if self.detection_summary is None and "detection_summary" in self.model_fields_set: + _dict['detection_summary'] = None + + # set to None if duration (nullable) is None + # and model_fields_set contains the field + if self.duration is None and "duration" in self.model_fields_set: + _dict['duration'] = None + + # set to None if ended_at (nullable) is None + # and model_fields_set contains the field + if self.ended_at is None and "ended_at" in self.model_fields_set: + _dict['ended_at'] = None + + # set to None if file_size (nullable) is None + # and model_fields_set contains the field + if self.file_size is None and "file_size" in self.model_fields_set: + _dict['file_size'] = None + + # set to None if referenced_by_detection_video_id_objects (nullable) is None + # and model_fields_set contains the field + if self.referenced_by_detection_video_id_objects is None and "referenced_by_detection_video_id_objects" in self.model_fields_set: + _dict['referenced_by_detection_video_id_objects'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PatchCustom0200Response from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "camera_id": obj.get("camera_id"), + "camera_id_object": Camera.from_dict(obj["camera_id_object"]) if obj.get("camera_id_object") is not None else None, + "created_at": obj.get("created_at"), + "deleted_at": obj.get("deleted_at"), + "detection_summary": obj.get("detection_summary"), + "duration": obj.get("duration"), + "ended_at": obj.get("ended_at"), + "file_name": obj.get("file_name"), + "file_size": obj.get("file_size"), + "id": obj.get("id"), + "object_detector_claimed_until": obj.get("object_detector_claimed_until"), + "object_tracker_claimed_until": obj.get("object_tracker_claimed_until"), + "referenced_by_detection_video_id_objects": [Detection.from_dict(_item) for _item in obj["referenced_by_detection_video_id_objects"]] if obj.get("referenced_by_detection_video_id_objects") is not None else None, + "started_at": obj.get("started_at"), + "status": obj.get("status"), + "thumbnail_name": obj.get("thumbnail_name"), + "updated_at": obj.get("updated_at") + }) + return _obj + + diff --git a/object_detector/api/openapi_client/models/patch_custom0_request.py b/object_detector/api/openapi_client/models/patch_custom0_request.py new file mode 100644 index 0000000..998fadf --- /dev/null +++ b/object_detector/api/openapi_client/models/patch_custom0_request.py @@ -0,0 +1,87 @@ +# coding: utf-8 + +""" + Djangolang + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictFloat, StrictInt +from typing import Any, ClassVar, Dict, List, Union +from typing import Optional, Set +from typing_extensions import Self + +class PatchCustom0Request(BaseModel): + """ + PatchCustom0Request + """ # noqa: E501 + claim_duration_seconds: Union[StrictFloat, StrictInt] + __properties: ClassVar[List[str]] = ["claim_duration_seconds"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PatchCustom0Request from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PatchCustom0Request from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "claim_duration_seconds": obj.get("claim_duration_seconds") + }) + return _obj + + diff --git a/object_detector/api/openapi_client/models/video.py b/object_detector/api/openapi_client/models/video.py index c306609..456799d 100644 --- a/object_detector/api/openapi_client/models/video.py +++ b/object_detector/api/openapi_client/models/video.py @@ -31,6 +31,7 @@ class Video(BaseModel): camera_id_object: Optional[Camera] = None created_at: Optional[datetime] = None deleted_at: Optional[datetime] = None + detection_summary: Optional[Dict[str, Any]] = None duration: Optional[StrictInt] = None ended_at: Optional[datetime] = None file_name: Optional[StrictStr] = None @@ -43,7 +44,7 @@ class Video(BaseModel): status: Optional[StrictStr] = None thumbnail_name: Optional[StrictStr] = None updated_at: Optional[datetime] = None - __properties: ClassVar[List[str]] = ["camera_id", "camera_id_object", "created_at", "deleted_at", "duration", "ended_at", "file_name", "file_size", "id", "object_detector_claimed_until", "object_tracker_claimed_until", "referenced_by_detection_video_id_objects", "started_at", "status", "thumbnail_name", "updated_at"] + __properties: ClassVar[List[str]] = ["camera_id", "camera_id_object", "created_at", "deleted_at", "detection_summary", "duration", "ended_at", "file_name", "file_size", "id", "object_detector_claimed_until", "object_tracker_claimed_until", "referenced_by_detection_video_id_objects", "started_at", "status", "thumbnail_name", "updated_at"] model_config = ConfigDict( populate_by_name=True, @@ -99,6 +100,11 @@ def to_dict(self) -> Dict[str, Any]: if self.deleted_at is None and "deleted_at" in self.model_fields_set: _dict['deleted_at'] = None + # set to None if detection_summary (nullable) is None + # and model_fields_set contains the field + if self.detection_summary is None and "detection_summary" in self.model_fields_set: + _dict['detection_summary'] = None + # set to None if duration (nullable) is None # and model_fields_set contains the field if self.duration is None and "duration" in self.model_fields_set: @@ -135,6 +141,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "camera_id_object": Camera.from_dict(obj["camera_id_object"]) if obj.get("camera_id_object") is not None else None, "created_at": obj.get("created_at"), "deleted_at": obj.get("deleted_at"), + "detection_summary": obj.get("detection_summary"), "duration": obj.get("duration"), "ended_at": obj.get("ended_at"), "file_name": obj.get("file_name"), diff --git a/object_detector/api/test/test_camera.py b/object_detector/api/test/test_camera.py index b0a9cdd..8263977 100644 --- a/object_detector/api/test/test_camera.py +++ b/object_detector/api/test/test_camera.py @@ -73,6 +73,7 @@ def make_instance(self, include_optional) -> Camera: camera_id = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -93,6 +94,7 @@ def make_instance(self, include_optional) -> Camera: camera_id = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -154,6 +156,7 @@ def make_instance(self, include_optional) -> Camera: camera_id = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -185,6 +188,7 @@ def make_instance(self, include_optional) -> Camera: updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', diff --git a/object_detector/api/test/test_custom0_api.py b/object_detector/api/test/test_custom0_api.py new file mode 100644 index 0000000..d7c7316 --- /dev/null +++ b/object_detector/api/test/test_custom0_api.py @@ -0,0 +1,37 @@ +# coding: utf-8 + +""" + Djangolang + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from openapi_client.api.custom0_api import Custom0Api + + +class TestCustom0Api(unittest.TestCase): + """Custom0Api unit test stubs""" + + def setUp(self) -> None: + self.api = Custom0Api() + + def tearDown(self) -> None: + pass + + def test_patch_custom0(self) -> None: + """Test case for patch_custom0 + + """ + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/object_detector/api/test/test_detection.py b/object_detector/api/test/test_detection.py index 7ab1429..8cfc0c2 100644 --- a/object_detector/api/test/test_detection.py +++ b/object_detector/api/test/test_detection.py @@ -67,6 +67,7 @@ def make_instance(self, include_optional) -> Detection: camera_id_object = , created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -93,6 +94,7 @@ def make_instance(self, include_optional) -> Detection: camera_id = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -177,6 +179,7 @@ def make_instance(self, include_optional) -> Detection: camera_id = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -212,6 +215,7 @@ def make_instance(self, include_optional) -> Detection: updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', diff --git a/object_detector/api/test/test_get_cameras200_response.py b/object_detector/api/test/test_get_cameras200_response.py index 53a7e4b..d922243 100644 --- a/object_detector/api/test/test_get_cameras200_response.py +++ b/object_detector/api/test/test_get_cameras200_response.py @@ -35,7 +35,11 @@ def make_instance(self, include_optional) -> GetCameras200Response: model = GetCameras200Response() if include_optional: return GetCameras200Response( - error = '', + count = 56, + error = [ + '' + ], + limit = 56, objects = [ openapi_client.models.camera.Camera( created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), @@ -63,6 +67,7 @@ def make_instance(self, include_optional) -> GetCameras200Response: camera_id_object = , created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -91,6 +96,7 @@ def make_instance(self, include_optional) -> GetCameras200Response: camera_id_object = , created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -133,8 +139,10 @@ def make_instance(self, include_optional) -> GetCameras200Response: stream_url = '', updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ) ], + offset = 56, status = 56, - success = True + success = True, + total_count = 56 ) else: return GetCameras200Response( diff --git a/object_detector/api/test/test_get_cameras_default_response.py b/object_detector/api/test/test_get_cameras_default_response.py index 3f22701..9da77fd 100644 --- a/object_detector/api/test/test_get_cameras_default_response.py +++ b/object_detector/api/test/test_get_cameras_default_response.py @@ -35,7 +35,9 @@ def make_instance(self, include_optional) -> GetCamerasDefaultResponse: model = GetCamerasDefaultResponse() if include_optional: return GetCamerasDefaultResponse( - error = '', + error = [ + '' + ], status = 56, success = True ) diff --git a/object_detector/api/test/test_get_detections200_response.py b/object_detector/api/test/test_get_detections200_response.py index dc48945..7533207 100644 --- a/object_detector/api/test/test_get_detections200_response.py +++ b/object_detector/api/test/test_get_detections200_response.py @@ -35,7 +35,11 @@ def make_instance(self, include_optional) -> GetDetections200Response: model = GetDetections200Response() if include_optional: return GetDetections200Response( - error = '', + count = 56, + error = [ + '' + ], + limit = 56, objects = [ openapi_client.models.detection.Detection( bounding_box = [ @@ -65,6 +69,7 @@ def make_instance(self, include_optional) -> GetDetections200Response: camera_id_object = , created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -101,6 +106,7 @@ def make_instance(self, include_optional) -> GetDetections200Response: camera_id_object = , created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -133,8 +139,10 @@ def make_instance(self, include_optional) -> GetDetections200Response: video_id = '', video_id_object = , ) ], + offset = 56, status = 56, - success = True + success = True, + total_count = 56 ) else: return GetDetections200Response( diff --git a/object_detector/api/test/test_get_videos200_response.py b/object_detector/api/test/test_get_videos200_response.py index 1a97631..fea9b75 100644 --- a/object_detector/api/test/test_get_videos200_response.py +++ b/object_detector/api/test/test_get_videos200_response.py @@ -35,7 +35,11 @@ def make_instance(self, include_optional) -> GetVideos200Response: model = GetVideos200Response() if include_optional: return GetVideos200Response( - error = '', + count = 56, + error = [ + '' + ], + limit = 56, objects = [ openapi_client.models.video.Video( camera_id = '', @@ -65,6 +69,7 @@ def make_instance(self, include_optional) -> GetVideos200Response: camera_id_object = , created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -120,6 +125,7 @@ def make_instance(self, include_optional) -> GetVideos200Response: updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -133,8 +139,10 @@ def make_instance(self, include_optional) -> GetVideos200Response: thumbnail_name = '', updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ) ], + offset = 56, status = 56, - success = True + success = True, + total_count = 56 ) else: return GetVideos200Response( diff --git a/object_detector/api/test/test_patch_custom0200_response.py b/object_detector/api/test/test_patch_custom0200_response.py new file mode 100644 index 0000000..45a48a9 --- /dev/null +++ b/object_detector/api/test/test_patch_custom0200_response.py @@ -0,0 +1,252 @@ +# coding: utf-8 + +""" + Djangolang + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from openapi_client.models.patch_custom0200_response import PatchCustom0200Response + +class TestPatchCustom0200Response(unittest.TestCase): + """PatchCustom0200Response unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PatchCustom0200Response: + """Test PatchCustom0200Response + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PatchCustom0200Response` + """ + model = PatchCustom0200Response() + if include_optional: + return PatchCustom0200Response( + camera_id = '', + camera_id_object = openapi_client.models.camera.Camera( + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + id = '', + last_seen = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + name = '', + referenced_by_detection_camera_id_objects = [ + openapi_client.models.detection.Detection( + bounding_box = [ + openapi_client.models.detection_bounding_box_inner.Detection_bounding_box_inner( + x = 1.337, + y = 1.337, ) + ], + camera_id = '', + camera_id_object = openapi_client.models.camera.Camera( + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + id = '', + last_seen = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + name = '', + referenced_by_video_camera_id_objects = [ + openapi_client.models.video.Video( + camera_id = '', + camera_id_object = , + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), + duration = 56, + ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + file_name = '', + file_size = 1.337, + id = '', + object_detector_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + object_tracker_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + referenced_by_detection_video_id_objects = [ + openapi_client.models.detection.Detection( + camera_id = '', + centroid = openapi_client.models.detection_bounding_box_inner.Detection_bounding_box_inner( + x = 1.337, + y = 1.337, ), + class_id = 56, + class_name = '', + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + id = '', + score = 1.337, + seen_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + video_id = '', + video_id_object = openapi_client.models.video.Video( + camera_id = '', + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), + duration = 56, + ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + file_name = '', + file_size = 1.337, + id = '', + object_detector_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + object_tracker_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + started_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + status = '', + thumbnail_name = '', + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), ) + ], + started_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + status = '', + thumbnail_name = '', + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ) + ], + segment_producer_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + stream_producer_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + stream_url = '', + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), + centroid = , + class_id = 56, + class_name = '', + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + id = '', + score = 1.337, + seen_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + video_id = '', + video_id_object = , ) + ], + referenced_by_video_camera_id_objects = [ + + ], + segment_producer_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + stream_producer_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + stream_url = '', + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = None, + duration = 56, + ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + file_name = '', + file_size = 1.337, + id = '', + object_detector_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + object_tracker_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + referenced_by_detection_video_id_objects = [ + openapi_client.models.detection.Detection( + bounding_box = [ + openapi_client.models.detection_bounding_box_inner.Detection_bounding_box_inner( + x = 1.337, + y = 1.337, ) + ], + camera_id = '', + camera_id_object = openapi_client.models.camera.Camera( + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + id = '', + last_seen = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + name = '', + referenced_by_video_camera_id_objects = [ + openapi_client.models.video.Video( + camera_id = '', + camera_id_object = openapi_client.models.camera.Camera( + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + id = '', + last_seen = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + name = '', + segment_producer_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + stream_producer_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + stream_url = '', + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), + duration = 56, + ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + file_name = '', + file_size = 1.337, + id = '', + object_detector_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + object_tracker_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + referenced_by_detection_video_id_objects = [ + openapi_client.models.detection.Detection( + camera_id = '', + camera_id_object = , + centroid = openapi_client.models.detection_bounding_box_inner.Detection_bounding_box_inner( + x = 1.337, + y = 1.337, ), + class_id = 56, + class_name = '', + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + id = '', + score = 1.337, + seen_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + video_id = '', + video_id_object = openapi_client.models.video.Video( + camera_id = '', + camera_id_object = , + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), + duration = 56, + ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + file_name = '', + file_size = 1.337, + id = '', + object_detector_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + object_tracker_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + referenced_by_detection_video_id_objects = , + started_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + status = '', + thumbnail_name = '', + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), ) + ], + started_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + status = '', + thumbnail_name = '', + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ) + ], + segment_producer_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + stream_producer_claimed_until = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + stream_url = '', + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), + centroid = , + class_id = 56, + class_name = '', + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + id = '', + score = 1.337, + seen_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + video_id = '', + video_id_object = , ) + ], + started_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + status = '', + thumbnail_name = '', + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f') + ) + else: + return PatchCustom0200Response( + ) + """ + + def testPatchCustom0200Response(self): + """Test PatchCustom0200Response""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/object_detector/api/test/test_patch_custom0_request.py b/object_detector/api/test/test_patch_custom0_request.py new file mode 100644 index 0000000..bd96c81 --- /dev/null +++ b/object_detector/api/test/test_patch_custom0_request.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Djangolang + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from openapi_client.models.patch_custom0_request import PatchCustom0Request + +class TestPatchCustom0Request(unittest.TestCase): + """PatchCustom0Request unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PatchCustom0Request: + """Test PatchCustom0Request + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PatchCustom0Request` + """ + model = PatchCustom0Request() + if include_optional: + return PatchCustom0Request( + claim_duration_seconds = 1.337 + ) + else: + return PatchCustom0Request( + claim_duration_seconds = 1.337, + ) + """ + + def testPatchCustom0Request(self): + """Test PatchCustom0Request""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/object_detector/api/test/test_video.py b/object_detector/api/test/test_video.py index 369f8c8..8cfc88e 100644 --- a/object_detector/api/test/test_video.py +++ b/object_detector/api/test/test_video.py @@ -62,6 +62,7 @@ def make_instance(self, include_optional) -> Video: camera_id_object = , created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -88,6 +89,7 @@ def make_instance(self, include_optional) -> Video: camera_id = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -130,6 +132,7 @@ def make_instance(self, include_optional) -> Video: updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -166,6 +169,7 @@ def make_instance(self, include_optional) -> Video: updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', @@ -194,6 +198,7 @@ def make_instance(self, include_optional) -> Video: camera_id_object = , created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), deleted_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + detection_summary = openapi_client.models.detection_summary.detection_summary(), duration = 56, ended_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), file_name = '', diff --git a/object_detector/object_detector.py b/object_detector/object_detector.py index fd429e6..6c2c31b 100644 --- a/object_detector/object_detector.py +++ b/object_detector/object_detector.py @@ -7,6 +7,8 @@ from ultralytics import YOLO from ultralytics.models.yolo.detect.predict import Results +from object_detector.api.openapi_client.models.patch_custom0_request import PatchCustom0Request + from .api.openapi_client import ( ApiClient, Configuration, @@ -17,33 +19,38 @@ DetectionApi, Detection, DetectionBoundingBoxInner, + Custom0Api, + PatchCustom0Request, ) debug = os.getenv("DEBUG", "") == "1" def do( - camera: Camera, + camera_api: CameraApi, video_api: VideoApi, detection_api: DetectionApi, + custom0_api: Custom0Api, source_path: str, one_shot_video_file_name: str | None = None, ): - videos_response = None + videos = [] if one_shot_video_file_name: videos_response = video_api.get_videos( file_name__eq=one_shot_video_file_name, started_at__desc="", ) + + videos = videos_response.objects else: - videos_response = video_api.get_videos( - camera_id__eq=camera.id, - status__eq="needs detection", - started_at__desc="", + video = custom0_api.patch_custom0( + PatchCustom0Request( + claim_duration_seconds=300, + ) ) - videos = videos_response.objects or [] + videos = [video] if not videos: return @@ -55,7 +62,7 @@ def do( if one_shot_video_file_name: detections_response = detection_api.get_detections( - camera_id__eq=camera.id, + camera_id__eq=video.camera_id, video_id__eq=video.id, ) @@ -153,14 +160,6 @@ def do( def run(): - net_cam_url = os.getenv("NET_CAM_URL") - if not net_cam_url: - raise ValueError("NET_CAM_URL env var empty or unset") - - camera_name = os.getenv("CAMERA_NAME") - if not camera_name: - raise ValueError("CAMERA_NAME env var empty or unset") - api_url = os.getenv("API_URL") if not api_url: raise ValueError("API_URL env var empty or unset") @@ -176,26 +175,11 @@ def run(): camera_api = CameraApi(api_client) video_api = VideoApi(api_client) detection_api = DetectionApi(api_client) - - cameras_response = camera_api.get_cameras( - stream_url__eq=net_cam_url, - name__eq=camera_name, - ) - - cameras = cameras_response.objects or [] - - if not cameras or len(cameras) != 1: - raise ValueError( - f"failed to find exactly 1 camera for NET_CAM_URL={repr(net_cam_url)} CAMERA_NAME={repr(camera_name)}" - ) - - camera = cameras[0] - - print(f"camera: {camera.id}, {camera.name}, {camera.stream_url}") + custom0_api = Custom0Api(api_client) while 1: try: - do(camera, video_api, detection_api, source_path, one_shot_file_name) + do(camera_api, video_api, detection_api, custom0_api, source_path, one_shot_file_name) if one_shot_file_name: break diff --git a/pkg/api/0_app.go b/pkg/api/0_app.go index 835bd70..a4a42ea 100755 --- a/pkg/api/0_app.go +++ b/pkg/api/0_app.go @@ -6,6 +6,7 @@ import ( "fmt" "log" + "github.com/go-chi/chi/v5" "github.com/gomodule/redigo/redis" "github.com/initialed85/djangolang/pkg/helpers" "github.com/initialed85/djangolang/pkg/server" @@ -16,12 +17,12 @@ import ( func RunDumpOpenAPIJSON() { openApi, err := GetOpenAPI() if err != nil { - log.Fatalf("err: %v", err) + log.Fatalf("%v", err) } b, err := json.MarshalIndent(openApi, "", " ") if err != nil { - log.Fatalf("err: %v", err) + log.Fatalf("%v", err) } fmt.Printf("%v", string(b)) @@ -30,12 +31,12 @@ func RunDumpOpenAPIJSON() { func RunDumpOpenAPIYAML() { openApi, err := GetOpenAPI() if err != nil { - log.Fatalf("err: %v", err) + log.Fatalf("%v", err) } b, err := yaml.Marshal(openApi) if err != nil { - log.Fatalf("err: %v", err) + log.Fatalf("%v", err) } fmt.Printf("%v", string(b)) @@ -49,7 +50,7 @@ func RunServeWithArguments( redisPool *redis.Pool, httpMiddlewares []server.HTTPMiddleware, objectMiddlewares []server.ObjectMiddleware, - customHandlers []server.CustomHTTPHandler[any, any, any, any], + addCustomHandlers func(chi.Router) error, ) { defer cancel() @@ -58,24 +59,28 @@ func RunServeWithArguments( cancel() }() - err := RunServer(ctx, nil, fmt.Sprintf("0.0.0.0:%v", port), db, redisPool, nil, nil, nil) + err := RunServer(ctx, nil, fmt.Sprintf("0.0.0.0:%v", port), db, redisPool, httpMiddlewares, objectMiddlewares, addCustomHandlers) if err != nil { - log.Fatalf("err: %v", err) + log.Fatalf("%v", err) } } -func RunServeWithEnvironment() { +func RunServeWithEnvironment( + httpMiddlewares []server.HTTPMiddleware, + objectMiddlewares []server.ObjectMiddleware, + addCustomHandlers func(chi.Router) error, +) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() port, err := helpers.GetPort() if err != nil { - log.Fatalf("err: %v", err) + log.Fatalf("%v", err) } db, err := helpers.GetDBFromEnvironment(ctx) if err != nil { - log.Fatalf("err: %v", err) + log.Fatalf("%v", err) } defer func() { db.Close() @@ -88,7 +93,7 @@ func RunServeWithEnvironment() { redisURL, err := helpers.GetRedisURL() if err != nil { - log.Fatalf("err: %v", err) + log.Fatalf("%v", err) } redisPool := &redis.Pool{ diff --git a/pkg/api/0_meta.go b/pkg/api/0_meta.go index 23641e0..6e68296 100755 --- a/pkg/api/0_meta.go +++ b/pkg/api/0_meta.go @@ -28,6 +28,8 @@ var allObjects = make([]any, 0) var openApi *types.OpenAPI var profile = helpers.GetEnvironmentVariable("DJANGOLANG_PROFILE") == "1" +var customHTTPHandlerSummaries []openapi.CustomHTTPHandlerSummary = make([]openapi.CustomHTTPHandlerSummary, 0) + func isRequired(columns map[string]*introspect.Column, columnName string) bool { column := columns[columnName] if column == nil { @@ -58,7 +60,7 @@ func GetOpenAPI() (*types.OpenAPI, error) { } var err error - openApi, err = openapi.NewFromIntrospectedSchema(allObjects) + openApi, err = openapi.NewFromIntrospectedSchema(allObjects, customHTTPHandlerSummaries) if err != nil { return nil, err } @@ -110,12 +112,12 @@ func GetRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []server lastHealthz = func() error { err := db.Ping(ctx) if err != nil { - return fmt.Errorf("db ping failed: %v", err) + return fmt.Errorf("db ping failed; %v", err) } redisConn, err := redisPool.GetContext(ctx) if err != nil { - return fmt.Errorf("redis pool get failed: %v", err) + return fmt.Errorf("redis pool get failed; %v", err) } defer func() { @@ -124,7 +126,7 @@ func GetRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []server _, err = redisConn.Do("PING") if err != nil { - return fmt.Errorf("redis ping failed: %v", err) + return fmt.Errorf("redis ping failed; %v", err) } return nil @@ -157,11 +159,11 @@ func GetRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []server err := healthz(ctx) if err != nil { - helpers.HandleErrorResponse(w, http.StatusInternalServerError, err) + server.HandleErrorResponse(w, http.StatusInternalServerError, err) return } - helpers.HandleObjectsResponse(w, http.StatusOK, nil) + server.HandleObjectsResponse(w, http.StatusOK, nil) }) r.Get("/openapi.json", func(w http.ResponseWriter, r *http.Request) { @@ -169,17 +171,17 @@ func GetRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []server openApi, err := GetOpenAPI() if err != nil { - helpers.HandleErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("failed to get OpenAPI schema: %v", err)) + server.HandleErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("failed to get OpenAPI schema; %v", err)) return } b, err := json.MarshalIndent(openApi, "", " ") if err != nil { - helpers.HandleErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("failed to get OpenAPI schema: %v", err)) + server.HandleErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("failed to get OpenAPI schema; %v", err)) return } - helpers.WriteResponse(w, http.StatusOK, b) + server.WriteResponse(w, http.StatusOK, b) }) r.Get("/openapi.yaml", func(w http.ResponseWriter, r *http.Request) { @@ -187,22 +189,41 @@ func GetRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []server openApi, err := GetOpenAPI() if err != nil { - helpers.HandleErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("failed to get OpenAPI schema: %v", err)) + server.HandleErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("failed to get OpenAPI schema; %v", err)) return } b, err := yaml.Marshal(openApi) if err != nil { - helpers.HandleErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("failed to get OpenAPI schema: %v", err)) + server.HandleErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("failed to get OpenAPI schema; %v", err)) return } - helpers.WriteResponse(w, http.StatusOK, b) + server.WriteResponse(w, http.StatusOK, b) }) return r } +func GetCustomHTTPHandler[T any, S any, Q any, R any](method string, path string, status int, handle func(context.Context, T, S, Q, any) (*R, error)) (*server.CustomHTTPHandler[T, S, Q, R], error) { + customHTTPHandler, err := server.GetCustomHTTPHandler(method, path, status, handle) + if err != nil { + return nil, err + } + + customHTTPHandlerSummaries = append(customHTTPHandlerSummaries, openapi.CustomHTTPHandlerSummary{ + PathParams: customHTTPHandler.PathParams, + QueryParams: customHTTPHandler.QueryParams, + Request: customHTTPHandler.Request, + Response: customHTTPHandler.Response, + Method: method, + Path: path, + Status: status, + }) + + return customHTTPHandler, nil +} + func RunServer( ctx context.Context, changes chan server.Change, @@ -1013,6 +1034,26 @@ var tableByNameAsJSON = []byte(`{ "query_type_template": "uuid.UUID", "stream_type_template": "[16]uint8", "type_template": "uuid.UUID" + }, + { + "column": "detection_summary", + "datatype": "jsonb", + "table": "video", + "pos": 15, + "typeid": "3802", + "typelen": -1, + "typemod": -1, + "notnull": true, + "hasdefault": true, + "hasmissing": true, + "ispkey": false, + "ftable": null, + "fcolumn": null, + "parent_id": "20331", + "zero_type": null, + "query_type_template": "any", + "stream_type_template": "any", + "type_template": "any" } ] } @@ -1023,6 +1064,6 @@ var tableByName introspect.TableByName func init() { err := json.Unmarshal(tableByNameAsJSON, &tableByName) if err != nil { - panic(fmt.Errorf("failed to unmarshal tableByNameAsJSON into introspect.TableByName: %v", err)) + panic(fmt.Errorf("failed to unmarshal tableByNameAsJSON into introspect.TableByName; %v", err)) } } diff --git a/pkg/api/bin/api b/pkg/api/bin/api index 77ebab3..cd579a9 100755 Binary files a/pkg/api/bin/api and b/pkg/api/bin/api differ diff --git a/pkg/api/camera.go b/pkg/api/camera.go index 25f7140..76bbc59 100755 --- a/pkg/api/camera.go +++ b/pkg/api/camera.go @@ -39,8 +39,8 @@ type Camera struct { LastSeen time.Time `json:"last_seen"` SegmentProducerClaimedUntil time.Time `json:"segment_producer_claimed_until"` StreamProducerClaimedUntil time.Time `json:"stream_producer_claimed_until"` - ReferencedByVideoCameraIDObjects []*Video `json:"referenced_by_video_camera_id_objects"` ReferencedByDetectionCameraIDObjects []*Detection `json:"referenced_by_detection_camera_id_objects"` + ReferencedByVideoCameraIDObjects []*Video `json:"referenced_by_video_camera_id_objects"` } var CameraTable = "camera" @@ -359,7 +359,7 @@ func (m *Camera) Reload(ctx context.Context, tx pgx.Tx, includeDeleteds ...bool) ctx, cleanup := query.WithQueryID(ctx) defer cleanup() - t, err := SelectCamera( + o, _, _, _, _, err := SelectCamera( ctx, tx, fmt.Sprintf("%v = $1%v", m.GetPrimaryKeyColumn(), extraWhere), @@ -369,17 +369,17 @@ func (m *Camera) Reload(ctx context.Context, tx pgx.Tx, includeDeleteds ...bool) return err } - m.ID = t.ID - m.CreatedAt = t.CreatedAt - m.UpdatedAt = t.UpdatedAt - m.DeletedAt = t.DeletedAt - m.Name = t.Name - m.StreamURL = t.StreamURL - m.LastSeen = t.LastSeen - m.SegmentProducerClaimedUntil = t.SegmentProducerClaimedUntil - m.StreamProducerClaimedUntil = t.StreamProducerClaimedUntil - m.ReferencedByVideoCameraIDObjects = t.ReferencedByVideoCameraIDObjects - m.ReferencedByDetectionCameraIDObjects = t.ReferencedByDetectionCameraIDObjects + m.ID = o.ID + m.CreatedAt = o.CreatedAt + m.UpdatedAt = o.UpdatedAt + m.DeletedAt = o.DeletedAt + m.Name = o.Name + m.StreamURL = o.StreamURL + m.LastSeen = o.LastSeen + m.SegmentProducerClaimedUntil = o.SegmentProducerClaimedUntil + m.StreamProducerClaimedUntil = o.StreamProducerClaimedUntil + m.ReferencedByDetectionCameraIDObjects = o.ReferencedByDetectionCameraIDObjects + m.ReferencedByVideoCameraIDObjects = o.ReferencedByVideoCameraIDObjects return nil } @@ -393,7 +393,7 @@ func (m *Camera) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZ v, err := types.FormatUUID(m.ID) if err != nil { - return fmt.Errorf("failed to handle m.ID: %v", err) + return fmt.Errorf("failed to handle m.ID; %v", err) } values = append(values, v) @@ -404,7 +404,7 @@ func (m *Camera) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZ v, err := types.FormatTime(m.CreatedAt) if err != nil { - return fmt.Errorf("failed to handle m.CreatedAt: %v", err) + return fmt.Errorf("failed to handle m.CreatedAt; %v", err) } values = append(values, v) @@ -415,7 +415,7 @@ func (m *Camera) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZ v, err := types.FormatTime(m.UpdatedAt) if err != nil { - return fmt.Errorf("failed to handle m.UpdatedAt: %v", err) + return fmt.Errorf("failed to handle m.UpdatedAt; %v", err) } values = append(values, v) @@ -426,7 +426,7 @@ func (m *Camera) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZ v, err := types.FormatTime(m.DeletedAt) if err != nil { - return fmt.Errorf("failed to handle m.DeletedAt: %v", err) + return fmt.Errorf("failed to handle m.DeletedAt; %v", err) } values = append(values, v) @@ -437,7 +437,7 @@ func (m *Camera) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZ v, err := types.FormatString(m.Name) if err != nil { - return fmt.Errorf("failed to handle m.Name: %v", err) + return fmt.Errorf("failed to handle m.Name; %v", err) } values = append(values, v) @@ -448,7 +448,7 @@ func (m *Camera) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZ v, err := types.FormatString(m.StreamURL) if err != nil { - return fmt.Errorf("failed to handle m.StreamURL: %v", err) + return fmt.Errorf("failed to handle m.StreamURL; %v", err) } values = append(values, v) @@ -459,7 +459,7 @@ func (m *Camera) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZ v, err := types.FormatTime(m.LastSeen) if err != nil { - return fmt.Errorf("failed to handle m.LastSeen: %v", err) + return fmt.Errorf("failed to handle m.LastSeen; %v", err) } values = append(values, v) @@ -470,7 +470,7 @@ func (m *Camera) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZ v, err := types.FormatTime(m.SegmentProducerClaimedUntil) if err != nil { - return fmt.Errorf("failed to handle m.SegmentProducerClaimedUntil: %v", err) + return fmt.Errorf("failed to handle m.SegmentProducerClaimedUntil; %v", err) } values = append(values, v) @@ -481,7 +481,7 @@ func (m *Camera) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZ v, err := types.FormatTime(m.StreamProducerClaimedUntil) if err != nil { - return fmt.Errorf("failed to handle m.StreamProducerClaimedUntil: %v", err) + return fmt.Errorf("failed to handle m.StreamProducerClaimedUntil; %v", err) } values = append(values, v) @@ -533,7 +533,7 @@ func (m *Camera) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZ err = m.Reload(ctx, tx, slices.Contains(forceSetValuesForFields, "deleted_at")) if err != nil { - return fmt.Errorf("failed to reload after insert: %v", err) + return fmt.Errorf("failed to reload after insert; %v", err) } return nil @@ -548,7 +548,7 @@ func (m *Camera) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, forc v, err := types.FormatTime(m.CreatedAt) if err != nil { - return fmt.Errorf("failed to handle m.CreatedAt: %v", err) + return fmt.Errorf("failed to handle m.CreatedAt; %v", err) } values = append(values, v) @@ -559,7 +559,7 @@ func (m *Camera) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, forc v, err := types.FormatTime(m.UpdatedAt) if err != nil { - return fmt.Errorf("failed to handle m.UpdatedAt: %v", err) + return fmt.Errorf("failed to handle m.UpdatedAt; %v", err) } values = append(values, v) @@ -570,7 +570,7 @@ func (m *Camera) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, forc v, err := types.FormatTime(m.DeletedAt) if err != nil { - return fmt.Errorf("failed to handle m.DeletedAt: %v", err) + return fmt.Errorf("failed to handle m.DeletedAt; %v", err) } values = append(values, v) @@ -581,7 +581,7 @@ func (m *Camera) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, forc v, err := types.FormatString(m.Name) if err != nil { - return fmt.Errorf("failed to handle m.Name: %v", err) + return fmt.Errorf("failed to handle m.Name; %v", err) } values = append(values, v) @@ -592,7 +592,7 @@ func (m *Camera) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, forc v, err := types.FormatString(m.StreamURL) if err != nil { - return fmt.Errorf("failed to handle m.StreamURL: %v", err) + return fmt.Errorf("failed to handle m.StreamURL; %v", err) } values = append(values, v) @@ -603,7 +603,7 @@ func (m *Camera) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, forc v, err := types.FormatTime(m.LastSeen) if err != nil { - return fmt.Errorf("failed to handle m.LastSeen: %v", err) + return fmt.Errorf("failed to handle m.LastSeen; %v", err) } values = append(values, v) @@ -614,7 +614,7 @@ func (m *Camera) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, forc v, err := types.FormatTime(m.SegmentProducerClaimedUntil) if err != nil { - return fmt.Errorf("failed to handle m.SegmentProducerClaimedUntil: %v", err) + return fmt.Errorf("failed to handle m.SegmentProducerClaimedUntil; %v", err) } values = append(values, v) @@ -625,7 +625,7 @@ func (m *Camera) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, forc v, err := types.FormatTime(m.StreamProducerClaimedUntil) if err != nil { - return fmt.Errorf("failed to handle m.StreamProducerClaimedUntil: %v", err) + return fmt.Errorf("failed to handle m.StreamProducerClaimedUntil; %v", err) } values = append(values, v) @@ -633,7 +633,7 @@ func (m *Camera) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, forc v, err := types.FormatUUID(m.ID) if err != nil { - return fmt.Errorf("failed to handle m.ID: %v", err) + return fmt.Errorf("failed to handle m.ID; %v", err) } values = append(values, v) @@ -679,7 +679,7 @@ func (m *Camera) Delete(ctx context.Context, tx pgx.Tx, hardDeletes ...bool) err values := make([]any, 0) v, err := types.FormatUUID(m.ID) if err != nil { - return fmt.Errorf("failed to handle m.ID: %v", err) + return fmt.Errorf("failed to handle m.ID; %v", err) } values = append(values, v) @@ -707,7 +707,7 @@ func (m *Camera) LockTable(ctx context.Context, tx pgx.Tx, noWait bool) error { return query.LockTable(ctx, tx, CameraTable, noWait) } -func SelectCameras(ctx context.Context, tx pgx.Tx, where string, orderBy *string, limit *int, offset *int, values ...any) ([]*Camera, error) { +func SelectCameras(ctx context.Context, tx pgx.Tx, where string, orderBy *string, limit *int, offset *int, values ...any) ([]*Camera, int64, int64, int64, int64, error) { if slices.Contains(CameraTableColumns, "deleted_at") { if !strings.Contains(where, "deleted_at") { if where != "" { @@ -721,7 +721,7 @@ func SelectCameras(ctx context.Context, tx pgx.Tx, where string, orderBy *string ctx, cleanup := query.WithQueryID(ctx) defer cleanup() - items, err := query.Select( + items, count, totalCount, page, totalPages, err := query.Select( ctx, tx, CameraTableColumnsWithTypeCasts, @@ -733,7 +733,7 @@ func SelectCameras(ctx context.Context, tx pgx.Tx, where string, orderBy *string values..., ) if err != nil { - return nil, fmt.Errorf("failed to call SelectCameras; err: %v", err) + return nil, 0, 0, 0, 0, fmt.Errorf("failed to call SelectCameras; %v", err) } objects := make([]*Camera, 0) @@ -743,7 +743,7 @@ func SelectCameras(ctx context.Context, tx pgx.Tx, where string, orderBy *string err = object.FromItem(item) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } thatCtx := ctx @@ -762,10 +762,10 @@ func SelectCameras(ctx context.Context, tx pgx.Tx, where string, orderBy *string thisCtx, ok2 := query.HandleQueryPathGraphCycles(thisCtx, fmt.Sprintf("__ReferencedBy__%s{%v}", CameraTable, object.GetPrimaryKeyValue())) if ok1 && ok2 { - object.ReferencedByVideoCameraIDObjects, err = SelectVideos( + object.ReferencedByDetectionCameraIDObjects, _, _, _, _, err = SelectDetections( thisCtx, tx, - fmt.Sprintf("%v = $1", VideoTableCameraIDColumn), + fmt.Sprintf("%v = $1", DetectionTableCameraIDColumn), nil, nil, nil, @@ -781,7 +781,7 @@ func SelectCameras(ctx context.Context, tx pgx.Tx, where string, orderBy *string return nil }() if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } err = func() error { @@ -790,10 +790,10 @@ func SelectCameras(ctx context.Context, tx pgx.Tx, where string, orderBy *string thisCtx, ok2 := query.HandleQueryPathGraphCycles(thisCtx, fmt.Sprintf("__ReferencedBy__%s{%v}", CameraTable, object.GetPrimaryKeyValue())) if ok1 && ok2 { - object.ReferencedByDetectionCameraIDObjects, err = SelectDetections( + object.ReferencedByVideoCameraIDObjects, _, _, _, _, err = SelectVideos( thisCtx, tx, - fmt.Sprintf("%v = $1", DetectionTableCameraIDColumn), + fmt.Sprintf("%v = $1", VideoTableCameraIDColumn), nil, nil, nil, @@ -809,20 +809,20 @@ func SelectCameras(ctx context.Context, tx pgx.Tx, where string, orderBy *string return nil }() if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } objects = append(objects, object) } - return objects, nil + return objects, count, totalCount, page, totalPages, nil } -func SelectCamera(ctx context.Context, tx pgx.Tx, where string, values ...any) (*Camera, error) { +func SelectCamera(ctx context.Context, tx pgx.Tx, where string, values ...any) (*Camera, int64, int64, int64, int64, error) { ctx, cleanup := query.WithQueryID(ctx) defer cleanup() - objects, err := SelectCameras( + objects, _, _, _, _, err := SelectCameras( ctx, tx, where, @@ -832,73 +832,78 @@ func SelectCamera(ctx context.Context, tx pgx.Tx, where string, values ...any) ( values..., ) if err != nil { - return nil, fmt.Errorf("failed to call SelectCamera; err: %v", err) + return nil, 0, 0, 0, 0, fmt.Errorf("failed to call SelectCamera; %v", err) } if len(objects) > 1 { - return nil, fmt.Errorf("attempt to call SelectCamera returned more than 1 row") + return nil, 0, 0, 0, 0, fmt.Errorf("attempt to call SelectCamera returned more than 1 row") } if len(objects) < 1 { - return nil, sql.ErrNoRows + return nil, 0, 0, 0, 0, sql.ErrNoRows } object := objects[0] - return object, nil + count := int64(1) + totalCount := count + page := int64(1) + totalPages := page + + return object, count, totalCount, page, totalPages, nil } -func handleGetCameras(arguments *server.SelectManyArguments, db *pgxpool.Pool) ([]*Camera, error) { +func handleGetCameras(arguments *server.SelectManyArguments, db *pgxpool.Pool) ([]*Camera, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } defer func() { _ = tx.Rollback(arguments.Ctx) }() - objects, err := SelectCameras(arguments.Ctx, tx, arguments.Where, arguments.OrderBy, arguments.Limit, arguments.Offset, arguments.Values...) + objects, count, totalCount, page, totalPages, err := SelectCameras(arguments.Ctx, tx, arguments.Where, arguments.OrderBy, arguments.Limit, arguments.Offset, arguments.Values...) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } err = tx.Commit(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } - return objects, nil + return objects, count, totalCount, page, totalPages, nil } -func handleGetCamera(arguments *server.SelectOneArguments, db *pgxpool.Pool, primaryKey uuid.UUID) ([]*Camera, error) { +func handleGetCamera(arguments *server.SelectOneArguments, db *pgxpool.Pool, primaryKey uuid.UUID) ([]*Camera, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } defer func() { _ = tx.Rollback(arguments.Ctx) }() - object, err := SelectCamera(arguments.Ctx, tx, arguments.Where, arguments.Values...) + object, count, totalCount, page, totalPages, err := SelectCamera(arguments.Ctx, tx, arguments.Where, arguments.Values...) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } err = tx.Commit(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } - return []*Camera{object}, nil + return []*Camera{object}, count, totalCount, page, totalPages, nil } -func handlePostCameras(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, objects []*Camera, forceSetValuesForFieldsByObjectIndex [][]string) ([]*Camera, error) { +func handlePostCameras(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, objects []*Camera, forceSetValuesForFieldsByObjectIndex [][]string) ([]*Camera, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to begin DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } defer func() { @@ -907,8 +912,8 @@ func handlePostCameras(arguments *server.LoadArguments, db *pgxpool.Pool, waitFo xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) - return nil, err + err = fmt.Errorf("failed to get xid; %v", err) + return nil, 0, 0, 0, 0, err } _ = xid @@ -916,7 +921,7 @@ func handlePostCameras(arguments *server.LoadArguments, db *pgxpool.Pool, waitFo err = object.Insert(arguments.Ctx, tx, false, false, forceSetValuesForFieldsByObjectIndex[i]...) if err != nil { err = fmt.Errorf("failed to insert %#+v; %v", object, err) - return nil, err + return nil, 0, 0, 0, 0, err } objects[i] = object @@ -926,7 +931,7 @@ func handlePostCameras(arguments *server.LoadArguments, db *pgxpool.Pool, waitFo go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.INSERT}, CameraTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -936,28 +941,33 @@ func handlePostCameras(arguments *server.LoadArguments, db *pgxpool.Pool, waitFo err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to commit DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } select { case <-arguments.Ctx.Done(): err = fmt.Errorf("context canceled") - return nil, err + return nil, 0, 0, 0, 0, err case err = <-errs: if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } } - return objects, nil + count := int64(len(objects)) + totalCount := count + page := int64(1) + totalPages := page + + return objects, count, totalCount, page, totalPages, nil } -func handlePutCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Camera) ([]*Camera, error) { +func handlePutCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Camera) ([]*Camera, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to begin DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } defer func() { @@ -966,22 +976,22 @@ func handlePutCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitForC xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) - return nil, err + err = fmt.Errorf("failed to get xid; %v", err) + return nil, 0, 0, 0, 0, err } _ = xid err = object.Update(arguments.Ctx, tx, true) if err != nil { err = fmt.Errorf("failed to update %#+v; %v", object, err) - return nil, err + return nil, 0, 0, 0, 0, err } errs := make(chan error, 1) go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.UPDATE, stream.SOFT_DELETE, stream.SOFT_RESTORE, stream.SOFT_UPDATE}, CameraTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -991,28 +1001,33 @@ func handlePutCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitForC err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to commit DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } select { case <-arguments.Ctx.Done(): err = fmt.Errorf("context canceled") - return nil, err + return nil, 0, 0, 0, 0, err case err = <-errs: if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } } - return []*Camera{object}, nil + count := int64(1) + totalCount := count + page := int64(1) + totalPages := page + + return []*Camera{object}, count, totalCount, page, totalPages, nil } -func handlePatchCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Camera, forceSetValuesForFields []string) ([]*Camera, error) { +func handlePatchCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Camera, forceSetValuesForFields []string) ([]*Camera, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to begin DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } defer func() { @@ -1021,22 +1036,22 @@ func handlePatchCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitFo xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) - return nil, err + err = fmt.Errorf("failed to get xid; %v", err) + return nil, 0, 0, 0, 0, err } _ = xid err = object.Update(arguments.Ctx, tx, false, forceSetValuesForFields...) if err != nil { err = fmt.Errorf("failed to update %#+v; %v", object, err) - return nil, err + return nil, 0, 0, 0, 0, err } errs := make(chan error, 1) go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.UPDATE, stream.SOFT_DELETE, stream.SOFT_RESTORE, stream.SOFT_UPDATE}, CameraTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -1046,27 +1061,32 @@ func handlePatchCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitFo err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to commit DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } select { case <-arguments.Ctx.Done(): err = fmt.Errorf("context canceled") - return nil, err + return nil, 0, 0, 0, 0, err case err = <-errs: if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } } - return []*Camera{object}, nil + count := int64(1) + totalCount := count + page := int64(1) + totalPages := page + + return []*Camera{object}, count, totalCount, page, totalPages, nil } func handleDeleteCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Camera) error { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) + err = fmt.Errorf("failed to begin DB transaction; %v", err) return err } @@ -1076,7 +1096,7 @@ func handleDeleteCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitF xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) + err = fmt.Errorf("failed to get xid; %v", err) return err } _ = xid @@ -1091,7 +1111,7 @@ func handleDeleteCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitF go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.DELETE, stream.SOFT_DELETE}, CameraTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -1101,7 +1121,7 @@ func handleDeleteCamera(arguments *server.LoadArguments, db *pgxpool.Pool, waitF err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) + err = fmt.Errorf("failed to commit DB transaction; %v", err) return err } @@ -1135,7 +1155,7 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] queryParams map[string]any, req server.EmptyRequest, rawReq any, - ) (*helpers.TypedResponse[Camera], error) { + ) (*server.Response[Camera], error) { redisConn := redisPool.Get() defer func() { _ = redisConn.Close() @@ -1146,47 +1166,61 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] return nil, err } - cachedObjectsAsJSON, cacheHit, err := helpers.GetCachedObjectsAsJSON(arguments.RequestHash, redisConn) + cachedResponseAsJSON, cacheHit, err := server.GetCachedResponseAsJSON(arguments.RequestHash, redisConn) if err != nil { return nil, err } if cacheHit { - var cachedObjects []*Camera - err = json.Unmarshal(cachedObjectsAsJSON, &cachedObjects) + var cachedResponse server.Response[Camera] + + /* TODO: it'd be nice to be able to avoid this (i.e. just pass straight through) */ + err = json.Unmarshal(cachedResponseAsJSON, &cachedResponse) if err != nil { return nil, err } - return &helpers.TypedResponse[Camera]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: cachedObjects, - }, nil + return &cachedResponse, nil } - objects, err := handleGetCameras(arguments, db) + objects, count, totalCount, _, _, err := handleGetCameras(arguments, db) if err != nil { return nil, err } - objectsAsJSON, err := json.Marshal(objects) + limit := int64(0) + if arguments.Limit != nil { + limit = int64(*arguments.Limit) + } + + offset := int64(0) + if arguments.Offset != nil { + offset = int64(*arguments.Offset) + } + + response := server.Response[Camera]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, + } + + /* TODO: it'd be nice to be able to avoid this (i.e. just marshal once, further out) */ + responseAsJSON, err := json.Marshal(response) if err != nil { return nil, err } - err = helpers.StoreCachedResponse(arguments.RequestHash, redisConn, string(objectsAsJSON)) + err = server.StoreCachedResponse(arguments.RequestHash, redisConn, responseAsJSON) if err != nil { - log.Printf("warning: %v", err) + log.Printf("warning; %v", err) } - return &helpers.TypedResponse[Camera]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, - }, nil + return &response, nil }, ) if err != nil { @@ -1204,7 +1238,7 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] queryParams CameraLoadQueryParams, req server.EmptyRequest, rawReq any, - ) (*helpers.TypedResponse[Camera], error) { + ) (*server.Response[Camera], error) { redisConn := redisPool.Get() defer func() { _ = redisConn.Close() @@ -1215,47 +1249,55 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] return nil, err } - cachedObjectsAsJSON, cacheHit, err := helpers.GetCachedObjectsAsJSON(arguments.RequestHash, redisConn) + cachedResponseAsJSON, cacheHit, err := server.GetCachedResponseAsJSON(arguments.RequestHash, redisConn) if err != nil { return nil, err } if cacheHit { - var cachedObjects []*Camera - err = json.Unmarshal(cachedObjectsAsJSON, &cachedObjects) + var cachedResponse server.Response[Camera] + + /* TODO: it'd be nice to be able to avoid this (i.e. just pass straight through) */ + err = json.Unmarshal(cachedResponseAsJSON, &cachedResponse) if err != nil { return nil, err } - return &helpers.TypedResponse[Camera]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: cachedObjects, - }, nil + return &cachedResponse, nil } - objects, err := handleGetCamera(arguments, db, pathParams.PrimaryKey) + objects, count, totalCount, _, _, err := handleGetCamera(arguments, db, pathParams.PrimaryKey) if err != nil { return nil, err } - objectsAsJSON, err := json.Marshal(objects) + limit := int64(0) + + offset := int64(0) + + response := server.Response[Camera]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, + } + + /* TODO: it'd be nice to be able to avoid this (i.e. just marshal once, further out) */ + responseAsJSON, err := json.Marshal(response) if err != nil { return nil, err } - err = helpers.StoreCachedResponse(arguments.RequestHash, redisConn, string(objectsAsJSON)) + err = server.StoreCachedResponse(arguments.RequestHash, redisConn, responseAsJSON) if err != nil { - log.Printf("warning: %v", err) + log.Printf("warning; %v", err) } - return &helpers.TypedResponse[Camera]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, - }, nil + return &response, nil }, ) if err != nil { @@ -1273,7 +1315,7 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] queryParams CameraLoadQueryParams, req []*Camera, rawReq any, - ) (*helpers.TypedResponse[Camera], error) { + ) (*server.Response[Camera], error) { allRawItems, ok := rawReq.([]any) if !ok { return nil, fmt.Errorf("failed to cast %#+v to []map[string]any", rawReq) @@ -1307,16 +1349,24 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] return nil, err } - objects, err := handlePostCameras(arguments, db, waitForChange, req, forceSetValuesForFieldsByObjectIndex) + objects, count, totalCount, _, _, err := handlePostCameras(arguments, db, waitForChange, req, forceSetValuesForFieldsByObjectIndex) if err != nil { return nil, err } - return &helpers.TypedResponse[Camera]{ - Status: http.StatusCreated, - Success: true, - Error: nil, - Objects: objects, + limit := int64(0) + + offset := int64(0) + + return &server.Response[Camera]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, }, nil }, ) @@ -1335,7 +1385,7 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] queryParams CameraLoadQueryParams, req Camera, rawReq any, - ) (*helpers.TypedResponse[Camera], error) { + ) (*server.Response[Camera], error) { item, ok := rawReq.(map[string]any) if !ok { return nil, fmt.Errorf("failed to cast %#+v to map[string]any", item) @@ -1349,16 +1399,24 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] object := &req object.ID = pathParams.PrimaryKey - objects, err := handlePutCamera(arguments, db, waitForChange, object) + objects, count, totalCount, _, _, err := handlePutCamera(arguments, db, waitForChange, object) if err != nil { return nil, err } - return &helpers.TypedResponse[Camera]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, + limit := int64(0) + + offset := int64(0) + + return &server.Response[Camera]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, }, nil }, ) @@ -1377,7 +1435,7 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] queryParams CameraLoadQueryParams, req Camera, rawReq any, - ) (*helpers.TypedResponse[Camera], error) { + ) (*server.Response[Camera], error) { item, ok := rawReq.(map[string]any) if !ok { return nil, fmt.Errorf("failed to cast %#+v to map[string]any", item) @@ -1400,16 +1458,24 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] object := &req object.ID = pathParams.PrimaryKey - objects, err := handlePatchCamera(arguments, db, waitForChange, object, forceSetValuesForFields) + objects, count, totalCount, _, _, err := handlePatchCamera(arguments, db, waitForChange, object, forceSetValuesForFields) if err != nil { return nil, err } - return &helpers.TypedResponse[Camera]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, + limit := int64(0) + + offset := int64(0) + + return &server.Response[Camera]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, }, nil }, ) @@ -1429,14 +1495,15 @@ func GetCameraRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares [] req server.EmptyRequest, rawReq any, ) (*server.EmptyResponse, error) { - arguments := &server.LoadArguments{ - Ctx: ctx, + arguments, err := server.GetLoadArguments(ctx, queryParams.Depth) + if err != nil { + return nil, err } object := &Camera{} object.ID = pathParams.PrimaryKey - err := handleDeleteCamera(arguments, db, waitForChange, object) + err = handleDeleteCamera(arguments, db, waitForChange, object) if err != nil { return nil, err } diff --git a/pkg/api/cmd/main.go b/pkg/api/cmd/main.go index 71aa46d..36ea883 100755 --- a/pkg/api/cmd/main.go +++ b/pkg/api/cmd/main.go @@ -24,6 +24,6 @@ func main() { api.RunDumpOpenAPIYAML() case "serve": - api.RunServeWithEnvironment() + api.RunServeWithEnvironment(nil, nil, nil) } } diff --git a/pkg/api/detection.go b/pkg/api/detection.go index 76627df..26d0c76 100755 --- a/pkg/api/detection.go +++ b/pkg/api/detection.go @@ -431,7 +431,7 @@ func (m *Detection) Reload(ctx context.Context, tx pgx.Tx, includeDeleteds ...bo ctx, cleanup := query.WithQueryID(ctx) defer cleanup() - t, err := SelectDetection( + o, _, _, _, _, err := SelectDetection( ctx, tx, fmt.Sprintf("%v = $1%v", m.GetPrimaryKeyColumn(), extraWhere), @@ -441,20 +441,20 @@ func (m *Detection) Reload(ctx context.Context, tx pgx.Tx, includeDeleteds ...bo return err } - m.ID = t.ID - m.CreatedAt = t.CreatedAt - m.UpdatedAt = t.UpdatedAt - m.DeletedAt = t.DeletedAt - m.SeenAt = t.SeenAt - m.ClassID = t.ClassID - m.ClassName = t.ClassName - m.Score = t.Score - m.Centroid = t.Centroid - m.BoundingBox = t.BoundingBox - m.VideoID = t.VideoID - m.VideoIDObject = t.VideoIDObject - m.CameraID = t.CameraID - m.CameraIDObject = t.CameraIDObject + m.ID = o.ID + m.CreatedAt = o.CreatedAt + m.UpdatedAt = o.UpdatedAt + m.DeletedAt = o.DeletedAt + m.SeenAt = o.SeenAt + m.ClassID = o.ClassID + m.ClassName = o.ClassName + m.Score = o.Score + m.Centroid = o.Centroid + m.BoundingBox = o.BoundingBox + m.VideoID = o.VideoID + m.VideoIDObject = o.VideoIDObject + m.CameraID = o.CameraID + m.CameraIDObject = o.CameraIDObject return nil } @@ -468,7 +468,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatUUID(m.ID) if err != nil { - return fmt.Errorf("failed to handle m.ID: %v", err) + return fmt.Errorf("failed to handle m.ID; %v", err) } values = append(values, v) @@ -479,7 +479,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatTime(m.CreatedAt) if err != nil { - return fmt.Errorf("failed to handle m.CreatedAt: %v", err) + return fmt.Errorf("failed to handle m.CreatedAt; %v", err) } values = append(values, v) @@ -490,7 +490,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatTime(m.UpdatedAt) if err != nil { - return fmt.Errorf("failed to handle m.UpdatedAt: %v", err) + return fmt.Errorf("failed to handle m.UpdatedAt; %v", err) } values = append(values, v) @@ -501,7 +501,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatTime(m.DeletedAt) if err != nil { - return fmt.Errorf("failed to handle m.DeletedAt: %v", err) + return fmt.Errorf("failed to handle m.DeletedAt; %v", err) } values = append(values, v) @@ -512,7 +512,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatTime(m.SeenAt) if err != nil { - return fmt.Errorf("failed to handle m.SeenAt: %v", err) + return fmt.Errorf("failed to handle m.SeenAt; %v", err) } values = append(values, v) @@ -523,7 +523,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatInt(m.ClassID) if err != nil { - return fmt.Errorf("failed to handle m.ClassID: %v", err) + return fmt.Errorf("failed to handle m.ClassID; %v", err) } values = append(values, v) @@ -534,7 +534,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatString(m.ClassName) if err != nil { - return fmt.Errorf("failed to handle m.ClassName: %v", err) + return fmt.Errorf("failed to handle m.ClassName; %v", err) } values = append(values, v) @@ -545,7 +545,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatFloat(m.Score) if err != nil { - return fmt.Errorf("failed to handle m.Score: %v", err) + return fmt.Errorf("failed to handle m.Score; %v", err) } values = append(values, v) @@ -556,7 +556,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatPoint(m.Centroid) if err != nil { - return fmt.Errorf("failed to handle m.Centroid: %v", err) + return fmt.Errorf("failed to handle m.Centroid; %v", err) } values = append(values, v) @@ -567,7 +567,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatPolygon(m.BoundingBox) if err != nil { - return fmt.Errorf("failed to handle m.BoundingBox: %v", err) + return fmt.Errorf("failed to handle m.BoundingBox; %v", err) } values = append(values, v) @@ -578,7 +578,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatUUID(m.VideoID) if err != nil { - return fmt.Errorf("failed to handle m.VideoID: %v", err) + return fmt.Errorf("failed to handle m.VideoID; %v", err) } values = append(values, v) @@ -589,7 +589,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s v, err := types.FormatUUID(m.CameraID) if err != nil { - return fmt.Errorf("failed to handle m.CameraID: %v", err) + return fmt.Errorf("failed to handle m.CameraID; %v", err) } values = append(values, v) @@ -641,7 +641,7 @@ func (m *Detection) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, s err = m.Reload(ctx, tx, slices.Contains(forceSetValuesForFields, "deleted_at")) if err != nil { - return fmt.Errorf("failed to reload after insert: %v", err) + return fmt.Errorf("failed to reload after insert; %v", err) } return nil @@ -656,7 +656,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatTime(m.CreatedAt) if err != nil { - return fmt.Errorf("failed to handle m.CreatedAt: %v", err) + return fmt.Errorf("failed to handle m.CreatedAt; %v", err) } values = append(values, v) @@ -667,7 +667,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatTime(m.UpdatedAt) if err != nil { - return fmt.Errorf("failed to handle m.UpdatedAt: %v", err) + return fmt.Errorf("failed to handle m.UpdatedAt; %v", err) } values = append(values, v) @@ -678,7 +678,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatTime(m.DeletedAt) if err != nil { - return fmt.Errorf("failed to handle m.DeletedAt: %v", err) + return fmt.Errorf("failed to handle m.DeletedAt; %v", err) } values = append(values, v) @@ -689,7 +689,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatTime(m.SeenAt) if err != nil { - return fmt.Errorf("failed to handle m.SeenAt: %v", err) + return fmt.Errorf("failed to handle m.SeenAt; %v", err) } values = append(values, v) @@ -700,7 +700,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatInt(m.ClassID) if err != nil { - return fmt.Errorf("failed to handle m.ClassID: %v", err) + return fmt.Errorf("failed to handle m.ClassID; %v", err) } values = append(values, v) @@ -711,7 +711,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatString(m.ClassName) if err != nil { - return fmt.Errorf("failed to handle m.ClassName: %v", err) + return fmt.Errorf("failed to handle m.ClassName; %v", err) } values = append(values, v) @@ -722,7 +722,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatFloat(m.Score) if err != nil { - return fmt.Errorf("failed to handle m.Score: %v", err) + return fmt.Errorf("failed to handle m.Score; %v", err) } values = append(values, v) @@ -733,7 +733,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatPoint(m.Centroid) if err != nil { - return fmt.Errorf("failed to handle m.Centroid: %v", err) + return fmt.Errorf("failed to handle m.Centroid; %v", err) } values = append(values, v) @@ -744,7 +744,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatPolygon(m.BoundingBox) if err != nil { - return fmt.Errorf("failed to handle m.BoundingBox: %v", err) + return fmt.Errorf("failed to handle m.BoundingBox; %v", err) } values = append(values, v) @@ -755,7 +755,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatUUID(m.VideoID) if err != nil { - return fmt.Errorf("failed to handle m.VideoID: %v", err) + return fmt.Errorf("failed to handle m.VideoID; %v", err) } values = append(values, v) @@ -766,7 +766,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatUUID(m.CameraID) if err != nil { - return fmt.Errorf("failed to handle m.CameraID: %v", err) + return fmt.Errorf("failed to handle m.CameraID; %v", err) } values = append(values, v) @@ -774,7 +774,7 @@ func (m *Detection) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, f v, err := types.FormatUUID(m.ID) if err != nil { - return fmt.Errorf("failed to handle m.ID: %v", err) + return fmt.Errorf("failed to handle m.ID; %v", err) } values = append(values, v) @@ -820,7 +820,7 @@ func (m *Detection) Delete(ctx context.Context, tx pgx.Tx, hardDeletes ...bool) values := make([]any, 0) v, err := types.FormatUUID(m.ID) if err != nil { - return fmt.Errorf("failed to handle m.ID: %v", err) + return fmt.Errorf("failed to handle m.ID; %v", err) } values = append(values, v) @@ -848,7 +848,7 @@ func (m *Detection) LockTable(ctx context.Context, tx pgx.Tx, noWait bool) error return query.LockTable(ctx, tx, DetectionTable, noWait) } -func SelectDetections(ctx context.Context, tx pgx.Tx, where string, orderBy *string, limit *int, offset *int, values ...any) ([]*Detection, error) { +func SelectDetections(ctx context.Context, tx pgx.Tx, where string, orderBy *string, limit *int, offset *int, values ...any) ([]*Detection, int64, int64, int64, int64, error) { if slices.Contains(DetectionTableColumns, "deleted_at") { if !strings.Contains(where, "deleted_at") { if where != "" { @@ -862,7 +862,7 @@ func SelectDetections(ctx context.Context, tx pgx.Tx, where string, orderBy *str ctx, cleanup := query.WithQueryID(ctx) defer cleanup() - items, err := query.Select( + items, count, totalCount, page, totalPages, err := query.Select( ctx, tx, DetectionTableColumnsWithTypeCasts, @@ -874,7 +874,7 @@ func SelectDetections(ctx context.Context, tx pgx.Tx, where string, orderBy *str values..., ) if err != nil { - return nil, fmt.Errorf("failed to call SelectDetections; err: %v", err) + return nil, 0, 0, 0, 0, fmt.Errorf("failed to call SelectDetections; %v", err) } objects := make([]*Detection, 0) @@ -884,7 +884,7 @@ func SelectDetections(ctx context.Context, tx pgx.Tx, where string, orderBy *str err = object.FromItem(item) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } thatCtx := ctx @@ -902,7 +902,7 @@ func SelectDetections(ctx context.Context, tx pgx.Tx, where string, orderBy *str thisCtx, ok1 := query.HandleQueryPathGraphCycles(thisCtx, fmt.Sprintf("%s{%v}", VideoTable, object.VideoID)) thisCtx, ok2 := query.HandleQueryPathGraphCycles(thisCtx, fmt.Sprintf("__ReferencedBy__%s{%v}", VideoTable, object.VideoID)) if ok1 && ok2 { - object.VideoIDObject, err = SelectVideo( + object.VideoIDObject, _, _, _, _, err = SelectVideo( thisCtx, tx, fmt.Sprintf("%v = $1", VideoTablePrimaryKeyColumn), @@ -910,7 +910,7 @@ func SelectDetections(ctx context.Context, tx pgx.Tx, where string, orderBy *str ) if err != nil { if !errors.Is(err, sql.ErrNoRows) { - return nil, err + return nil, 0, 0, 0, 0, err } } } @@ -921,7 +921,7 @@ func SelectDetections(ctx context.Context, tx pgx.Tx, where string, orderBy *str thisCtx, ok1 := query.HandleQueryPathGraphCycles(thisCtx, fmt.Sprintf("%s{%v}", CameraTable, object.CameraID)) thisCtx, ok2 := query.HandleQueryPathGraphCycles(thisCtx, fmt.Sprintf("__ReferencedBy__%s{%v}", CameraTable, object.CameraID)) if ok1 && ok2 { - object.CameraIDObject, err = SelectCamera( + object.CameraIDObject, _, _, _, _, err = SelectCamera( thisCtx, tx, fmt.Sprintf("%v = $1", CameraTablePrimaryKeyColumn), @@ -929,7 +929,7 @@ func SelectDetections(ctx context.Context, tx pgx.Tx, where string, orderBy *str ) if err != nil { if !errors.Is(err, sql.ErrNoRows) { - return nil, err + return nil, 0, 0, 0, 0, err } } } @@ -938,14 +938,14 @@ func SelectDetections(ctx context.Context, tx pgx.Tx, where string, orderBy *str objects = append(objects, object) } - return objects, nil + return objects, count, totalCount, page, totalPages, nil } -func SelectDetection(ctx context.Context, tx pgx.Tx, where string, values ...any) (*Detection, error) { +func SelectDetection(ctx context.Context, tx pgx.Tx, where string, values ...any) (*Detection, int64, int64, int64, int64, error) { ctx, cleanup := query.WithQueryID(ctx) defer cleanup() - objects, err := SelectDetections( + objects, _, _, _, _, err := SelectDetections( ctx, tx, where, @@ -955,73 +955,78 @@ func SelectDetection(ctx context.Context, tx pgx.Tx, where string, values ...any values..., ) if err != nil { - return nil, fmt.Errorf("failed to call SelectDetection; err: %v", err) + return nil, 0, 0, 0, 0, fmt.Errorf("failed to call SelectDetection; %v", err) } if len(objects) > 1 { - return nil, fmt.Errorf("attempt to call SelectDetection returned more than 1 row") + return nil, 0, 0, 0, 0, fmt.Errorf("attempt to call SelectDetection returned more than 1 row") } if len(objects) < 1 { - return nil, sql.ErrNoRows + return nil, 0, 0, 0, 0, sql.ErrNoRows } object := objects[0] - return object, nil + count := int64(1) + totalCount := count + page := int64(1) + totalPages := page + + return object, count, totalCount, page, totalPages, nil } -func handleGetDetections(arguments *server.SelectManyArguments, db *pgxpool.Pool) ([]*Detection, error) { +func handleGetDetections(arguments *server.SelectManyArguments, db *pgxpool.Pool) ([]*Detection, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } defer func() { _ = tx.Rollback(arguments.Ctx) }() - objects, err := SelectDetections(arguments.Ctx, tx, arguments.Where, arguments.OrderBy, arguments.Limit, arguments.Offset, arguments.Values...) + objects, count, totalCount, page, totalPages, err := SelectDetections(arguments.Ctx, tx, arguments.Where, arguments.OrderBy, arguments.Limit, arguments.Offset, arguments.Values...) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } err = tx.Commit(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } - return objects, nil + return objects, count, totalCount, page, totalPages, nil } -func handleGetDetection(arguments *server.SelectOneArguments, db *pgxpool.Pool, primaryKey uuid.UUID) ([]*Detection, error) { +func handleGetDetection(arguments *server.SelectOneArguments, db *pgxpool.Pool, primaryKey uuid.UUID) ([]*Detection, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } defer func() { _ = tx.Rollback(arguments.Ctx) }() - object, err := SelectDetection(arguments.Ctx, tx, arguments.Where, arguments.Values...) + object, count, totalCount, page, totalPages, err := SelectDetection(arguments.Ctx, tx, arguments.Where, arguments.Values...) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } err = tx.Commit(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } - return []*Detection{object}, nil + return []*Detection{object}, count, totalCount, page, totalPages, nil } -func handlePostDetections(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, objects []*Detection, forceSetValuesForFieldsByObjectIndex [][]string) ([]*Detection, error) { +func handlePostDetections(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, objects []*Detection, forceSetValuesForFieldsByObjectIndex [][]string) ([]*Detection, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to begin DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } defer func() { @@ -1030,8 +1035,8 @@ func handlePostDetections(arguments *server.LoadArguments, db *pgxpool.Pool, wai xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) - return nil, err + err = fmt.Errorf("failed to get xid; %v", err) + return nil, 0, 0, 0, 0, err } _ = xid @@ -1039,7 +1044,7 @@ func handlePostDetections(arguments *server.LoadArguments, db *pgxpool.Pool, wai err = object.Insert(arguments.Ctx, tx, false, false, forceSetValuesForFieldsByObjectIndex[i]...) if err != nil { err = fmt.Errorf("failed to insert %#+v; %v", object, err) - return nil, err + return nil, 0, 0, 0, 0, err } objects[i] = object @@ -1049,7 +1054,7 @@ func handlePostDetections(arguments *server.LoadArguments, db *pgxpool.Pool, wai go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.INSERT}, DetectionTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -1059,28 +1064,33 @@ func handlePostDetections(arguments *server.LoadArguments, db *pgxpool.Pool, wai err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to commit DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } select { case <-arguments.Ctx.Done(): err = fmt.Errorf("context canceled") - return nil, err + return nil, 0, 0, 0, 0, err case err = <-errs: if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } } - return objects, nil + count := int64(len(objects)) + totalCount := count + page := int64(1) + totalPages := page + + return objects, count, totalCount, page, totalPages, nil } -func handlePutDetection(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Detection) ([]*Detection, error) { +func handlePutDetection(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Detection) ([]*Detection, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to begin DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } defer func() { @@ -1089,22 +1099,22 @@ func handlePutDetection(arguments *server.LoadArguments, db *pgxpool.Pool, waitF xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) - return nil, err + err = fmt.Errorf("failed to get xid; %v", err) + return nil, 0, 0, 0, 0, err } _ = xid err = object.Update(arguments.Ctx, tx, true) if err != nil { err = fmt.Errorf("failed to update %#+v; %v", object, err) - return nil, err + return nil, 0, 0, 0, 0, err } errs := make(chan error, 1) go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.UPDATE, stream.SOFT_DELETE, stream.SOFT_RESTORE, stream.SOFT_UPDATE}, DetectionTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -1114,28 +1124,33 @@ func handlePutDetection(arguments *server.LoadArguments, db *pgxpool.Pool, waitF err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to commit DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } select { case <-arguments.Ctx.Done(): err = fmt.Errorf("context canceled") - return nil, err + return nil, 0, 0, 0, 0, err case err = <-errs: if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } } - return []*Detection{object}, nil + count := int64(1) + totalCount := count + page := int64(1) + totalPages := page + + return []*Detection{object}, count, totalCount, page, totalPages, nil } -func handlePatchDetection(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Detection, forceSetValuesForFields []string) ([]*Detection, error) { +func handlePatchDetection(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Detection, forceSetValuesForFields []string) ([]*Detection, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to begin DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } defer func() { @@ -1144,22 +1159,22 @@ func handlePatchDetection(arguments *server.LoadArguments, db *pgxpool.Pool, wai xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) - return nil, err + err = fmt.Errorf("failed to get xid; %v", err) + return nil, 0, 0, 0, 0, err } _ = xid err = object.Update(arguments.Ctx, tx, false, forceSetValuesForFields...) if err != nil { err = fmt.Errorf("failed to update %#+v; %v", object, err) - return nil, err + return nil, 0, 0, 0, 0, err } errs := make(chan error, 1) go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.UPDATE, stream.SOFT_DELETE, stream.SOFT_RESTORE, stream.SOFT_UPDATE}, DetectionTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -1169,27 +1184,32 @@ func handlePatchDetection(arguments *server.LoadArguments, db *pgxpool.Pool, wai err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to commit DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } select { case <-arguments.Ctx.Done(): err = fmt.Errorf("context canceled") - return nil, err + return nil, 0, 0, 0, 0, err case err = <-errs: if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } } - return []*Detection{object}, nil + count := int64(1) + totalCount := count + page := int64(1) + totalPages := page + + return []*Detection{object}, count, totalCount, page, totalPages, nil } func handleDeleteDetection(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Detection) error { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) + err = fmt.Errorf("failed to begin DB transaction; %v", err) return err } @@ -1199,7 +1219,7 @@ func handleDeleteDetection(arguments *server.LoadArguments, db *pgxpool.Pool, wa xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) + err = fmt.Errorf("failed to get xid; %v", err) return err } _ = xid @@ -1214,7 +1234,7 @@ func handleDeleteDetection(arguments *server.LoadArguments, db *pgxpool.Pool, wa go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.DELETE, stream.SOFT_DELETE}, DetectionTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -1224,7 +1244,7 @@ func handleDeleteDetection(arguments *server.LoadArguments, db *pgxpool.Pool, wa err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) + err = fmt.Errorf("failed to commit DB transaction; %v", err) return err } @@ -1258,7 +1278,7 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares queryParams map[string]any, req server.EmptyRequest, rawReq any, - ) (*helpers.TypedResponse[Detection], error) { + ) (*server.Response[Detection], error) { redisConn := redisPool.Get() defer func() { _ = redisConn.Close() @@ -1269,47 +1289,61 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares return nil, err } - cachedObjectsAsJSON, cacheHit, err := helpers.GetCachedObjectsAsJSON(arguments.RequestHash, redisConn) + cachedResponseAsJSON, cacheHit, err := server.GetCachedResponseAsJSON(arguments.RequestHash, redisConn) if err != nil { return nil, err } if cacheHit { - var cachedObjects []*Detection - err = json.Unmarshal(cachedObjectsAsJSON, &cachedObjects) + var cachedResponse server.Response[Detection] + + /* TODO: it'd be nice to be able to avoid this (i.e. just pass straight through) */ + err = json.Unmarshal(cachedResponseAsJSON, &cachedResponse) if err != nil { return nil, err } - return &helpers.TypedResponse[Detection]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: cachedObjects, - }, nil + return &cachedResponse, nil } - objects, err := handleGetDetections(arguments, db) + objects, count, totalCount, _, _, err := handleGetDetections(arguments, db) if err != nil { return nil, err } - objectsAsJSON, err := json.Marshal(objects) + limit := int64(0) + if arguments.Limit != nil { + limit = int64(*arguments.Limit) + } + + offset := int64(0) + if arguments.Offset != nil { + offset = int64(*arguments.Offset) + } + + response := server.Response[Detection]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, + } + + /* TODO: it'd be nice to be able to avoid this (i.e. just marshal once, further out) */ + responseAsJSON, err := json.Marshal(response) if err != nil { return nil, err } - err = helpers.StoreCachedResponse(arguments.RequestHash, redisConn, string(objectsAsJSON)) + err = server.StoreCachedResponse(arguments.RequestHash, redisConn, responseAsJSON) if err != nil { - log.Printf("warning: %v", err) + log.Printf("warning; %v", err) } - return &helpers.TypedResponse[Detection]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, - }, nil + return &response, nil }, ) if err != nil { @@ -1327,7 +1361,7 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares queryParams DetectionLoadQueryParams, req server.EmptyRequest, rawReq any, - ) (*helpers.TypedResponse[Detection], error) { + ) (*server.Response[Detection], error) { redisConn := redisPool.Get() defer func() { _ = redisConn.Close() @@ -1338,47 +1372,55 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares return nil, err } - cachedObjectsAsJSON, cacheHit, err := helpers.GetCachedObjectsAsJSON(arguments.RequestHash, redisConn) + cachedResponseAsJSON, cacheHit, err := server.GetCachedResponseAsJSON(arguments.RequestHash, redisConn) if err != nil { return nil, err } if cacheHit { - var cachedObjects []*Detection - err = json.Unmarshal(cachedObjectsAsJSON, &cachedObjects) + var cachedResponse server.Response[Detection] + + /* TODO: it'd be nice to be able to avoid this (i.e. just pass straight through) */ + err = json.Unmarshal(cachedResponseAsJSON, &cachedResponse) if err != nil { return nil, err } - return &helpers.TypedResponse[Detection]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: cachedObjects, - }, nil + return &cachedResponse, nil } - objects, err := handleGetDetection(arguments, db, pathParams.PrimaryKey) + objects, count, totalCount, _, _, err := handleGetDetection(arguments, db, pathParams.PrimaryKey) if err != nil { return nil, err } - objectsAsJSON, err := json.Marshal(objects) + limit := int64(0) + + offset := int64(0) + + response := server.Response[Detection]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, + } + + /* TODO: it'd be nice to be able to avoid this (i.e. just marshal once, further out) */ + responseAsJSON, err := json.Marshal(response) if err != nil { return nil, err } - err = helpers.StoreCachedResponse(arguments.RequestHash, redisConn, string(objectsAsJSON)) + err = server.StoreCachedResponse(arguments.RequestHash, redisConn, responseAsJSON) if err != nil { - log.Printf("warning: %v", err) + log.Printf("warning; %v", err) } - return &helpers.TypedResponse[Detection]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, - }, nil + return &response, nil }, ) if err != nil { @@ -1396,7 +1438,7 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares queryParams DetectionLoadQueryParams, req []*Detection, rawReq any, - ) (*helpers.TypedResponse[Detection], error) { + ) (*server.Response[Detection], error) { allRawItems, ok := rawReq.([]any) if !ok { return nil, fmt.Errorf("failed to cast %#+v to []map[string]any", rawReq) @@ -1430,16 +1472,24 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares return nil, err } - objects, err := handlePostDetections(arguments, db, waitForChange, req, forceSetValuesForFieldsByObjectIndex) + objects, count, totalCount, _, _, err := handlePostDetections(arguments, db, waitForChange, req, forceSetValuesForFieldsByObjectIndex) if err != nil { return nil, err } - return &helpers.TypedResponse[Detection]{ - Status: http.StatusCreated, - Success: true, - Error: nil, - Objects: objects, + limit := int64(0) + + offset := int64(0) + + return &server.Response[Detection]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, }, nil }, ) @@ -1458,7 +1508,7 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares queryParams DetectionLoadQueryParams, req Detection, rawReq any, - ) (*helpers.TypedResponse[Detection], error) { + ) (*server.Response[Detection], error) { item, ok := rawReq.(map[string]any) if !ok { return nil, fmt.Errorf("failed to cast %#+v to map[string]any", item) @@ -1472,16 +1522,24 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares object := &req object.ID = pathParams.PrimaryKey - objects, err := handlePutDetection(arguments, db, waitForChange, object) + objects, count, totalCount, _, _, err := handlePutDetection(arguments, db, waitForChange, object) if err != nil { return nil, err } - return &helpers.TypedResponse[Detection]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, + limit := int64(0) + + offset := int64(0) + + return &server.Response[Detection]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, }, nil }, ) @@ -1500,7 +1558,7 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares queryParams DetectionLoadQueryParams, req Detection, rawReq any, - ) (*helpers.TypedResponse[Detection], error) { + ) (*server.Response[Detection], error) { item, ok := rawReq.(map[string]any) if !ok { return nil, fmt.Errorf("failed to cast %#+v to map[string]any", item) @@ -1523,16 +1581,24 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares object := &req object.ID = pathParams.PrimaryKey - objects, err := handlePatchDetection(arguments, db, waitForChange, object, forceSetValuesForFields) + objects, count, totalCount, _, _, err := handlePatchDetection(arguments, db, waitForChange, object, forceSetValuesForFields) if err != nil { return nil, err } - return &helpers.TypedResponse[Detection]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, + limit := int64(0) + + offset := int64(0) + + return &server.Response[Detection]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, }, nil }, ) @@ -1552,14 +1618,15 @@ func GetDetectionRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares req server.EmptyRequest, rawReq any, ) (*server.EmptyResponse, error) { - arguments := &server.LoadArguments{ - Ctx: ctx, + arguments, err := server.GetLoadArguments(ctx, queryParams.Depth) + if err != nil { + return nil, err } object := &Detection{} object.ID = pathParams.PrimaryKey - err := handleDeleteDetection(arguments, db, waitForChange, object) + err = handleDeleteDetection(arguments, db, waitForChange, object) if err != nil { return nil, err } diff --git a/pkg/api/video.go b/pkg/api/video.go index 438d419..f3105a3 100755 --- a/pkg/api/video.go +++ b/pkg/api/video.go @@ -45,6 +45,7 @@ type Video struct { ObjectTrackerClaimedUntil time.Time `json:"object_tracker_claimed_until"` CameraID uuid.UUID `json:"camera_id"` CameraIDObject *Camera `json:"camera_id_object"` + DetectionSummary any `json:"detection_summary"` ReferencedByDetectionVideoIDObjects []*Detection `json:"referenced_by_detection_video_id_objects"` } @@ -65,6 +66,7 @@ var ( VideoTableObjectDetectorClaimedUntilColumn = "object_detector_claimed_until" VideoTableObjectTrackerClaimedUntilColumn = "object_tracker_claimed_until" VideoTableCameraIDColumn = "camera_id" + VideoTableDetectionSummaryColumn = "detection_summary" ) var ( @@ -82,6 +84,7 @@ var ( VideoTableObjectDetectorClaimedUntilColumnWithTypeCast = `"object_detector_claimed_until" AS object_detector_claimed_until` VideoTableObjectTrackerClaimedUntilColumnWithTypeCast = `"object_tracker_claimed_until" AS object_tracker_claimed_until` VideoTableCameraIDColumnWithTypeCast = `"camera_id" AS camera_id` + VideoTableDetectionSummaryColumnWithTypeCast = `"detection_summary" AS detection_summary` ) var VideoTableColumns = []string{ @@ -99,6 +102,7 @@ var VideoTableColumns = []string{ VideoTableObjectDetectorClaimedUntilColumn, VideoTableObjectTrackerClaimedUntilColumn, VideoTableCameraIDColumn, + VideoTableDetectionSummaryColumn, } var VideoTableColumnsWithTypeCasts = []string{ @@ -116,6 +120,7 @@ var VideoTableColumnsWithTypeCasts = []string{ VideoTableObjectDetectorClaimedUntilColumnWithTypeCast, VideoTableObjectTrackerClaimedUntilColumnWithTypeCast, VideoTableCameraIDColumnWithTypeCast, + VideoTableDetectionSummaryColumnWithTypeCast, } var VideoIntrospectedTable *introspect.Table @@ -462,6 +467,25 @@ func (m *Video) FromItem(item map[string]any) error { m.CameraID = temp2 + case "detection_summary": + if v == nil { + continue + } + + temp1, err := types.ParseJSON(v) + if err != nil { + return wrapError(k, v, err) + } + + temp2, ok := temp1, true + if !ok { + if temp1 != nil { + return wrapError(k, v, fmt.Errorf("failed to cast %#+v to uudetection_summary.UUID", temp1)) + } + } + + m.DetectionSummary = temp2 + } } @@ -479,7 +503,7 @@ func (m *Video) Reload(ctx context.Context, tx pgx.Tx, includeDeleteds ...bool) ctx, cleanup := query.WithQueryID(ctx) defer cleanup() - t, err := SelectVideo( + o, _, _, _, _, err := SelectVideo( ctx, tx, fmt.Sprintf("%v = $1%v", m.GetPrimaryKeyColumn(), extraWhere), @@ -489,22 +513,23 @@ func (m *Video) Reload(ctx context.Context, tx pgx.Tx, includeDeleteds ...bool) return err } - m.ID = t.ID - m.CreatedAt = t.CreatedAt - m.UpdatedAt = t.UpdatedAt - m.DeletedAt = t.DeletedAt - m.FileName = t.FileName - m.StartedAt = t.StartedAt - m.EndedAt = t.EndedAt - m.Duration = t.Duration - m.FileSize = t.FileSize - m.ThumbnailName = t.ThumbnailName - m.Status = t.Status - m.ObjectDetectorClaimedUntil = t.ObjectDetectorClaimedUntil - m.ObjectTrackerClaimedUntil = t.ObjectTrackerClaimedUntil - m.CameraID = t.CameraID - m.CameraIDObject = t.CameraIDObject - m.ReferencedByDetectionVideoIDObjects = t.ReferencedByDetectionVideoIDObjects + m.ID = o.ID + m.CreatedAt = o.CreatedAt + m.UpdatedAt = o.UpdatedAt + m.DeletedAt = o.DeletedAt + m.FileName = o.FileName + m.StartedAt = o.StartedAt + m.EndedAt = o.EndedAt + m.Duration = o.Duration + m.FileSize = o.FileSize + m.ThumbnailName = o.ThumbnailName + m.Status = o.Status + m.ObjectDetectorClaimedUntil = o.ObjectDetectorClaimedUntil + m.ObjectTrackerClaimedUntil = o.ObjectTrackerClaimedUntil + m.CameraID = o.CameraID + m.CameraIDObject = o.CameraIDObject + m.DetectionSummary = o.DetectionSummary + m.ReferencedByDetectionVideoIDObjects = o.ReferencedByDetectionVideoIDObjects return nil } @@ -518,7 +543,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatUUID(m.ID) if err != nil { - return fmt.Errorf("failed to handle m.ID: %v", err) + return fmt.Errorf("failed to handle m.ID; %v", err) } values = append(values, v) @@ -529,7 +554,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatTime(m.CreatedAt) if err != nil { - return fmt.Errorf("failed to handle m.CreatedAt: %v", err) + return fmt.Errorf("failed to handle m.CreatedAt; %v", err) } values = append(values, v) @@ -540,7 +565,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatTime(m.UpdatedAt) if err != nil { - return fmt.Errorf("failed to handle m.UpdatedAt: %v", err) + return fmt.Errorf("failed to handle m.UpdatedAt; %v", err) } values = append(values, v) @@ -551,7 +576,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatTime(m.DeletedAt) if err != nil { - return fmt.Errorf("failed to handle m.DeletedAt: %v", err) + return fmt.Errorf("failed to handle m.DeletedAt; %v", err) } values = append(values, v) @@ -562,7 +587,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatString(m.FileName) if err != nil { - return fmt.Errorf("failed to handle m.FileName: %v", err) + return fmt.Errorf("failed to handle m.FileName; %v", err) } values = append(values, v) @@ -573,7 +598,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatTime(m.StartedAt) if err != nil { - return fmt.Errorf("failed to handle m.StartedAt: %v", err) + return fmt.Errorf("failed to handle m.StartedAt; %v", err) } values = append(values, v) @@ -584,7 +609,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatTime(m.EndedAt) if err != nil { - return fmt.Errorf("failed to handle m.EndedAt: %v", err) + return fmt.Errorf("failed to handle m.EndedAt; %v", err) } values = append(values, v) @@ -595,7 +620,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatDuration(m.Duration) if err != nil { - return fmt.Errorf("failed to handle m.Duration: %v", err) + return fmt.Errorf("failed to handle m.Duration; %v", err) } values = append(values, v) @@ -606,7 +631,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatFloat(m.FileSize) if err != nil { - return fmt.Errorf("failed to handle m.FileSize: %v", err) + return fmt.Errorf("failed to handle m.FileSize; %v", err) } values = append(values, v) @@ -617,7 +642,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatString(m.ThumbnailName) if err != nil { - return fmt.Errorf("failed to handle m.ThumbnailName: %v", err) + return fmt.Errorf("failed to handle m.ThumbnailName; %v", err) } values = append(values, v) @@ -628,7 +653,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatString(m.Status) if err != nil { - return fmt.Errorf("failed to handle m.Status: %v", err) + return fmt.Errorf("failed to handle m.Status; %v", err) } values = append(values, v) @@ -639,7 +664,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatTime(m.ObjectDetectorClaimedUntil) if err != nil { - return fmt.Errorf("failed to handle m.ObjectDetectorClaimedUntil: %v", err) + return fmt.Errorf("failed to handle m.ObjectDetectorClaimedUntil; %v", err) } values = append(values, v) @@ -650,7 +675,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatTime(m.ObjectTrackerClaimedUntil) if err != nil { - return fmt.Errorf("failed to handle m.ObjectTrackerClaimedUntil: %v", err) + return fmt.Errorf("failed to handle m.ObjectTrackerClaimedUntil; %v", err) } values = append(values, v) @@ -661,7 +686,18 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe v, err := types.FormatUUID(m.CameraID) if err != nil { - return fmt.Errorf("failed to handle m.CameraID: %v", err) + return fmt.Errorf("failed to handle m.CameraID; %v", err) + } + + values = append(values, v) + } + + if setZeroValues || !types.IsZeroJSON(m.DetectionSummary) || slices.Contains(forceSetValuesForFields, VideoTableDetectionSummaryColumn) || isRequired(VideoTableColumnLookup, VideoTableDetectionSummaryColumn) { + columns = append(columns, VideoTableDetectionSummaryColumn) + + v, err := types.FormatJSON(m.DetectionSummary) + if err != nil { + return fmt.Errorf("failed to handle m.DetectionSummary; %v", err) } values = append(values, v) @@ -713,7 +749,7 @@ func (m *Video) Insert(ctx context.Context, tx pgx.Tx, setPrimaryKey bool, setZe err = m.Reload(ctx, tx, slices.Contains(forceSetValuesForFields, "deleted_at")) if err != nil { - return fmt.Errorf("failed to reload after insert: %v", err) + return fmt.Errorf("failed to reload after insert; %v", err) } return nil @@ -728,7 +764,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatTime(m.CreatedAt) if err != nil { - return fmt.Errorf("failed to handle m.CreatedAt: %v", err) + return fmt.Errorf("failed to handle m.CreatedAt; %v", err) } values = append(values, v) @@ -739,7 +775,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatTime(m.UpdatedAt) if err != nil { - return fmt.Errorf("failed to handle m.UpdatedAt: %v", err) + return fmt.Errorf("failed to handle m.UpdatedAt; %v", err) } values = append(values, v) @@ -750,7 +786,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatTime(m.DeletedAt) if err != nil { - return fmt.Errorf("failed to handle m.DeletedAt: %v", err) + return fmt.Errorf("failed to handle m.DeletedAt; %v", err) } values = append(values, v) @@ -761,7 +797,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatString(m.FileName) if err != nil { - return fmt.Errorf("failed to handle m.FileName: %v", err) + return fmt.Errorf("failed to handle m.FileName; %v", err) } values = append(values, v) @@ -772,7 +808,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatTime(m.StartedAt) if err != nil { - return fmt.Errorf("failed to handle m.StartedAt: %v", err) + return fmt.Errorf("failed to handle m.StartedAt; %v", err) } values = append(values, v) @@ -783,7 +819,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatTime(m.EndedAt) if err != nil { - return fmt.Errorf("failed to handle m.EndedAt: %v", err) + return fmt.Errorf("failed to handle m.EndedAt; %v", err) } values = append(values, v) @@ -794,7 +830,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatDuration(m.Duration) if err != nil { - return fmt.Errorf("failed to handle m.Duration: %v", err) + return fmt.Errorf("failed to handle m.Duration; %v", err) } values = append(values, v) @@ -805,7 +841,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatFloat(m.FileSize) if err != nil { - return fmt.Errorf("failed to handle m.FileSize: %v", err) + return fmt.Errorf("failed to handle m.FileSize; %v", err) } values = append(values, v) @@ -816,7 +852,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatString(m.ThumbnailName) if err != nil { - return fmt.Errorf("failed to handle m.ThumbnailName: %v", err) + return fmt.Errorf("failed to handle m.ThumbnailName; %v", err) } values = append(values, v) @@ -827,7 +863,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatString(m.Status) if err != nil { - return fmt.Errorf("failed to handle m.Status: %v", err) + return fmt.Errorf("failed to handle m.Status; %v", err) } values = append(values, v) @@ -838,7 +874,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatTime(m.ObjectDetectorClaimedUntil) if err != nil { - return fmt.Errorf("failed to handle m.ObjectDetectorClaimedUntil: %v", err) + return fmt.Errorf("failed to handle m.ObjectDetectorClaimedUntil; %v", err) } values = append(values, v) @@ -849,7 +885,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatTime(m.ObjectTrackerClaimedUntil) if err != nil { - return fmt.Errorf("failed to handle m.ObjectTrackerClaimedUntil: %v", err) + return fmt.Errorf("failed to handle m.ObjectTrackerClaimedUntil; %v", err) } values = append(values, v) @@ -860,7 +896,18 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatUUID(m.CameraID) if err != nil { - return fmt.Errorf("failed to handle m.CameraID: %v", err) + return fmt.Errorf("failed to handle m.CameraID; %v", err) + } + + values = append(values, v) + } + + if setZeroValues || !types.IsZeroJSON(m.DetectionSummary) || slices.Contains(forceSetValuesForFields, VideoTableDetectionSummaryColumn) { + columns = append(columns, VideoTableDetectionSummaryColumn) + + v, err := types.FormatJSON(m.DetectionSummary) + if err != nil { + return fmt.Errorf("failed to handle m.DetectionSummary; %v", err) } values = append(values, v) @@ -868,7 +915,7 @@ func (m *Video) Update(ctx context.Context, tx pgx.Tx, setZeroValues bool, force v, err := types.FormatUUID(m.ID) if err != nil { - return fmt.Errorf("failed to handle m.ID: %v", err) + return fmt.Errorf("failed to handle m.ID; %v", err) } values = append(values, v) @@ -914,7 +961,7 @@ func (m *Video) Delete(ctx context.Context, tx pgx.Tx, hardDeletes ...bool) erro values := make([]any, 0) v, err := types.FormatUUID(m.ID) if err != nil { - return fmt.Errorf("failed to handle m.ID: %v", err) + return fmt.Errorf("failed to handle m.ID; %v", err) } values = append(values, v) @@ -942,7 +989,7 @@ func (m *Video) LockTable(ctx context.Context, tx pgx.Tx, noWait bool) error { return query.LockTable(ctx, tx, VideoTable, noWait) } -func SelectVideos(ctx context.Context, tx pgx.Tx, where string, orderBy *string, limit *int, offset *int, values ...any) ([]*Video, error) { +func SelectVideos(ctx context.Context, tx pgx.Tx, where string, orderBy *string, limit *int, offset *int, values ...any) ([]*Video, int64, int64, int64, int64, error) { if slices.Contains(VideoTableColumns, "deleted_at") { if !strings.Contains(where, "deleted_at") { if where != "" { @@ -956,7 +1003,7 @@ func SelectVideos(ctx context.Context, tx pgx.Tx, where string, orderBy *string, ctx, cleanup := query.WithQueryID(ctx) defer cleanup() - items, err := query.Select( + items, count, totalCount, page, totalPages, err := query.Select( ctx, tx, VideoTableColumnsWithTypeCasts, @@ -968,7 +1015,7 @@ func SelectVideos(ctx context.Context, tx pgx.Tx, where string, orderBy *string, values..., ) if err != nil { - return nil, fmt.Errorf("failed to call SelectVideos; err: %v", err) + return nil, 0, 0, 0, 0, fmt.Errorf("failed to call SelectVideos; %v", err) } objects := make([]*Video, 0) @@ -978,7 +1025,7 @@ func SelectVideos(ctx context.Context, tx pgx.Tx, where string, orderBy *string, err = object.FromItem(item) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } thatCtx := ctx @@ -996,7 +1043,7 @@ func SelectVideos(ctx context.Context, tx pgx.Tx, where string, orderBy *string, thisCtx, ok1 := query.HandleQueryPathGraphCycles(thisCtx, fmt.Sprintf("%s{%v}", CameraTable, object.CameraID)) thisCtx, ok2 := query.HandleQueryPathGraphCycles(thisCtx, fmt.Sprintf("__ReferencedBy__%s{%v}", CameraTable, object.CameraID)) if ok1 && ok2 { - object.CameraIDObject, err = SelectCamera( + object.CameraIDObject, _, _, _, _, err = SelectCamera( thisCtx, tx, fmt.Sprintf("%v = $1", CameraTablePrimaryKeyColumn), @@ -1004,7 +1051,7 @@ func SelectVideos(ctx context.Context, tx pgx.Tx, where string, orderBy *string, ) if err != nil { if !errors.Is(err, sql.ErrNoRows) { - return nil, err + return nil, 0, 0, 0, 0, err } } } @@ -1016,7 +1063,7 @@ func SelectVideos(ctx context.Context, tx pgx.Tx, where string, orderBy *string, thisCtx, ok2 := query.HandleQueryPathGraphCycles(thisCtx, fmt.Sprintf("__ReferencedBy__%s{%v}", VideoTable, object.GetPrimaryKeyValue())) if ok1 && ok2 { - object.ReferencedByDetectionVideoIDObjects, err = SelectDetections( + object.ReferencedByDetectionVideoIDObjects, _, _, _, _, err = SelectDetections( thisCtx, tx, fmt.Sprintf("%v = $1", DetectionTableVideoIDColumn), @@ -1035,20 +1082,20 @@ func SelectVideos(ctx context.Context, tx pgx.Tx, where string, orderBy *string, return nil }() if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } objects = append(objects, object) } - return objects, nil + return objects, count, totalCount, page, totalPages, nil } -func SelectVideo(ctx context.Context, tx pgx.Tx, where string, values ...any) (*Video, error) { +func SelectVideo(ctx context.Context, tx pgx.Tx, where string, values ...any) (*Video, int64, int64, int64, int64, error) { ctx, cleanup := query.WithQueryID(ctx) defer cleanup() - objects, err := SelectVideos( + objects, _, _, _, _, err := SelectVideos( ctx, tx, where, @@ -1058,73 +1105,78 @@ func SelectVideo(ctx context.Context, tx pgx.Tx, where string, values ...any) (* values..., ) if err != nil { - return nil, fmt.Errorf("failed to call SelectVideo; err: %v", err) + return nil, 0, 0, 0, 0, fmt.Errorf("failed to call SelectVideo; %v", err) } if len(objects) > 1 { - return nil, fmt.Errorf("attempt to call SelectVideo returned more than 1 row") + return nil, 0, 0, 0, 0, fmt.Errorf("attempt to call SelectVideo returned more than 1 row") } if len(objects) < 1 { - return nil, sql.ErrNoRows + return nil, 0, 0, 0, 0, sql.ErrNoRows } object := objects[0] - return object, nil + count := int64(1) + totalCount := count + page := int64(1) + totalPages := page + + return object, count, totalCount, page, totalPages, nil } -func handleGetVideos(arguments *server.SelectManyArguments, db *pgxpool.Pool) ([]*Video, error) { +func handleGetVideos(arguments *server.SelectManyArguments, db *pgxpool.Pool) ([]*Video, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } defer func() { _ = tx.Rollback(arguments.Ctx) }() - objects, err := SelectVideos(arguments.Ctx, tx, arguments.Where, arguments.OrderBy, arguments.Limit, arguments.Offset, arguments.Values...) + objects, count, totalCount, page, totalPages, err := SelectVideos(arguments.Ctx, tx, arguments.Where, arguments.OrderBy, arguments.Limit, arguments.Offset, arguments.Values...) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } err = tx.Commit(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } - return objects, nil + return objects, count, totalCount, page, totalPages, nil } -func handleGetVideo(arguments *server.SelectOneArguments, db *pgxpool.Pool, primaryKey uuid.UUID) ([]*Video, error) { +func handleGetVideo(arguments *server.SelectOneArguments, db *pgxpool.Pool, primaryKey uuid.UUID) ([]*Video, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } defer func() { _ = tx.Rollback(arguments.Ctx) }() - object, err := SelectVideo(arguments.Ctx, tx, arguments.Where, arguments.Values...) + object, count, totalCount, page, totalPages, err := SelectVideo(arguments.Ctx, tx, arguments.Where, arguments.Values...) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } err = tx.Commit(arguments.Ctx) if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } - return []*Video{object}, nil + return []*Video{object}, count, totalCount, page, totalPages, nil } -func handlePostVideos(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, objects []*Video, forceSetValuesForFieldsByObjectIndex [][]string) ([]*Video, error) { +func handlePostVideos(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, objects []*Video, forceSetValuesForFieldsByObjectIndex [][]string) ([]*Video, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to begin DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } defer func() { @@ -1133,8 +1185,8 @@ func handlePostVideos(arguments *server.LoadArguments, db *pgxpool.Pool, waitFor xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) - return nil, err + err = fmt.Errorf("failed to get xid; %v", err) + return nil, 0, 0, 0, 0, err } _ = xid @@ -1142,7 +1194,7 @@ func handlePostVideos(arguments *server.LoadArguments, db *pgxpool.Pool, waitFor err = object.Insert(arguments.Ctx, tx, false, false, forceSetValuesForFieldsByObjectIndex[i]...) if err != nil { err = fmt.Errorf("failed to insert %#+v; %v", object, err) - return nil, err + return nil, 0, 0, 0, 0, err } objects[i] = object @@ -1152,7 +1204,7 @@ func handlePostVideos(arguments *server.LoadArguments, db *pgxpool.Pool, waitFor go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.INSERT}, VideoTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -1162,28 +1214,33 @@ func handlePostVideos(arguments *server.LoadArguments, db *pgxpool.Pool, waitFor err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to commit DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } select { case <-arguments.Ctx.Done(): err = fmt.Errorf("context canceled") - return nil, err + return nil, 0, 0, 0, 0, err case err = <-errs: if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } } - return objects, nil + count := int64(len(objects)) + totalCount := count + page := int64(1) + totalPages := page + + return objects, count, totalCount, page, totalPages, nil } -func handlePutVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Video) ([]*Video, error) { +func handlePutVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Video) ([]*Video, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to begin DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } defer func() { @@ -1192,22 +1249,22 @@ func handlePutVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitForCh xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) - return nil, err + err = fmt.Errorf("failed to get xid; %v", err) + return nil, 0, 0, 0, 0, err } _ = xid err = object.Update(arguments.Ctx, tx, true) if err != nil { err = fmt.Errorf("failed to update %#+v; %v", object, err) - return nil, err + return nil, 0, 0, 0, 0, err } errs := make(chan error, 1) go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.UPDATE, stream.SOFT_DELETE, stream.SOFT_RESTORE, stream.SOFT_UPDATE}, VideoTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -1217,28 +1274,33 @@ func handlePutVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitForCh err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to commit DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } select { case <-arguments.Ctx.Done(): err = fmt.Errorf("context canceled") - return nil, err + return nil, 0, 0, 0, 0, err case err = <-errs: if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } } - return []*Video{object}, nil + count := int64(1) + totalCount := count + page := int64(1) + totalPages := page + + return []*Video{object}, count, totalCount, page, totalPages, nil } -func handlePatchVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Video, forceSetValuesForFields []string) ([]*Video, error) { +func handlePatchVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Video, forceSetValuesForFields []string) ([]*Video, int64, int64, int64, int64, error) { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to begin DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } defer func() { @@ -1247,22 +1309,22 @@ func handlePatchVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitFor xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) - return nil, err + err = fmt.Errorf("failed to get xid; %v", err) + return nil, 0, 0, 0, 0, err } _ = xid err = object.Update(arguments.Ctx, tx, false, forceSetValuesForFields...) if err != nil { err = fmt.Errorf("failed to update %#+v; %v", object, err) - return nil, err + return nil, 0, 0, 0, 0, err } errs := make(chan error, 1) go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.UPDATE, stream.SOFT_DELETE, stream.SOFT_RESTORE, stream.SOFT_UPDATE}, VideoTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -1272,27 +1334,32 @@ func handlePatchVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitFor err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) - return nil, err + err = fmt.Errorf("failed to commit DB transaction; %v", err) + return nil, 0, 0, 0, 0, err } select { case <-arguments.Ctx.Done(): err = fmt.Errorf("context canceled") - return nil, err + return nil, 0, 0, 0, 0, err case err = <-errs: if err != nil { - return nil, err + return nil, 0, 0, 0, 0, err } } - return []*Video{object}, nil + count := int64(1) + totalCount := count + page := int64(1) + totalPages := page + + return []*Video{object}, count, totalCount, page, totalPages, nil } func handleDeleteVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitForChange server.WaitForChange, object *Video) error { tx, err := db.Begin(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to begin DB transaction: %v", err) + err = fmt.Errorf("failed to begin DB transaction; %v", err) return err } @@ -1302,7 +1369,7 @@ func handleDeleteVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitFo xid, err := query.GetXid(arguments.Ctx, tx) if err != nil { - err = fmt.Errorf("failed to get xid: %v", err) + err = fmt.Errorf("failed to get xid; %v", err) return err } _ = xid @@ -1317,7 +1384,7 @@ func handleDeleteVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitFo go func() { _, err = waitForChange(arguments.Ctx, []stream.Action{stream.DELETE, stream.SOFT_DELETE}, VideoTable, xid) if err != nil { - err = fmt.Errorf("failed to wait for change: %v", err) + err = fmt.Errorf("failed to wait for change; %v", err) errs <- err return } @@ -1327,7 +1394,7 @@ func handleDeleteVideo(arguments *server.LoadArguments, db *pgxpool.Pool, waitFo err = tx.Commit(arguments.Ctx) if err != nil { - err = fmt.Errorf("failed to commit DB transaction: %v", err) + err = fmt.Errorf("failed to commit DB transaction; %v", err) return err } @@ -1361,7 +1428,7 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s queryParams map[string]any, req server.EmptyRequest, rawReq any, - ) (*helpers.TypedResponse[Video], error) { + ) (*server.Response[Video], error) { redisConn := redisPool.Get() defer func() { _ = redisConn.Close() @@ -1372,47 +1439,61 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s return nil, err } - cachedObjectsAsJSON, cacheHit, err := helpers.GetCachedObjectsAsJSON(arguments.RequestHash, redisConn) + cachedResponseAsJSON, cacheHit, err := server.GetCachedResponseAsJSON(arguments.RequestHash, redisConn) if err != nil { return nil, err } if cacheHit { - var cachedObjects []*Video - err = json.Unmarshal(cachedObjectsAsJSON, &cachedObjects) + var cachedResponse server.Response[Video] + + /* TODO: it'd be nice to be able to avoid this (i.e. just pass straight through) */ + err = json.Unmarshal(cachedResponseAsJSON, &cachedResponse) if err != nil { return nil, err } - return &helpers.TypedResponse[Video]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: cachedObjects, - }, nil + return &cachedResponse, nil } - objects, err := handleGetVideos(arguments, db) + objects, count, totalCount, _, _, err := handleGetVideos(arguments, db) if err != nil { return nil, err } - objectsAsJSON, err := json.Marshal(objects) + limit := int64(0) + if arguments.Limit != nil { + limit = int64(*arguments.Limit) + } + + offset := int64(0) + if arguments.Offset != nil { + offset = int64(*arguments.Offset) + } + + response := server.Response[Video]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, + } + + /* TODO: it'd be nice to be able to avoid this (i.e. just marshal once, further out) */ + responseAsJSON, err := json.Marshal(response) if err != nil { return nil, err } - err = helpers.StoreCachedResponse(arguments.RequestHash, redisConn, string(objectsAsJSON)) + err = server.StoreCachedResponse(arguments.RequestHash, redisConn, responseAsJSON) if err != nil { - log.Printf("warning: %v", err) + log.Printf("warning; %v", err) } - return &helpers.TypedResponse[Video]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, - }, nil + return &response, nil }, ) if err != nil { @@ -1430,7 +1511,7 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s queryParams VideoLoadQueryParams, req server.EmptyRequest, rawReq any, - ) (*helpers.TypedResponse[Video], error) { + ) (*server.Response[Video], error) { redisConn := redisPool.Get() defer func() { _ = redisConn.Close() @@ -1441,47 +1522,55 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s return nil, err } - cachedObjectsAsJSON, cacheHit, err := helpers.GetCachedObjectsAsJSON(arguments.RequestHash, redisConn) + cachedResponseAsJSON, cacheHit, err := server.GetCachedResponseAsJSON(arguments.RequestHash, redisConn) if err != nil { return nil, err } if cacheHit { - var cachedObjects []*Video - err = json.Unmarshal(cachedObjectsAsJSON, &cachedObjects) + var cachedResponse server.Response[Video] + + /* TODO: it'd be nice to be able to avoid this (i.e. just pass straight through) */ + err = json.Unmarshal(cachedResponseAsJSON, &cachedResponse) if err != nil { return nil, err } - return &helpers.TypedResponse[Video]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: cachedObjects, - }, nil + return &cachedResponse, nil } - objects, err := handleGetVideo(arguments, db, pathParams.PrimaryKey) + objects, count, totalCount, _, _, err := handleGetVideo(arguments, db, pathParams.PrimaryKey) if err != nil { return nil, err } - objectsAsJSON, err := json.Marshal(objects) + limit := int64(0) + + offset := int64(0) + + response := server.Response[Video]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, + } + + /* TODO: it'd be nice to be able to avoid this (i.e. just marshal once, further out) */ + responseAsJSON, err := json.Marshal(response) if err != nil { return nil, err } - err = helpers.StoreCachedResponse(arguments.RequestHash, redisConn, string(objectsAsJSON)) + err = server.StoreCachedResponse(arguments.RequestHash, redisConn, responseAsJSON) if err != nil { - log.Printf("warning: %v", err) + log.Printf("warning; %v", err) } - return &helpers.TypedResponse[Video]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, - }, nil + return &response, nil }, ) if err != nil { @@ -1499,7 +1588,7 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s queryParams VideoLoadQueryParams, req []*Video, rawReq any, - ) (*helpers.TypedResponse[Video], error) { + ) (*server.Response[Video], error) { allRawItems, ok := rawReq.([]any) if !ok { return nil, fmt.Errorf("failed to cast %#+v to []map[string]any", rawReq) @@ -1533,16 +1622,24 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s return nil, err } - objects, err := handlePostVideos(arguments, db, waitForChange, req, forceSetValuesForFieldsByObjectIndex) + objects, count, totalCount, _, _, err := handlePostVideos(arguments, db, waitForChange, req, forceSetValuesForFieldsByObjectIndex) if err != nil { return nil, err } - return &helpers.TypedResponse[Video]{ - Status: http.StatusCreated, - Success: true, - Error: nil, - Objects: objects, + limit := int64(0) + + offset := int64(0) + + return &server.Response[Video]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, }, nil }, ) @@ -1561,7 +1658,7 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s queryParams VideoLoadQueryParams, req Video, rawReq any, - ) (*helpers.TypedResponse[Video], error) { + ) (*server.Response[Video], error) { item, ok := rawReq.(map[string]any) if !ok { return nil, fmt.Errorf("failed to cast %#+v to map[string]any", item) @@ -1575,16 +1672,24 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s object := &req object.ID = pathParams.PrimaryKey - objects, err := handlePutVideo(arguments, db, waitForChange, object) + objects, count, totalCount, _, _, err := handlePutVideo(arguments, db, waitForChange, object) if err != nil { return nil, err } - return &helpers.TypedResponse[Video]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, + limit := int64(0) + + offset := int64(0) + + return &server.Response[Video]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, }, nil }, ) @@ -1603,7 +1708,7 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s queryParams VideoLoadQueryParams, req Video, rawReq any, - ) (*helpers.TypedResponse[Video], error) { + ) (*server.Response[Video], error) { item, ok := rawReq.(map[string]any) if !ok { return nil, fmt.Errorf("failed to cast %#+v to map[string]any", item) @@ -1626,16 +1731,24 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s object := &req object.ID = pathParams.PrimaryKey - objects, err := handlePatchVideo(arguments, db, waitForChange, object, forceSetValuesForFields) + objects, count, totalCount, _, _, err := handlePatchVideo(arguments, db, waitForChange, object, forceSetValuesForFields) if err != nil { return nil, err } - return &helpers.TypedResponse[Video]{ - Status: http.StatusOK, - Success: true, - Error: nil, - Objects: objects, + limit := int64(0) + + offset := int64(0) + + return &server.Response[Video]{ + Status: http.StatusOK, + Success: true, + Error: nil, + Objects: objects, + Count: count, + TotalCount: totalCount, + Limit: limit, + Offset: offset, }, nil }, ) @@ -1655,14 +1768,15 @@ func GetVideoRouter(db *pgxpool.Pool, redisPool *redis.Pool, httpMiddlewares []s req server.EmptyRequest, rawReq any, ) (*server.EmptyResponse, error) { - arguments := &server.LoadArguments{ - Ctx: ctx, + arguments, err := server.GetLoadArguments(ctx, queryParams.Depth) + if err != nil { + return nil, err } object := &Video{} object.ID = pathParams.PrimaryKey - err := handleDeleteVideo(arguments, db, waitForChange, object) + err = handleDeleteVideo(arguments, db, waitForChange, object) if err != nil { return nil, err } diff --git a/pkg/segment_producer/segment_producer.go b/pkg/segment_producer/segment_producer.go index c4f85ab..6fb6232 100644 --- a/pkg/segment_producer/segment_producer.go +++ b/pkg/segment_producer/segment_producer.go @@ -400,7 +400,7 @@ func Run() error { return err } - cameras, err := api.SelectCameras( + cameras, _, _, _, _, err := api.SelectCameras( ctx, tx, fmt.Sprintf( @@ -429,7 +429,7 @@ func Run() error { now := time.Now().UTC() camera.LastSeen = now camera.SegmentProducerClaimedUntil = now.Add(time.Second * time.Duration(durationSeconds) * 2) - camera.StreamProducerClaimedUntil = time.Time{} + camera.StreamProducerClaimedUntil = time.Time{} // zero to ensure we don't wipe out an existing value err = camera.Update(ctx, tx, false) if err != nil { @@ -459,7 +459,7 @@ func Run() error { _ = tx.Rollback(ctx) }() - orphanedVideos, err := api.SelectVideos( + orphanedVideos, _, _, _, _, err := api.SelectVideos( ctx, tx, fmt.Sprintf( diff --git a/pkg/stream_producer/stream_producer.go b/pkg/stream_producer/stream_producer.go index 08363ce..6e7c69b 100644 --- a/pkg/stream_producer/stream_producer.go +++ b/pkg/stream_producer/stream_producer.go @@ -331,7 +331,7 @@ func Run() error { return err } - cameras, err := api.SelectCameras( + cameras, _, _, _, _, err := api.SelectCameras( ctx, tx, fmt.Sprintf( @@ -422,7 +422,7 @@ func Run() error { }() if camera != nil { - camera.StreamProducerClaimedUntil = time.Time{} + camera.SegmentProducerClaimedUntil = time.Time{} // zero to ensure we don't wipe out an existing value camera.StreamProducerClaimedUntil = time.Now().UTC().Add(time.Second * 1) err = camera.Update(ctx, tx, false) diff --git a/schema/openapi.json b/schema/openapi.json index 4b8e91b..5a6efcb 100644 --- a/schema/openapi.json +++ b/schema/openapi.json @@ -1564,8 +1564,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -1573,12 +1584,20 @@ "$ref": "#/components/schemas/Camera" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -1597,7 +1616,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -1654,8 +1676,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -1663,12 +1696,20 @@ "$ref": "#/components/schemas/Camera" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -1687,7 +1728,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -1740,8 +1784,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -1749,12 +1804,20 @@ "$ref": "#/components/schemas/Camera" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -1773,7 +1836,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -1834,8 +1900,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -1843,12 +1920,20 @@ "$ref": "#/components/schemas/Camera" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -1867,7 +1952,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -1928,8 +2016,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -1937,12 +2036,20 @@ "$ref": "#/components/schemas/Camera" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -1961,7 +2068,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -2016,8 +2126,148 @@ "type": "object", "properties": { "error": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "integer", + "format": "int32" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "status", + "success" + ] + } + } + } + } + } + } + }, + "/api/custom/claim-video-for-object-detector": { + "patch": { + "tags": [ + "Custom0" + ], + "operationId": "PatchCustom0", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "claim_duration_seconds": { + "type": "number", + "format": "double" + } + }, + "required": [ + "claim_duration_seconds" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Custom0 success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "camera_id": { + "type": "string", + "format": "uuid" + }, + "camera_id_object": { + "$ref": "#/components/schemas/NullableCamera" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "deleted_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "detection_summary": { + "type": "object", + "nullable": true + }, + "duration": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "ended_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "file_name": { "type": "string" }, + "file_size": { + "type": "number", + "format": "double", + "nullable": true + }, + "id": { + "type": "string", + "format": "uuid" + }, + "object_detector_claimed_until": { + "type": "string", + "format": "date-time" + }, + "object_tracker_claimed_until": { + "type": "string", + "format": "date-time" + }, + "referenced_by_detection_video_id_objects": { + "$ref": "#/components/schemas/NullableArrayOfDetection" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + }, + "thumbnail_name": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + } + } + } + }, + "default": { + "description": "Custom0 failure", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "array", + "items": { + "type": "string" + } + }, "status": { "type": "integer", "format": "int32" @@ -3774,8 +4024,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -3783,12 +4044,20 @@ "$ref": "#/components/schemas/Detection" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -3807,7 +4076,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -3864,8 +4136,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -3873,12 +4156,20 @@ "$ref": "#/components/schemas/Detection" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -3897,7 +4188,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -3950,8 +4244,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -3959,12 +4264,20 @@ "$ref": "#/components/schemas/Detection" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -3983,7 +4296,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -4044,8 +4360,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -4053,12 +4380,20 @@ "$ref": "#/components/schemas/Detection" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -4077,7 +4412,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -4138,8 +4476,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -4147,12 +4496,20 @@ "$ref": "#/components/schemas/Detection" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -4171,7 +4528,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -4226,7 +4586,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -6648,8 +7011,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -6657,12 +7031,20 @@ "$ref": "#/components/schemas/Video" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -6681,7 +7063,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -6738,8 +7123,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -6747,12 +7143,20 @@ "$ref": "#/components/schemas/Video" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -6771,7 +7175,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -6824,8 +7231,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -6833,12 +7251,20 @@ "$ref": "#/components/schemas/Video" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -6857,7 +7283,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -6918,8 +7347,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -6927,12 +7367,20 @@ "$ref": "#/components/schemas/Video" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -6951,7 +7399,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -7012,8 +7463,19 @@ "schema": { "type": "object", "properties": { + "count": { + "type": "integer", + "format": "int64" + }, "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "format": "int64" }, "objects": { "type": "array", @@ -7021,12 +7483,20 @@ "$ref": "#/components/schemas/Video" } }, + "offset": { + "type": "integer", + "format": "int64" + }, "status": { "type": "integer", "format": "int32" }, "success": { "type": "boolean" + }, + "total_count": { + "type": "integer", + "format": "int64" } }, "required": [ @@ -7045,7 +7515,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -7100,7 +7573,10 @@ "type": "object", "properties": { "error": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "integer", @@ -7124,6 +7600,10 @@ }, "components": { "schemas": { + "Any": { + "type": "object", + "nullable": true + }, "Camera": { "type": "object", "properties": { @@ -7316,6 +7796,10 @@ "format": "date-time", "nullable": true }, + "detection_summary": { + "type": "object", + "nullable": true + }, "duration": { "type": "integer", "format": "int64",