diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index c6369340b5c3..e587e26aa1b8 100644 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -55,7 +55,7 @@ jobs: cache-from: type=local,src=/tmp/cvat_cache_server context: . file: Dockerfile - tags: cvat/server + tags: cvat/server:${{ env.CVAT_VERSION }} outputs: type=docker,dest=/tmp/cvat_server/image.tar - name: CVAT UI. Build and push @@ -64,7 +64,7 @@ jobs: cache-from: type=local,src=/tmp/cvat_cache_ui context: . file: Dockerfile.ui - tags: cvat/ui + tags: cvat/ui:${{ env.CVAT_VERSION }} outputs: type=docker,dest=/tmp/cvat_ui/image.tar - name: CVAT SDK. Build @@ -102,7 +102,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.9' - name: Download CVAT server image uses: actions/download-artifact@v4 @@ -126,8 +126,6 @@ jobs: run: | docker load --input /tmp/cvat_server/image.tar docker load --input /tmp/cvat_ui/image.tar - docker tag cvat/server:latest cvat/server:${CVAT_VERSION} - docker tag cvat/ui:latest cvat/ui:${CVAT_VERSION} docker image ls -a - name: Verify API schema @@ -203,7 +201,6 @@ jobs: - name: Load Docker server image run: | docker load --input /tmp/cvat_server/image.tar - docker tag cvat/server:latest cvat/server:${CVAT_VERSION} docker image ls -a - name: Running OPA tests @@ -280,8 +277,6 @@ jobs: run: | docker load --input /tmp/cvat_server/image.tar docker load --input /tmp/cvat_ui/image.tar - docker tag cvat/server:latest cvat/server:${CVAT_VERSION} - docker tag cvat/ui:latest cvat/ui:${CVAT_VERSION} docker image ls -a - name: Run CVAT instance diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0c9211b0c4a5..f4e3f11d1052 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,7 +67,7 @@ jobs: cache-from: type=local,src=/tmp/cvat_cache_server context: . file: Dockerfile - tags: cvat/server + tags: cvat/server:${{ env.CVAT_VERSION }} outputs: type=docker,dest=/tmp/cvat_server/image.tar - name: Instrumentation of the code then rebuilding the CVAT UI @@ -81,7 +81,7 @@ jobs: cache-from: type=local,src=/tmp/cvat_cache_ui context: . file: Dockerfile.ui - tags: cvat/ui + tags: cvat/ui:${{ env.CVAT_VERSION }} outputs: type=docker,dest=/tmp/cvat_ui/image.tar - name: CVAT SDK. Build @@ -95,7 +95,7 @@ jobs: id: verify_schema run: | docker load --input /tmp/cvat_server/image.tar - docker run --rm cvat/server bash \ + docker run --rm "cvat/server:${CVAT_VERSION}" bash \ -c 'python manage.py spectacular' > cvat/schema-expected.yml if ! git diff --no-index cvat/schema.yml cvat/schema-expected.yml; then @@ -109,7 +109,7 @@ jobs: - name: Verify migrations run: | - docker run --rm cvat/server bash \ + docker run --rm "cvat/server:${CVAT_VERSION}" bash \ -c 'python manage.py makemigrations --check' - name: Upload CVAT server artifact @@ -138,7 +138,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.9' - name: Download CVAT server image uses: actions/download-artifact@v4 @@ -156,8 +156,6 @@ jobs: run: | docker load --input /tmp/cvat_server/image.tar docker load --input /tmp/cvat_ui/image.tar - docker tag cvat/server:latest cvat/server:${CVAT_VERSION} - docker tag cvat/ui:latest cvat/ui:${CVAT_VERSION} docker image ls -a - name: Generate SDK @@ -221,7 +219,6 @@ jobs: - name: Load Docker server image run: | docker load --input /tmp/cvat_server/image.tar - docker tag cvat/server:latest cvat/server:${CVAT_VERSION} docker image ls -a - name: Running OPA tests @@ -304,8 +301,6 @@ jobs: run: | docker load --input /tmp/cvat_server/image.tar docker load --input /tmp/cvat_ui/image.tar - docker tag cvat/server:latest cvat/server:${CVAT_VERSION} - docker tag cvat/ui:latest cvat/ui:${CVAT_VERSION} docker image ls -a - name: Run CVAT instance @@ -426,10 +421,10 @@ jobs: SERVER_IMAGE_REPO: ${{ secrets.DOCKERHUB_WORKSPACE }}/server UI_IMAGE_REPO: ${{ secrets.DOCKERHUB_WORKSPACE }}/ui run: | - docker tag cvat/server:latest "${SERVER_IMAGE_REPO}:dev" + docker tag "cvat/server:${CVAT_VERSION}" "${SERVER_IMAGE_REPO}:dev" docker push "${SERVER_IMAGE_REPO}:dev" - docker tag cvat/ui:latest "${UI_IMAGE_REPO}:dev" + docker tag "cvat/ui:${CVAT_VERSION}" "${UI_IMAGE_REPO}:dev" docker push "${UI_IMAGE_REPO}:dev" codecov: diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml index c2071cd85d13..bf74b30df047 100644 --- a/.github/workflows/schedule.yml +++ b/.github/workflows/schedule.yml @@ -5,9 +5,8 @@ on: workflow_dispatch: env: - SERVER_IMAGE_TEST_REPO: cvat_server - UI_IMAGE_TEST_REPO: instrumentation_cvat_ui CYPRESS_VERIFY_TIMEOUT: 180000 # https://docs.cypress.io/guides/guides/command-line#cypress-verify + CVAT_VERSION: "local" jobs: check_updates: @@ -48,12 +47,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_CI_USERNAME }} - password: ${{ secrets.DOCKERHUB_CI_TOKEN }} - - name: CVAT server. Getting cache from the default branch uses: actions/cache@v4 with: @@ -66,34 +59,23 @@ jobs: path: /tmp/cvat_cache_ui key: ${{ runner.os }}-build-ui-${{ needs.search_cache.outputs.sha }} - - name: CVAT server. Extract metadata (tags, labels) for Docker - id: meta-server - uses: docker/metadata-action@master - with: - images: ${{ secrets.DOCKERHUB_CI_WORKSPACE }}/${{ env.SERVER_IMAGE_TEST_REPO }} - tags: - type=raw,value=nightly - - - name: CVAT UI. Extract metadata (tags, labels) for Docker - id: meta-ui - uses: docker/metadata-action@master - with: - images: ${{ secrets.DOCKERHUB_CI_WORKSPACE }}/${{ env.UI_IMAGE_TEST_REPO }} - tags: - type=raw,value=nightly - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Create artifact directories + run: | + mkdir /tmp/cvat_server + mkdir /tmp/cvat_ui + mkdir /tmp/cvat_sdk + - name: CVAT server. Build and push uses: docker/build-push-action@v6 with: cache-from: type=local,src=/tmp/cvat_cache_server context: . file: Dockerfile - push: true - tags: ${{ steps.meta-server.outputs.tags }} - labels: ${{ steps.meta-server.outputs.labels }} + tags: cvat/server:${{ env.CVAT_VERSION }} + outputs: type=docker,dest=/tmp/cvat_server/image.tar - name: CVAT UI. Build and push uses: docker/build-push-action@v6 @@ -101,9 +83,20 @@ jobs: cache-from: type=local,src=/tmp/cvat_cache_ui context: . file: Dockerfile.ui - push: true - tags: ${{ steps.meta-ui.outputs.tags }} - labels: ${{ steps.meta-ui.outputs.labels }} + tags: cvat/ui:${{ env.CVAT_VERSION }} + outputs: type=docker,dest=/tmp/cvat_ui/image.tar + + - name: Upload CVAT server artifact + uses: actions/upload-artifact@v4 + with: + name: cvat_server + path: /tmp/cvat_server/image.tar + + - name: Upload CVAT UI artifact + uses: actions/upload-artifact@v4 + with: + name: cvat_ui + path: /tmp/cvat_ui/image.tar unit_testing: needs: build @@ -113,43 +106,25 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.9' - - name: Getting CVAT UI cache from the default branch - uses: actions/cache@v4 + - name: Download CVAT server image + uses: actions/download-artifact@v4 with: - path: /tmp/cvat_cache_ui - key: ${{ runner.os }}-build-ui-${{ needs.search_cache.outputs.sha }} + name: cvat_server + path: /tmp/cvat_server/ - - name: Building CVAT UI image - uses: docker/build-push-action@v6 + - name: Download CVAT UI images + uses: actions/download-artifact@v4 with: - context: . - file: ./Dockerfile.ui - cache-from: type=local,src=/tmp/cvat_cache_ui - tags: cvat/ui:latest - load: true + name: cvat_ui + path: /tmp/cvat_ui/ - - name: CVAT server. Extract metadata (tags, labels) for Docker - id: meta-server - uses: docker/metadata-action@master - with: - images: ${{ secrets.DOCKERHUB_CI_WORKSPACE }}/${{ env.SERVER_IMAGE_TEST_REPO }} - tags: - type=raw,value=nightly - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_CI_USERNAME }} - password: ${{ secrets.DOCKERHUB_CI_TOKEN }} - - - name: Pull CVAT server image + - name: Load Docker images run: | - docker pull ${{ steps.meta-server.outputs.tags }} - docker tag ${{ steps.meta-server.outputs.tags }} cvat/server:local - docker tag ${{ steps.meta-server.outputs.tags }} cvat/server:latest - docker tag cvat/ui:latest cvat/ui:local + docker load --input /tmp/cvat_server/image.tar + docker load --input /tmp/cvat_ui/image.tar + docker image ls -a - name: OPA tests run: | @@ -210,35 +185,23 @@ jobs: with: node-version: '16.x' - - name: Login to Docker Hub - uses: docker/login-action@v3 + - name: Download CVAT server image + uses: actions/download-artifact@v4 with: - username: ${{ secrets.DOCKERHUB_CI_USERNAME }} - password: ${{ secrets.DOCKERHUB_CI_TOKEN }} + name: cvat_server + path: /tmp/cvat_server/ - - name: CVAT server. Extract metadata (tags, labels) for Docker - id: meta-server - uses: docker/metadata-action@master + - name: Download CVAT UI image + uses: actions/download-artifact@v4 with: - images: ${{ secrets.DOCKERHUB_CI_WORKSPACE }}/${{ env.SERVER_IMAGE_TEST_REPO }} - tags: - type=raw,value=nightly + name: cvat_ui + path: /tmp/cvat_ui/ - - name: CVAT UI. Extract metadata (tags, labels) for Docker - id: meta-ui - uses: docker/metadata-action@master - with: - images: ${{ secrets.DOCKERHUB_CI_USERNAME }}/${{ env.UI_IMAGE_TEST_REPO }} - tags: - type=raw,value=nightly - - - name: Pull CVAT UI image + - name: Load Docker images run: | - docker pull ${{ steps.meta-server.outputs.tags }} - docker tag ${{ steps.meta-server.outputs.tags }} cvat/server:dev - - docker pull ${{ steps.meta-ui.outputs.tags }} - docker tag ${{ steps.meta-ui.outputs.tags }} cvat/ui:dev + docker load --input /tmp/cvat_server/image.tar + docker load --input /tmp/cvat_ui/image.tar + docker image ls -a - name: Run CVAT instance run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9a4fd9584c..42d2893080be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + +## \[2.21.3\] - 2024-10-31 + +### Changed + +- CLI no longer prints the stack trace in case of HTTP errors + () + +### Removed + +- Dropped support for Python 3.8 since its EOL was on 2024-10-07 + () + +### Fixed + +- Requests page crush with `Cannot read property 'target' of undefined` error + () + +- Tags in ground truth job were displayed as `tag (GT)` + () + +- Tags in ground truth job couldn't be deleted via `x` button + () + +- Exception 'Canvas is busy' when change frame during drag/resize a track + () + +- A shape gets shifted if auto save triggered during dragging + () + ## \[2.21.2\] - 2024-10-24 diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 480a5d3aea52..ab9a96682746 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -245,6 +245,53 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.dispatchEvent(event); } + private resetViewPosition(clientID: number): void { + const drawnState = this.drawnStates[clientID]; + const drawnShape = this.svgShapes[clientID]; + + if (drawnState && drawnShape) { + const { shapeType, points } = drawnState; + const translatedPoints: number[] = this.translateToCanvas(points); + const stringified = stringifyPoints(translatedPoints); + if (shapeType === 'cuboid') { + drawnShape.attr('points', stringified); + } else if (['polygon', 'polyline', 'points'].includes(shapeType)) { + (drawnShape as SVG.PolyLine | SVG.Polygon).plot(stringified); + if (shapeType === 'points') { + this.selectize(false, drawnShape); + this.setupPoints(drawnShape as SVG.PolyLine, drawnState); + } + } else if (shapeType === 'rectangle') { + const [xtl, ytl, xbr, ybr] = translatedPoints; + drawnShape.rotate(0); + drawnShape.size(xbr - xtl, ybr - ytl).move(xtl, ytl); + drawnShape.rotate(drawnState.rotation); + } else if (shapeType === 'ellipse') { + const [cx, cy, rightX, topY] = translatedPoints; + const [rx, ry] = [rightX - cx, cy - topY]; + drawnShape.rotate(0); + drawnShape.size(rx * 2, ry * 2).center(cx, cy); + drawnShape.rotate(drawnState.rotation); + } else if (shapeType === 'skeleton') { + drawnShape.rotate(0); + for (const child of (drawnShape as SVG.G).children()) { + if (child.type === 'circle') { + const childClientID = child.attr('data-client-id'); + const element = drawnState.elements.find((el: any) => el.clientID === childClientID); + const [x, y] = this.translateToCanvas(element.points); + child.center(x, y); + } + } + drawnShape.rotate(drawnState.rotation); + } else if (shapeType === 'mask') { + const [left, top] = points.slice(-4); + drawnShape.move(this.geometry.offset + left, this.geometry.offset + top); + } else { + throw new Error('Not implemented'); + } + } + } + private onInteraction = ( shapes: InteractionResult[] | null, shapesUpdated = true, @@ -1114,6 +1161,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } }).on('dragend', (e: CustomEvent): void => { if (aborted) { + this.resetViewPosition(state.clientID); return; } @@ -1172,6 +1220,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.draggableShape = null; aborted = true; // disable internal drag events of SVG.js + // call chain is (mouseup -> SVG.handler.end -> SVG.handler.drag -> dragend) window.dispatchEvent(new MouseEvent('mouseup')); }); } else { @@ -1303,6 +1352,7 @@ export class CanvasViewImpl implements CanvasView, Listener { }) .on('resizedone', (): void => { if (aborted) { + this.resetViewPosition(state.clientID); return; } @@ -1359,7 +1409,8 @@ export class CanvasViewImpl implements CanvasView, Listener { onResizeEnd(); aborted = true; this.resizableShape = null; - // disable internal drag events of SVG.js + // disable internal resize events of SVG.js + // call chain is (mouseup -> SVG.handler.end -> SVG.handler.resize-> resizeend) window.dispatchEvent(new MouseEvent('mouseup')); }); } else { @@ -3401,7 +3452,7 @@ export class CanvasViewImpl implements CanvasView, Listener { return skeleton; } - private setupPoints(basicPolyline: SVG.PolyLine, state: any): any { + private setupPoints(basicPolyline: SVG.PolyLine, state: any | DrawnState): any { this.selectize(true, basicPolyline); const group: SVG.G = basicPolyline diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt index c307b6c96895..063da9c864b4 100644 --- a/cvat-cli/requirements/base.txt +++ b/cvat-cli/requirements/base.txt @@ -1,3 +1,3 @@ -cvat-sdk~=2.21.2 +cvat-sdk~=2.21.3 Pillow>=10.3.0 setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/cvat-cli/setup.py b/cvat-cli/setup.py index 454ce2f00956..05b20a9165e1 100644 --- a/cvat-cli/setup.py +++ b/cvat-cli/setup.py @@ -56,7 +56,7 @@ def parse_requirements(filename=BASE_REQUIREMENTS_FILE): "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], - python_requires=">=3.8", + python_requires=">=3.9", install_requires=BASE_REQUIREMENTS, entry_points={ "console_scripts": [ diff --git a/cvat-cli/src/cvat_cli/__main__.py b/cvat-cli/src/cvat_cli/__main__.py index 2448587245f9..b18c8d8bb751 100755 --- a/cvat-cli/src/cvat_cli/__main__.py +++ b/cvat-cli/src/cvat_cli/__main__.py @@ -9,6 +9,7 @@ from types import SimpleNamespace from typing import List +import urllib3.exceptions from cvat_sdk import exceptions from cvat_sdk.core.client import Client, Config @@ -70,7 +71,7 @@ def main(args: List[str] = None): try: cli = CLI(client=client, credentials=parsed_args.auth) actions[parsed_args.action](cli, **vars(action_args)) - except exceptions.ApiException as e: + except (exceptions.ApiException, urllib3.exceptions.HTTPError) as e: logger.critical(e) return 1 diff --git a/cvat-cli/src/cvat_cli/version.py b/cvat-cli/src/cvat_cli/version.py index 528bf553abd1..3899bcb7cd96 100644 --- a/cvat-cli/src/cvat_cli/version.py +++ b/cvat-cli/src/cvat_cli/version.py @@ -1 +1 @@ -VERSION = "2.21.2" +VERSION = "2.21.3" diff --git a/cvat-core/package.json b/cvat-core/package.json index 782d74c15b65..a769b74bf78c 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "15.2.0", + "version": "15.2.1", "type": "module", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", diff --git a/cvat-core/src/core-types.ts b/cvat-core/src/core-types.ts index e44a354cb5bd..c05b7b6ba4a5 100644 --- a/cvat-core/src/core-types.ts +++ b/cvat-core/src/core-types.ts @@ -2,7 +2,9 @@ // // SPDX-License-Identifier: MIT -import { ModelKind, ModelReturnType, ShapeType } from './enums'; +import { + ModelKind, ModelReturnType, RQStatus, ShapeType, +} from './enums'; export interface ModelAttribute { name: string; @@ -54,4 +56,10 @@ export interface SerializedModel { updated_date?: string; } +export interface UpdateStatusData { + status: RQStatus; + progress: number; + message: string; +} + export type PaginatedResource = T[] & { count: number }; diff --git a/cvat-core/src/request.ts b/cvat-core/src/request.ts index 66ae49b4c96b..1935f78b2f0c 100644 --- a/cvat-core/src/request.ts +++ b/cvat-core/src/request.ts @@ -6,10 +6,10 @@ import { RQStatus } from './enums'; import User from './user'; import { SerializedRequest } from './server-response-types'; -type Operation = { +export type RequestOperation = { target: string; type: string; - format: string; + format: string | null; jobID: number | null; taskID: number | null; projectID: number | null; @@ -44,9 +44,7 @@ export class Request { this.#finishedDate = initialData.finished_date; this.#expiryDate = initialData.expiry_date; - if (initialData.owner) { - this.#owner = new User(initialData.owner); - } + this.#owner = new User(initialData.owner); } get id(): string { @@ -57,7 +55,7 @@ export class Request { return this.#status.toLowerCase() as RQStatus; } - get progress(): number { + get progress(): number | undefined { return this.#progress; } @@ -65,7 +63,7 @@ export class Request { return this.#message; } - get operation(): Operation { + get operation(): RequestOperation { return { target: this.#operation.target, type: this.#operation.type, @@ -77,11 +75,11 @@ export class Request { }; } - get url(): string { + get url(): string | undefined { return this.#resultUrl; } - get resultID(): number { + get resultID(): number | undefined { return this.#resultID; } @@ -89,19 +87,49 @@ export class Request { return this.#createdDate; } - get startedDate(): string { + get startedDate(): string | undefined { return this.#startedDate; } - get finishedDate(): string { + get finishedDate(): string | undefined { return this.#finishedDate; } - get expiryDate(): string { + get expiryDate(): string | undefined { return this.#expiryDate; } get owner(): User { return this.#owner; } + + public toJSON(): SerializedRequest { + const result: SerializedRequest = { + id: this.#id, + status: this.#status, + operation: { + target: this.#operation.target, + type: this.#operation.type, + format: this.#operation.format, + job_id: this.#operation.job_id, + task_id: this.#operation.task_id, + project_id: this.#operation.project_id, + function_id: this.#operation.function_id, + }, + progress: this.#progress, + message: this.#message, + result_url: this.#resultUrl, + result_id: this.#resultID, + created_date: this.#createdDate, + started_date: this.#startedDate, + finished_date: this.#finishedDate, + expiry_date: this.#expiryDate, + owner: { + id: this.#owner.id, + username: this.#owner.username, + }, + }; + + return result; + } } diff --git a/cvat-core/src/requests-manager.ts b/cvat-core/src/requests-manager.ts index c348923e68bc..711073988955 100644 --- a/cvat-core/src/requests-manager.ts +++ b/cvat-core/src/requests-manager.ts @@ -122,14 +122,15 @@ class RequestsManager { } } catch (error) { if (requestID in this.listening) { - const { onUpdate } = this.listening[requestID]; - - onUpdate - .forEach((update) => update(new Request({ - id: requestID, - status: RQStatus.FAILED, - message: `Could not get a status of the request ${requestID}. ${error.toString()}`, - }))); + const { onUpdate, request } = this.listening[requestID]; + if (request) { + onUpdate + .forEach((update) => update(new Request({ + ...request.toJSON(), + status: RQStatus.FAILED, + message: `Could not get a status of the request ${requestID}. ${error.toString()}`, + }))); + } reject(error); } } diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index eb9c15ce64b9..37f2337c0e52 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -21,7 +21,7 @@ import { SerializedQualityReportData, APIQualityReportsFilter, SerializedAnalyticsReport, APIAnalyticsReportFilter, SerializedRequest, SerializedJobValidationLayout, SerializedTaskValidationLayout, } from './server-response-types'; -import { PaginatedResource } from './core-types'; +import { PaginatedResource, UpdateStatusData } from './core-types'; import { Request } from './request'; import { Storage } from './storage'; import { SerializedEvent } from './event'; @@ -1069,7 +1069,7 @@ type LongProcessListener = Record, taskDataSpec: any, - onUpdate: (request: Request) => void, + onUpdate: (request: Request | UpdateStatusData) => void, ): Promise<{ taskID: number, rqID: string }> { const { backendAPI, origin } = config; // keep current default params to 'freeze" them during this request @@ -1104,11 +1104,11 @@ async function createTask( let response = null; - onUpdate(new Request({ + onUpdate({ status: RQStatus.UNKNOWN, progress: 0, message: 'CVAT is creating your task', - })); + }); try { response = await Axios.post(`${backendAPI}/tasks`, taskSpec, { @@ -1118,11 +1118,11 @@ async function createTask( throw generateError(errorData); } - onUpdate(new Request({ + onUpdate({ status: RQStatus.UNKNOWN, progress: 0, message: 'CVAT is uploading task data to the server', - })); + }); async function bulkUpload(taskId, files) { const fileBulks = files.reduce((fileGroups, file) => { @@ -1142,11 +1142,11 @@ async function createTask( taskData.append(`client_files[${idx}]`, element); } const percentage = totalSentSize / totalSize; - onUpdate(new Request({ + onUpdate({ status: RQStatus.UNKNOWN, progress: percentage, message: 'CVAT is uploading task data to the server', - })); + }); await Axios.post(`${backendAPI}/tasks/${taskId}/data`, taskData, { ...params, headers: { 'Upload-Multiple': true }, @@ -1170,11 +1170,11 @@ async function createTask( const uploadConfig = { endpoint: `${origin}${backendAPI}/tasks/${response.data.id}/data/`, onUpdate: (percentage) => { - onUpdate(new Request({ + onUpdate({ status: RQStatus.UNKNOWN, progress: percentage, message: 'CVAT is uploading task data to the server', - })); + }); }, chunkSize, totalSize, @@ -2250,16 +2250,32 @@ async function getRequestsList(): Promise> } } +// Temporary solution for server availability problems +const retryTimeouts = [5000, 10000, 15000]; async function getRequestStatus(rqID: string): Promise { const { backendAPI } = config; + let retryCount = 0; + let lastError = null; - try { - const response = await Axios.get(`${backendAPI}/requests/${rqID}`); + while (retryCount < 3) { + try { + const response = await Axios.get(`${backendAPI}/requests/${rqID}`); - return response.data; - } catch (errorData) { - throw generateError(errorData); + return response.data; + } catch (errorData) { + lastError = generateError(errorData); + const { response } = errorData; + if (response && [502, 503, 504].includes(response.status)) { + const timeout = retryTimeouts[retryCount]; + await new Promise((resolve) => { setTimeout(resolve, timeout); }); + retryCount++; + } else { + throw generateError(errorData); + } + } } + + throw lastError; } async function cancelRequest(requestID): Promise { diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index af6cd760ed40..4bf7a482bccb 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -504,25 +504,26 @@ export interface SerializedAPISchema { } export interface SerializedRequest { - id?: string; + id: string; + message: string; status: string; - operation?: { + operation: { target: string; type: string; - format: string; + format: string | null; job_id: number | null; task_id: number | null; project_id: number | null; + function_id: string | null; }; progress?: number; - message: string; result_url?: string; result_id?: number; - created_date?: string; + created_date: string; started_date?: string; finished_date?: string; expiry_date?: string; - owner?: any; + owner: any; } export interface SerializedJobValidationLayout { diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index 369d0c9d5393..1c2194250155 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -756,12 +756,12 @@ export function implementTask(Task: typeof TaskClass): typeof TaskClass { const { taskID, rqID } = await serverProxy.tasks.create( taskSpec, taskDataSpec, - options?.requestStatusCallback || (() => {}), + options?.updateStatusCallback || (() => {}), ); await requestsManager.listen(rqID, { callback: (request: Request) => { - options?.requestStatusCallback(request); + options?.updateStatusCallback(request); if (request.status === RQStatus.FAILED) { serverProxy.tasks.delete(taskID, config.organization.organizationSlug || null); } diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 8ecef7e0e632..1164ae0c07de 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -29,6 +29,7 @@ import logger from './logger'; import Issue from './issue'; import ObjectState from './object-state'; import { JobValidationLayout, TaskValidationLayout } from './validation-layout'; +import { UpdateStatusData } from './core-types'; function buildDuplicatedAPI(prototype) { Object.defineProperties(prototype, { @@ -1141,7 +1142,7 @@ export class Task extends Session { async save( fields: Record = {}, - options?: { requestStatusCallback?: (request: Request) => void }, + options?: { updateStatusCallback?: (updateData: Request | UpdateStatusData) => void }, ): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.save, fields, options); return result; diff --git a/cvat-sdk/cvat_sdk/core/client.py b/cvat-sdk/cvat_sdk/core/client.py index add7ccb5f3d3..0ae0b88ecad9 100644 --- a/cvat-sdk/cvat_sdk/core/client.py +++ b/cvat-sdk/cvat_sdk/core/client.py @@ -10,7 +10,7 @@ from contextlib import contextmanager, suppress from pathlib import Path from time import sleep -from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, TypeVar +from typing import Any, Dict, Generator, Optional, Sequence, Tuple, TypeVar import attrs import packaging.specifiers as specifiers @@ -121,7 +121,7 @@ def organization_slug(self, org_slug: Optional[str]): self.api_client.default_headers[self._ORG_SLUG_HEADER] = org_slug @contextmanager - def organization_context(self, slug: str) -> Iterator[None]: + def organization_context(self, slug: str) -> Generator[None, None, None]: prev_slug = self.organization_slug self.organization_slug = slug try: diff --git a/cvat-sdk/cvat_sdk/core/downloading.py b/cvat-sdk/cvat_sdk/core/downloading.py index c2be936c9aa4..2e8263373350 100644 --- a/cvat-sdk/cvat_sdk/core/downloading.py +++ b/cvat-sdk/cvat_sdk/core/downloading.py @@ -58,8 +58,15 @@ def download_file( except ValueError: file_size = None - with atomic_writer(output_path, "wb") as fd, pbar.task( - total=file_size, desc="Downloading", unit_scale=True, unit="B", unit_divisor=1024 + with ( + atomic_writer(output_path, "wb") as fd, + pbar.task( + total=file_size, + desc="Downloading", + unit_scale=True, + unit="B", + unit_divisor=1024, + ), ): while True: chunk = response.read(amt=CHUNK_SIZE, decode_content=False) diff --git a/cvat-sdk/cvat_sdk/core/progress.py b/cvat-sdk/cvat_sdk/core/progress.py index 7fd2d13a2cd2..fd844de722a0 100644 --- a/cvat-sdk/cvat_sdk/core/progress.py +++ b/cvat-sdk/cvat_sdk/core/progress.py @@ -6,7 +6,7 @@ from __future__ import annotations import contextlib -from typing import ContextManager, Iterable, Optional, TypeVar +from typing import Generator, Iterable, Optional, TypeVar T = TypeVar("T") @@ -26,7 +26,7 @@ class ProgressReporter: """ @contextlib.contextmanager - def task(self, **kwargs) -> ContextManager[None]: + def task(self, **kwargs) -> Generator[None, None, None]: """ Returns a context manager that represents a long-running task for which progress can be reported. diff --git a/cvat-sdk/cvat_sdk/core/utils.py b/cvat-sdk/cvat_sdk/core/utils.py index 0706a2eec613..1ef434e3ad5b 100644 --- a/cvat-sdk/cvat_sdk/core/utils.py +++ b/cvat-sdk/cvat_sdk/core/utils.py @@ -13,7 +13,7 @@ BinaryIO, ContextManager, Dict, - Iterator, + Generator, Literal, Sequence, TextIO, @@ -43,7 +43,7 @@ def atomic_writer( @contextlib.contextmanager def atomic_writer( path: Union[os.PathLike, str], mode: Literal["w", "wb"], encoding: str = "UTF-8" -) -> Iterator[IO]: +) -> Generator[IO, None, None]: """ Returns a context manager that, when entered, returns a handle to a temporary file opened with the specified `mode` and `encoding`. If the context manager diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh index b85479836a7f..c506564999b6 100755 --- a/cvat-sdk/gen/generate.sh +++ b/cvat-sdk/gen/generate.sh @@ -8,7 +8,7 @@ set -e GENERATOR_VERSION="v6.0.1" -VERSION="2.21.2" +VERSION="2.21.3" LIB_NAME="cvat_sdk" LAYER1_LIB_NAME="${LIB_NAME}/api_client" DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)" diff --git a/cvat-sdk/gen/generator-config.yml b/cvat-sdk/gen/generator-config.yml index 26e78cb8a3a9..82f46fda971a 100644 --- a/cvat-sdk/gen/generator-config.yml +++ b/cvat-sdk/gen/generator-config.yml @@ -4,7 +4,7 @@ additionalProperties: packageName: "cvat_sdk.api_client" initRequiredVars: true generateSourceCodeOnly: false - generatorLanguageVersion: '>=3.8' + generatorLanguageVersion: '>=3.9' globalProperties: generateAliasAsModel: true apiTests: false diff --git a/cvat-ui/src/actions/requests-actions.ts b/cvat-ui/src/actions/requests-actions.ts index 6a62e7cecf7c..1f3972746e7e 100644 --- a/cvat-ui/src/actions/requests-actions.ts +++ b/cvat-ui/src/actions/requests-actions.ts @@ -3,12 +3,21 @@ // SPDX-License-Identifier: MIT import { ActionUnion, createAction } from 'utils/redux'; -import { RequestsQuery, RequestsState } from 'reducers'; +import { CombinedState, RequestsQuery, RequestsState } from 'reducers'; import { Request, ProjectOrTaskOrJob, getCore, RQStatus, } from 'cvat-core-wrapper'; +import { Store } from 'redux'; +import { getCVATStore } from 'cvat-store'; const core = getCore(); +let store: null | Store = null; +function getStore(): Store { + if (store === null) { + store = getCVATStore(); + } + return store; +} export enum RequestsActionsTypes { GET_REQUESTS = 'GET_REQUESTS', @@ -79,7 +88,7 @@ export function updateRequestProgress(request: Request, dispatch: (action: Reque ); } -export function shouldListenForProgress(rqID: string | undefined, state: RequestsState): boolean { +export function shouldListenForProgress(rqID: string | void, state: RequestsState): boolean { return ( typeof rqID === 'string' && (!state.requests[rqID] || [RQStatus.FINISHED, RQStatus.FAILED].includes(state.requests[rqID]?.status)) @@ -89,13 +98,13 @@ export function shouldListenForProgress(rqID: string | undefined, state: Request export function listen( requestID: string, dispatch: (action: RequestsActions) => void, - initialRequest?: Request, ) : Promise { + const { requests } = getStore().getState().requests; return core.requests .listen(requestID, { callback: (updatedRequest) => { updateRequestProgress(updatedRequest, dispatch); }, - initialRequest, + initialRequest: requests[requestID], }); } diff --git a/cvat-ui/src/actions/requests-async-actions.ts b/cvat-ui/src/actions/requests-async-actions.ts index 04a5ffd0a5c5..06a137eafd28 100644 --- a/cvat-ui/src/actions/requests-async-actions.ts +++ b/cvat-ui/src/actions/requests-async-actions.ts @@ -8,7 +8,9 @@ import { getCore, RQStatus, Request, Project, Task, Job, } from 'cvat-core-wrapper'; import { listenExportBackupAsync, listenExportDatasetAsync } from './export-actions'; -import { RequestInstanceType, listen, requestsActions } from './requests-actions'; +import { + RequestInstanceType, listen, requestsActions, +} from './requests-actions'; import { listenImportBackupAsync, listenImportDatasetAsync } from './import-actions'; const core = getCore(); @@ -28,6 +30,7 @@ export function getRequestsAsync(query: RequestsQuery): ThunkAction { try { const requests = await core.requests.list(); + dispatch(requestsActions.getRequestsSuccess(requests)); requests .filter((request: Request) => [RQStatus.STARTED, RQStatus.QUEUED].includes(request.status)) @@ -80,7 +83,6 @@ export function getRequestsAsync(query: RequestsQuery): ThunkAction { } } }); - dispatch(requestsActions.getRequestsSuccess(requests)); } catch (error) { dispatch(requestsActions.getRequestsFailed(error)); } diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 60e4da022ef2..d15f033f1e6f 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -6,7 +6,7 @@ import { AnyAction } from 'redux'; import { TasksQuery, StorageLocation } from 'reducers'; import { - getCore, RQStatus, Storage, Task, + getCore, RQStatus, Storage, Task, UpdateStatusData, Request, } from 'cvat-core-wrapper'; import { filterNull } from 'utils/filter-null'; import { ThunkDispatch, ThunkAction } from 'utils/redux'; @@ -274,10 +274,10 @@ ThunkAction { taskInstance.remoteFiles = data.files.remote; try { const savedTask = await taskInstance.save(extras, { - requestStatusCallback(request) { - let { message } = request; + updateStatusCallback(updateData: Request | UpdateStatusData) { + let { message } = updateData; + const { status, progress } = updateData; let helperMessage = ''; - const { status, progress } = request; if (!message) { if ([RQStatus.QUEUED, RQStatus.STARTED].includes(status)) { message = 'CVAT queued the task to import'; @@ -291,7 +291,7 @@ ThunkAction { } } onProgress?.(`${message} ${progress ? `${Math.floor(progress * 100)}%` : ''}. ${helperMessage}`); - if (request.id) updateRequestProgress(request, dispatch); + if (updateData instanceof Request) updateRequestProgress(updateData, dispatch); }, }); diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/frame-tags.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/frame-tags.tsx index 145ec6c1c9b5..4c286122ceb1 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/frame-tags.tsx +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/frame-tags.tsx @@ -1,64 +1,34 @@ // Copyright (C) 2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import './styles.scss'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import Tag from 'antd/lib/tag'; -import { connect } from 'react-redux'; -import { Action } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { removeObject as removeObjectAction, } from 'actions/annotation-actions'; -import { CombinedState, ObjectType, Workspace } from 'reducers'; -import { - QualityConflict, ObjectState, AnnotationConflict, getCore, -} from 'cvat-core-wrapper'; +import { CombinedState, ObjectType } from 'reducers'; +import { ObjectState, AnnotationConflict } from 'cvat-core-wrapper'; import { filterAnnotations } from 'utils/filter-annotations'; -const core = getCore(); - -interface StateToProps { - highlightedConflict: QualityConflict | null; - states: ObjectState[]; - workspace: Workspace; -} - -interface DispatchToProps { - removeObject(objectState: any): void; -} - -function mapStateToProps(state: CombinedState): StateToProps { - const { - annotation: { - annotations: { highlightedConflict, states }, - workspace, - }, - } = state; +function FrameTags(): JSX.Element { + const dispatch = useDispatch(); - return { highlightedConflict, states, workspace }; -} - -function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { - return { - removeObject(objectState: ObjectState): void { - dispatch(removeObjectAction(objectState, false)); - }, - }; -} - -function FrameTags(props: StateToProps & DispatchToProps): JSX.Element { - const { - highlightedConflict, states, workspace, removeObject, - } = props; + const { highlightedConflict, states, workspace } = useSelector((state: CombinedState) => ({ + highlightedConflict: state.annotation.annotations.highlightedConflict, + states: state.annotation.annotations.states, + workspace: state.annotation.workspace, + }), shallowEqual); - const [frameTags, setFrameTags] = useState([] as ObjectState[]); + const [frameTags, setFrameTags] = useState([]); const onRemoveState = (objectState: ObjectState): void => { - removeObject(objectState); + dispatch(removeObjectAction(objectState, false)); }; useEffect(() => { @@ -67,16 +37,20 @@ function FrameTags(props: StateToProps & DispatchToProps): JSX.Element { ); }, [states]); + const tagClassName = useCallback((tag: ObjectState): string => { + const tagHighlighted = (highlightedConflict?.annotationConflicts || []) + .find((conflict: AnnotationConflict) => conflict.serverID === tag.serverID); + return tagHighlighted ? 'cvat-frame-tag-highlighted' : 'cvat-frame-tag'; + }, [highlightedConflict]); + return ( <> -
+
{frameTags - .filter((tag: any) => tag.source !== core.enums.Source.GT) - .map((tag: any) => ( + .filter((tag: ObjectState) => !tag.isGroundTruth) + .map((tag: ObjectState) => ( conflict.serverID === tag.serverID).length !== 0 ? 'cvat-frame-tag-highlighted' : 'cvat-frame-tag' - } + className={tagClassName(tag)} color={tag.label.color} onClose={() => { onRemoveState(tag); @@ -88,14 +62,12 @@ function FrameTags(props: StateToProps & DispatchToProps): JSX.Element { ))}
-
+
{frameTags - .filter((tag: any) => tag.source === core.enums.Source.GT) - .map((tag: any) => ( + .filter((tag: ObjectState) => tag.isGroundTruth) + .map((tag: ObjectState) => ( conflict.serverID === tag.serverID).length !== 0 ? 'cvat-frame-tag-highlighted' : 'cvat-frame-tag' - } + className={tagClassName(tag)} color={tag.label.color} onClose={() => { onRemoveState(tag); @@ -112,4 +84,4 @@ function FrameTags(props: StateToProps & DispatchToProps): JSX.Element { ); } -export default connect(mapStateToProps, mapDispatchToProps)(FrameTags); +export default React.memo(FrameTags); diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss index 8ac9962e01e8..0740e3c9046d 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss @@ -96,3 +96,7 @@ transform: scale(1.1); } + +.cvat-canvas-annotation-frame-tags { + margin-bottom: $grid-unit-size; +} diff --git a/cvat-ui/src/components/requests-page/request-card.tsx b/cvat-ui/src/components/requests-page/request-card.tsx index 980af114563a..52c109e3822c 100644 --- a/cvat-ui/src/components/requests-page/request-card.tsx +++ b/cvat-ui/src/components/requests-page/request-card.tsx @@ -100,11 +100,15 @@ function constructTimestamps(request: Request): JSX.Element { ); } case RQStatus.FAILED: { - return ( + return (request.startedDate ? ( {`Started by ${request.owner.username} on ${started}`} - ); + ) : ( + + {`Enqueued by ${request.owner.username} on ${created}`} + + )); } case RQStatus.STARTED: { return ( diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts index 275cedcc8ab9..94b70373a1c7 100644 --- a/cvat-ui/src/cvat-core-wrapper.ts +++ b/cvat-ui/src/cvat-core-wrapper.ts @@ -17,6 +17,7 @@ import { import { SerializedAttribute, SerializedLabel, SerializedAPISchema, } from 'cvat-core/src/server-response-types'; +import { UpdateStatusData } from 'cvat-core/src/core-types'; import { Job, Task } from 'cvat-core/src/session'; import Project from 'cvat-core/src/project'; import QualityReport, { QualitySummary } from 'cvat-core/src/quality-report'; @@ -41,7 +42,7 @@ import { Dumper } from 'cvat-core/src/annotation-formats'; import { Event } from 'cvat-core/src/event'; import { APIWrapperEnterOptions } from 'cvat-core/src/plugins'; import BaseSingleFrameAction, { ActionParameterType, FrameSelectionType } from 'cvat-core/src/annotations-actions'; -import { Request } from 'cvat-core/src/request'; +import { Request, RequestOperation } from 'cvat-core/src/request'; const cvat: CVATCore = _cvat; @@ -120,4 +121,6 @@ export type { CVATCore, SerializedAPISchema, ProjectOrTaskOrJob, + RequestOperation, + UpdateStatusData, }; diff --git a/cvat-ui/src/utils/is-able-to-change-frame.ts b/cvat-ui/src/utils/is-able-to-change-frame.ts index b3029c4b2670..3cbc127a8a86 100644 --- a/cvat-ui/src/utils/is-able-to-change-frame.ts +++ b/cvat-ui/src/utils/is-able-to-change-frame.ts @@ -17,15 +17,15 @@ export default function isAbleToChangeFrame(frame?: number): boolean { return false; } - const frameInTheJob = true; + let frameInTheJob = true; if (typeof frame === 'number') { if (meta.includedFrames) { // frame argument comes in job coordinates // hovewer includedFrames contains absolute data values - return meta.includedFrames.includes(meta.getDataFrameNumber(frame - job.startFrame)); + frameInTheJob = meta.includedFrames.includes(meta.getDataFrameNumber(frame - job.startFrame)); } - return frame >= job.startFrame && frame <= job.stopFrame; + frameInTheJob = frame >= job.startFrame && frame <= job.stopFrame; } return canvas.isAbleToChangeFrame() && frameInTheJob && !state.annotation.player.navigationBlocked; diff --git a/cvat/__init__.py b/cvat/__init__.py index 33ddf9c436cf..ac47316ad0a1 100644 --- a/cvat/__init__.py +++ b/cvat/__init__.py @@ -4,6 +4,6 @@ from cvat.utils.version import get_version -VERSION = (2, 21, 2, 'final', 0) +VERSION = (2, 21, 3, 'final', 0) __version__ = get_version(VERSION) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index a64637359ff5..c923083b18b3 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -539,7 +539,9 @@ def extract(self): class _AvVideoReading: @contextmanager - def read_av_container(self, source: Union[str, io.BytesIO]) -> av.container.InputContainer: + def read_av_container( + self, source: Union[str, io.BytesIO] + ) -> Generator[av.container.InputContainer, None, None]: if isinstance(source, io.BytesIO): source.seek(0) # required for re-reading diff --git a/cvat/schema.yml b/cvat/schema.yml index b05b2989d53b..3af7944889b4 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CVAT REST API - version: 2.21.2 + version: 2.21.3 description: REST API for Computer Vision Annotation Tool (CVAT) termsOfService: https://www.google.com/policies/terms/ contact: diff --git a/docker-compose.yml b/docker-compose.yml index 45399e0c39ea..b51e38fce7d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,7 +79,7 @@ services: cvat_server: container_name: cvat_server - image: cvat/server:${CVAT_VERSION:-v2.21.2} + image: cvat/server:${CVAT_VERSION:-v2.21.3} restart: always depends_on: <<: *backend-deps @@ -113,7 +113,7 @@ services: cvat_utils: container_name: cvat_utils - image: cvat/server:${CVAT_VERSION:-v2.21.2} + image: cvat/server:${CVAT_VERSION:-v2.21.3} restart: always depends_on: *backend-deps environment: @@ -130,7 +130,7 @@ services: cvat_worker_import: container_name: cvat_worker_import - image: cvat/server:${CVAT_VERSION:-v2.21.2} + image: cvat/server:${CVAT_VERSION:-v2.21.3} restart: always depends_on: *backend-deps environment: @@ -146,7 +146,7 @@ services: cvat_worker_export: container_name: cvat_worker_export - image: cvat/server:${CVAT_VERSION:-v2.21.2} + image: cvat/server:${CVAT_VERSION:-v2.21.3} restart: always depends_on: *backend-deps environment: @@ -162,7 +162,7 @@ services: cvat_worker_annotation: container_name: cvat_worker_annotation - image: cvat/server:${CVAT_VERSION:-v2.21.2} + image: cvat/server:${CVAT_VERSION:-v2.21.3} restart: always depends_on: *backend-deps environment: @@ -178,7 +178,7 @@ services: cvat_worker_webhooks: container_name: cvat_worker_webhooks - image: cvat/server:${CVAT_VERSION:-v2.21.2} + image: cvat/server:${CVAT_VERSION:-v2.21.3} restart: always depends_on: *backend-deps environment: @@ -194,7 +194,7 @@ services: cvat_worker_quality_reports: container_name: cvat_worker_quality_reports - image: cvat/server:${CVAT_VERSION:-v2.21.2} + image: cvat/server:${CVAT_VERSION:-v2.21.3} restart: always depends_on: *backend-deps environment: @@ -210,7 +210,7 @@ services: cvat_worker_analytics_reports: container_name: cvat_worker_analytics_reports - image: cvat/server:${CVAT_VERSION:-v2.21.2} + image: cvat/server:${CVAT_VERSION:-v2.21.3} restart: always depends_on: *backend-deps environment: @@ -226,7 +226,7 @@ services: cvat_ui: container_name: cvat_ui - image: cvat/ui:${CVAT_VERSION:-v2.21.2} + image: cvat/ui:${CVAT_VERSION:-v2.21.3} restart: always depends_on: - cvat_server diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index fc1e841bec43..d0906952f96d 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -129,7 +129,7 @@ cvat: additionalVolumeMounts: [] replicas: 1 image: cvat/server - tag: v2.21.2 + tag: v2.21.3 imagePullPolicy: Always permissionFix: enabled: true @@ -153,7 +153,7 @@ cvat: frontend: replicas: 1 image: cvat/ui - tag: v2.21.2 + tag: v2.21.3 imagePullPolicy: Always labels: {} # test: test diff --git a/pyproject.toml b/pyproject.toml index 581552a67ebc..6d0772451578 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,4 +6,4 @@ skip_gitignore = true # align tool behavior with Black [tool.black] line-length = 100 -target-version = ['py38'] +target-version = ['py39'] diff --git a/site/content/en/docs/administration/advanced/analytics.md b/site/content/en/docs/administration/advanced/analytics.md index 36425c341a32..445c212f9687 100644 --- a/site/content/en/docs/administration/advanced/analytics.md +++ b/site/content/en/docs/administration/advanced/analytics.md @@ -18,6 +18,10 @@ and enhance user satisfaction. CVAT analytics are available from the top menu. +Superusers and users with administrator role have access to analytics. +Permission to access analytics can also be granted when editing a user +on admin page by `Has access to analytics` checkbox. + ![CVAT Analytics](/images/analytics_menu.jpg) > Note: CVAT analytics and monitoring are available only for on-prem solution. diff --git a/site/content/en/docs/api_sdk/cli/_index.md b/site/content/en/docs/api_sdk/cli/_index.md index f17d712717b2..541e29f30611 100644 --- a/site/content/en/docs/api_sdk/cli/_index.md +++ b/site/content/en/docs/api_sdk/cli/_index.md @@ -30,7 +30,7 @@ To install an [official release of CVAT CLI](https://pypi.org/project/cvat-cli/) pip install cvat-cli ``` -We support Python versions 3.8 and higher. +We support Python versions 3.9 and higher. ## Usage diff --git a/site/content/en/docs/api_sdk/sdk/_index.md b/site/content/en/docs/api_sdk/sdk/_index.md index 4c133a7b0231..e9683583ab0e 100644 --- a/site/content/en/docs/api_sdk/sdk/_index.md +++ b/site/content/en/docs/api_sdk/sdk/_index.md @@ -48,7 +48,7 @@ To use the PyTorch adapter, request the `pytorch` extra: pip install "cvat-sdk[pytorch]" ``` -We support Python versions 3.8 and higher. +We support Python versions 3.9 and higher. ## Usage diff --git a/tests/cypress.config.js b/tests/cypress.config.js index da1f20d8dee1..17f157c59c8d 100644 --- a/tests/cypress.config.js +++ b/tests/cypress.config.js @@ -1,3 +1,7 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + const { defineConfig } = require('cypress'); const baseConfig = require('./cypress.base.config'); diff --git a/tests/cypress/e2e/actions_objects/regression_tests.js b/tests/cypress/e2e/actions_objects/regression_tests.js index 8cf00b90e0c1..7bf11c7b0d7b 100644 --- a/tests/cypress/e2e/actions_objects/regression_tests.js +++ b/tests/cypress/e2e/actions_objects/regression_tests.js @@ -9,9 +9,9 @@ context('Regression tests', () => { let jobID = null; const taskPayload = { - name: 'Test annotations actions', + name: 'Regression tests', labels: [{ - name: 'label 1', + name: 'car', attributes: [], type: 'any', }], @@ -29,12 +29,9 @@ context('Regression tests', () => { }; const rectanglePayload = { - frame: 99, - objectType: 'shape', shapeType: 'rectangle', - points: [250, 64, 491, 228], occluded: false, - labelName: 'label 1', + labelName: taskPayload.labels[0].name, }; before(() => { @@ -45,41 +42,65 @@ context('Regression tests', () => { taskID = response.taskID; [jobID] = response.jobIDs; - cy.headlessCreateObjects([rectanglePayload], jobID); - cy.visit(`/tasks/${taskID}/jobs/${jobID}`); + cy.headlessCreateObjects([ + { + ...rectanglePayload, frame: 99, points: [250, 64, 491, 228], objectType: 'shape', + }, + { + ...rectanglePayload, frame: 0, points: [10, 10, 30, 30], objectType: 'track', + }, + ], jobID); }); }); - describe('Regression tests', () => { + describe('UI does not crash', () => { + beforeEach(() => { + cy.visit(`/tasks/${taskID}/jobs/${jobID}`); + cy.get('.cvat-canvas-container').should('not.exist'); + cy.get('.cvat-canvas-container').should('exist').and('be.visible'); + }); + it('UI does not crash if to activate an object while frame fetching', () => { - cy.reload(); cy.intercept('GET', '/api/jobs/**/data?**', (req) => { req.continue((res) => { - res.setDelay(1000); + res.setDelay(3000); }); }).as('delayedRequest'); + cy.get('.cvat-player-last-button').click(); - cy.get('#cvat_canvas_shape_1').trigger('mousemove'); - cy.get('#cvat_canvas_shape_1').should('not.have.class', 'cvat_canvas_shape_activated'); + cy.get('#cvat-objects-sidebar-state-item-1').trigger('mousemove'); + cy.get('#cvat-objects-sidebar-state-item-1').should('not.have.class', 'cvat-objects-sidebar-state-active-item'); cy.wait('@delayedRequest'); cy.get('#cvat_canvas_shape_1').trigger('mousemove'); cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated'); }); + + it('UI does not crash if to navigate during an element resizing (issue 1922)', { scrollBehavior: false }, () => { + cy.get('#cvat_canvas_shape_2').then(([el]) => { + const rect = el.getBoundingClientRect(); + + cy.get('body').trigger('mousemove', rect.x + rect.width / 2, rect.y + rect.height / 2); + cy.get('#cvat_canvas_shape_2').should('have.class', 'cvat_canvas_shape_activated'); + + cy.get('body').trigger('mousedown', rect.right, rect.bottom, { button: 0 }); + cy.get('body').trigger('mousemove', rect.right + 100, rect.bottom + 100); + + cy.get('body').type('f'); // go to next frame + cy.get('body').trigger('mouseup'); + + // Page with the error is missing + cy.get('.cvat-global-boundary').should('not.exist'); + cy.checkFrameNum(0); + }); + }); }); after(() => { + if (taskID !== null) { + cy.headlessDeleteTask(taskID); + } cy.logout(); - cy.getAuthKey().then((response) => { - const authKey = response.body.key; - cy.request({ - method: 'DELETE', - url: `/api/tasks/${taskID}`, - headers: { - Authorization: `Token ${authKey}`, - }, - }); - }); }); }); diff --git a/tests/cypress/e2e/features/masks_basics.js b/tests/cypress/e2e/features/masks_basics.js index b39d6ea769d7..3e119e97f039 100644 --- a/tests/cypress/e2e/features/masks_basics.js +++ b/tests/cypress/e2e/features/masks_basics.js @@ -156,6 +156,12 @@ context('Manipulations with masks', { scrollBehavior: false }, () => { cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Edit'); cy.drawMask(editingActions); + + // Check issue fixed in https://github.com/cvat-ai/cvat/pull/8598 + // Frames navigation should not work during editing + cy.get('.cvat-player-next-button').click(); + cy.checkFrameNum(0); + cy.finishMaskDrawing(); }); diff --git a/tests/cypress/e2e/features/requests_page.js b/tests/cypress/e2e/features/requests_page.js index 3cdf187a9825..8622092b89d8 100644 --- a/tests/cypress/e2e/features/requests_page.js +++ b/tests/cypress/e2e/features/requests_page.js @@ -357,5 +357,24 @@ context('Requests page', () => { }); }); }); + + it('Export task. Request for status fails, UI is not crushing', () => { + cy.intercept('GET', '/api/requests/**', { + statusCode: 500, + body: 'Network error', + }); + + cy.exportTask({ + type: 'annotations', + format: exportFormat, + archiveCustomName: annotationsArchiveNameLocal, + }); + + cy.contains('Could not export dataset').should('be.visible'); + cy.closeNotification('.ant-notification-notice-error'); + + cy.contains('.cvat-header-button', 'Requests').should('be.visible').click(); + cy.get('.cvat-requests-page').should('be.visible'); + }); }); }); diff --git a/tests/cypress/e2e/issues_prs/issue_1922_error_canvas_is_busy_at_resize_element.js b/tests/cypress/e2e/issues_prs/issue_1922_error_canvas_is_busy_at_resize_element.js deleted file mode 100644 index 53b5606f457b..000000000000 --- a/tests/cypress/e2e/issues_prs/issue_1922_error_canvas_is_busy_at_resize_element.js +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -/// - -import { taskName, labelName } from '../../support/const'; - -context('Check error canvas is busy at resize element', () => { - const issueId = '1922'; - const createRectangleShape2Points = { - points: 'By 2 Points', - type: 'Shape', - labelName, - firstX: 100, - firstY: 100, - secondX: 300, - secondY: 300, - }; - - before(() => { - cy.openTaskJob(taskName); - }); - - describe(`Testing issue "${issueId}"`, () => { - it('Create an object in first frame', () => { - cy.createRectangle(createRectangleShape2Points); - }); - - it('Go to next frame and create an object in second frame', () => { - cy.get('.cvat-player-next-button').click(); - cy.createRectangle(createRectangleShape2Points); - }); - - it('Switching mode of button on "back with a filter"', () => { - cy.get('.cvat-player-previous-button').rightclick(); - cy.get('.cvat-player-previous-filtered-inlined-button').click(); - }); - - it('Resize element on second frame and go to previous frame at resizing element', () => { - const { secondX, secondY } = createRectangleShape2Points; - cy.get('.cvat-canvas-container').trigger('mousemove', secondX - 10, secondY - 10); // activate second shape - cy.get('.cvat-canvas-container').trigger('mousedown', secondX, secondY, { button: 0 }); - cy.get('.cvat-canvas-container').trigger('mousemove', secondX + 100, secondY + 100); - cy.get('body').type('d'); // go to previous frame - cy.get('body').trigger('mouseup'); - }); - - it('Page with the error is missing', () => { - cy.get('.cvat-global-boundary').should('not.exist'); - }); - }); -}); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 76f7ffe2640c..91322e9c3695 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -1272,7 +1272,7 @@ Cypress.Commands.add('exportTask', ({ cy.get('.cvat-cloud-storage-select-provider').click(); } } - cy.contains('button', 'OK').click(); + cy.contains('.cvat-modal-export-task button', 'OK').click(); cy.get('.cvat-notification-notice-export-task-start').should('be.visible'); cy.closeNotification('.cvat-notification-notice-export-task-start'); }); diff --git a/tests/cypress_canvas3d.config.js b/tests/cypress_canvas3d.config.js index e1cd5ede69f2..f542fe78bde9 100644 --- a/tests/cypress_canvas3d.config.js +++ b/tests/cypress_canvas3d.config.js @@ -1,3 +1,7 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + const { defineConfig } = require('cypress'); const baseConfig = require('./cypress.base.config'); diff --git a/tests/python/cli/self-signed.crt b/tests/python/cli/self-signed.crt new file mode 100644 index 000000000000..815373bf286c --- /dev/null +++ b/tests/python/cli/self-signed.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBPDCB76ADAgECAhQksQwFGcyVwF0+gIOPMPBB+/NjNTAFBgMrZXAwFDESMBAG +A1UEAwwJbG9jYWxob3N0MB4XDTI0MTAyODEyMTkyNFoXDTI0MTAyOTEyMTkyNFow +FDESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEAzGOv96vkrHr0GPcWL7vN +8mgR4XMg9ItNpJ2nbMmjYCKjUzBRMB0GA1UdDgQWBBR6Hn0aG/ZGAJjY9HIUK7El +84qAgzAfBgNVHSMEGDAWgBR6Hn0aG/ZGAJjY9HIUK7El84qAgzAPBgNVHRMBAf8E +BTADAQH/MAUGAytlcANBAMj2zWdIa8oOiEtUWFMv+KYf1kyP1lUnlcC2xUpOj8d3 +kRYtlRX4E7F5zzzgKgNpbanRAg72qnqPiFAFCGVAhgY= +-----END CERTIFICATE----- diff --git a/tests/python/cli/self-signed.key b/tests/python/cli/self-signed.key new file mode 100644 index 000000000000..f81d8519bcac --- /dev/null +++ b/tests/python/cli/self-signed.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIKe5zj/UrVJ/LySjKm9BBVHXziqFIwJ6w+HuTHnldCLo +-----END PRIVATE KEY----- diff --git a/tests/python/cli/test_cli.py b/tests/python/cli/test_cli.py index 364c7011e7ca..d6b19cfe0a3c 100644 --- a/tests/python/cli/test_cli.py +++ b/tests/python/cli/test_cli.py @@ -10,7 +10,6 @@ import packaging.version as pv import pytest -from cvat_cli.cli import CLI from cvat_sdk import Client, make_client from cvat_sdk.api_client import exceptions, models from cvat_sdk.core.proxies.tasks import ResourceType, Task @@ -20,7 +19,7 @@ from shared.utils.config import BASE_URL, USER_PASS from shared.utils.helpers import generate_image_file -from .util import generate_images, run_cli +from .util import generate_images, https_reverse_proxy, run_cli class TestCLI: @@ -243,23 +242,26 @@ def mocked_version(_): assert "Server version '0' is not compatible with SDK version" in caplog.text @pytest.mark.parametrize("verify", [True, False]) - def test_can_control_ssl_verification_with_arg(self, monkeypatch, verify: bool): - # TODO: Very hacky implementation, improve it, if possible - class MyException(Exception): - pass - - normal_init = CLI.__init__ - - def my_init(self, *args, **kwargs): - normal_init(self, *args, **kwargs) - raise MyException(self.client.api_client.configuration.verify_ssl) - - monkeypatch.setattr(CLI, "__init__", my_init) - - with pytest.raises(MyException) as capture: - self.run_cli(*(["--insecure"] if not verify else []), "ls") - - assert capture.value.args[0] == verify + def test_can_control_ssl_verification_with_arg(self, verify: bool): + with https_reverse_proxy() as proxy_url: + if verify: + insecure_args = [] + else: + insecure_args = ["--insecure"] + + run_cli( + self, + f"--auth={self.user}:{self.password}", + f"--server-host={proxy_url}", + *insecure_args, + "ls", + expected_code=1 if verify else 0, + ) + stdout = self.stdout.getvalue() + + if not verify: + for line in stdout.splitlines(): + int(line) def test_can_control_organization_context(self): org = "cli-test-org" diff --git a/tests/python/cli/util.py b/tests/python/cli/util.py index 034d5d073ace..ff1173fa4a8d 100644 --- a/tests/python/cli/util.py +++ b/tests/python/cli/util.py @@ -3,10 +3,17 @@ # SPDX-License-Identifier: MIT +import contextlib +import http.server +import ssl +import threading import unittest from pathlib import Path -from typing import Any, List, Union +from typing import Any, Dict, Generator, List, Union +import requests + +from shared.utils.config import BASE_URL from shared.utils.helpers import generate_image_file @@ -29,3 +36,50 @@ def generate_images(dst_dir: Path, count: int) -> List[Path]: filename.write_bytes(generate_image_file(filename.name).getvalue()) filenames.append(filename) return filenames + + +@contextlib.contextmanager +def https_reverse_proxy() -> Generator[str, None, None]: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + cert_dir = Path(__file__).parent + ssl_context.load_cert_chain(cert_dir / "self-signed.crt", cert_dir / "self-signed.key") + + with http.server.HTTPServer(("localhost", 0), _ProxyHttpRequestHandler) as proxy_server: + proxy_server.socket = ssl_context.wrap_socket( + proxy_server.socket, + server_side=True, + ) + server_thread = threading.Thread(target=proxy_server.serve_forever) + server_thread.start() + try: + yield f"https://localhost:{proxy_server.server_port}" + finally: + proxy_server.shutdown() + server_thread.join() + + +class _ProxyHttpRequestHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + response = requests.get(**self._shared_request_args()) + self._translate_response(response) + + def do_POST(self): + body_length = int(self.headers["Content-Length"]) + + response = requests.post(data=self.rfile.read(body_length), **self._shared_request_args()) + self._translate_response(response) + + def _shared_request_args(self) -> Dict[str, Any]: + headers = {k.lower(): v for k, v in self.headers.items()} + del headers["host"] + + return {"url": BASE_URL + self.path, "headers": headers, "timeout": 60, "stream": True} + + def _translate_response(self, response: requests.Response) -> None: + self.send_response(response.status_code) + for key, value in response.headers.items(): + self.send_header(key, value) + self.end_headers() + # Need to use raw here to prevent requests from handling Content-Encoding. + self.wfile.write(response.raw.read()) diff --git a/tests/python/shared/fixtures/init.py b/tests/python/shared/fixtures/init.py index aa1192a0acf5..b29c5c30528b 100644 --- a/tests/python/shared/fixtures/init.py +++ b/tests/python/shared/fixtures/init.py @@ -300,9 +300,10 @@ def dump_db(): def create_compose_files(container_name_files): for filename in container_name_files: - with open(filename.with_name(filename.name.replace(".tests", "")), "r") as dcf, open( - filename, "w" - ) as ndcf: + with ( + open(filename.with_name(filename.name.replace(".tests", "")), "r") as dcf, + open(filename, "w") as ndcf, + ): dc_config = yaml.safe_load(dcf) for service_name, service_config in dc_config["services"].items(): diff --git a/yarn.lock b/yarn.lock index 111a2b86f925..1f42b7b8dccc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6294,9 +6294,9 @@ http-proxy-agent@^5.0.0: debug "4" http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + version "2.0.7" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1"